// ==UserScript==
// @name 5ch.net donguri Hit Response Getter
// @namespace https://gf.qytechs.cn/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
// ==/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} <a href="${origin}/test/read.cgi/${bbs}/${key}/#date=${date}" target="_blank">${bbsName} | ${subject}</a> `);
})();
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} <a href="${origin}/test/read.cgi/${bbs}/${key}/#date=${date}" target="_blank">${bbsName} | ${subject}</a> `);
}
} 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');
})();