// ==UserScript== // @name JVChat Premium // @description Outil de discussion instantanée pour les forums de Jeuxvideo.com // @author Blaff // @namespace JVChatPremium // @version 0.1.1 // @match http*://*.jeuxvideo.com/forums/42-* // @grant none // @downloadURL none // ==/UserScript== /* ROADMAP: - Smooth transition on append messages + Fade-in - Détection captcha - Separator des nouveaux messages au changement d'onglet - Notification des nouveaux messages dans le titre de l'onglet + erreurs aussi - Shift + Enter pour nouvelle ligne sans envoyer - Citations - Notification avec @pseudo - Blacklist - Pouvoir voir les anciens messages */ let CSS = ``; let PANEL = `

Profil

Topic

`; function getForm(doc) { return doc.getElementsByClassName('form-post-message')[0]; } let freshForm = getForm(document); let firstMessageId = undefined; let allMessagesId = new Set(); let userConnected = undefined; let updateIntervals = [2, 3, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 10, 10, 10, 10, 10, 10, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 30, 30, 30, 30, 30, 30, 30, 30, 60]; let transisitions = [0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 19, 19, 19, 19, 19, 19, 19, 19, 31]; let updateIntervalIdx = 0; let updateIntervalMax = updateIntervals.length - 1; let isLocked = false; let isError = false; function getTopicLocked(elem) { let lock = elem.getElementsByClassName("message-lock-topic")[0]; if (lock === undefined) { return lock; } let reason = lock.getElementsByTagName("span")[0].textContent.trim(); return `Le topic a été vérouillé pour la raison suivante : "${reason}"`; } function getTopicError(elem) { let error = elem.getElementsByClassName("img-erreur")[0]; if (error === undefined) { return error; } return `Le topic présente une erreur: ${error.getAttribute("alt")}`; } function tryCatch(func) { function wrapped(optArg) { try { func(optArg); } catch(err) { let message = `Une erreur est survenue dans JVChat Premium: '${err.message}' (line ${err.lineNumber})` console.error(message); try { addAlertbox("danger", message); } catch(e) { alert(message); } } } return wrapped; } function toggleTextarea() { let isDown = isScrollDown(); document.getElementById("bloc-formulaire-forum").getElementsByClassName("jv-editor-toolbar")[0].classList.toggle("jvchat-hide"); document.getElementById("jvchat-enlarge").classList.toggle("jvchat-hide"); document.getElementById("jvchat-reduce").classList.toggle("jvchat-hide"); document.getElementById("jvchat-post").classList.toggle("jvchat-hide"); document.getElementById("bloc-formulaire-forum").classList.toggle("jvchat-reduced"); if (isDown) { setScrollDown(); } } function parseURL(url) { let regex = /^(.*?)(\/42-\d+-\d+-)(\d+)(-\d+-\d+-\d+-)(.*?)(\.htm)(.*)$/i; let [_, domain, ids, page, nums, title, htm, anchor] = url.match(regex); return {domain: domain, ids: ids, page: page, nums: nums, title: title, htm: htm, anchor: anchor}; } function buildURL(dict) { return `${dict.domain}${dict.ids}${dict.page}${dict.nums}${dict.title}${dict.htm}${dict.anchor}`; } function getLastPage(document) { let blocPages = document.getElementsByClassName("bloc-liste-num-page")[0]; let spans = blocPages.getElementsByTagName("span"); let lastPage = 1; for (let span of spans) { let page = parseInt(span.textContent.trim()); if (!isNaN(page) && page > lastPage) { lastPage = page; } } return lastPage; } function parseMessage(elem) { let author = elem.getElementsByClassName("bloc-pseudo-msg")[0].textContent.trim(); let avatar = elem.getElementsByClassName("user-avatar-msg")[0]; if (avatar !== undefined) { avatar = avatar.getAttribute("data-srcset"); } let date = elem.getElementsByClassName("bloc-date-msg")[0].textContent.trim(); let content = fixMessage(elem.getElementsByClassName("text-enrichi-forum")[0]); let id = parseInt(elem.getAttribute("data-id")); return {author: author, date: date, avatar: avatar, id: id, content: content}; } function parseUserInfo(elem) { let mpBox = elem.getElementsByClassName("jv-account-number-mp")[0]; if (mpBox === undefined) { return undefined; } let notifBox = elem.getElementsByClassName("jv-account-number-notif")[0]; let avatarBox = elem.getElementsByClassName("account-avatar-box")[0] let authorBox = elem.getElementsByClassName("account-pseudo")[0]; let mp = parseInt(mpBox.getAttribute("data-val")); let notif = parseInt(notifBox.getAttribute("data-val")); let avatar = avatarBox.style["background-image"].slice(5, -2).replace("/avatar-md/", "/avatar/"); let author = authorBox.textContent.trim() return {author: author, avatar: avatar, mp: mp, notif: notif}; } function parseTopicInfo(elem) { let title = elem.querySelector("#bloc-title-forum").textContent.trim(); let connected = parseInt(elem.getElementsByClassName("nb-connect-fofo")[0].textContent.trim()); let lastPage = getLastPage(elem); let pageActive = elem.getElementsByClassName("page-active")[0]; let page = 1; if (pageActive !== undefined) { page = parseInt(pageActive.textContent.trim()); } return {title: title, connected: connected, lastPage: lastPage, page: page}; } function fixMessage(elem) { let jvcares = Array.from(elem.getElementsByClassName("JvCare")); for (let jvcare of jvcares) { let a = document.createElement("a"); a.setAttribute("target", "_blank"); a.setAttribute("href", jvCake(jvcare.getAttribute("class"))); a.innerHTML = jvcare.innerHTML; jvcare.outerHTML = a.outerHTML; } return elem; } function jvCake(cls) { let base16 = '0A12B34C56D78E9F', lien = '', s = cls.split(' ')[1]; for (let i = 0; i < s.length; i += 2) { lien += String.fromCharCode(base16.indexOf(s.charAt(i)) * 16 + base16.indexOf(s.charAt(i + 1))); } return lien; } function clearPage(document) { let buttons = `
`; document.head.insertAdjacentHTML("beforeend", CSS); let messageTopic = document.getElementById("message_topic"); if (messageTopic) { messageTopic.setAttribute("placeholder", "Hop hop hop, le message ne va pas s'écrire tout seul !"); messageTopic.insertAdjacentHTML("afterend", buttons); messageTopic.addEventListener("keydown", tryCatch(postMessageIfEnter)); document.getElementById("jvchat-post").addEventListener("click", tryCatch(postMessage)); document.getElementById("jvchat-enlarge").addEventListener("click", tryCatch(toggleTextarea)); document.getElementById("jvchat-reduce").addEventListener("click", tryCatch(toggleTextarea)); } document.getElementsByClassName("conteneur-messages-pagi")[0].insertAdjacentHTML("afterbegin", "

"); document.getElementById("page-messages-forum").insertAdjacentHTML("afterbegin", "
"); document.getElementById("content-context").insertAdjacentHTML("afterbegin", PANEL); document.getElementById("bloc-formulaire-forum").classList.add("jvchat-reduced"); document.getElementById("bloc-formulaire-forum").classList.add("jvchat-hide"); let toolbar = document.getElementById("bloc-formulaire-forum").getElementsByClassName("jv-editor-toolbar")[0]; if (toolbar) { toolbar.classList.add("jvchat-hide"); } document.getElementById("jvchat-main").addEventListener("click", tryCatch(dontScrollOnExpand)); document.getElementById("jvchat-alerts").addEventListener("click", tryCatch(closeAlert)); } function closeAlert(event) { let target = event.target; if (!target) { return; } if (target.classList.contains("jvchat-alert-close")) { let parent = target.parentElement; parent.parentElement.removeChild(parent); } } function postMessage() { let textarea = document.getElementById("message_topic"); if (freshForm === undefined) { addAlertbox("danger", "Impossible de poster le message, aucun formulaire trouvé"); return; } let formData = serializeForm(freshForm); formData["message_topic"] = textarea.value; let formulaire = document.getElementById("bloc-formulaire-forum"); formulaire.classList.add("jvchat-disabled-form"); function onSuccess(res) { formulaire.classList.remove("jvchat-disabled-form"); let alert = parsePage(res).alert; if (!alert) { textarea.value = ""; } } function onError(err) { addAlertbox("danger", err); formulaire.classList.remove("jvchat-disabled-form"); } function onTimeout(err) { addAlertbox("warning", err); formulaire.classList.remove("jvchat-disabled-form"); } request("POST", document.URL, onSuccess, onError, onTimeout, makeFormData(formData)); } function postMessageIfEnter(event) { let classes = document.getElementById("bloc-formulaire-forum").classList; if (classes.contains("jvchat-reduced") && (event.which == 13 || event.keyCode == 13)) { event.preventDefault(); postMessage(); } } function serializeForm(form) { // Useless actually, just use new FormData(form) let dict = {}; for (let select of form.getElementsByTagName("select")) { dict[select.name] = select.querySelector("option[selected]").value; } for (let input of form.getElementsByTagName("input")) { dict[input.name] = input.value; } for (let textarea of form.getElementsByTagName("textarea")) { dict[textarea.name] = textarea.value; } return dict; } function makeFormData(dict) { var formData = new FormData(); for (let key in dict) { formData.append(key, dict[key]); } return formData; } function getMessages(document) { let blocMessages = document.getElementsByClassName("bloc-message-forum"); let messages = []; for (let bloc of blocMessages) { messages.push(parseMessage(bloc)); } return messages; } function addMessages(document, messages) { let main = document.getElementById("jvchat-main"); let isDown = isScrollDown(); let hasNewMessages = false; for (let message of messages) { let id = message.id; if (firstMessageId === undefined) { firstMessageId = id; } if (allMessagesId.has(id) || id < firstMessageId) { continue; } allMessagesId.add(id); hasNewMessages = true; let avatar = message.avatar; let date = message.date; let exists = avatar !== undefined; let author = exists ? message.author : `${message.author}`; let avatarSrc = exists ? avatar : "http://image.jeuxvideo.com/avatar-sm/default.jpg"; let authorHref = exists ? `href="http://www.jeuxvideo.com/profil/${author.toLowerCase()}?mode=infos"` : ""; let authorTitle = exists ? `title="Ouvrir le profil de ${author}"` : ""; let html = `
${author}
${date.slice(-8)}
${message.content.outerHTML}

