// ==UserScript==
// @name JVChat Premium
// @description Outil de discussion instantanée pour les forums de Jeuxvideo.com
// @author Blaff
// @namespace JVChatPremium
// @version 0.1.3
// @match http://*.jeuxvideo.com/forums/42-*
// @match https://*.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("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.lastChild.scrollIntoView();
}
function main() {
addJVChatButton(document);
bindJVChatButton(document);
}
main();