// ==UserScript== // @name 5ch.net donguri Hit Response Getter // @namespace https://greasyfork.org/users/1310758 // @description Fetches and filters hit responses from donguri 5ch boards // @match https://donguri.5ch.net/cannonlogs // @connect 5ch.net // @license MIT License // @author pachimonta // @grant GM_xmlhttpRequest // @version 2024-06-01_002 // @downloadURL none // ==/UserScript== (function() { 'use strict'; // Storage for bbs list and subject list const bbsList = {}; const subjectList = {}; const completedURL = {}; // Helper functions const $ = (selector, context = document) => context.querySelector(selector); const $$ = (selector, context = document) => [...context.querySelectorAll(selector)]; // Sanitize user input to avoid XSS and other injections const sanitize = (value) => value.replace(/[^a-zA-Z0-9_:/.\-]/g, ''); // Update elements visibility based on filtering criteria const filterRows = (input, table) => { const value = input.value.trim(); if (!value) { $$('tr', table).forEach(row => { row.style.display = ''; return; }); return; } const criteria = value.split(',').map(item => item.split('=')).reduce((acc, [key, val]) => { if (key && val) { acc[key.trim()] = key.match(/^(?:log|bbsname|subject)$/) ? val.trim() : sanitize(val.trim()); } return acc; }, {}); $$('tr', table).forEach(row => { const isVisible = Object.entries(criteria).every(([key, val]) => { if (key === 'ita') { key = 'bbs'; } if (key === 'dat') { key = 'key'; } if (row.hasAttribute(`data-${key}`)) { if (key.match(/^(?:log|bbsname|subject)$/)) { return row.getAttribute(`data-${key}`).includes(val); } else { return row.getAttribute(`data-${key}`).indexOf(val) === 0; } } else { return false; } }); row.style.display = isVisible ? '' : 'none'; }); }; // Initialize the filter input and its functionalities const createFilterInput = () => { const input = document.createElement('input'); input.type = 'text'; input.placeholder = 'Filter (e.g., bbs=av, key=1711038453, date=06/01(土) 01:55, id=ac351e30, log=アブソルさん[97a65812])'; input.style = 'width: 100%; padding: 5px; margin-bottom: 10px;'; const table = $('table'); if (table) { table.parentNode.insertBefore(input, table); input.addEventListener('input', () => { location.hash = '#' + input.value; return; }); if (location.hash) { input.value = decodeURIComponent(location.hash.substring(1)); filterRows(input, table); } window.addEventListener('hashchange', () => { input.value = decodeURIComponent(location.hash.substring(1)); filterRows(input, table); }); } }; // Regular expression to detect and replace unwanted characters const rloRegex = /[\x00-\x1F\x7F\u200E\u200F\u202A\u202B\u202C\u202D\u202E]/g; const replaceTextRecursively = (element) => { element.childNodes.forEach(node => { if (node.nodeType === Node.TEXT_NODE) { node.textContent = node.textContent.replace(rloRegex, match => `[U+${match.codePointAt(0).toString(16).toUpperCase()}]`); } else if (node.nodeType === Node.ELEMENT_NODE) { replaceTextRecursively(node); } }); }; // Async function to wait until the subject list is loaded const waitForSubject = async (bbs, key) => { let retryCount = 0; while (retryCount < 30 && !(bbs in subjectList && `${key}.dat` in subjectList[bbs])) { await new Promise(resolve => setTimeout(resolve, 100)); retryCount++; } }; // GM_xmlhttpRequest wrapper to handle HTTP Get requests const getDat = (url, func, mime = 'text/plain; charset=shift_jis', obj = null, bbs = null, key = null, date = null) => { if (typeof obj === 'object' && url in completedURL) { (async () => { await waitForSubject(bbs, key); const origin = bbsList[bbs] || "https://origin"; const bbsName = bbsList[`${bbs}_txt`] || "???"; const subject = subjectList[bbs][`${key}.dat`] || "???"; obj.dataset.origin = origin; obj.dataset.subject = subject; obj.dataset.bbsname = bbsName; obj.lastElementChild.insertAdjacentHTML('afterbegin', `${bbs} ${key} ${date} ${bbsName} | ${subject} `); })(); return; } GM_xmlhttpRequest({ method: 'GET', url: url, timeout: 60 * 1000, overrideMimeType: mime, onload: function(response) { if (obj !== null) { func(response, obj, bbs, key, date); } else { func(response); } }, onerror: function(error) { console.error('An error occurred during the request:', error); } }); }; // Process subject line to update subject list and modify the row content const subjectFunc = (response, obj = null, bbs = null, key = null, date = null) => { completedURL[response.finalUrl] = true; if (response.status === 200) { const lastmodify = response.responseText; lastmodify.split(/[\r\n]+/).forEach(line => { const [key, subject] = line.split(/\s*<>\s*/, 2); subjectList[bbs][key] = subject; }); if (obj) { const origin = bbsList[bbs] || "https://origin"; const bbsName = bbsList[`${bbs}_txt`] || "???"; const subject = subjectList[bbs][`${key}.dat`] || "???"; obj.dataset.origin = origin; obj.dataset.subject = subject; obj.dataset.bbsname = bbsName; obj.lastElementChild.insertAdjacentHTML('afterbegin', `${bbs} ${key} ${date} ${bbsName} | ${subject} `); } } else { console.error('Failed to load data. Status code:', response.status); } }; // Function to handle each table row for subject processing const nextFunc = async () => { $$('tr[style^="background-color:white"]').forEach(tr => { replaceTextRecursively(tr); const log = $('td:nth-of-type(2)', tr).textContent.slice(0, $('td:nth-of-type(2)', tr).textContent.lastIndexOf('はnanashiさんを撃った |')).trim(); const txt = tr.textContent.slice(tr.textContent.lastIndexOf('|') + 1).trim(); const [bbs, key, date] = txt.split(' ', 3); tr.dataset.bbs = bbs; tr.dataset.key = key; tr.dataset.date = date; tr.dataset.log = log; tr.dataset.id = log.slice(-9, -1); if (!Object.hasOwn(subjectList, bbs)) { subjectList[bbs] = {}; } getDat(`${bbsList[bbs]}/${bbs}/lastmodify.txt`, subjectFunc, 'text/plain; charset=shift_jis', tr, bbs, key, date); }); createFilterInput(); }; // Function to process the bbsmenu response const bbsmenuFunc = (response) => { if (response.status === 200) { const html = document.createElement('html'); html.innerHTML = response.responseText; $$('a[href*=".5ch.net/"],a[href*=".bbspink.com/"]', html).forEach(bbsLink => { const match = bbsLink.href.match(/\.(?:5ch\.net|bbspink\.com)\/([a-zA-Z0-9_-]+)\/$/); if (match) { bbsList[match[1]] = new URL(bbsLink.href).origin; bbsList[`${match[1]}_txt`] = bbsLink.textContent.trim(); } }); if (Object.keys(bbsList).length === 0) { console.error('No boards found.'); return; } nextFunc(); } else { console.error('Failed to fetch bbsmenu. Status code:', response.status); } }; // Initial data fetch from bbsmenu getDat('https://menu.5ch.net/bbsmenu.html', bbsmenuFunc, 'text/html; charset=shift_jis'); })();