==UserScript== @name 新着荒らし一括削除依頼ツール @namespace httptampermonkey.net @version 1.4 @description futaba-id.siteから最新荒らしIDを取得し、ふたば上で一括削除依頼を送るツール(全自動モード対応) @author Antigravity @match https.2chan.net @grant GM_xmlhttpRequest @grant GM_setValue @grant GM_getValue @grant GM_registerMenuCommand @connect futaba-id.site @connect img.2chan.net @connect may.2chan.net ==UserScript== (function () { 'use strict'; ====== スタイル ====== const style = document.createElement('style'); style.textContent = ` #spam-panel { position fixed; top 20px; right 20px; width 280px; min-width 200px; min-height 36px; max-height 85vh; background rgba(10, 10, 20, 0.55); backdrop-filter blur(24px) saturate(180%); -webkit-backdrop-filter blur(24px) saturate(180%); border 1px solid rgba(255,255,255,0.18); border-radius 14px; box-shadow 0 8px 40px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.1); color #f3f4f6; font-family sans-serif; font-size 12px; z-index 999999; display flex; flex-direction column; overflow hidden; } #spam-header { padding 7px 10px; background linear-gradient(135deg, rgba(124,58,237,0.75), rgba(79,70,229,0.75)); backdrop-filter blur(10px); font-weight bold; font-size 11px; cursor move; display flex; justify-content space-between; align-items center; user-select none; gap 6px; flex-shrink 0; border-bottom 1px solid rgba(255,255,255,0.12); } #spam-header span { flex1; } #sp-header-btns { display flex; gap 4px; } .sp-hbtn { background rgba(255,255,255,0.15); border none; color #fff; font-size 12px; width 20px; height 20px; border-radius 4px; cursor pointer; display flex; align-items center; justify-content center; line-height 1; padding 0; transition background 0.15s; } .sp-hbtnhover { background rgba(255,255,255,0.3); } #spam-body { padding 10px; display flex; flex-direction column; gap 8px; overflow-y auto; flex 1; } #spam-body.collapsed { display none; } .sp-label { font-size 9px; color #9ca3af; text-transform uppercase; letter-spacing 0.06em; margin-bottom 2px; } #sp-source-select, #sp-reason { background rgba(30, 30, 50, 0.85); border 1px solid rgba(255,255,255,0.15); border-radius 6px; padding 5px 8px; color #e2e8f0; font-size 11px; outline none; width 100%; -webkit-appearance none; appearance none; } #sp-source-select option, #sp-reason option { background #1e1e3a; color #e2e8f0; } #sp-scan-btn { background linear-gradient(135deg, #7c3aed, #4f46e5); border none; border-radius 6px; padding 6px; color white; font-weight bold; font-size 11px; cursor pointer; transition opacity 0.2s, transform 0.1s; } #sp-scan-btnactive { transform scale(0.97); } #sp-scan-btndisabled { opacity 0.5; cursor not-allowed; } #sp-thread-wrap { border 1px solid rgba(255,255,255,0.08); background rgba(0,0,0,0.18); backdrop-filter blur(8px); border-radius 6px; max-height 180px; overflow-y auto; padding 5px; display flex; flex-direction column; gap 3px; } .sp-thread-item { display flex; align-items flex-start; gap 5px; padding 3px; border-radius 4px; transition background 0.15s; } .sp-thread-itemhover { background rgba(255,255,255,0.07); } .sp-thread-item input[type=checkbox] { margin-top 2px; cursor pointer; flex-shrink 0; } .sp-thread-meta { display flex; flex-direction column; overflow hidden; } .sp-thread-link { color #818cf8; font-size 10px; font-weight bold; text-decoration none; } .sp-thread-linkhover { text-decoration underline; } .sp-thread-sub { font-size 9px; color #9ca3af; white-space nowrap; overflow hidden; text-overflow ellipsis; max-width 210px; } .sp-row { display flex; gap 4px; } .sp-mini-btn { flex 1; background rgba(255,255,255,0.09); border 1px solid rgba(255,255,255,0.1); border-radius 5px; padding 4px; color #d1d5db; font-size 10px; cursor pointer; transition background 0.15s; } .sp-mini-btnhover { background rgba(255,255,255,0.17); } #sp-send-btn { background linear-gradient(135deg, #ef4444, #b91c1c); border none; border-radius 6px; padding 6px; color white; font-weight bold; font-size 11px; cursor pointer; transition opacity 0.2s, transform 0.1s; } #sp-send-btnactive { transform scale(0.97); } #sp-send-btndisabled { opacity 0.5; cursor not-allowed; } #sp-log { background rgba(0,0,0,0.25); border 1px solid rgba(255,255,255,0.06); border-radius 6px; padding 6px 8px; font-family monospace; font-size 9px; color #6ee7b7; height 80px; overflow-y auto; white-space pre-wrap; line-height 1.5; backdrop-filter blur(4px); } #sp-empty { font-size 10px; color #6b7280; text-align center; padding 10px 0; } ── 全自動モード行 ── #sp-auto-row { display flex; align-items center; justify-content space-between; background rgba(255,255,255,0.05); border 1px solid rgba(255,255,255,0.12); border-radius 8px; padding 6px 10px; backdrop-filter blur(6px); } #sp-auto-label { font-size 11px; font-weight bold; color #e2e8f0; display flex; flex-direction column; gap 1px; } #sp-auto-sublabel { font-size 9px; color #6b7280; font-weight normal; } トグルスイッチ .sp-toggle-wrap { position relative; width 38px; height 20px; flex-shrink 0; } .sp-toggle-wrap input { opacity 0; width 0; height 0; } .sp-toggle-slider { position absolute; inset 0; background rgba(255,255,255,0.15); border-radius 20px; cursor pointer; transition background 0.25s; } .sp-toggle-sliderbefore { content ''; position absolute; width 14px; height 14px; left 3px; top 3px; background white; border-radius 50%; transition transform 0.25s; } .sp-toggle-wrap inputchecked + .sp-toggle-slider { background linear-gradient(135deg, #10b981, #059669); } .sp-toggle-wrap inputchecked + .sp-toggle-sliderbefore { transform translateX(18px); } 全自動中のアニメーションバッジ #sp-auto-badge { display none; font-size 9px; background linear-gradient(135deg, #10b981, #059669); color white; border-radius 4px; padding 2px 5px; animation sp-pulse 1.5s infinite; } @keyframes sp-pulse { 0%, 100% { opacity 1; } 50% { opacity 0.4; } } 常駐ミニボタン(パネル非表示時に表示) #sp-mini-launcher { position fixed; top 20px; right 20px; width 32px; height 32px; background linear-gradient(135deg, rgba(124,58,237,0.8), rgba(79,70,229,0.8)); backdrop-filter blur(12px); -webkit-backdrop-filter blur(12px); border 1px solid rgba(255,255,255,0.2); border-radius 50%; box-shadow 0 4px 16px rgba(0,0,0,0.4); cursor pointer; z-index 999999; display none; align-items center; justify-content center; font-size 16px; transition transform 0.15s, opacity 0.2s; color white; user-select none; } #sp-mini-launcherhover { transform scale(1.12); } #sp-mini-launcher .sp-shortcut-hint { position absolute; top 36px; right 0; font-size 9px; color rgba(255,255,255,0.6); white-space nowrap; background rgba(0,0,0,0.5); padding 2px 5px; border-radius 4px; pointer-events none; opacity 0; transition opacity 0.2s; } #sp-mini-launcherhover .sp-shortcut-hint { opacity 1; } リサイズハンドル #sp-resize-handle { position absolute; right 0; bottom 0; width 16px; height 16px; cursor nwse-resize; z-index 10; } #sp-resize-handleafter { content ''; position absolute; right 3px; bottom 3px; width 8px; height 8px; border-right 2px solid rgba(255,255,255,0.25); border-bottom 2px solid rgba(255,255,255,0.25); } `; document.head.appendChild(style); ====== パネルHTML ====== ====== 起動時:永続非表示フラグチェック ====== if (GM_getValue('panelHidden', false)) { パネルは起動しない。全自動モードのみ裏で継続実行。 if (GM_getValue('autoMode', false)) { (async () = { await sleep(1500); const threads = await runScan(); if (threads.length 0) await runSend(threads.map(t = ({ dataset { board t.board, no t.threadNo, url t.threadUrl } }))); })(); } Tampermonkeyメニューから復活できるよう登録 GM_registerMenuCommand('🛡️ パネルを再表示する', () = { GM_setValue('panelHidden', false); location.reload(); }); return; 以降のUI生成をスキップ } Tampermonkeyメニュー(パネル表示中も一応登録しておく) GM_registerMenuCommand('🛡️ パネルを隠す (永続)', () = { GM_setValue('panelHidden', true); location.reload(); }); ====== 常駐ミニボタン ====== const miniLauncher = document.createElement('div'); miniLauncher.id = 'sp-mini-launcher'; miniLauncher.innerHTML = '🛡️span class=sp-shortcut-hintAlt+Dspan'; document.body.appendChild(miniLauncher); const panel = document.createElement('div'); panel.id = 'spam-panel'; panel.innerHTML = ` div id=spam-header span🛡️ 荒らし一括削除依頼span span id=sp-auto-badge⚡ 全自動span div id=sp-header-btns button class=sp-hbtn id=sp-toggle-btn title=折りたたむ-button button class=sp-hbtn id=sp-close-btn title=閉じる×button div div div id=spam-body !-- 全自動モード -- div id=sp-auto-row div id=sp-auto-label ⚡ 全自動モード span id=sp-auto-sublabelページ読込時に自動スキャン&一括送信span div label class=sp-toggle-wrap input type=checkbox id=sp-auto-toggle span class=sp-toggle-sliderspan label div div div class=sp-label取得元リストdiv select id=sp-source-select option value=httpsfutaba-id.siteid_list_recent.html最新出現順option option value=httpsfutaba-id.siteid_list_img.htmlimg出現数順option option value=httpsfutaba-id.siteid_list_count.html総出現数順option select div button id=sp-scan-btn🔍 最新荒らしスレをスキャンbutton div div style=displayflex; justify-contentspace-between; align-itemscenter; margin-bottom3px; span class=sp-label生存スレ一覧span div class=sp-row style=gap3px; button class=sp-mini-btn id=sp-all全選択button button class=sp-mini-btn id=sp-none解除button div div div id=sp-thread-wrap div id=sp-emptyスキャンボタンを押してくださいdiv div div div div class=sp-label削除理由div select id=sp-reason option value=110110 - スパム・連続投稿option option value=111111 - 個人情報・晒しoption option value=112112 - 著作権侵害option option value=113113 - その他option select div button id=sp-send-btn disabled🗑️ 選択したスレに一括削除依頼button div div class=sp-label実行ログdiv div id=sp-log待機中...ndiv div div div id=sp-resize-handlediv `; document.body.appendChild(panel); ====== 要素取得 ====== const header = document.getElementById('spam-header'); const body = document.getElementById('spam-body'); const toggleBtn = document.getElementById('sp-toggle-btn'); const closeBtn = document.getElementById('sp-close-btn'); const autoToggle = document.getElementById('sp-auto-toggle'); const autoBadge = document.getElementById('sp-auto-badge'); const sourceSelect = document.getElementById('sp-source-select'); const scanBtn = document.getElementById('sp-scan-btn'); const threadWrap = document.getElementById('sp-thread-wrap'); const allBtn = document.getElementById('sp-all'); const noneBtn = document.getElementById('sp-none'); const reasonSel = document.getElementById('sp-reason'); const sendBtn = document.getElementById('sp-send-btn'); const logEl = document.getElementById('sp-log'); const resizeHandle = document.getElementById('sp-resize-handle'); ====== 設定を永続化 ====== autoToggle.checked = GM_getValue('autoMode', false); sourceSelect.value = GM_getValue('source', 'httpsfutaba-id.siteid_list_recent.html'); reasonSel.value = GM_getValue('reason', '110'); autoToggle.addEventListener('change', () = { GM_setValue('autoMode', autoToggle.checked); autoBadge.style.display = autoToggle.checked 'inline-block' 'none'; }); sourceSelect.addEventListener('change', () = GM_setValue('source', sourceSelect.value)); reasonSel.addEventListener('change', () = GM_setValue('reason', reasonSel.value)); 起動時バッジ反映 autoBadge.style.display = autoToggle.checked 'inline-block' 'none'; ====== ユーティリティ ====== function log(msg) { logEl.innerText += msg + 'n'; logEl.scrollTop = logEl.scrollHeight; } function gmFetch(url) { return new Promise((resolve, reject) = { GM_xmlhttpRequest({ method 'GET', url url, onload r = resolve(r.responseText), onerror e = reject(e) }); }); } function sleep(ms) { return new Promise(r = setTimeout(r, ms)); } ====== スキャン処理(共通) ====== async function runScan() { scanBtn.disabled = true; sendBtn.disabled = true; threadWrap.innerHTML = 'div id=sp-emptyスキャン中...div'; logEl.innerText = ''; log('📡 リスト取得中 ' + sourceSelect.value); let listHtml; try { listHtml = await gmFetch(sourceSelect.value); } catch (e) { log('❌ リスト取得失敗 ' + e); scanBtn.disabled = false; return []; } const parser = new DOMParser(); const listDoc = parser.parseFromString(listHtml, 'texthtml'); const idLinks = [...listDoc.querySelectorAll('a[href^=id_view.phpid=]')]; const ids = idLinks.slice(0, 10).map(a = { const url = new URL(a.href, 'httpsfutaba-id.site'); return { id url.searchParams.get('id'), label a.textContent.trim() }; }); log(`✅ ${ids.length} 件のIDを検出。生存スレをスキャン中...`); const allThreads = []; for (const { id, label } of ids) { log(`🔎 ${label} をスキャン中...`); let viewHtml; try { viewHtml = await gmFetch(`httpsfutaba-id.siteid_view.phpid=${encodeURIComponent(id)}`); } catch (e) { log(` ⚠️ 取得失敗 ${label}`); await sleep(500); continue; } const viewDoc = parser.parseFromString(viewHtml, 'texthtml'); viewDoc.querySelectorAll('table tr').forEach((row, i) = { if (i === 0) return; const tds = row.querySelectorAll('td'); if (tds.length 5) return; const board = tds[0].textContent.trim(); const link = tds[2].querySelector('a'); if (!link) return; const threadNo = (link.href.match(res(d+).htm) [])[1]; if (!threadNo) return; allThreads.push({ board, threadNo, threadUrl link.href, text tds[4].textContent.trim(), idLabel label }); }); await sleep(400); } return allThreads; } ====== スレ一覧を描画 ====== function renderThreads(allThreads) { threadWrap.innerHTML = ''; if (allThreads.length === 0) { threadWrap.innerHTML = 'div id=sp-empty生存スレが見つかりませんでしたdiv'; log('😴 生存スレなし'); scanBtn.disabled = false; return; } allThreads.forEach(t = { const item = document.createElement('div'); item.className = 'sp-thread-item'; item.innerHTML = ` input type=checkbox class=sp-chk data-board=${t.board} data-no=${t.threadNo} data-url=${t.threadUrl} checked div class=sp-thread-meta a class=sp-thread-link href=${t.threadUrl} target=_blank [${t.board.toUpperCase()}] No.${t.threadNo} (${t.idLabel}) a span class=sp-thread-sub title=${t.text}${t.text}span div `; threadWrap.appendChild(item); }); log(`✅ 合計 ${allThreads.length} 件の生存スレを検出しました`); sendBtn.disabled = false; scanBtn.disabled = false; } ====== 一括送信処理(共通) ====== async function runSend(targets) { sendBtn.disabled = true; scanBtn.disabled = true; const reason = reasonSel.value; log(`n🚀 一括送信開始 (${targets.length}件, 理由コード${reason})`); for (let i = 0; i targets.length; i++) { const cb = targets[i]; const board = cb.dataset.board; const threadNo = cb.dataset.no; const threadUrl = cb.dataset.url; log(`[${i + 1}${targets.length}] No.${threadNo} (${board}) → 送信中...`); const body = new URLSearchParams({ mode 'post', b 'b', d threadNo, reason reason, responsemode 'ajax' }); await new Promise(resolve = { GM_xmlhttpRequest({ method 'POST', url `https${board}.2chan.netdel.php`, headers { 'Content-Type' 'applicationx-www-form-urlencoded', 'Referer' threadUrl, 'Origin' `https${board}.2chan.net` }, data body.toString(), onload r = { log(` → ${r.responseText.trim() '送信完了'}`); resolve(); }, onerror e = { log(` → ❌ エラー ${e}`); resolve(); } }); }); await sleep(3000); } log('🎉 全件送信完了!'); sendBtn.disabled = false; scanBtn.disabled = false; } ====== 手動スキャンボタン ====== scanBtn.addEventListener('click', async () = { const threads = await runScan(); renderThreads(threads); }); ====== 手動送信ボタン ====== sendBtn.addEventListener('click', async () = { const targets = [...document.querySelectorAll('.sp-chkchecked')]; if (targets.length === 0) { alert('送信対象のスレを選択してください'); return; } if (!confirm(`${targets.length} 件のスレに削除依頼を送信しますか?`)) return; await runSend(targets); }); ====== 全選択・解除 ====== allBtn.addEventListener('click', () = document.querySelectorAll('.sp-chk').forEach(cb = cb.checked = true)); noneBtn.addEventListener('click', () = document.querySelectorAll('.sp-chk').forEach(cb = cb.checked = false)); ====== 全自動モード(ページ読込時) ====== if (autoToggle.checked) { (async () = { await sleep(1500); ページ安定待ち log('⚡ 全自動モード起動中...'); const threads = await runScan(); if (threads.length === 0) { renderThreads([]); return; } renderThreads(threads); await sleep(500); 全チェックを確認してそのまま全送信 const targets = [...document.querySelectorAll('.sp-chkchecked')]; await runSend(targets); })(); } ====== 折りたたみ ====== let collapsed = false; toggleBtn.addEventListener('click', () = { collapsed = !collapsed; body.classList.toggle('collapsed', collapsed); toggleBtn.textContent = collapsed '+' '-'; panel.style.maxHeight = collapsed '36px' '85vh'; }); ====== ドラッグ移動 ====== let dragging = false, sx, sy, il, it; header.addEventListener('mousedown', e = { if (e.target.closest('#sp-header-btns')) return; dragging = true; sx = e.clientX; sy = e.clientY; const r = panel.getBoundingClientRect(); il = r.left; it = r.top; panel.style.right = 'auto'; panel.style.left = il + 'px'; panel.style.top = it + 'px'; }); document.addEventListener('mousemove', e = { if (!dragging) return; panel.style.left = (il + e.clientX - sx) + 'px'; panel.style.top = (it + e.clientY - sy) + 'px'; }); document.addEventListener('mouseup', () = dragging = false); ====== リサイズ ====== let resizing = false, rsx, rsy, rw, rh; resizeHandle.addEventListener('mousedown', e = { e.stopPropagation(); resizing = true; rsx = e.clientX; rsy = e.clientY; const r = panel.getBoundingClientRect(); rw = r.width; rh = r.height; panel.style.maxHeight = 'none'; }); document.addEventListener('mousemove', e = { if (!resizing) return; panel.style.width = Math.max(200, rw + (e.clientX - rsx)) + 'px'; panel.style.height = Math.max(80, rh + (e.clientY - rsy)) + 'px'; }); document.addEventListener('mouseup', () = resizing = false); ====== パネル表示・非表示の切り替え ====== function showPanel() { panel.style.display = 'flex'; miniLauncher.style.display = 'none'; } function hidePanel() { panel.style.display = 'none'; ミニボタンをパネルの位置に合わせて表示 const r = panel.getBoundingClientRect(); miniLauncher.style.top = (r.top) + 'px'; miniLauncher.style.left = (r.left) + 'px'; miniLauncher.style.right = 'auto'; miniLauncher.style.display = 'flex'; } function togglePanel() { panel.style.display === 'none' showPanel() hidePanel(); } closeBtn.addEventListener('click', () = { GM_setValue('panelHidden', true); panel.style.display = 'none'; miniLauncher.style.display = 'none'; 次回ロード時から永続非表示。Tampermonkeyメニューから「パネルを再表示」で復活可能。 }); miniLauncher.addEventListener('click', showPanel); Alt + D でトグル document.addEventListener('keydown', e = { if (e.altKey && e.key === 'd') { e.preventDefault(); togglePanel(); } }); })();