// ==UserScript==
// @name ふたば新着荒らし一括削除依頼ツール
// @namespace http://tampermonkey.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 { flex:1; }
#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-hbtn:hover { 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-btn:active { transform: scale(0.97); }
#sp-scan-btn:disabled { 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-item:hover { 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-link:hover { 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-btn:hover { 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-btn:active { transform: scale(0.97); }
#sp-send-btn:disabled { 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-slider::before {
content: '';
position: absolute;
width: 14px;
height: 14px;
left: 3px;
top: 3px;
background: white;
border-radius: 50%;
transition: transform 0.25s;
}
.sp-toggle-wrap input:checked + .sp-toggle-slider {
background: linear-gradient(135deg, #10b981, #059669);
}
.sp-toggle-wrap input:checked + .sp-toggle-slider::before {
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-launcher:hover {
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-launcher:hover .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-handle::after {
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 = '🛡️Alt+D';
document.body.appendChild(miniLauncher);
const panel = document.createElement('div');
panel.id = 'spam-panel';
panel.innerHTML = `
取得元リスト
削除理由
`;
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', 'https://futaba-id.site/id_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 = 'スキャン中...
';
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, 'text/html');
const idLinks = [...listDoc.querySelectorAll('a[href^="id_view.php?id="]')];
const ids = idLinks.slice(0, 10).map(a => {
const url = new URL(a.href, 'https://futaba-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(`https://futaba-id.site/id_view.php?id=${encodeURIComponent(id)}`);
} catch (e) {
log(` ⚠️ 取得失敗: ${label}`);
await sleep(500);
continue;
}
const viewDoc = parser.parseFromString(viewHtml, 'text/html');
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 = '生存スレが見つかりませんでした
';
log('😴 生存スレなし');
scanBtn.disabled = false;
return;
}
allThreads.forEach(t => {
const item = document.createElement('div');
item.className = 'sp-thread-item';
item.innerHTML = `
`;
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.net/del.php`,
headers: {
'Content-Type': 'application/x-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-chk:checked')];
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-chk:checked')];
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();
}
});
})();