` main.insertAdjacentHTML("beforeend", html); } if (isDown) { let blocMessages = Array.from(main.getElementsByClassName("jvchat-bloc-message")); for (let i = 0; i + 100 < blocMessages.length; i++) { main.removeChild(blocMessages[i]); } setScrollDown(); } if (hasNewMessages) { decreaseUpdateInterval(); } else { increaseUpdateInterval(); } } function setUser(document, user) { let isConnected = (user !== undefined); let isDown = isScrollDown(); if (isConnected) { let pseudo = document.getElementById("jvchat-user-pseudo"); let avatar = document.getElementById("jvchat-user-avatar"); let mp = document.getElementById("jvchat-user-mp"); let notif = document.getElementById("jvchat-user-notif"); let avatarLink = document.getElementById("jvchat-user-avatar-link"); let notifLink = document.getElementById("jvchat-user-notif-link"); pseudo.innerHTML = user.author; avatar.style["background-image"] = `url("${user.avatar}")`; mp.setAttribute("data-val", user.mp); if (user.mp > 0) { mp.classList.add("has-notif"); } else { mp.classList.remove("has-notif"); } notif.setAttribute("data-val", user.notif); if (user.notif > 0) { notif.classList.add("has-notif"); } else { notif.classList.remove("has-notif"); } avatarLink.setAttribute("href", `http://www.jeuxvideo.com/profil/${user.author.toLowerCase()}?mode=infos`); notifLink.setAttribute("href", `http://www.jeuxvideo.com/profil/${user.author.toLowerCase()}?mode=abonnements`); } if ((userConnected === undefined && isConnected) || (userConnected !== undefined && isConnected !== userConnected)) { document.getElementById("jvchat-profil").classList.toggle("jvchat-hide"); document.getElementById("bloc-formulaire-forum").classList.toggle("jvchat-hide"); if (isDown) { setScrollDown(); } } if (userConnected !== undefined) { if (isConnected && !userConnected) { addAlertbox("success", "Vous êtes désormais connecté") } else if (!isConnected && userConnected) { addAlertbox("warning", "Vous avez été déconnecté") } } userConnected = isConnected; } function setTopicTitle(document, topicTitle) { document.getElementById("jvchat-topic-title").innerHTML = topicTitle; } function setTopicNbConnected(document, nbConnected) { let txt = `${nbConnected} connectés`; if (!(nbConnected > 1)) { if (nbConnected === undefined) { txt = "? connectés" } else { txt = txt.slice(0, -1); } } document.getElementById("jvchat-topic-nb-connected").innerHTML = txt; } function setTopicNbMessages(document, nbMessages) { let txt = `${nbMessages} messages`; if (!(nbMessages > 1)) { if (nbMessages === undefined) { txt = "? messages" } else { txt = txt.slice(0, -1); } } document.getElementById("jvchat-topic-nb-messages").innerHTML = txt; } function triggerJVChat() { let topicUrl = document.URL; let topic = parseTopicInfo(document); let user = parseUserInfo(document); let url = parseURL(topicUrl); // let currentPage = getCurrentPage(topicUrl); clearPage(document); setUser(document, user); setTopicTitle(document, topic.title); setTopicNbMessages(document, undefined); setTopicNbConnected(document, topic.connected); url.page = 1; url.anchor = ""; document.getElementById("jvchat-topic-title").setAttribute("href", buildURL(url)); updateMessages(url, topic.lastPage); } function updateMessages(url, page) { url.page = page; let urlLastPage = buildURL(url); function scheduleNextUpdate(interval) { setTimeout(tryCatch(function() { updateMessages(url, page); }), interval); }; function onSuccess(res) { let lastPage = parsePage(res).lastPage; if (page == lastPage || lastPage === undefined) { scheduleNextUpdate(updateIntervals[updateIntervalIdx] * 1000); } else if (page < lastPage) { updateMessages(url, page + 1); } else if (page > lastPage) { updateMessages(url, lastPage); } } function onError(err) { if (!isError) { isError = true; setFixedAlert("danger", err); } scheduleNextUpdate(60000); } function onTimeout(err) { addAlertbox("warning", err); scheduleNextUpdate(20000); } request("GET", urlLastPage, onSuccess, onError, onTimeout); } function parseAlerts(res) { let alerts = [] let alertsDiv = res.getElementsByClassName("alert"); for (let a of alertsDiv) { let type = "danger"; if (a.classList.contains("alert-warning")) { type = "warning"; } else if (a.classList.contains("alert-success")) { type = "success"; } let message = a.getElementsByClassName("alert-row")[0].textContent.trim(); alerts.push({type: type, message: message}); } return alerts; } function increaseUpdateInterval() { if (updateIntervalIdx < updateIntervalMax) { updateIntervalIdx++; } } function decreaseUpdateInterval() { updateIntervalIdx = transisitions[updateIntervalIdx]; } function parsePage(res) { let error = getTopicError(res); if (error !== undefined) { if (!isError) { updateIntervalIdx = updateIntervalMax; isError = true; setFixedAlert("danger", error); } return {lastPage: undefined, alert: true} } else { if (isError) { isError = false; updateIntervalIdx = 0; removeFixedAlert("Le topic ne retourne plus d'erreur"); } let form = getForm(res); if (form !== undefined) { freshForm = form; } let messages = getMessages(res); addMessages(document, messages); let user = parseUserInfo(res); setUser(document, user); let topic = parseTopicInfo(res); let nbMessages = (topic.lastPage - 1) * 20; if (topic.page == topic.lastPage) { nbMessages += messages.length; } setTopicNbMessages(document, nbMessages); setTopicNbConnected(document, topic.connected); let alerts = parseAlerts(res); for (let alert of alerts) { addAlertbox(alert.type, alert.message); } let locked = getTopicLocked(res); let isLocked_ = (locked !== undefined); if (isLocked_ && !isLocked) { updateInterval = updateIntervalMax; setFixedAlert("warning", locked); } else if (!isLocked_ && isLocked) { updateInterval = 0; removeFixedAlert("Le topic a été dévérouillé"); } isLocked = isLocked_; return {lastPage: topic.lastPage, alert: isLocked_ || (alerts.length > 0)}; } } function addAlertbox(type, message) { // type: success / warning / danger let alert = `
${message}
`; document.getElementById("jvchat-fixed-alert").insertAdjacentHTML("afterend", alert); } function setFixedAlert(type, message) { document.getElementById("jvchat-fixed-alert").getElementsByClassName("alert-row")[0].innerHTML = message; document.getElementById("jvchat-fixed-alert").setAttribute("class", `alert alert-${type}`); } function removeFixedAlert(message) { document.getElementById("jvchat-fixed-alert").classList.add("jvchat-hide"); if (message !== undefined) { addAlertbox("success", message); } } function makeJVChatButton() { let cls = 'btn-jvchat'; let text = 'JVChat'; let css = 'margin: 0px 5px 0px 5px'; let btn = ``; return btn; } function addJVChatButton(document) { let blocPreRight = document.getElementsByClassName("bloc-pre-right") let jvchatButton = makeJVChatButton(); for (let bloc of blocPreRight) { bloc.insertAdjacentHTML('afterbegin', jvchatButton); } } function bindJVChatButton(document) { let buttons = document.getElementsByClassName('btn-jvchat'); for (let btn of buttons) { btn.addEventListener('click', tryCatch(triggerJVChat)); } } function raiseHTTPError(text) { console.error(text); } function request(mode, url, callbackSuccess, callbackError, callbackTimeout, data) { let xhr = new XMLHttpRequest(); xhr.timeout = 20000; xhr.ontimeout = tryCatch(function() { callbackTimeout(`La delai d'attente de la requête a expiré`); }); xhr.onerror = tryCatch(function() { callbackError(`La requête a échoué (${xhr.status}): ${xhr.statusText}`); }); xhr.onload = tryCatch(function() { if (xhr.status !== 200) { callbackError(`La requête a retourné une erreur (${xhr.status}): ${xhr.statusText}`); return; } let dom = document.createElement("html"); dom.innerHTML = xhr.responseText; callbackSuccess(dom); }); if (data === undefined) { data = null; } xhr.open(mode, url, true); xhr.send(data); }; function dontScrollOnExpand(event) { let target = event.target; if (!target) { return; } let classes = target.classList; if (classes.contains("blockquote-jv")) { let isDown = isScrollDown() target.setAttribute('data-visible', '1'); if (isDown) { setScrollDown(); } } else if (classes.contains("open-spoil")) { // TODO: Fix scroll down not detected on spoilers } } function isScrollDown() { let element = document.getElementById("jvchat-main"); return element.scrollHeight - element.scrollTop === element.clientHeight } function setScrollDown() { let element = document.getElementById("jvchat-main"); element.scrollTop = element.scrollHeight - element.clientHeight; } function main() { addJVChatButton(document); bindJVChatButton(document); } main();