jvchat debug

// ==UserScript== // @name JVChat DEBUG // @description JVChat avec debug intégré // @author m7r-227 // @namespace JVChat // @license MIT // @version 0.1.0-debug // @match https://*.jeuxvideo.com/forums/42-* // @match https://*.jeuxvideo.com/forums/1-* // @grant none // ==/UserScript== const _DBG = true; const _P = '%c[JVChat]'; const _S = 'color:#00e5ff;font-weight:bold'; const _W = 'color:#ffab00;font-weight:bold'; const _E = 'color:#ff1744;font-weight:bold'; function dbg(...a) { if (_DBG) console.log(_P, _S, ...a); } function wrn(...a) { if (_DBG) console.warn(_P, _W, ...a); } function err(...a) { if (_DBG) console.error(_P, _E, ...a); } (function addStyle() { const style = document.createElement('style'); style.textContent = ` @font-face { font-family: "jvchat-icons"; src: url(data:font/woff;base64,d09GRgABAAAAAAXkAAsAAAAAB4wAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAAFuAAAACkAAAAquPq49E9TLzIAAAS0AAAARQAAAGBAg1QHY21hcAAABPwAAABYAAAAhJQqfw1nbHlmAAABCAAAAwQAAARovY2OvmhlYWQAAARIAAAANgAAADYpthI3aGhlYQAABJQAAAAfAAAAJAzUCM1obXR4AAAEgAAAABEAAAAiEM0AAGxvY2EAAAQsAAAAGgAAABoHyAZhbWF4cAAABAwAAAAfAAAAIAEiAV5uYW1lAAAFVAAAAFIAAAB8BIgdIHBvc3QAAAWoAAAAEAAAACAADQAAeNp1UkOALEkQjcisrur5KMxkVWtYao6y9bG2bdu2cV9797rutXGa47+s7dMaffrHrfmRYzYCGREvCBxg+nj+EnsJBJRhHAB9Qzd0kfEynmy32q1GMS7G3Fe0oXSpLEL59GHRyCCD6849e8v27VvOPvePeeG60088fIw+hx/54bxwYfL7lbu9hMezl5Z5zgj/wzLXGYH1Jr9dsVsHjwekCr/iVVaFjQBYNCipkWln2kVe3a3bnf9hd4kyEwPaxxxgE0Dk9KAzHBY9VTnr4i/JyDQkD94VHXPGQ886SRd/xp+TP15868MHn7/oegBGsR2ayAlggQvQ5+suF119RLaaTiMO3FTUTEUvhZMoQ9YJpQyTlxJqjFo7XgYJBFIGjGjn66+P/+qrGbQL+APsKdgPjgaIaJ7tPXAcx9DCWLEq+rqFulGMFfOGUHdFxjBxCAdxT/QU246ytSe22plPsbiY8sewsQfKQRQm8knGT9QZP0YzjQv8wMaN6WfSG9EK/AvSpna0xvUTtTUdomHlcAznenJL5YBK5YDjFPm2r9q7n2YZ9xopVs5Zl6U3bEhfZubLLEVPlrZfxvFW263cvH1sDobI7CRBQ46QhdLMZZmoTmsQ1W3tgeq4xrAY9xkZNygaQbNdbNYz7TqDi8487b5iqVS877QzP10Un3juuVMWfhyWm+fF5LklXrM1HM9PYI+BDoMAKdpiIDxsNuiei0R9fQB1qmsADXekObM4icFotbZb1Ip2q1VHj2edl2QYSoSv9746LpoDhSgqDJjF+Kp9vj6+WgVAoA+v4GsgACLaaeyrferCk3uqRlsNntt87dX2hH31tZvtkZrYsUPURt41r7jUcS69wrQng96pqd5gcgZp+k6O7G5wSKFB9aBCaSPNiVBxL9dFkfzjlre7eKDrJh+I3dhlYqtIdiY7xdaSwN1Iwe1iDol9PYfkCaOHiO4TSoPQJJui6H9QEBKhuIS2HtLy7qitSd2nDotxoyWHaIHSEzzn/riiOTzE+2x5c7sAaMv133jaY2BkYGDgYQxi4GEAASYg5gJCBob/DGAAABK+AYIAAAAAAABRAGsAiQCwAS4BcAGhAcYB6wIQAjQAAAABAAAAAQAAHiteY18PPPUACwQAAAAAAOBi554AAAAA4GLnnv/H/vwKCgMCAAAACAACAAAAAAAAeNpjAAIWGD4LotEBABJzAN4AAAB42mNgZGBgZvjPwMDAxfD/+P/jXFxAEVTACgBxLwS0AHjaY2Bh4WacwMDKwMAWwXSGgYGhH0IzvmYwYuRgYGBiYGVmwAoC0lxTGA684nvVx8zwn4EhhjmGkQUozIiiiAkANzILFgAAAHjaY2BgYAJiZiAWAZKMYJqFoQBISzAIAEU4XvG9En6l8cr5Vf6r8lfVr1pedbzqedX3/z8DAy4Z0c+i70Vvix4UnSbaL9oj2iLaKFonWgs0GwcAALUUKS542kzIgQYCMQAA0Le2RkOA9FUhBEAyGenY3P8fDAOeh+whCukiKEyfXJXpuHxafHZzn84Kmo/N3/BUfe1+3rqXqhvaMbCsAoMhgx6DAQBDxwnBAAB42mNgZoAALgasAAABYwAOeNpjYGRgYOBiUGPQYGBycfMJYeDLSSzJY5BgYGEAgv//GeAAAG2XBV0AAAA=); } .refresh-loader { --size: 30px; --thickness: 3px; --value: 0; width: var(--size); aspect-ratio: 1; border-radius: 50%; background: conic-gradient(var(--jv-text-secondary) calc(var(--value) * 1%), var(--jv-block-bg-color) 0); position: relative; display: flex; align-items: center; justify-content: center; font: 600 calc(var(--size) * 0.25)/1.1 system-ui; color: var(--jv-text-secondary); transition: background .4s ease; align-self: end; } .refresh-loader::before { content: ''; position: absolute; inset: calc(var(--thickness)); border-radius: 50%; background: var(--jv-block-bg-color); } .refresh-loader__text { position: relative; pointer-events: none; } .btn-open-jvchat { margin-right: 0.3125rem; } #jvchat-user-notif.has-notif::after, #jvchat-user-mp.has-notif::after { z-index: 2; content: " " attr(data-val) ""; color: #fff; line-height: 1.25rem; font-size: 0.9rem; padding: 0 .25rem; position: absolute; top: .6875rem; right: -.6875rem; background: #ff3c00; width: 1.1rem; height: 1.1rem; border-radius: 1rem; } #jvchat-mp-and-notif .nav-link, .nav-link-search, .account-pseudo { color: white !important; } .jvchat-container { display: flex; height: 100vh; background-color: var(--jv-bg-color); font-family: system-ui, sans-serif; } .jvchat-sidebar { flex: 0 0 15%; max-width: 254px; background-color: var(--jv-block-bg-color); border-right: 1px solid #2b2e30; padding: 1rem; box-sizing: border-box; overflow-y: auto; display: flex; flex-direction: column; position: relative; } .jvchat-profile { width: 100%; display: flex; flex-direction: column; align-items: center; position: relative; } .jvchat-username { margin: 0 0 0.5rem 0; font-size: 1.25rem; font-weight: 700; text-align: center; color: #fff; } .jvchat-profile-avatar { width: 150px; height: 150px; border-radius: 50%; object-fit: cover; } .jvchat-profile-actions { display: flex; gap: 0.75rem; } .jvchat-topic-heading { font-size: 0.875rem; line-height: 1.42857; margin: 0; align-self: flex-start; } .jvchat-chat { flex: 1; display: flex; flex-direction: column; height: 100%; } .jvchat-messages { flex: 1; padding: 1rem; overflow-y: auto; display: flex; flex-direction: column; } .jvchat-message { display: flex; gap: 0.75rem; background: var(--jv-block-bg-color); padding: 0.75rem; border-radius: 0.5rem; border: 0.0625rem solid var(--jv-border-color); } .jvchat-message:nth-of-type(2n) { background: var(--jv-block-even-bg-color); } .jvchat-content p:last-of-type { margin-bottom: 0px; } .jvchat-message-controls { display: flex; align-items: center; gap: 0.75rem; visibility: hidden; } .jvchat-message-controls span { cursor: pointer; } .jvchat-message:hover .jvchat-message-controls { visibility: visible; } .jvchat-avatar { width: 40px; height: 40px; border-radius: 50%; flex-shrink: 0; } .jvchat-message-body { flex: 1; color: var(--jv-text-color) } .jvchat-message-header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.5rem; margin-bottom: 0.25rem; } .jvchat-user { font-weight: 600; } .jvchat-date { font-size: 0.75rem; opacity: 0.7; } .jvchat-form { padding: 1rem; padding-top: 0; } .jvchat-badge { position: absolute; top: 8px; right: -10px; min-width: 16px; padding: 1px 4px; font: 16px/16px Arial, sans-serif; color: #fff; background: #ff3c00; border-radius: 9999px; text-align: center; z-index: 10; pointer-events: none; } .jvchat-settings-panel { position: absolute; inset: 0; background-color: var(--jv-block-bg-color); transform: translateX(-100%); transition: transform 0.35s cubic-bezier(.4,0,.2,1); z-index: 2000; display: flex; flex-direction: column; gap: .75rem; padding: 1rem 1.25rem; box-shadow: 2px 0 12px rgba(0,0,0,.15); overflow-y: auto; } .jvchat-settings-panel.open { transform: translateX(0); } #jvchat-settings-list { display: flex; flex-direction: column; gap: .75rem; } .jvchat-message-deleted > div { opacity: 0.2; filter: grayscale(100%); } .jvchat-message-deleted { position: relative; } .jvchat-message-deleted:after { content: "Message supprimé"; position: absolute; top: 40%; left: 50%; color: gray; font-weight: bold; opacity: 0.7; } .jvchat-message-deleted .jvchat-message-controls { display: none; } `; document.head.appendChild(style); })(); (function createJVChatButton() { const html = '<button class="buttonsNavbar__button btn-open-jvchat" type="button"><i class="buttonsNavbar__icon icon-topics-list"></i><div class="buttonsNavbar__label">JVChat</div></button>' const refreshBtn = document.querySelector('.buttonsNavbar .buttonsNavbar__icon.icon-refresh').parentNode; refreshBtn.insertAdjacentHTML('beforebegin', html); const button = document.querySelector('.btn-open-jvchat'); button.addEventListener('click', () => { const jvchat = new JVChat(); jvchat.start(); }); })(); class JVChat { constructor() { this.jvcApi = null; this.jvcClient = null; this.lastPage = 1; this.messages = []; this.refreshRate = 3000; this.isTabActive = true; this.unreadMessagesCount = 0; this.autoScrollingEnabled = true; } async start() { this.jvcClient = new JVCClient(); const info = this.jvcClient.parseURL(location.href); this.topicId = info.topicId; this.forumId = info.forumId; this.viewId = info.viewId; this.topicTitle = info.title; this.jvcApi = new JVCAPI(this.viewId, this.forumId, this.topicId, this.topicTitle); this.jvchatSettings = new JVChatSettings(); this.createJVChatInterface(); const page = parsePage(document); this.lastPage = page.lastPage; dbg(`start() | lastPage initial = ${this.lastPage}`); // Charger les 2 dernières pages au démarrage (40 msgs) await this.loadInitialMessages(); const loader = document.querySelector('.refresh-loader'); let lastRefresh = performance.now(); setInterval(() => { const now = performance.now(); const elapsed = now - lastRefresh; const percent = (elapsed / this.refreshRate) * 100; loader.style.setProperty('--value', percent); if (elapsed >= this.refreshRate) { lastRefresh = now; this.fetchNewMessages(); } }, 50); document.addEventListener('visibilitychange', () => { this.isTabActive = document.visibilityState === 'visible'; if (this.isTabActive) { this.unreadMessagesCount = 0; this.jvcClient.updateFaviconWithCount(0); this.scrollToBottom(); this.autoScrollingEnabled = true; } }); } async loadInitialMessages() { dbg(`📦 Chargement initial — lastPage=${this.lastPage}`); const t0 = performance.now(); try { // Si le topic a au moins 2 pages, charger l'avant-dernière d'abord if (this.lastPage >= 2) { const prevPageNum = this.lastPage - 1; dbg(`📦 Chargement page ${prevPageNum} (avant-dernière)`); const prevDoc = await this.jvcApi.getPageDocument(prevPageNum); const prevPage = parsePage(prevDoc); for (const message of prevPage.messages) { this.addMessage(message); } dbg(`📦 Page ${prevPageNum}: ${prevPage.messages.length} messages chargés`); } // Charger la dernière page dbg(`📦 Chargement page ${this.lastPage} (dernière)`); const lastDoc = await this.jvcApi.getPageDocument(this.lastPage); const lastPage = parsePage(lastDoc); this.payload = getPayload(lastDoc); this.jvcApi.payload = this.payload; this.lastPage = lastPage.lastPage; this.connectedUser = this.getConnectedUser(lastDoc); document.querySelector('#jvchat-topic-title').textContent = lastPage.title; document.querySelector('#jvchat-connected-count').textContent = lastPage.connectedCount + (lastPage.connectedCount === 1 ? ' connecté' : ' connectés'); document.querySelector('#jvchat-messages-count').textContent = lastPage.messagesCount + (lastPage.messagesCount === 1 ? ' message' : ' messages'); const profileContainer = document.querySelector('.jvchat-profile'); if (this.connectedUser) { profileContainer.style.display = ''; document.querySelector('#jvchat-username').textContent = this.connectedUser.username; document.querySelector('#jvchat-user-avatar').src = this.connectedUser.avatarUrl; const messageBadge = document.querySelector('#jvchat-user-mp-badge'); if (this.connectedUser.messageCount > 0) { messageBadge.textContent = this.connectedUser.messageCount; messageBadge.style.display = ''; } else { messageBadge.style.display = 'none'; } const notificationBadge = document.querySelector('#jvchat-user-notif-badge'); if (this.connectedUser.notificationCount > 0) { notificationBadge.textContent = this.connectedUser.notificationCount; notificationBadge.style.display = ''; } else { notificationBadge.style.display = 'none'; } const profileAnchors = document.querySelectorAll('.jvchat-profile-url'); for (const anchor of profileAnchors) { anchor.href = this.connectedUser.profileUrl; } document.querySelector('.jvchat-subscriptions-url').href = this.connectedUser.subscriptionsUrl; } else { profileContainer.style.display = 'none'; } for (const message of lastPage.messages) { this.addMessage(message); } this.scrollToBottom(); const dt = (performance.now() - t0).toFixed(0); dbg(`📦 Chargement initial OK (${dt}ms) | ${this.messages.length} messages au total | lastPage=${this.lastPage}`); } catch (error) { err(`📦 Chargement initial ❌:`, error.message); // Fallback: lancer le polling normal qui chargera au moins la dernière page } } async fetchNewMessages() { const cycle = (this._cycle = (this._cycle || 0) + 1); const prevLastPage = this.lastPage; const t0 = performance.now(); dbg(`━━━ Cycle #${cycle} ━━━ polling page ${this.lastPage}`); try { const doc = await this.jvcApi.getPageDocument(this.lastPage); const page = parsePage(doc); this.payload = getPayload(doc); this.jvcApi.payload = this.payload; // ═══ Mise à jour de lastPage (provisoire, avant le check page pleine) ═══ this.lastPage = page.lastPage; // ═══ DEBUG: payload check ═══ if (!this.payload) { err(`Cycle #${cycle} payload est NULL !`); } else if (!this.payload.ajaxToken) { wrn(`Cycle #${cycle} payload sans ajaxToken`); } document.querySelector('#jvchat-topic-title').textContent = page.title; document.querySelector('#jvchat-connected-count').textContent = page.connectedCount + (page.connectedCount === 1 ? ' connecté' : ' connectés'); document.querySelector('#jvchat-messages-count').textContent = page.messagesCount + (page.messagesCount === 1 ? ' message' : ' messages'); this.connectedUser = this.getConnectedUser(doc); const profileContainer = document.querySelector('.jvchat-profile'); if (this.connectedUser) { profileContainer.style.display = ''; document.querySelector('#jvchat-username').textContent = this.connectedUser.username; document.querySelector('#jvchat-user-avatar').src = this.connectedUser.avatarUrl; const messageBadge = document.querySelector('#jvchat-user-mp-badge'); if (this.connectedUser.messageCount > 0) { messageBadge.textContent = this.connectedUser.messageCount; messageBadge.style.display = ''; } else { messageBadge.style.display = 'none'; } const notificationBadge = document.querySelector('#jvchat-user-notif-badge'); if (this.connectedUser.notificationCount > 0) { notificationBadge.textContent = this.connectedUser.notificationCount; notificationBadge.style.display = ''; } else { notificationBadge.style.display = 'none'; } const profileAnchors = document.querySelectorAll('.jvchat-profile-url'); for (const anchor of profileAnchors) { anchor.href = this.connectedUser.profileUrl; } document.querySelector('.jvchat-subscriptions-url').href = this.connectedUser.subscriptionsUrl; } else { profileContainer.style.display = 'none'; } let newMsgCount = 0; for (const message of page.messages) { const existed = this.messages.some(m => m.id === message.id); this.addMessage(message); if (!existed) newMsgCount++; } if (newMsgCount > 0) { dbg(`➕ ${newMsgCount} nouveaux messages ajoutés`); } // ═══ FIX PRINCIPAL v3 ═══ // La pagination JVC est injectée côté client (JS), absente du HTML brut. // Quand la page courante est pleine (20 msgs), on probe la page suivante. // Si JVC retourne la page N+1 → elle existe, on avance. // Si JVC redirige vers la page N → elle n'existe pas encore, on reste. if (page.messages.length >= 20) { try { const nextPageNum = this.lastPage + 1; const nextDoc = await this.jvcApi.getPageDocument(nextPageNum); const nextPageData = parsePage(nextDoc); // Si le numéro de page retourné est supérieur, la page existe if (nextPageData.lastPage > this.lastPage) { this.lastPage = nextPageData.lastPage; dbg(`📄 Page suivante ${this.lastPage} existe → avance`); // Traiter aussi les messages de la nouvelle page for (const msg of nextPageData.messages) { this.addMessage(msg); } } } catch (e) { // Fetch de la page suivante échoué, on reste sur la page courante dbg(`📄 Probe page suivante échoué: ${e.message}`); } } if (this.lastPage !== prevLastPage) { dbg(`📄 lastPage: ${prevLastPage} → ${this.lastPage}`); } const lastMessages = this.messages.slice(this.messages.length - 20, this.messages.length); for (const message of lastMessages) { if (message.page < this.lastPage) { continue; } const existing = page.messages.find((m) => m.id === message.id); if (!existing && !message.isDeleted) { const msg = await this.jvcApi.getMessage(message.id); if (!msg) { message.element.classList.add('jvchat-message-deleted'); message.isDeleted = true; dbg(`🗑️ Message #${message.id} marqué supprimé`); } } } if (!this.isTabActive) { this.jvcClient.updateFaviconWithCount(this.unreadMessagesCount); } if (this.autoScrollingEnabled) { this.scrollToBottom(); } const dt = (performance.now() - t0).toFixed(0); dbg(`Cycle #${cycle} OK (${dt}ms) | lastPage=${this.lastPage} | total msgs=${this.messages.length} | autoScroll=${this.autoScrollingEnabled}`); } catch (error) { err(`Cycle #${cycle} ❌ CRASH:`, error.message); err(`Stack:`, error.stack); } } scrollToBottom() { requestAnimationFrame(() => { this.messagesContainer.scrollTo({ top: this.messagesContainer.scrollHeight, behavior: 'smooth' }); }); } addMessage(message) { const existing = this.messages.find((m) => m.id === message.id); if (existing) { return; } if (!this.isTabActive) { this.unreadMessagesCount++; } const html = ` <article class="jvchat-message mt-2" data-id="${message.id}"> <div> <a href="${message.profileUrl}" target="_blank"> <img src="${message.avatarUrl}" alt="Alice avatar" class="jvchat-avatar mb-2"> </a> </div> <div class="jvchat-message-body"> <div class="jvchat-message-header"> <a href="${message.profileUrl}" target="_blank"> <span class="jvchat-user">${message.username}</span> </a> <div class="d-flex"> <div class="jvchat-message-controls me-3"> <span class="picto-msg-quote jvchat-quote-btn" title="Citer"><span>Citer</span></span> <span class="picto-msg-crayon jvchat-edit-btn" title="Editer" style="display: none;"><span>Editer</span></span> <span class="picto-msg-croix jvchat-delete-btn" title="Supprimer" style="display: none;"><span>Supprimer</span></span> </div> <span class="jvchat-date">${message.creationDate.slice(message.creationDate.length - 8, message.creationDate.length)}</span> </div> </div> <div class="jvchat-content text-enrichi-forum txt-msg"> ${message.content} </div> </div> </article> `; this.messagesContainer.insertAdjacentHTML('beforeend', html); const messageElement = document.querySelector(`.jvchat-message[data-id="${message.id}"]`); message.element = messageElement; if (this.jvchatSettings.getSettingValue('display_page_separator')) { let pageSeparator = document.querySelector(`.jvchat-page-separator-${message.page}`); if (!pageSeparator) { const template = document.createElement('template'); template.innerHTML = ` <div class="jvchat-page-separator-${message.page} text-center"> <small class="text-muted"> Page ${message.page} </small> </div> `.trim(); pageSeparator = template.content.firstElementChild; } messageElement.insertAdjacentElement('afterend', pageSeparator); } const togglableQuotes = Array.from(messageElement.querySelectorAll('.text-enrichi-forum > blockquote > blockquote')); for (const togglableQuote of togglableQuotes) { const toggleButton = document.createElement('div'); toggleButton.classList.add('nested-quote-toggle-box'); togglableQuote.insertBefore(toggleButton, togglableQuote.firstChild); toggleButton.addEventListener('click', () => { const blockQuote = toggleButton.closest('.blockquote-jv'); const visible = blockQuote.getAttribute('data-visible'); const value = visible === '1' ? '' : '1'; blockQuote.setAttribute('data-visible', value); }); } if (this.connectedUser && this.connectedUser.username === message.username) { const deleteBtn = messageElement.querySelector('.jvchat-delete-btn'); deleteBtn.style.display = 'block'; deleteBtn.addEventListener('click', () => { this.deleteMessage(message); }); const editBtn = messageElement.querySelector('.jvchat-edit-btn'); editBtn.style.display = 'block'; editBtn.addEventListener('click', () => { this.editMessage(message, messageElement); }); } const quoteBtn = messageElement.querySelector('.jvchat-quote-btn'); quoteBtn.addEventListener('click', async () => { dbg(`💬 Citation demandée pour #${message.id}`); try { // L'endpoint ajax_citation.php a été supprimé par JVC (404). // On extrait le texte directement du contenu HTML en mémoire. const tempDiv = document.createElement('div'); tempDiv.innerHTML = message.content; const txt = tempDiv.textContent.trim(); let content = `\n> Le ${message.creationDate} ${message.username} a écrit :\n> `; content += txt.split('\n').join('\n> '); content = content.replace(/^[\r\n]+|[\r\n]+$/g, ''); content += '\n\n'; if (this.jvcClient.createMessageTextarea.value.trim() !== '') { content = '\n\n' + content; } this.jvcClient.insertAtCursor(content); this.jvcClient.createMessageTextarea.focus(); dbg(`💬 Citation insérée OK`); } catch (e) { err(`💬 Citation ❌:`, e.message, e.stack); this.jvcClient.alert('error', `Erreur citation : ${e.message}`); } }); const images = messageElement.querySelectorAll('.img-shack'); for (const image of images) { const parent = image.parentElement; const link = this.jvcClient.jvCake(parent.getAttribute('class')); const anchor = document.createElement('a'); anchor.href = link; anchor.target = '_blank'; anchor.appendChild(image); parent.insertAdjacentElement('afterend', anchor); parent.remove(); } const mosaics = this.findMosaics(messageElement); for (const mosaic of mosaics) { const container = document.createElement('div'); container.style.lineHeight = 0; mosaic[0].parentElement.insertAdjacentElement('beforebegin', container); for (const image of mosaic) { let insertLineBreak = false; if (image.parentElement.nextElementSibling && image.parentElement.nextElementSibling.tagName === 'BR') { insertLineBreak = true; } container.appendChild(image.parentElement); if (insertLineBreak) { container.appendChild(document.createElement('br')); } } const btn = document.createElement('button'); btn.classList.add('btn', 'btn-annuler-modif-msg', 'mt-2'); if (this.jvchatSettings.getSettingValue('hide_mosaics')) { btn.textContent = 'Cacher'; container.classList.add('d-none'); } else { btn.textContent = 'Afficher'; } btn.addEventListener('click', () => { container.classList.toggle('d-none'); if (container.classList.contains('d-none')) { btn.textContent = 'Afficher'; } else { btn.textContent = 'Cacher'; } }); container.insertAdjacentElement('afterend', btn); } this.messages.push(message); } findMosaics(messageEl) { const tol = 6; const nodes = [...messageEl.querySelectorAll('img.img-shack')].map(img => { const { x, y, width: w, height: h } = img.getBoundingClientRect(); return { img, x, y, w, h }; }); if (nodes.length < 4) return []; const snap = (value, buckets) => { const bucket = buckets.find(v => Math.abs(v - value) < tol); if (bucket !== undefined) return bucket; buckets.push(value); return value; }; const rowVals = []; const colVals = []; nodes.forEach(n => { n.row = snap(n.y, rowVals); n.col = snap(n.x, colVals); }); const adj = new Map(nodes.map(n => [n, new Set])); const byRow = new Map(); const byCol = new Map(); nodes.forEach(n => { (byRow.get(n.row) || byRow.set(n.row, []).get(n.row)).push(n); (byCol.get(n.col) || byCol.set(n.col, []).get(n.col)).push(n); }); byRow.forEach(list => { list.sort((a, b) => a.x - b.x); for (let i = 0; i < list.length - 1; i++) { if (Math.abs(list[i + 1].x - list[i].x - list[i].w) < tol) { adj.get(list[i]).add(list[i + 1]); adj.get(list[i + 1]).add(list[i]); } } }); byCol.forEach(list => { list.sort((a, b) => a.y - b.y); for (let i = 0; i < list.length - 1; i++) { if (Math.abs(list[i + 1].y - list[i].y - list[i].h) < tol) { adj.get(list[i]).add(list[i + 1]); adj.get(list[i + 1]).add(list[i]); } } }); const mosaics = []; const seen = new Set(); nodes.forEach(start => { if (seen.has(start)) return; const stack = [start]; const component = []; while (stack.length) { const n = stack.pop(); if (seen.has(n)) continue; seen.add(n); component.push(n); adj.get(n).forEach(nb => stack.push(nb)); } const row = new Set(component.map(n => n.row)); const col = new Set(component.map(n => n.col)); if (row.size < 2 || col.size < 2) { return; } component.sort((a, b) => (a.row === b.row ? a.col - b.col : a.row - b.row)); mosaics.push(component.map(n => n.img)); }); return mosaics; } getPageUrl(page) { return `https://www.jeuxvideo.com/forums/${this.viewId}-${this.forumId}-${this.topicId}-${page}-0-1-0-${this.topicTitle}.htm`; } createJVChatInterface() { for (const child of document.body.children) { child.style.display = 'none'; } const html = ` <div class="jvchat-container"> <aside class="jvchat-sidebar"> <span class="breadcrumb-icon icon-config align-self-end jvchat-toggle-settings" title="Paramètres"><span>Réglages</span></span> <div class="jvchat-profile" style="display: none;"> <a class="jvchat-profile-url" target="_blank"> <h2 id="jvchat-username" class="jvchat-username"></h2> </a> <a class="jvchat-profile-url" target="_blank"> <img id="jvchat-user-avatar" src="https://image.jeuxvideo.com/avatar/default.jpg" alt="Profile avatar" class="jvchat-profile-avatar"> </a> <div class="jvchat-profile-actions"> <a href="https://www.jeuxvideo.com/messages-prives/boite-reception.php" target="_blank"> <span class="headerAccount__pm" data-val="0"> <i class="icon-pm"></i> <span id="jvchat-user-mp-badge" class="jvchat-badge" style="display: none;">0</span> </span> </a> <a class="jvchat-subscriptions-url" target="_blank"> <span class="headerAccount__notif" data-val="0"> <i class="icon-bell-off"></i> <span id="jvchat-user-notif-badge" class="jvchat-badge" style="display: none;">0</span> </span> </a> <button type="button" class="toggleTheme" onclick="window.jvc.toggleTheme();"></button> </div> </div> <a href="${this.getPageUrl(1)}" class="mt-3"> <h3 id="jvchat-topic-title" class="jvchat-topic-heading"></h3> </a> <span id="jvchat-connected-count"s> </span> <span id="jvchat-messages-count"s> </span> <div style="flex-grow: 1;"></div> <div class="refresh-loader" style="--value:0"> <span class="refresh-loader__text" style="display: none;">0%</span> </div> <div class="jvchat-settings-panel" id="jvchat-settings-panel"> <span class="breadcrumb-icon icon-config align-self-end jvchat-toggle-settings" title="Paramètres"><span>Réglages</span></span> <div class="jvchat-settings-title">Settings</div> <div id="jvchat-settings-list"> </div> <button class="jvchat-toggle-settings btn btn-annuler-modif-msg mt-2"> Fermer </button> </div> </aside> <section class="jvchat-chat"> <div class="jvchat-messages"> </div> <div class="jvchat-form"> </div> </section> </div> `; document.body.insertAdjacentHTML('beforeend', html); this.messagesContainer = document.querySelector('.jvchat-messages'); const JVCForm = document.querySelector('#forums-post-message-editor'); const formContainer = document.querySelector('.jvchat-form'); formContainer.appendChild(JVCForm); const observer = new MutationObserver(() => { const captcha = JVCForm.querySelector('.js-captcha-logo'); if (captcha) { captcha.parentElement.parentElement.style.display = 'none'; observer.disconnect(); } }); observer.observe(JVCForm, { attributes: true, childList: true, subtree: true }); const postButton = JVCForm.querySelector('.postMessage'); const editorButtons = JVCForm.querySelector('.buttonsEditor'); editorButtons.appendChild(postButton); postButton.classList.add('float-end'); const previewToggleButton = document.querySelector('.buttonSwitch'); if (previewToggleButton.classList.contains('buttonSwitch--isActive') && !this.jvchatSettings.getSettingValue('display_preview_by_default')) { previewToggleButton.click(); } if (!previewToggleButton.classList.contains('buttonSwitch--isActive') && this.jvchatSettings.getSettingValue('display_preview_by_default')) { previewToggleButton.click(); } previewToggleButton.addEventListener('click', () => { setTimeout(() => { if (previewToggleButton.classList.contains('buttonSwitch--isActive')) { JVCForm.querySelector('.messageEditor__containerPreview').classList.add('mt-3'); } }, 0); }); const textarea = JVCForm.querySelector('textarea#message_reponse'); textarea.setAttribute('rows', 1); textarea.setAttribute('placeholder', 'Hop hop hop, le message ne va pas s\'écrire tout seul !'); textarea.style.height = ''; textarea.addEventListener('blur', () => { textarea.style.height = ''; }); const textareaObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.attributeName === 'style') { if (textarea.style.height === '160px') { textarea.style.height = ''; } } } }); textareaObserver.observe(textarea, { attributes: true, attributeFilter: ['style'] }); postButton.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); this.createMessage(textarea.value); }); JVCForm.classList.remove('mb-3'); JVCForm.querySelector('.messageEditor__containerEdit').style.marginBottom = '0px'; JVCForm.querySelector('.messageEditor__containerPreview').style.marginBottom = '0px'; const settingPanel = document.querySelector('#jvchat-settings-panel'); const settingsToggleBtns = document.querySelectorAll('.jvchat-toggle-settings'); for (const btn of settingsToggleBtns) { btn.addEventListener('click', () => { settingPanel.classList.toggle('open'); }); } this.messagesContainer.addEventListener('scroll', () => { const isAtTheBottom = this.messagesContainer.scrollHeight - this.messagesContainer.scrollTop <= this.messagesContainer.clientHeight + 1; this.autoScrollingEnabled = isAtTheBottom; }); this.setupSettings(); } getConnectedUser(doc) { if (doc.querySelector('.headerAccount__pm')) { const messageCount = parseInt(doc.querySelector('.headerAccount__pm').getAttribute('data-val')); const notificationCount = parseInt(doc.querySelector('.headerAccount__notif').getAttribute('data-val')); const avatarUrl = doc.querySelector('.headerAccount__avatar').style.backgroundImage.slice(5, -2).replace('/avatar-md/', '/avatar/'); const username = doc.querySelector('.headerAccount__pseudo').textContent.trim(); const profileUrl = `https://www.jeuxvideo.com/profil/${username.toLowerCase()}?mode=infos`; const subscriptionsUrl = `https://www.jeuxvideo.com/profil/${username.toLowerCase()}?mode=abonnements`; return { username, avatarUrl, profileUrl, subscriptionsUrl, messageCount, notificationCount }; } return null; } setupSettings() { const settingsList = document.querySelector('#jvchat-settings-list'); for (const setting of this.jvchatSettings.settings) { const html = ` <div class="jvchat-setting"> <label for="${setting.key}"> ${setting.name} </label> <input type="checkbox" id="${setting.key}" ${setting.value ? 'checked' : ''} /> </div> `; settingsList.insertAdjacentHTML('beforeend', html); const input = settingsList.querySelector(`#${setting.key}`); input.addEventListener('change', () => { this.jvchatSettings.setSetting(setting.key, input.checked); }); } } async createMessage(message) { try { const formData = new FormData(); formData.set('text', message); formData.set('topicId', this.topicId); formData.set('forumId', this.forumId); formData.set('group', 1); formData.set('messageId', 'undefined'); formData.set('ajax_hash', this.payload.ajaxToken); for (const key in this.payload.formSession) { formData.append(key, this.payload.formSession[key]); } const response = await fetch('https://www.jeuxvideo.com/forums/message/add', { credentials: 'include', method: 'POST', mode: 'cors', body: formData }); if (!response.ok) { throw new Error(await response.text()); } const result = await response.json(); if (result.errors) { const messages = []; for (const key of Object.keys(result.errors)) { messages.push(result.errors[key]); } throw new Error(messages.join(' | ')); } this.jvcClient.setTextAreaValue(''); this.fetchNewMessages(); } catch (err) { this.jvcClient.alert('error', err.message); console.error(err); } } async deleteMessage(message) { const deleteHash = document.querySelector('#ajax_hash_moderation_forum').value; const url = `https://www.jeuxvideo.com/forums/message/delete?type=delete&ajax_hash=${deleteHash}&ids=${message.id}`; const response = await fetch(url, { credentials: 'include', method: 'POST', mode: 'cors' }); const result = await response.json(); if (result.erreur.length > 0) { throw new Error(result.erreur.join(' | ')); } this.fetchNewMessages(); } async editMessage(message, messageElement) { try { const content = await this.jvcApi.getMessageContentToUpdate(message.id); const messageContent = messageElement.querySelector('.jvchat-content'); const oldMessageContent = messageContent.innerHTML; messageContent.innerHTML = ` <div class="messageEditor__containerEdit"> <textarea class="messageEditor__edit mb-2"></textarea> </div> <div class="d-flex justify-content-start gap-2"> <button class="simpleButton cancelMessage"> <div class="cancelMessage__label">Annuler</div> </button> <button class="simpleButton postMessage" tabindex="1"> <div class="postMessage__label">Modifier</div> <i class="postMessage__icon icon-post-message"></i> </button> </div> `; const textarea = messageContent.querySelector('.messageEditor__edit'); textarea.value = content; const cancelBtn = messageContent.querySelector('.cancelMessage'); cancelBtn.addEventListener('click', async () => { messageContent.innerHTML = oldMessageContent; }); const submitBtn = messageContent.querySelector('.postMessage'); submitBtn.addEventListener('click', async () => { const html = await this.jvcApi.updateMessage(message.id, textarea.value); messageContent.innerHTML = html; }); } catch (error) { err(`editMessage ❌: ${error.message}`); } } } function parsePage(doc) { const title = doc.querySelector('#title-display-container').textContent.trim(); const connectedContainer = doc.querySelector('#forums-info-app .sideCardForum__headerExtra .sideCardForum__Link'); let connectedCount = 0; if (connectedContainer) { connectedCount = parseInt(connectedContainer.textContent.trim()); } let lastPage = 1; // ═══ BUG FIX: utiliser doc au lieu de document ═══ const pageAnchors = doc.querySelectorAll('.pagination .pagination__navigation a'); // ═══ DEBUG ═══ const liveAnchors = document.querySelectorAll('.pagination .pagination__navigation a'); dbg(`parsePage | doc anchors: ${pageAnchors.length} | document (live/caché) anchors: ${liveAnchors.length}`); if (liveAnchors.length === 0 && doc !== document) { dbg(`✅ Confirmation: le DOM live est caché (0 anchors). On utilise bien doc.`); } for (const anchor of pageAnchors) { const page = parseInt(anchor.textContent.trim()); if (!isNaN(page) && page > lastPage) { lastPage = page; } } const currentPage = doc.querySelector('.pagination__item--current'); if (currentPage) { const page = parseInt(currentPage.textContent); if (!isNaN(page) && page > lastPage) { lastPage = page; } } const items = doc.querySelectorAll('.bloc-liste-num-page span a'); for (const item of items) { const page = parseInt(item.textContent.trim()); if (!isNaN(page) && page > lastPage) { lastPage = page; } } const messageList = doc.querySelectorAll('#listMessages .messageUser'); const messages = []; for (const message of messageList) { let avatarUrl = 'https://image.jeuxvideo.com/avatar/default.jpg'; const avatar = message.querySelector('img.avatar__image'); if (avatar) { avatarUrl = avatar.dataset.src; } const username = message.querySelector('.messageUser__label').textContent.trim(); const profileUrl = `https://www.jeuxvideo.com/profil/${username.toLowerCase()}?mode=infos`; const id = message.id.split('-').pop(); messages.push({ id, page: lastPage, username, avatarUrl, profileUrl, content: message.querySelector('.messageUser__msg').innerHTML, creationDate: message.querySelector('.messageUser__date').textContent.trim() }); } const messagesCount = (lastPage * 20) - 20 + messages.length; dbg(`parsePage résultat | lastPage=${lastPage} | msgs sur page=${messages.length} | total estimé=${messagesCount}`); return { title, connectedCount, messagesCount, lastPage, messages }; } function getPayload(doc) { const scripts = doc.getElementsByTagName('script'); let rawPayloadString = null; for (let i = 0; i < scripts.length; i++) { const scriptContent = scripts[i].textContent || scripts[i].innerText; if (scriptContent) { const match = scriptContent.match(/window\.jvc\.forumsAppPayload\s*=\s*['"]([^'"]+)['"]/); if (match && match[1]) { rawPayloadString = match[1]; break; } const jvcVarMatch = scriptContent.match(/jvc\.forumsAppPayload\s*=\s*['"]([^'"]+)['"]/); if (!rawPayloadString && jvcVarMatch && jvcVarMatch[1]) { rawPayloadString = jvcVarMatch[1]; break; } } } if (rawPayloadString) { try { const decodedPayload = JSON.parse(atob(rawPayloadString)); return decodedPayload; } catch (e) { err('getPayload décodage ❌:', e); return null; } } else { wrn('getPayload: forumsAppPayload introuvable dans le doc'); return null; } } class JVCAPI { constructor(viewId, forumId, topicId, topicTitle) { this.viewId = viewId; this.forumId = forumId; this.topicId = topicId; this.topicTitle = topicTitle; this.payload = null; } async getMessageContentToUpdate(messageId) { try { const url = `https://www.jeuxvideo.com/forums/ajax_edit_message.php?id_message=${messageId}&ajax_hash=${this.payload.ajaxToken}&action=get`; const response = await fetch(url, { method: 'GET', credentials: 'include' }); if (!response.ok) { throw new Error('Echec de la requête pour récupérer le message à editer'); } const data = await response.json(); if (data.errors.length === 0) { return data.jvcode; } else { throw new Error(data.errors[0]); } } catch (error) { throw new Error(error); } } async updateMessage(messageId, content) { const formData = new FormData(); formData.set('text', content); formData.set('topicId', this.topicId); formData.set('forumId', this.forumId); formData.set('group', 1); formData.set('messageId', messageId); formData.set('ajax_hash', this.payload.ajaxToken); for (const key in this.payload.formSession) { formData.append(key, this.payload.formSession[key]); } try { const response = await fetch('https://www.jeuxvideo.com/forums/message/edit', { method: 'POST', credentials: 'include', body: formData }); const data = await response.json(); if (!data.errors) { return data.html; } else { throw new Error(data.errors[0]); } } catch (error) { throw new Error(error); } } async getMessage(messageId) { try { const url = `https://www.jeuxvideo.com/forums/message/${messageId}`; const response = await fetch(url); if (!response.ok) { return null; } return await response.text(); } catch (err) { return null; } } async getPageDocument(page) { try { const url = `https://www.jeuxvideo.com/forums/${this.viewId}-${this.forumId}-${this.topicId}-${page}-0-1-0-${this.topicTitle}.htm`; dbg(`⬇ Fetch page ${page}: ${url}`); const t0 = performance.now(); const response = await fetch(url); if (!response.ok) { throw new Error(`Erreur ${response.status}`); } const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); dbg(`⬆ Page ${page} OK (${(performance.now() - t0).toFixed(0)}ms)`); return doc; } catch (error) { err(`⬆ Fetch page ${page} ❌:`, error.message); throw new Error(error); } } async getMessageQuote(messageId) { try { const url = `https://www.jeuxvideo.com/forums/ajax_citation.php?id_message=${messageId}&ajax_hash=${this.payload.ajaxToken}`; const response = await fetch(url); if (!response.ok) { throw new Error(`Erreur ${response.status}`); } const result = await response.json(); return result.txt; } catch (error) { throw new Error(error); } } } class JVCClient { constructor() { const link = document.querySelector('link[rel*=\'icon\']'); this.faviconUrl = link ? link.href : '/favicon.ico'; this.createMessageTextarea = document.querySelector('textarea#message_reponse'); } parseURL(url) { const matches = url.match(/^https:\/\/www\.jeuxvideo\.com\/(?:recherche\/)?forums\/(\d+)-(\d+)-(\d+)-(\d+)-(\d+)-(\d+)-(\d+)-(.*?)\.htm/); if (matches === null) { throw new Error('Url mismatch'); } const viewId = parseInt(matches[1]); const forumId = parseInt(matches[2]); const topicId = parseInt(matches[3]); const topicPage = parseInt(matches[4]); const forumOffset = parseInt(matches[6]); const title = matches[8]; return { viewId, forumId, topicId, topicPage, forumOffset, title }; } updateFaviconWithCount(count) { const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = function () { const canvas = document.createElement('canvas'); const size = 16; canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, size, size); if (count > 0) { ctx.fillStyle = 'DodgerBlue'; ctx.fillRect(0, 0, ctx.measureText(count).width + 3, 11); ctx.fillStyle = 'white'; ctx.font = 'bold 10px Verdana'; ctx.textBaseline = 'bottom'; ctx.fillText(count, 1, 11); } const newFavicon = canvas.toDataURL('image/png'); let link = document.querySelector('link[rel*=\'icon\']'); if (!link) { link = document.createElement('link'); link.rel = 'icon'; document.head.appendChild(link); } link.href = newFavicon; }; img.src = this.faviconUrl; } alert(type, message) { let color = 'text-white'; let bgColor = 'bg-danger'; switch (type) { case 'error': bgColor = 'bg-danger'; break; case 'warning': color = 'text-black'; bgColor = 'bg-warning'; break; case 'success': bgColor = 'bg-success'; break; } const id = `jvchat-alert-${Date.now()}`; const html = ` <div id="${id}" class="position-fixed top-0 w-100 pe-none" style="z-index: 2147483647;"> <div class="toast-container w-100 d-flex align-items-center flex-column p-3 pe-auto"> <div class="toast align-items-center ${color} ${bgColor} border-0 fade show"> <div class="d-flex"> <div class="toast-body"> <div>${message}</div> </div> <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button> </div> </div> </div> </div> `; document.body.insertAdjacentHTML('beforeend', html.trim()); const alert = document.querySelector(`#${id}`); setTimeout(() => { alert.remove(); }, 5000); } jvCake(str) { const base16 = '0A12B34C56D78E9F'; const s = str.split(' ')[1]; let lien = ''; 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; } setTextAreaValue(value) { const prototype = Object.getPrototypeOf(this.createMessageTextarea); const nativeSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set; nativeSetter.call(this.createMessageTextarea, value); this.createMessageTextarea.dispatchEvent(new Event('input', { bubbles: true })); } insertAtCursor(textToInsert) { const value = this.createMessageTextarea.value; const start = this.createMessageTextarea.selectionStart; const end = this.createMessageTextarea.selectionEnd; const newValue = value.slice(0, start) + textToInsert + value.slice(end); this.setTextAreaValue(newValue); this.createMessageTextarea.selectionStart = this.createMessageTextarea.selectionEnd = start + textToInsert.length; } } class JVChatSettings { constructor() { this.settings = [ { key: 'hide_mosaics', name: 'Masquer les mosaïques', description: 'Cache automatiquement les mosaïques d\'images NoelShack pour réduire le flooding.', value: true }, { key: 'display_page_separator', name: 'Afficher le numéro de page courante', description: '', value: true }, { key: 'display_preview_by_default', name: 'Afficher la prévisualisation par défaut', description: '', value: false } ]; this.loadSettings(); } getSetting(key) { return this.settings.find((setting) => setting.key === key); } getSettingValue(key) { return this.getSetting(key).value; } setSetting(key, value) { const setting = this.getSetting(key); setting.value = value; this.saveSettings(); } saveSettings() { for (const setting of this.settings) { localStorage.setItem(setting.key, setting.value); } } loadSettings() { for (const setting of this.settings) { const item = localStorage.getItem(setting.key); if (item) { setting.value = JSON.parse(item); } } } }

Public Last updated: 2026-04-16 12:06:14 AM