// ==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.');
});
})();