您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Sends a minimal version of successful 4chan Xt posts to a Discord webhook.
// ==UserScript== // @name 4chan Xt Minimal Post Archiver to Discord // @namespace http://tampermonkey.net/ // @version 0.7 // @description Sends a minimal version of successful 4chan Xt posts to a Discord webhook. // @author wormpilled // @match *://boards.4chan.org/* // @match *://boards.4channel.org/* // @grant GM_xmlhttpRequest // @grant GM_log // @connect discord.com // @connect boards.4chan.org // @connect boards.4channel.org // @connect desuarchive.org // @connect archive.4plebs.org // @run-at document-start // ==/UserScript== (function() { 'use strict'; const DISCORD_WEBHOOK_URL = ''; const logPrefix = '[4chanX MinArchiver]'; const MAX_CONTENT_LENGTH = 1900; // Discord message limit is 2000, leave some room for header console.log(logPrefix, 'Script loaded. Listening for QRPostSuccessful event...'); GM_log(logPrefix + ' Script loaded. Listening for QRPostSuccessful event...'); function getArchiveUrl(boardID, threadID, postID) { let archiveBaseUrl = ''; let postAnchor = `#q${postID}`; if (boardID === 'g') { archiveBaseUrl = `https://desuarchive.org/${boardID}/thread/${threadID}/`; postAnchor = `#${postID}`; } else if (['pol', 'tv'].includes(boardID)) { archiveBaseUrl = `https://archive.4plebs.org/${boardID}/thread/${threadID}/`; } else { return null; } return archiveBaseUrl + postAnchor; } function cleanHtmlForDiscord(htmlString) { if (typeof htmlString !== 'string' || !htmlString) return ""; let text = htmlString; text = text.replace(/<br\s*\/?>/gi, '\n'); text = text.replace(/<a[^>]*class="quotelink"[^>]*>(>>\d+)<\/a>/gi, '$1'); // Keeps >>12345 text = text.replace(/<s[\s\S]*?>([\s\S]*?)<\/s>/gi, '||$1||'); // Spoilers const tempDiv = document.createElement('div'); tempDiv.innerHTML = text; text = tempDiv.textContent || tempDiv.innerText || ""; text = text.replace(/>/g, '>') .replace(/</g, '<') .replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, "'"); return text.trim(); } function sendToDiscord(postContentHTML, boardID, threadID, postID, postUrl, archiveUrl, source) { GM_log(logPrefix + `Preparing minimal message. Content source: ${source}. Raw: ${String(postContentHTML).substring(0,50)}...`); let cleanedContent = cleanHtmlForDiscord(postContentHTML); // Construct the header line let headerLine = `**/${boardID}/** `; if (archiveUrl) { headerLine += `[Archive](${archiveUrl}) `; } headerLine += `[4chan](${postUrl})`; // Combine header and content let fullMessage = headerLine + '\n```\n' + (cleanedContent || "[No text content retrieved]") + '\n```'; // Check total length and truncate content if necessary const headerLength = headerLine.length + 8; // 8 for \n```\n and \n``` if (cleanedContent.length > (2000 - headerLength)) { const maxCleanedContentLength = 2000 - headerLength - 3; // -3 for "..." cleanedContent = cleanedContent.substring(0, maxCleanedContentLength) + "..."; fullMessage = headerLine + '\n```\n' + cleanedContent + '\n```'; } if (fullMessage.length > 2000) { // Final safeguard fullMessage = fullMessage.substring(0, 1997) + "..."; } const payload = { content: fullMessage, // To prevent pinging @everyone or @here, if the content accidentally contains them allowed_mentions: { parse: [] // "users", "roles" if you want to allow those, but empty for none } }; GM_log(logPrefix + ' Sending to Discord: ' + JSON.stringify(payload).substring(0, 300) + "..."); GM_xmlhttpRequest({ method: "POST", url: DISCORD_WEBHOOK_URL, headers: { "Content-Type": "application/json" }, data: JSON.stringify(payload), onload: function(response) { if (response.status >= 200 && response.status < 300) { console.log(logPrefix, 'Successfully sent minimal data to Discord webhook.'); GM_log(logPrefix + ' Successfully sent minimal data to Discord webhook. Status: ' + response.status); } else { console.error(logPrefix, 'Error sending minimal data to Discord. Status:', response.status, response.statusText, response.responseText); GM_log(logPrefix + ' Error sending minimal data to Discord. Status: ' + response.status + ' Response: ' + response.responseText); } }, onerror: function(error) { console.error(logPrefix, 'Error with GM_xmlhttpRequest to Discord:', error); GM_log(logPrefix + ' Error with GM_xmlhttpRequest to Discord: ' + JSON.stringify(error)); }, ontimeout: function() { console.error(logPrefix, 'Request to Discord webhook timed out.'); GM_log(logPrefix + ' Request to Discord webhook timed out.'); } }); } function fetchPostContentFromAPI(boardID, threadID, postID, postUrl, archiveUrl) { const currentHostname = window.location.hostname; const apiUrl = `https://${currentHostname}/${boardID}/thread/${threadID}.json`; GM_log(logPrefix + `Fetching post content from Board API: ${apiUrl} for post ${postID}`); GM_xmlhttpRequest({ method: "GET", url: apiUrl, onload: function(response) { if (response.status === 200) { try { const threadData = JSON.parse(response.responseText); const numericPostID = Number(postID); const postObject = threadData.posts.find(p => p.no === numericPostID); if (postObject && typeof postObject.com === 'string') { GM_log(logPrefix + `Successfully fetched content for post ${numericPostID} via Board API.`); sendToDiscord(postObject.com, boardID, threadID, numericPostID, postUrl, archiveUrl, "Board API"); } else if (postObject && !postObject.com) { GM_log(logPrefix + `Post ${numericPostID} found via Board API but has no text comment (com field).`); sendToDiscord("", boardID, threadID, numericPostID, postUrl, archiveUrl, "Board API (no text)"); // Send empty string for no content } else { GM_log(logPrefix + `Post ${numericPostID} not found in Board API response.`); sendToDiscord(`[Content for post ${numericPostID} not found in Board API response]`, boardID, threadID, numericPostID, postUrl, archiveUrl, "Board API (not found)"); } } catch (e) { console.error(logPrefix, "Error parsing Board API response:", e); GM_log(logPrefix + "Error parsing Board API response: " + e.message); sendToDiscord("[Error parsing Board API response]", boardID, threadID, postID, postUrl, archiveUrl, "Board API (parse error)"); } } else { console.error(logPrefix, `Board API request failed for ${threadID}. Status:`, response.status); GM_log(logPrefix + `Board API request failed. Status: ${response.status}`); sendToDiscord(`[Board API request failed: ${response.status}]`, boardID, threadID, postID, postUrl, archiveUrl, "Board API (request error)"); } }, onerror: function(error) { console.error(logPrefix, "Error with GM_xmlhttpRequest to Board API:", error); GM_log(logPrefix + "Error with GM_xmlhttpRequest to Board API: " + JSON.stringify(error)); sendToDiscord("[Board API request network error]", boardID, threadID, postID, postUrl, archiveUrl, "Board API (network error)"); }, ontimeout: function() { console.error(logPrefix, "Request to Board API timed out."); GM_log(logPrefix + "Request to Board API timed out."); sendToDiscord("[Board API request timed out]", boardID, threadID, postID, postUrl, archiveUrl, "Board API (timeout)"); } }); } document.addEventListener('QRPostSuccessful', function(e) { GM_log(logPrefix + ' Event: QRPostSuccessful caught. Details: ' + JSON.stringify(e.detail)); if (e.detail && e.detail.boardID && e.detail.threadID && e.detail.postID) { const boardID = String(e.detail.boardID); const threadID = String(e.detail.threadID); const postID = String(e.detail.postID); // Will be converted to Number where needed let postContentFromEvent = ""; let source = "Event (unknown)"; if (e.detail.context && e.detail.context.post && typeof e.detail.context.post.commentHTML === 'string') { postContentFromEvent = e.detail.context.post.commentHTML; source = "Event (post.commentHTML)"; } else if (typeof e.detail.textPost === 'string' && e.detail.textPost.trim() !== "") { postContentFromEvent = e.detail.textPost; source = "Event (textPost raw)"; } else if (typeof e.detail.comment === 'string' && e.detail.comment.trim() !== "") { postContentFromEvent = e.detail.comment; source = "Event (comment raw)"; } const currentHostname = window.location.hostname; const postUrl = `https://${currentHostname}/${boardID}/thread/${threadID}#p${postID}`; const archiveUrl = getArchiveUrl(boardID, threadID, postID); // postID as string is fine here if (postContentFromEvent && postContentFromEvent.trim() !== "") { GM_log(logPrefix + `Content found in event detail (Source: ${source}). Using that.`); sendToDiscord(postContentFromEvent, boardID, threadID, Number(postID), postUrl, archiveUrl, source); } else { GM_log(logPrefix + "Content not found or empty in event. Attempting Board API fallback."); fetchPostContentFromAPI(boardID, threadID, Number(postID), postUrl, archiveUrl); } } else { GM_log(logPrefix + ' QRPostSuccessful event missing critical details.'); } }); document.addEventListener('4chanXInitFinished', function() { GM_log(logPrefix + ' 4chan X initialization finished.'); }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址