Guest User

warosu filter tampermonkey

a guest
Aug 15th, 2026
25
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 20.55 KB | None | 0 0
  1. // ==UserScript==
  2. // @name Warosu Post Filter
  3. // @namespace https://github.com/fadhilnorraji/warosu-filter
  4. // @version 1.0.0
  5. // @description 4chan-X-style regex post filter for warosu.org. Hides or collapses posts client-side based on comment/name/tripcode/subject/filename/post-ID rules. Does not touch the site itself.
  6. // @author you
  7. // @match https://warosu.org/*
  8. // @match http://warosu.org/*
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_registerMenuCommand
  12. // @grant GM_addStyle
  13. // @grant GM_setClipboard
  14. // @run-at document-idle
  15. // @noframes
  16. // ==/UserScript==
  17.  
  18. (function () {
  19. 'use strict';
  20.  
  21. const STORAGE_KEY = 'warosuFilterRules_v1';
  22. const FIELD_OPTIONS = ['any', 'comment', 'name', 'tripcode', 'subject', 'filename', 'id'];
  23.  
  24. // ------------------------------------------------------------------
  25. // storage
  26. // ------------------------------------------------------------------
  27.  
  28. function loadRules() {
  29. try {
  30. const raw = GM_getValue(STORAGE_KEY, '[]');
  31. const parsed = JSON.parse(raw);
  32. return Array.isArray(parsed) ? parsed : [];
  33. } catch (e) {
  34. console.error('[Warosu Filter] failed to load rules, resetting', e);
  35. return [];
  36. }
  37. }
  38.  
  39. function saveRules(nextRules) {
  40. rules = nextRules;
  41. GM_setValue(STORAGE_KEY, JSON.stringify(rules));
  42. }
  43.  
  44. function uid() {
  45. return 'r_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
  46. }
  47.  
  48. let rules = loadRules();
  49. const revealed = new Set(); // post ids the user has manually un-hidden this pageload
  50.  
  51. // ------------------------------------------------------------------
  52. // rule compilation
  53. // ------------------------------------------------------------------
  54.  
  55. function compileRule(r) {
  56. if (!r.enabled || !r.pattern) return { ...r, re: null };
  57. try {
  58. return { ...r, re: new RegExp(r.pattern, r.flags || 'i'), error: null };
  59. } catch (e) {
  60. return { ...r, re: null, error: String(e.message || e) };
  61. }
  62. }
  63.  
  64. function testRule(re, text) {
  65. re.lastIndex = 0; // guard against 'g' flag statefulness
  66. return re.test(text);
  67. }
  68.  
  69. // ------------------------------------------------------------------
  70. // page helpers
  71. // ------------------------------------------------------------------
  72.  
  73. function getCurrentBoard() {
  74. const m = location.pathname.match(/^\/([a-zA-Z0-9]+)\//);
  75. return m ? m[1] : '';
  76. }
  77.  
  78. function getContainer(postEl) {
  79. // OP posts are <div class="comment" id="p...">, self-contained.
  80. // Replies are <td class="comment reply" id="p...">, wrapped in their
  81. // own <table><tr><td class="doubledash">&gt;&gt;</td><td>...</td></tr></table>.
  82. if (postEl.tagName === 'DIV') return postEl;
  83. return postEl.closest('table') || postEl;
  84. }
  85.  
  86. function extractFields(postEl, id) {
  87. const nameEl = postEl.querySelector('.postername');
  88. const tripEl = postEl.querySelector('.postertrip, .trip');
  89. const subjEl = postEl.querySelector('.filetitle');
  90. const fileEl = postEl.querySelector('.fileinfo');
  91. const bq = postEl.querySelector('blockquote');
  92.  
  93. const name = nameEl ? nameEl.textContent.trim() : '';
  94. const tripcode = tripEl ? tripEl.textContent.trim() : '';
  95. const subject = subjEl ? subjEl.textContent.trim() : '';
  96. const comment = bq ? bq.textContent.trim() : '';
  97.  
  98. let filename = '';
  99. if (fileEl) {
  100. const m = fileEl.textContent.match(/,\s*([^,]+)\s*$/);
  101. filename = (m ? m[1] : fileEl.textContent).trim();
  102. }
  103.  
  104. const any = [id, name, tripcode, subject, filename, comment].join('\n');
  105. return { id, name, tripcode, subject, filename, comment, any };
  106. }
  107.  
  108. // ------------------------------------------------------------------
  109. // hide / show / stub
  110. // ------------------------------------------------------------------
  111.  
  112. function removeStub(id) {
  113. const stub = document.querySelector(`.wf-stub[data-wf-for="${id}"]`);
  114. if (stub) stub.remove();
  115. }
  116.  
  117. function showPost(postEl) {
  118. const id = postEl.id.slice(1);
  119. getContainer(postEl).style.display = '';
  120. removeStub(id);
  121. }
  122.  
  123. function hidePost(postEl, rule, id) {
  124. const container = getContainer(postEl);
  125.  
  126. if (rule.mode === 'stub') {
  127. container.style.display = 'none';
  128. let stub = document.querySelector(`.wf-stub[data-wf-for="${id}"]`);
  129. if (!stub) {
  130. stub = document.createElement('div');
  131. stub.className = 'wf-stub';
  132. stub.dataset.wfFor = id;
  133. container.parentNode.insertBefore(stub, container);
  134. }
  135. stub.textContent = '';
  136. const label = rule.label || rule.pattern;
  137. const span = document.createElement('span');
  138. span.textContent = `Filtered post No.${id} (matched "${label}") `;
  139. const btn = document.createElement('a');
  140. btn.href = '#';
  141. btn.className = 'wf-stub-show';
  142. btn.textContent = '[Show]';
  143. btn.addEventListener('click', (e) => {
  144. e.preventDefault();
  145. revealed.add(id);
  146. applyFilters();
  147. });
  148. stub.append(span, btn);
  149. } else {
  150. removeStub(id);
  151. container.style.display = 'none';
  152. }
  153. }
  154.  
  155. // ------------------------------------------------------------------
  156. // main pass
  157. // ------------------------------------------------------------------
  158.  
  159. function applyFilters() {
  160. const compiled = rules.map(compileRule).filter((r) => r.enabled && r.re);
  161. if (!compiled.length) {
  162. document.querySelectorAll('div.comment[id^="p"], td.comment.reply[id^="p"]').forEach(showPost);
  163. document.querySelectorAll('.wf-stub').forEach((s) => s.remove());
  164. return;
  165. }
  166.  
  167. const board = getCurrentBoard();
  168. const posts = document.querySelectorAll('div.comment[id^="p"], td.comment.reply[id^="p"]');
  169.  
  170. posts.forEach((postEl) => {
  171. const id = postEl.id.slice(1);
  172. if (revealed.has(id)) {
  173. showPost(postEl);
  174. return;
  175. }
  176.  
  177. const fields = extractFields(postEl, id);
  178. let matched = null;
  179.  
  180. for (const r of compiled) {
  181. if (r.boards) {
  182. const list = r.boards.split(',').map((s) => s.trim()).filter(Boolean);
  183. if (list.length && !list.includes(board)) continue;
  184. }
  185. const text = fields[r.field] ?? fields.any;
  186. if (testRule(r.re, text)) {
  187. matched = r;
  188. break;
  189. }
  190. }
  191.  
  192. if (matched) {
  193. hidePost(postEl, matched, id);
  194. } else {
  195. showPost(postEl);
  196. }
  197. });
  198. }
  199.  
  200. // Re-run on DOM changes (defensive: covers any future live-update
  201. // behaviour warosu might add, and the quote-preview popup clones).
  202. let applyScheduled = false;
  203. function scheduleApply() {
  204. if (applyScheduled) return;
  205. applyScheduled = true;
  206. requestAnimationFrame(() => {
  207. applyScheduled = false;
  208. applyFilters();
  209. });
  210. }
  211.  
  212. const observer = new MutationObserver((mutations) => {
  213. for (const m of mutations) {
  214. if (m.addedNodes.length) {
  215. scheduleApply();
  216. break;
  217. }
  218. }
  219. });
  220.  
  221. // ------------------------------------------------------------------
  222. // settings panel
  223. // ------------------------------------------------------------------
  224.  
  225. function addStyle(css) {
  226. if (typeof GM_addStyle === 'function') {
  227. GM_addStyle(css);
  228. } else {
  229. const style = document.createElement('style');
  230. style.textContent = css;
  231. document.head.appendChild(style);
  232. }
  233. }
  234.  
  235. addStyle(`
  236. #wf-toggle-btn {
  237. position: fixed;
  238. right: 12px;
  239. bottom: 12px;
  240. z-index: 999999;
  241. background: #2c2c2c;
  242. color: #fff;
  243. border: 1px solid #555;
  244. border-radius: 4px;
  245. padding: 6px 10px;
  246. font: 12px/1.2 sans-serif;
  247. cursor: pointer;
  248. opacity: 0.85;
  249. }
  250. #wf-toggle-btn:hover { opacity: 1; }
  251.  
  252. #wf-overlay {
  253. position: fixed;
  254. inset: 0;
  255. background: rgba(0,0,0,0.5);
  256. z-index: 1000000;
  257. display: flex;
  258. align-items: center;
  259. justify-content: center;
  260. }
  261. #wf-panel {
  262. background: #fff;
  263. color: #111;
  264. width: min(920px, 95vw);
  265. max-height: 88vh;
  266. overflow: auto;
  267. border-radius: 6px;
  268. padding: 16px 18px 18px;
  269. font: 13px/1.4 sans-serif;
  270. box-shadow: 0 4px 24px rgba(0,0,0,0.4);
  271. }
  272. #wf-panel h2 { margin: 0 0 10px; font-size: 16px; }
  273. #wf-panel table { width: 100%; border-collapse: collapse; margin-bottom: 10px; }
  274. #wf-panel th { text-align: left; font-size: 11px; color: #555; padding: 2px 4px; }
  275. #wf-panel td { padding: 3px 4px; vertical-align: middle; }
  276. #wf-panel input[type=text] { width: 100%; box-sizing: border-box; font: 12px monospace; padding: 3px 4px; }
  277. #wf-panel input.wf-pattern { font-family: monospace; }
  278. #wf-panel select { font: 12px sans-serif; }
  279. #wf-panel .wf-err { color: #c0392b; font-size: 11px; }
  280. #wf-panel .wf-row-actions { display: flex; gap: 8px; margin: 8px 0 14px; }
  281. #wf-panel button { cursor: pointer; padding: 5px 10px; font: 12px sans-serif; }
  282. #wf-panel .wf-del { color: #c0392b; border: none; background: none; font-size: 14px; }
  283. #wf-panel .wf-footer { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-top: 10px; border-top: 1px solid #ddd; padding-top: 10px; }
  284. #wf-panel .wf-footer-left, #wf-panel .wf-footer-right { display: flex; gap: 8px; }
  285. #wf-panel textarea { width: 100%; height: 120px; font: 11px monospace; box-sizing: border-box; }
  286. #wf-panel .wf-io { margin-top: 12px; display: none; }
  287. #wf-panel .wf-io.wf-open { display: block; }
  288. #wf-panel .wf-hint { color: #666; font-size: 11px; margin: 4px 0 12px; }
  289.  
  290. .wf-stub {
  291. border: 1px dashed #999;
  292. background: rgba(128,128,128,0.08);
  293. padding: 4px 8px;
  294. margin: 2px 0;
  295. font-size: 12px;
  296. color: #666;
  297. }
  298. .wf-stub a { margin-left: 6px; }
  299. `);
  300.  
  301. function fieldSelect(value) {
  302. const sel = document.createElement('select');
  303. for (const f of FIELD_OPTIONS) {
  304. const opt = document.createElement('option');
  305. opt.value = f;
  306. opt.textContent = f;
  307. if (f === value) opt.selected = true;
  308. sel.appendChild(opt);
  309. }
  310. return sel;
  311. }
  312.  
  313. function buildRow(rule) {
  314. const tr = document.createElement('tr');
  315. tr.dataset.id = rule.id;
  316.  
  317. const tdOn = document.createElement('td');
  318. const cb = document.createElement('input');
  319. cb.type = 'checkbox';
  320. cb.checked = rule.enabled !== false;
  321. cb.className = 'wf-f-enabled';
  322. tdOn.appendChild(cb);
  323.  
  324. const tdField = document.createElement('td');
  325. const sel = fieldSelect(rule.field || 'comment');
  326. sel.className = 'wf-f-field';
  327. tdField.appendChild(sel);
  328.  
  329. const tdPattern = document.createElement('td');
  330. const pat = document.createElement('input');
  331. pat.type = 'text';
  332. pat.className = 'wf-f-pattern wf-pattern';
  333. pat.placeholder = 'regex, e.g. \\bkeyword\\b';
  334. pat.value = rule.pattern || '';
  335. tdPattern.appendChild(pat);
  336. const err = document.createElement('div');
  337. err.className = 'wf-err';
  338. tdPattern.appendChild(err);
  339.  
  340. const tdFlags = document.createElement('td');
  341. const flags = document.createElement('input');
  342. flags.type = 'text';
  343. flags.className = 'wf-f-flags';
  344. flags.placeholder = 'i';
  345. flags.value = rule.flags || 'i';
  346. flags.style.width = '40px';
  347. tdFlags.appendChild(flags);
  348.  
  349. const tdBoards = document.createElement('td');
  350. const boards = document.createElement('input');
  351. boards.type = 'text';
  352. boards.className = 'wf-f-boards';
  353. boards.placeholder = 'all boards';
  354. boards.value = rule.boards || '';
  355. boards.style.width = '70px';
  356. tdBoards.appendChild(boards);
  357.  
  358. const tdMode = document.createElement('td');
  359. const mode = document.createElement('select');
  360. mode.className = 'wf-f-mode';
  361. for (const m of ['hide', 'stub']) {
  362. const opt = document.createElement('option');
  363. opt.value = m;
  364. opt.textContent = m === 'hide' ? 'hide' : 'collapse';
  365. if ((rule.mode || 'hide') === m) opt.selected = true;
  366. mode.appendChild(opt);
  367. }
  368. tdMode.appendChild(mode);
  369.  
  370. const tdLabel = document.createElement('td');
  371. const label = document.createElement('input');
  372. label.type = 'text';
  373. label.className = 'wf-f-label';
  374. label.placeholder = 'note (optional)';
  375. label.value = rule.label || '';
  376. tdLabel.appendChild(label);
  377.  
  378. const tdDel = document.createElement('td');
  379. const del = document.createElement('button');
  380. del.className = 'wf-del';
  381. del.textContent = '✕';
  382. del.title = 'Delete rule';
  383. del.addEventListener('click', () => tr.remove());
  384. tdDel.appendChild(del);
  385.  
  386. tr.append(tdOn, tdField, tdPattern, tdFlags, tdBoards, tdMode, tdLabel, tdDel);
  387. return tr;
  388. }
  389.  
  390. function readRow(tr) {
  391. const pattern = tr.querySelector('.wf-f-pattern').value;
  392. const errBox = tr.querySelector('.wf-err');
  393. let error = null;
  394. if (pattern) {
  395. try {
  396. new RegExp(pattern, tr.querySelector('.wf-f-flags').value || 'i');
  397. } catch (e) {
  398. error = String(e.message || e);
  399. }
  400. }
  401. errBox.textContent = error || '';
  402.  
  403. return {
  404. id: tr.dataset.id,
  405. enabled: tr.querySelector('.wf-f-enabled').checked,
  406. field: tr.querySelector('.wf-f-field').value,
  407. pattern,
  408. flags: tr.querySelector('.wf-f-flags').value || 'i',
  409. boards: tr.querySelector('.wf-f-boards').value.trim(),
  410. mode: tr.querySelector('.wf-f-mode').value,
  411. label: tr.querySelector('.wf-f-label').value.trim(),
  412. error,
  413. };
  414. }
  415.  
  416. function openPanel() {
  417. if (document.getElementById('wf-overlay')) return;
  418.  
  419. const overlay = document.createElement('div');
  420. overlay.id = 'wf-overlay';
  421.  
  422. const panel = document.createElement('div');
  423. panel.id = 'wf-panel';
  424.  
  425. panel.innerHTML = `
  426. <h2>Warosu Post Filter</h2>
  427. <div class="wf-hint">
  428. Rules run client-side only, in your browser. "hide" removes the post entirely;
  429. "collapse" replaces it with a small clickable placeholder, like 4chan X.
  430. Leave "boards" empty to apply everywhere, or list board codes like <code>jp,vt</code>.
  431. </div>
  432. <table>
  433. <thead>
  434. <tr>
  435. <th>On</th><th>Field</th><th>Pattern (regex)</th><th>Flags</th>
  436. <th>Boards</th><th>Mode</th><th>Note</th><th></th>
  437. </tr>
  438. </thead>
  439. <tbody id="wf-rows"></tbody>
  440. </table>
  441. <div class="wf-row-actions">
  442. <button id="wf-add">+ Add rule</button>
  443. <button id="wf-save">Save &amp; apply</button>
  444. <button id="wf-close">Close</button>
  445. </div>
  446. <div class="wf-footer">
  447. <div class="wf-footer-left">
  448. <button id="wf-export">Export rules</button>
  449. <button id="wf-import">Import rules</button>
  450. </div>
  451. </div>
  452. <div class="wf-io" id="wf-io-box">
  453. <textarea id="wf-io-text" spellcheck="false"></textarea>
  454. <div class="wf-row-actions">
  455. <button id="wf-io-copy">Copy to clipboard</button>
  456. <button id="wf-io-apply">Apply pasted JSON</button>
  457. <button id="wf-io-cancel">Cancel</button>
  458. </div>
  459. </div>
  460. `;
  461.  
  462. overlay.appendChild(panel);
  463. document.body.appendChild(overlay);
  464.  
  465. const rowsBody = panel.querySelector('#wf-rows');
  466. rules.forEach((r) => rowsBody.appendChild(buildRow(r)));
  467.  
  468. panel.querySelector('#wf-add').addEventListener('click', () => {
  469. rowsBody.appendChild(
  470. buildRow({ id: uid(), enabled: true, field: 'comment', pattern: '', flags: 'i', mode: 'hide' })
  471. );
  472. });
  473.  
  474. panel.querySelector('#wf-save').addEventListener('click', () => {
  475. const rows = [...rowsBody.querySelectorAll('tr')];
  476. const next = rows.map(readRow).filter((r) => r.pattern);
  477. if (next.some((r) => r.error)) return; // errors shown inline, block save
  478. saveRules(next.map(({ error, ...rest }) => rest));
  479. revealed.clear();
  480. applyFilters();
  481. });
  482.  
  483. panel.querySelector('#wf-close').addEventListener('click', () => overlay.remove());
  484. overlay.addEventListener('click', (e) => {
  485. if (e.target === overlay) overlay.remove();
  486. });
  487.  
  488. const ioBox = panel.querySelector('#wf-io-box');
  489. const ioText = panel.querySelector('#wf-io-text');
  490.  
  491. panel.querySelector('#wf-export').addEventListener('click', () => {
  492. ioText.value = JSON.stringify(rules, null, 2);
  493. ioBox.classList.add('wf-open');
  494. });
  495. panel.querySelector('#wf-import').addEventListener('click', () => {
  496. ioText.value = '';
  497. ioBox.classList.add('wf-open');
  498. });
  499. panel.querySelector('#wf-io-cancel').addEventListener('click', () => {
  500. ioBox.classList.remove('wf-open');
  501. });
  502. panel.querySelector('#wf-io-copy').addEventListener('click', () => {
  503. if (typeof GM_setClipboard === 'function') GM_setClipboard(ioText.value);
  504. else ioText.select();
  505. });
  506. panel.querySelector('#wf-io-apply').addEventListener('click', () => {
  507. try {
  508. const parsed = JSON.parse(ioText.value);
  509. if (!Array.isArray(parsed)) throw new Error('Expected a JSON array of rules');
  510. rowsBody.textContent = '';
  511. parsed.forEach((r) => rowsBody.appendChild(buildRow({ ...r, id: r.id || uid() })));
  512. ioBox.classList.remove('wf-open');
  513. } catch (e) {
  514. alert('Invalid JSON: ' + (e.message || e));
  515. }
  516. });
  517. }
  518.  
  519. // ------------------------------------------------------------------
  520. // entry points
  521. // ------------------------------------------------------------------
  522.  
  523. function addToggleButton() {
  524. if (document.getElementById('wf-toggle-btn')) return;
  525. const btn = document.createElement('button');
  526. btn.id = 'wf-toggle-btn';
  527. btn.textContent = 'Filters';
  528. btn.title = 'Warosu Post Filter settings';
  529. btn.addEventListener('click', openPanel);
  530. document.body.appendChild(btn);
  531. }
  532.  
  533. if (typeof GM_registerMenuCommand === 'function') {
  534. GM_registerMenuCommand('Warosu Filter: manage rules', openPanel);
  535. }
  536.  
  537. function init() {
  538. addToggleButton();
  539. applyFilters();
  540. observer.observe(document.body, { childList: true, subtree: true });
  541. }
  542.  
  543. if (document.body) init();
  544. else document.addEventListener('DOMContentLoaded', init, { once: true });
  545. })();
  546.  
Advertisement
Add Comment
Please, Sign In to add comment