From 3fa3423595b6dd50b2fbcab9bbf8acb8c7426243 Mon Sep 17 00:00:00 2001 From: boomzero Date: Tue, 10 Feb 2026 21:03:00 +0800 Subject: [PATCH 01/14] Add WebSocket notification system for real-time BBS and mail mentions Replaces focus-based polling with persistent WebSocket connection to the backend notification service. Notifications now arrive within 1-2 seconds with automatic reconnection and exponential backoff. Maintains polling as fallback for reliability when WebSocket is unavailable. Co-Authored-By: Claude Sonnet 4.5 --- XMOJ.user.js | 448 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 321 insertions(+), 127 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index 035aa0e0..cd8accee 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -533,6 +533,306 @@ let RequestAPI = (Action, Data, CallBack) => { } }; +// WebSocket Notification System +let NotificationSocket = null; +let NotificationSocketReconnectAttempts = 0; +let NotificationSocketReconnectDelay = 1000; +let NotificationSocketPingInterval = null; + +function GetPHPSESSID() { + let Session = ""; + let Temp = document.cookie.split(";"); + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].includes("PHPSESSID")) { + Session = Temp[i].split("=")[1]; + break; + } + } + return Session; +} + +function ConnectNotificationSocket() { + try { + let Session = GetPHPSESSID(); + if (Session === "") { + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: PHPSESSID not available, skipping connection"); + } + return; + } + + let wsUrl = (UtilityEnabled("SuperDebug") ? "ws://127.0.0.1:8787" : "wss://api.xmoj-bbs.me") + "/ws/notifications?SessionID=" + Session; + + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: Connecting to", wsUrl); + } + + NotificationSocket = new WebSocket(wsUrl); + + NotificationSocket.onopen = () => { + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: Connected successfully"); + } + NotificationSocketReconnectAttempts = 0; + NotificationSocketReconnectDelay = 1000; + + // Start ping keepalive + if (NotificationSocketPingInterval) { + clearInterval(NotificationSocketPingInterval); + } + NotificationSocketPingInterval = setInterval(() => { + if (NotificationSocket && NotificationSocket.readyState === WebSocket.OPEN) { + NotificationSocket.send(JSON.stringify({ type: 'ping' })); + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: Sent ping"); + } + } else { + clearInterval(NotificationSocketPingInterval); + } + }, 30000); + }; + + NotificationSocket.onmessage = (event) => { + HandleNotificationMessage(event); + }; + + NotificationSocket.onerror = (error) => { + if (UtilityEnabled("DebugMode")) { + console.error("WebSocket: Error", error); + } + }; + + NotificationSocket.onclose = (event) => { + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: Connection closed", event.code, event.reason); + } + if (NotificationSocketPingInterval) { + clearInterval(NotificationSocketPingInterval); + } + ReconnectNotificationSocket(); + }; + } catch (e) { + console.error("WebSocket: Failed to connect", e); + ReconnectNotificationSocket(); + } +} + +function ReconnectNotificationSocket() { + const delay = Math.min(NotificationSocketReconnectDelay * Math.pow(2, NotificationSocketReconnectAttempts), 30000); + NotificationSocketReconnectAttempts++; + + if (UtilityEnabled("DebugMode")) { + console.log(`WebSocket: Reconnecting in ${delay}ms (attempt ${NotificationSocketReconnectAttempts})`); + } + + setTimeout(() => { + ConnectNotificationSocket(); + }, delay); +} + +function HandleNotificationMessage(event) { + try { + const notification = JSON.parse(event.data); + + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: Received message", notification); + } + + if (notification.type === 'connected') { + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: Server confirmed connection at timestamp", notification.timestamp); + } + } else if (notification.type === 'bbs_mention') { + // Fetch full mention details from API to get PostTitle and PageNumber + RequestAPI("GetBBSMentionList", {}, (Response) => { + if (Response.Success) { + let MentionList = Response.Data.MentionList; + // Find the matching mention by PostID and ReplyID + for (let i = 0; i < MentionList.length; i++) { + if (MentionList[i].PostID == notification.data.PostID && + MentionList[i].ReplyID == notification.data.ReplyID) { + CreateAndShowBBSMentionToast(MentionList[i]); + break; + } + } + } + }); + } else if (notification.type === 'mail_mention') { + // Fetch full mail mention details from API + RequestAPI("GetMailMentionList", {}, (Response) => { + if (Response.Success) { + let MentionList = Response.Data.MentionList; + // Find the matching mention by FromUserID + for (let i = 0; i < MentionList.length; i++) { + if (MentionList[i].FromUserID === notification.data.FromUserID) { + CreateAndShowMailMentionToast(MentionList[i]); + break; + } + } + } + }); + } else if (notification.type === 'pong') { + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: Received pong"); + } + } + } catch (e) { + console.error("WebSocket: Failed to handle message", e); + } +} + +function CreateAndShowBBSMentionToast(mention) { + let ToastContainer = document.querySelector(".toast-container"); + if (!ToastContainer) return; + + let Toast = document.createElement("div"); + Toast.classList.add("toast"); + Toast.setAttribute("role", "alert"); + let ToastHeader = document.createElement("div"); + ToastHeader.classList.add("toast-header"); + let ToastTitle = document.createElement("strong"); + ToastTitle.classList.add("me-auto"); + ToastTitle.innerHTML = "提醒:有人@你"; + ToastHeader.appendChild(ToastTitle); + let ToastTime = document.createElement("small"); + ToastTime.classList.add("text-body-secondary"); + ToastTime.innerHTML = GetRelativeTime(mention.MentionTime); + ToastHeader.appendChild(ToastTime); + let ToastCloseButton = document.createElement("button"); + ToastCloseButton.type = "button"; + ToastCloseButton.classList.add("btn-close"); + ToastCloseButton.setAttribute("data-bs-dismiss", "toast"); + ToastHeader.appendChild(ToastCloseButton); + Toast.appendChild(ToastHeader); + let ToastBody = document.createElement("div"); + ToastBody.classList.add("toast-body"); + ToastBody.innerHTML = "讨论" + mention.PostTitle + "有新回复"; + let ToastFooter = document.createElement("div"); + ToastFooter.classList.add("mt-2", "pt-2", "border-top"); + let ToastDismissButton = document.createElement("button"); + ToastDismissButton.type = "button"; + ToastDismissButton.classList.add("btn", "btn-secondary", "btn-sm", "me-2"); + ToastDismissButton.innerText = "忽略"; + ToastDismissButton.addEventListener("click", () => { + RequestAPI("ReadBBSMention", { + "MentionID": Number(mention.MentionID) + }, () => { + }); + Toast.remove(); + }); + ToastFooter.appendChild(ToastDismissButton); + let ToastViewButton = document.createElement("button"); + ToastViewButton.type = "button"; + ToastViewButton.classList.add("btn", "btn-primary", "btn-sm"); + ToastViewButton.innerText = "查看"; + ToastViewButton.addEventListener("click", () => { + open("https://www.xmoj.tech/discuss3/thread.php?tid=" + mention.PostID + '&page=' + mention.PageNumber, "_blank"); + RequestAPI("ReadBBSMention", { + "MentionID": Number(mention.MentionID) + }, () => { + }); + }); + ToastFooter.appendChild(ToastViewButton); + ToastBody.appendChild(ToastFooter); + Toast.appendChild(ToastBody); + ToastContainer.appendChild(Toast); + new bootstrap.Toast(Toast).show(); +} + +function CreateAndShowMailMentionToast(mention) { + let ToastContainer = document.querySelector(".toast-container"); + if (!ToastContainer) return; + + let Toast = document.createElement("div"); + Toast.classList.add("toast"); + Toast.setAttribute("role", "alert"); + let ToastHeader = document.createElement("div"); + ToastHeader.classList.add("toast-header"); + let ToastTitle = document.createElement("strong"); + ToastTitle.classList.add("me-auto"); + ToastTitle.innerHTML = "提醒:有新消息"; + ToastHeader.appendChild(ToastTitle); + let ToastTime = document.createElement("small"); + ToastTime.classList.add("text-body-secondary"); + ToastTime.innerHTML = GetRelativeTime(mention.MentionTime); + ToastHeader.appendChild(ToastTime); + let ToastCloseButton = document.createElement("button"); + ToastCloseButton.type = "button"; + ToastCloseButton.classList.add("btn-close"); + ToastCloseButton.setAttribute("data-bs-dismiss", "toast"); + ToastHeader.appendChild(ToastCloseButton); + Toast.appendChild(ToastHeader); + let ToastBody = document.createElement("div"); + ToastBody.classList.add("toast-body"); + let ToastUser = document.createElement("span"); + GetUsernameHTML(ToastUser, mention.FromUserID); + ToastBody.appendChild(ToastUser); + ToastBody.innerHTML += " 给你发了一封短消息"; + let ToastFooter = document.createElement("div"); + ToastFooter.classList.add("mt-2", "pt-2", "border-top"); + let ToastDismissButton = document.createElement("button"); + ToastDismissButton.type = "button"; + ToastDismissButton.classList.add("btn", "btn-secondary", "btn-sm", "me-2"); + ToastDismissButton.setAttribute("data-bs-dismiss", "toast"); + ToastDismissButton.innerText = "忽略"; + ToastDismissButton.addEventListener("click", () => { + RequestAPI("ReadMailMention", { + "MentionID": Number(mention.MentionID) + }, () => { + }); + }); + ToastFooter.appendChild(ToastDismissButton); + let ToastViewButton = document.createElement("button"); + ToastViewButton.type = "button"; + ToastViewButton.classList.add("btn", "btn-primary", "btn-sm"); + ToastViewButton.innerText = "查看"; + ToastViewButton.addEventListener("click", () => { + open("https://www.xmoj.tech/mail.php?to_user=" + mention.FromUserID, "_blank"); + RequestAPI("ReadMailMention", { + "MentionID": Number(mention.MentionID) + }, () => { + }); + }); + ToastFooter.appendChild(ToastViewButton); + ToastBody.appendChild(ToastFooter); + Toast.appendChild(ToastBody); + ToastContainer.appendChild(Toast); + new bootstrap.Toast(Toast).show(); +} + +function PollNotifications() { + if (UtilityEnabled("BBSPopup")) { + RequestAPI("GetBBSMentionList", {}, (Response) => { + if (Response.Success) { + let ToastContainer = document.querySelector(".toast-container"); + if (ToastContainer) { + ToastContainer.innerHTML = ""; + } + let MentionList = Response.Data.MentionList; + for (let i = 0; i < MentionList.length; i++) { + CreateAndShowBBSMentionToast(MentionList[i]); + } + } + }); + } + if (UtilityEnabled("MessagePopup")) { + RequestAPI("GetMailMentionList", {}, (Response) => { + if (Response.Success) { + if (!UtilityEnabled("BBSPopup")) { + let ToastContainer = document.querySelector(".toast-container"); + if (ToastContainer) { + ToastContainer.innerHTML = ""; + } + } + let MentionList = Response.Data.MentionList; + for (let i = 0; i < MentionList.length; i++) { + CreateAndShowMailMentionToast(MentionList[i]); + } + } + }); + } +} + GM_registerMenuCommand("清除缓存", () => { let Temp = []; for (let i = 0; i < localStorage.length; i++) { @@ -1232,135 +1532,29 @@ async function main() { let ToastContainer = document.createElement("div"); ToastContainer.classList.add("toast-container", "position-fixed", "bottom-0", "end-0", "p-3"); document.body.appendChild(ToastContainer); + // Initialize WebSocket notification system + if (CurrentUsername && (UtilityEnabled("BBSPopup") || UtilityEnabled("MessagePopup"))) { + ConnectNotificationSocket(); + } + + // Fallback polling when WebSocket is not connected addEventListener("focus", () => { - if (UtilityEnabled("BBSPopup")) { - RequestAPI("GetBBSMentionList", {}, (Response) => { - if (Response.Success) { - ToastContainer.innerHTML = ""; - let MentionList = Response.Data.MentionList; - for (let i = 0; i < MentionList.length; i++) { - let Toast = document.createElement("div"); - Toast.classList.add("toast"); - Toast.setAttribute("role", "alert"); - let ToastHeader = document.createElement("div"); - ToastHeader.classList.add("toast-header"); - let ToastTitle = document.createElement("strong"); - ToastTitle.classList.add("me-auto"); - ToastTitle.innerHTML = "提醒:有人@你"; - ToastHeader.appendChild(ToastTitle); - let ToastTime = document.createElement("small"); - ToastTime.classList.add("text-body-secondary"); - ToastTime.innerHTML = GetRelativeTime(MentionList[i].MentionTime); - ToastHeader.appendChild(ToastTime); - let ToastCloseButton = document.createElement("button"); - ToastCloseButton.type = "button"; - ToastCloseButton.classList.add("btn-close"); - ToastCloseButton.setAttribute("data-bs-dismiss", "toast"); - ToastHeader.appendChild(ToastCloseButton); - Toast.appendChild(ToastHeader); - let ToastBody = document.createElement("div"); - ToastBody.classList.add("toast-body"); - ToastBody.innerHTML = "讨论" + MentionList[i].PostTitle + "有新回复"; - let ToastFooter = document.createElement("div"); - ToastFooter.classList.add("mt-2", "pt-2", "border-top"); - let ToastDismissButton = document.createElement("button"); - ToastDismissButton.type = "button"; - ToastDismissButton.classList.add("btn", "btn-secondary", "btn-sm", "me-2"); - ToastDismissButton.innerText = "忽略"; - ToastDismissButton.addEventListener("click", () => { - RequestAPI("ReadBBSMention", { - "MentionID": Number(MentionList[i].MentionID) - }, () => { - }); - Toast.remove(); - }); - ToastFooter.appendChild(ToastDismissButton); - let ToastViewButton = document.createElement("button"); - ToastViewButton.type = "button"; - ToastViewButton.classList.add("btn", "btn-primary", "btn-sm"); - ToastViewButton.innerText = "查看"; - ToastViewButton.addEventListener("click", () => { - open("https://www.xmoj.tech/discuss3/thread.php?tid=" + MentionList[i].PostID + '&page=' + MentionList[i].PageNumber, "_blank"); - RequestAPI("ReadBBSMention", { - "MentionID": Number(MentionList[i].MentionID) - }, () => { - }); - }); - ToastFooter.appendChild(ToastViewButton); - ToastBody.appendChild(ToastFooter); - Toast.appendChild(ToastBody); - ToastContainer.appendChild(Toast); - new bootstrap.Toast(Toast).show(); - } - } - }); + if (!NotificationSocket || NotificationSocket.readyState !== WebSocket.OPEN) { + PollNotifications(); } - if (UtilityEnabled("MessagePopup")) { - RequestAPI("GetMailMentionList", {}, async (Response) => { - if (Response.Success) { - if (!UtilityEnabled("BBSPopup")) { - ToastContainer.innerHTML = ""; - } - let MentionList = Response.Data.MentionList; - for (let i = 0; i < MentionList.length; i++) { - let Toast = document.createElement("div"); - Toast.classList.add("toast"); - Toast.setAttribute("role", "alert"); - let ToastHeader = document.createElement("div"); - ToastHeader.classList.add("toast-header"); - let ToastTitle = document.createElement("strong"); - ToastTitle.classList.add("me-auto"); - ToastTitle.innerHTML = "提醒:有新消息"; - ToastHeader.appendChild(ToastTitle); - let ToastTime = document.createElement("small"); - ToastTime.classList.add("text-body-secondary"); - ToastTime.innerHTML = GetRelativeTime(MentionList[i].MentionTime); - ToastHeader.appendChild(ToastTime); - let ToastCloseButton = document.createElement("button"); - ToastCloseButton.type = "button"; - ToastCloseButton.classList.add("btn-close"); - ToastCloseButton.setAttribute("data-bs-dismiss", "toast"); - ToastHeader.appendChild(ToastCloseButton); - Toast.appendChild(ToastHeader); - let ToastBody = document.createElement("div"); - ToastBody.classList.add("toast-body"); - let ToastUser = document.createElement("span"); - GetUsernameHTML(ToastUser, MentionList[i].FromUserID); - ToastBody.appendChild(ToastUser); - ToastBody.innerHTML += " 给你发了一封短消息"; - let ToastFooter = document.createElement("div"); - ToastFooter.classList.add("mt-2", "pt-2", "border-top"); - let ToastDismissButton = document.createElement("button"); - ToastDismissButton.type = "button"; - ToastDismissButton.classList.add("btn", "btn-secondary", "btn-sm", "me-2"); - ToastDismissButton.setAttribute("data-bs-dismiss", "toast"); - ToastDismissButton.innerText = "忽略"; - ToastDismissButton.addEventListener("click", () => { - RequestAPI("ReadMailMention", { - "MentionID": Number(MentionList[i].MentionID) - }, () => { - }); - }); - ToastFooter.appendChild(ToastDismissButton); - let ToastViewButton = document.createElement("button"); - ToastViewButton.type = "button"; - ToastViewButton.classList.add("btn", "btn-primary", "btn-sm"); - ToastViewButton.innerText = "查看"; - ToastViewButton.addEventListener("click", () => { - open("https://www.xmoj.tech/mail.php?to_user=" + MentionList[i].FromUserID, "_blank"); - RequestAPI("ReadMailMention", { - "MentionID": Number(MentionList[i].MentionID) - }, () => { - }); - }); - ToastFooter.appendChild(ToastViewButton); - ToastBody.appendChild(ToastFooter); - Toast.appendChild(ToastBody); - ToastContainer.appendChild(Toast); - new bootstrap.Toast(Toast).show(); - } - } - }); + }); + + // Periodic fallback polling every 60 seconds when WebSocket is down + setInterval(() => { + if (!NotificationSocket || NotificationSocket.readyState !== WebSocket.OPEN) { + PollNotifications(); + } + }, 60000); + + // Handle tab visibility changes - reconnect if connection dropped + document.addEventListener('visibilitychange', () => { + if (!document.hidden && NotificationSocket && NotificationSocket.readyState !== WebSocket.OPEN) { + ConnectNotificationSocket(); } }); dispatchEvent(new Event("focus")); From beb75c01b9ebf9d84ec524eb7f4767d2f8e037ca Mon Sep 17 00:00:00 2001 From: boomzero Date: Wed, 11 Feb 2026 19:06:50 +0800 Subject: [PATCH 02/14] Use enriched WebSocket data for instant notifications The backend now includes all required fields (PostTitle, PageNumber, MentionID) in WebSocket notifications, eliminating the need for additional API calls to fetch mention details. This reduces latency and server load for real-time notifications. Co-Authored-By: Claude Sonnet 4.5 --- XMOJ.user.js | 31 ++++--------------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index cd8accee..c792f75b 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -643,34 +643,11 @@ function HandleNotificationMessage(event) { console.log("WebSocket: Server confirmed connection at timestamp", notification.timestamp); } } else if (notification.type === 'bbs_mention') { - // Fetch full mention details from API to get PostTitle and PageNumber - RequestAPI("GetBBSMentionList", {}, (Response) => { - if (Response.Success) { - let MentionList = Response.Data.MentionList; - // Find the matching mention by PostID and ReplyID - for (let i = 0; i < MentionList.length; i++) { - if (MentionList[i].PostID == notification.data.PostID && - MentionList[i].ReplyID == notification.data.ReplyID) { - CreateAndShowBBSMentionToast(MentionList[i]); - break; - } - } - } - }); + // Backend now provides all data needed for immediate display + CreateAndShowBBSMentionToast(notification.data); } else if (notification.type === 'mail_mention') { - // Fetch full mail mention details from API - RequestAPI("GetMailMentionList", {}, (Response) => { - if (Response.Success) { - let MentionList = Response.Data.MentionList; - // Find the matching mention by FromUserID - for (let i = 0; i < MentionList.length; i++) { - if (MentionList[i].FromUserID === notification.data.FromUserID) { - CreateAndShowMailMentionToast(MentionList[i]); - break; - } - } - } - }); + // Backend now provides all data needed for immediate display + CreateAndShowMailMentionToast(notification.data); } else if (notification.type === 'pong') { if (UtilityEnabled("DebugMode")) { console.log("WebSocket: Received pong"); From bc2099ddccda8f1c16f49892166709491ca0a88f Mon Sep 17 00:00:00 2001 From: boomzero Date: Wed, 11 Feb 2026 19:20:34 +0800 Subject: [PATCH 03/14] Fix WebSocket race condition, XSS vulnerability, and DOM destruction issues 1. Race condition (P1): Store reconnect timer ID to prevent duplicate WebSocket connections when visibilitychange handler and delayed reconnect fire simultaneously 2. XSS vulnerability (P1): Sanitize user-supplied PostTitle with escapeHTML() before rendering to prevent script injection attacks 3. DOM destruction (P2): Replace innerHTML += with appendChild to preserve async GetUsernameHTML() results in mail mention toasts Note: Mail mention matching issue (violation #2) was already resolved by previous commit that passes notification.data directly Co-Authored-By: Claude Sonnet 4.5 --- XMOJ.user.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index c792f75b..91435441 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -538,6 +538,7 @@ let NotificationSocket = null; let NotificationSocketReconnectAttempts = 0; let NotificationSocketReconnectDelay = 1000; let NotificationSocketPingInterval = null; +let NotificationSocketReconnectTimer = null; function GetPHPSESSID() { let Session = ""; @@ -553,6 +554,12 @@ function GetPHPSESSID() { function ConnectNotificationSocket() { try { + // Clear any pending reconnection timer to prevent duplicate connections + if (NotificationSocketReconnectTimer) { + clearTimeout(NotificationSocketReconnectTimer); + NotificationSocketReconnectTimer = null; + } + let Session = GetPHPSESSID(); if (Session === "") { if (UtilityEnabled("DebugMode")) { @@ -625,7 +632,7 @@ function ReconnectNotificationSocket() { console.log(`WebSocket: Reconnecting in ${delay}ms (attempt ${NotificationSocketReconnectAttempts})`); } - setTimeout(() => { + NotificationSocketReconnectTimer = setTimeout(() => { ConnectNotificationSocket(); }, delay); } @@ -683,7 +690,7 @@ function CreateAndShowBBSMentionToast(mention) { Toast.appendChild(ToastHeader); let ToastBody = document.createElement("div"); ToastBody.classList.add("toast-body"); - ToastBody.innerHTML = "讨论" + mention.PostTitle + "有新回复"; + ToastBody.innerHTML = "讨论" + escapeHTML(mention.PostTitle) + "有新回复"; let ToastFooter = document.createElement("div"); ToastFooter.classList.add("mt-2", "pt-2", "border-top"); let ToastDismissButton = document.createElement("button"); @@ -744,7 +751,7 @@ function CreateAndShowMailMentionToast(mention) { let ToastUser = document.createElement("span"); GetUsernameHTML(ToastUser, mention.FromUserID); ToastBody.appendChild(ToastUser); - ToastBody.innerHTML += " 给你发了一封短消息"; + ToastBody.appendChild(document.createTextNode(" 给你发了一封短消息")); let ToastFooter = document.createElement("div"); ToastFooter.classList.add("mt-2", "pt-2", "border-top"); let ToastDismissButton = document.createElement("button"); From 5dd5fec53caa79011ad895dabd256b341c7e7eb1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Feb 2026 19:36:15 +0800 Subject: [PATCH 04/14] 2.7.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d558b7ac..3c315c9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmoj-script", - "version": "2.7.2", + "version": "2.7.3", "description": "an improvement script for xmoj.tech", "main": "AddonScript.js", "scripts": { From a3034be705aadd936422770ce8544c38e7481f04 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Feb 2026 19:36:21 +0800 Subject: [PATCH 05/14] Update version info to 2.7.3 --- Update.json | 11 +++++++++++ XMOJ.user.js | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 1c7bd178..4a955140 100644 --- a/Update.json +++ b/Update.json @@ -3288,6 +3288,17 @@ } ], "Notes": "No release notes were provided for this release." + }, + "2.7.3": { + "UpdateDate": 1770809777402, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 905, + "Description": "Add WebSocket notification system for real-time BBS and mail mentions" + } + ], + "Notes": "No release notes were provided for this release." } } } \ No newline at end of file diff --git a/XMOJ.user.js b/XMOJ.user.js index 91435441..ba150fd2 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name XMOJ -// @version 2.7.2 +// @version 2.7.3 // @description XMOJ增强脚本 // @author @XMOJ-Script-dev, @langningchen and the community // @namespace https://github/langningchen From fec8f59e86040a7bd2fd95b6e273d6a33af9957d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Feb 2026 11:36:48 +0000 Subject: [PATCH 06/14] Update time and description of 2.7.3 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 4a955140..88d5a096 100644 --- a/Update.json +++ b/Update.json @@ -3290,7 +3290,7 @@ "Notes": "No release notes were provided for this release." }, "2.7.3": { - "UpdateDate": 1770809777402, + "UpdateDate": 1770809803348, "Prerelease": true, "UpdateContents": [ { From d3697a08e293cbdd1b14e89f5addab26930d7a0b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Feb 2026 11:37:14 +0000 Subject: [PATCH 07/14] Update time and description of 2.7.3 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 88d5a096..1bd03942 100644 --- a/Update.json +++ b/Update.json @@ -3290,7 +3290,7 @@ "Notes": "No release notes were provided for this release." }, "2.7.3": { - "UpdateDate": 1770809803348, + "UpdateDate": 1770809829755, "Prerelease": true, "UpdateContents": [ { From 3a933685fe7d46d8fee091593eeef133683a6783 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Feb 2026 11:37:43 +0000 Subject: [PATCH 08/14] Update time and description of 2.7.3 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 1bd03942..e139b0fe 100644 --- a/Update.json +++ b/Update.json @@ -3290,7 +3290,7 @@ "Notes": "No release notes were provided for this release." }, "2.7.3": { - "UpdateDate": 1770809829755, + "UpdateDate": 1770809858358, "Prerelease": true, "UpdateContents": [ { From 3ba202267b0b33a2244efe7f4be6f1fcd4f80f51 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Feb 2026 11:38:07 +0000 Subject: [PATCH 09/14] Update time and description of 2.7.3 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index e139b0fe..d7d2a8ac 100644 --- a/Update.json +++ b/Update.json @@ -3290,7 +3290,7 @@ "Notes": "No release notes were provided for this release." }, "2.7.3": { - "UpdateDate": 1770809858358, + "UpdateDate": 1770809882732, "Prerelease": true, "UpdateContents": [ { From d7d60fdd31f2ee6b59c74f773321679044292262 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Feb 2026 11:38:31 +0000 Subject: [PATCH 10/14] Update time and description of 2.7.3 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index d7d2a8ac..f3b277ae 100644 --- a/Update.json +++ b/Update.json @@ -3290,7 +3290,7 @@ "Notes": "No release notes were provided for this release." }, "2.7.3": { - "UpdateDate": 1770809882732, + "UpdateDate": 1770809906275, "Prerelease": true, "UpdateContents": [ { From 60ccaccc7b30ce3c6ad9eaeedf26c682b230f469 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Feb 2026 11:38:51 +0000 Subject: [PATCH 11/14] Update time and description of 2.7.3 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index f3b277ae..16ed1b0d 100644 --- a/Update.json +++ b/Update.json @@ -3290,7 +3290,7 @@ "Notes": "No release notes were provided for this release." }, "2.7.3": { - "UpdateDate": 1770809906275, + "UpdateDate": 1770809930805, "Prerelease": true, "UpdateContents": [ { From 7979646dbf44955e1f3b468581e3f264f295ba97 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Feb 2026 20:53:15 +0800 Subject: [PATCH 12/14] Clear toast container before fetching notifications to prevent race condition --- XMOJ.user.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index ba150fd2..0b3066fd 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -785,13 +785,16 @@ function CreateAndShowMailMentionToast(mention) { } function PollNotifications() { + // Clear toast container once before fetching to prevent race condition + if (UtilityEnabled("BBSPopup") || UtilityEnabled("MessagePopup")) { + let ToastContainer = document.querySelector(".toast-container"); + if (ToastContainer) { + ToastContainer.innerHTML = ""; + } + } if (UtilityEnabled("BBSPopup")) { RequestAPI("GetBBSMentionList", {}, (Response) => { if (Response.Success) { - let ToastContainer = document.querySelector(".toast-container"); - if (ToastContainer) { - ToastContainer.innerHTML = ""; - } let MentionList = Response.Data.MentionList; for (let i = 0; i < MentionList.length; i++) { CreateAndShowBBSMentionToast(MentionList[i]); @@ -802,12 +805,6 @@ function PollNotifications() { if (UtilityEnabled("MessagePopup")) { RequestAPI("GetMailMentionList", {}, (Response) => { if (Response.Success) { - if (!UtilityEnabled("BBSPopup")) { - let ToastContainer = document.querySelector(".toast-container"); - if (ToastContainer) { - ToastContainer.innerHTML = ""; - } - } let MentionList = Response.Data.MentionList; for (let i = 0; i < MentionList.length; i++) { CreateAndShowMailMentionToast(MentionList[i]); @@ -1537,7 +1534,9 @@ async function main() { // Handle tab visibility changes - reconnect if connection dropped document.addEventListener('visibilitychange', () => { - if (!document.hidden && NotificationSocket && NotificationSocket.readyState !== WebSocket.OPEN) { + if (!document.hidden && NotificationSocket && + NotificationSocket.readyState !== WebSocket.OPEN && + NotificationSocket.readyState !== WebSocket.CONNECTING) { ConnectNotificationSocket(); } }); From d939dd5534a05c235ac8e9ce26cbadaab9b3a424 Mon Sep 17 00:00:00 2001 From: boomzero Date: Wed, 11 Feb 2026 20:59:12 +0800 Subject: [PATCH 13/14] Prevent UpdateVersion from running if last commit was by github-actions[bot] This prevents infinite loops where the bot commits version updates, which triggers the workflow again, causing another commit. Co-Authored-By: Claude Sonnet 4.5 --- Update/UpdateVersion.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Update/UpdateVersion.js b/Update/UpdateVersion.js index 840ec2b8..cd744cd8 100644 --- a/Update/UpdateVersion.js +++ b/Update/UpdateVersion.js @@ -7,6 +7,14 @@ process.env.GITHUB_TOKEN = GithubToken; execSync("gh pr checkout " + PRNumber); console.info("PR #" + PRNumber + " has been checked out."); +// Check if the last commit was made by github-actions[bot] +const lastCommitAuthor = execSync("git log -1 --pretty=format:'%an'").toString().trim(); +console.log("Last commit author: " + lastCommitAuthor); +if (lastCommitAuthor === "github-actions[bot]") { + console.log("Last commit was made by github-actions[bot]. Skipping to prevent infinite loop."); + process.exit(0); +} + const JSONFileName = "./Update.json"; const JSFileName = "./XMOJ.user.js"; var JSONFileContent = readFileSync(JSONFileName, "utf8"); From 25ab8d76aa0f8da3383fdfca2af2f9c55fe38fb8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Feb 2026 13:21:38 +0000 Subject: [PATCH 14/14] Update time and description of 2.7.3 --- Update.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Update.json b/Update.json index 16ed1b0d..ac65ef40 100644 --- a/Update.json +++ b/Update.json @@ -3290,7 +3290,7 @@ "Notes": "No release notes were provided for this release." }, "2.7.3": { - "UpdateDate": 1770809930805, + "UpdateDate": 1770816092957, "Prerelease": true, "UpdateContents": [ { @@ -3298,7 +3298,7 @@ "Description": "Add WebSocket notification system for real-time BBS and mail mentions" } ], - "Notes": "No release notes were provided for this release." + "Notes": "Adds WebSocket-based real-time notification system for BBS mentions and mail notifications. Notifications now arrive within 1-2 seconds instead of requiring page focus. Includes automatic reconnection with exponential backoff and fallback to polling when WebSocket is unavailable." } } } \ No newline at end of file