Массовое удаление твитов, без расширений
Бесплатный скрипт для вставки в консоль Chrome, чтобы навести порядок в старых постах X: сначала симуляция, свежие твиты под защитой, удаление ограниченными пакетами. Никаких расширений, никаких сторонних сервисов, ничто не покидает ваш браузер.
DRY_RUN = true): он показывает список того, что будет удалено, ничего не трогая. Заранее скачайте свой архив X, проверьте симуляцию и только затем переходите к реальному удалению. Автоматизация интерфейса не предусмотрена условиями использования X: действуйте осторожно и на свой страх и риск.
Порядок действий
- Откройте свой профиль: перейдите на
x.com/vash_login, вкладка Посты, войдя в свою учётную запись. - Откройте консоль: нажмите
F12(илиCmd+Option+Jна Mac), затем перейдите на вкладку Console. - Настройте скрипт: скопируйте код ниже, замените
vash_loginсвоим идентификатором X (без @), настройтеKEEP_RECENT(сколько свежих твитов сохранить) и при необходимости перечислите вKEEP_STATUS_IDSидентификаторы твитов, которые никогда не следует удалять. - Запустите симуляцию: вставьте скрипт в консоль и подтвердите клавишей Enter. При
DRY_RUN = true(настройка по умолчанию) он покажет список твитов, которые были бы удалены, ничего не удаляя. - Удалите: если симуляция вас устраивает, переключите
DRY_RUNнаfalseи запустите заново. Скрипт удаляет не более 40 твитов за один запуск, с паузами от 6 до 14 секунд. - Повторите: запускайте скрипт столько раз, сколько нужно, чтобы обработать остальное, пакетами.
Скрипт
(() => {
// ============ КОНФИГУРАЦИЯ ============
const HANDLE = "vash_login"; // ваш идентификатор X, без @
const KEEP_RECENT = 15; // НИКОГДА не удаляет 15 самых свежих твитов (сверху)
const KEEP_STATUS_IDS = [ // дополнительная защита (по желанию): идентификаторы, которые нельзя удалять
"1234567890123456789",
];
let DRY_RUN = true; // true = симуляция (только список). false = РЕАЛЬНОЕ УДАЛЕНИЕ.
const MIN_DELAY_MS = 6000;
const MAX_DELAY_MS = 14000;
const MAX_DELETIONS = 40; // предел за один запуск (запустите снова, чтобы продолжить)
// =====================================
const me = HANDLE.toLowerCase();
const KEEP = new Set(KEEP_STATUS_IDS.map(String));
const protectedRecent = new Set();
const RX_DELETE = /(Удалить|delete|supprimer)/i;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const rand = (a, b) => Math.floor(a + Math.random() * (b - a));
const skipped = new Set();
if (!location.pathname.toLowerCase().startsWith("/" + me)) {
console.warn(`%c[СТОП] Перейдите на https://x.com/${HANDLE} (вкладка Посты) и запустите снова.`,
"color:#e0245e;font-weight:bold");
return;
}
const articles = () => [...document.querySelectorAll('article[data-testid="tweet"]')];
const idOf = (art) => {
const href = [...art.querySelectorAll('a[href*="/status/"]')]
.map((x) => x.getAttribute("href"))
.find((h) => new RegExp(`^/${HANDLE}/status/\\d+`, "i").test(h));
if (!href) return null;
const m = href.match(/status\/(\d+)/);
return m ? m[1] : null;
};
// Загружает и защищает KEEP_RECENT самых свежих твитов (те, что сверху).
async function protectRecent() {
const order = [], seen = new Set();
let noNew = 0;
window.scrollTo(0, 0);
await sleep(1200);
while (order.length < KEEP_RECENT && noNew < 6) {
let added = 0;
for (const art of articles()) {
const id = idOf(art);
if (id && !seen.has(id)) { seen.add(id); order.push(id); added++; }
}
noNew = added ? 0 : noNew + 1;
if (order.length >= KEEP_RECENT) break;
const cs = articles();
(cs[cs.length - 1] || document.body).scrollIntoView({ block: "end" });
await sleep(1400);
}
for (const id of order.slice(0, KEEP_RECENT)) protectedRecent.add(id);
return order.slice(0, KEEP_RECENT);
}
const eligible = (art) => {
const id = idOf(art);
if (!id) return null;
if (protectedRecent.has(id)) return null; // один из самых свежих
if (KEEP.has(id)) return null; // ID под дополнительной защитой
if (skipped.has(id)) return null;
return id;
};
async function deleteOne(art, id) {
const caret = art.querySelector('[data-testid="caret"]');
if (!caret) { skipped.add(id); return false; }
caret.click();
await sleep(700);
const del = [...document.querySelectorAll('[role="menuitem"]')]
.find((i) => RX_DELETE.test((i.innerText || "").trim()));
if (!del) { document.body.click(); skipped.add(id); return false; }
del.click();
await sleep(700);
const confirmBtn = document.querySelector('[data-testid="confirmationSheetConfirm"]');
if (!confirmBtn) { skipped.add(id); return false; }
confirmBtn.click();
await sleep(1500);
return true;
}
async function dryScan() {
const seen = new Set();
let noNew = 0;
console.log("%c[СИМУЛЯЦИЯ] Ничего не будет удалено. Подсчёт...", "color:#1d9bf0;font-weight:bold");
while (noNew < 4) {
let added = 0;
for (const art of articles()) {
const id = eligible(art);
if (id && !seen.has(id)) { seen.add(id); added++; }
}
noNew = added ? 0 : noNew + 1;
const cs = articles();
(cs[cs.length - 1] || document.body).scrollIntoView({ block: "end" });
await sleep(1200);
}
console.log(`%c[СИМУЛЯЦИЯ] БЫЛО БЫ удалено твитов: ${seen.size} (самые свежие под защитой).`,
"color:#1d9bf0;font-weight:bold");
console.log([...seen].slice(0, 60));
}
async function run() {
let count = 0, guard = 0, noHit = 0, started = false;
while (count < MAX_DELETIONS && guard++ < 1500) {
const pair = articles().map((a) => [a, eligible(a)]).find(([, id]) => id);
if (!pair) {
if ((started && noHit >= 6) || noHit >= 40) break;
const cs = articles();
(cs[cs.length - 1] || document.body).scrollIntoView({ block: "end" });
await sleep(1500);
noHit++;
continue;
}
started = true; noHit = 0;
const [node, id] = pair;
node.scrollIntoView({ block: "center" });
await sleep(600);
const ok = await deleteOne(node, id);
if (ok) count++;
console.log(`${ok ? "удалён" : "пропущен"} ${id} (всего: ${count}/${MAX_DELETIONS})`);
await sleep(rand(MIN_DELAY_MS, MAX_DELAY_MS));
}
console.log(`%c[ГОТОВО] Удалено за этот проход: ${count}. Запустите скрипт снова, если что-то осталось.`,
"color:#00ba7c;font-weight:bold");
}
(async () => {
console.log(`Загрузка и защита ${KEEP_RECENT} самых свежих твитов...`);
const recent = await protectRecent();
if (recent.length < KEEP_RECENT) {
console.warn(`%c[СТОП] Загружено только ${recent.length} твитов (X ограничил скорость). Обновите страницу (F5) и попробуйте снова.`,
"color:#e0245e;font-weight:bold");
return;
}
console.log("%cПод защитой (самые свежие):", "color:#00ba7c;font-weight:bold", recent);
if (DRY_RUN) { await dryScan(); return; }
await run();
})();
})();
Как скрипт вас защищает
В него встроены четыре предохранителя. Прежде всего симуляция по умолчанию: пока DRY_RUN равен true, ничего не удаляется, вы получаете лишь список затронутых твитов. Затем защита свежих: скрипт сначала запоминает ваши самые свежие твиты (15 по умолчанию) и никогда их не трогает. Далее белый список: любой идентификатор, помещённый в KEEP_STATUS_IDS, неприкосновенен, что удобно для закреплённых твитов или важных объявлений. Наконец, осторожный ритм: случайные паузы от 6 до 14 секунд между удалениями и автоматическая остановка после 40, чтобы не вызвать ограничения X.
Перед большой уборкой сохраните то, что важно: извлечение e-mail и URL соберёт все ссылки из выгрузки твитов, счётчик слов покажет, сколько весили ваши годы постов, а очистка текста приведёт в порядок вставленный из архива фрагмент. А для будущих твитов пригодятся стилизованный текст Unicode и конвертер регистра. Все инструменты из набора инструментов работают в вашем браузере, как и это руководство, в духе нашей политики конфиденциальности.
Частые вопросы
Можно ли отменить удаление?
Нет. Удалённый твит исчезает навсегда, в X нет корзины. Именно поэтому скрипт поставляется в режиме симуляции (DRY_RUN = true): сначала запустите его как есть, чтобы получить список того, что будет удалено, проверьте его и переключайте на false только тогда, когда полностью уверены. Также заранее скачайте свой архив X (Настройки, затем Ваша учётная запись, затем Скачать архив данных).
Может ли скрипт удалить чужие твиты?
Нет. X показывает кнопку «Удалить» только на ваших собственных постах: скрипт может работать лишь с той учётной записью, под которой вы вошли, на вашей же странице профиля. Он не трогает ни чужие ответы, ни любой сторонний контент.
Зачем ограничение в 40 удалений и случайные паузы?
Чтобы вести себя бережно с платформой. Слишком быстрые или слишком массовые удаления могут вызвать ограничения X и даже временную блокировку учётной записи. Скрипт ждёт от 6 до 14 секунд между удалениями и останавливается после 40: запустите его снова, чтобы продолжить пакетами.
Разрешено ли это правилами X?
Вы удаляете собственный контент, что является вашим правом. Однако автоматизация веб-интерфейса не предусмотрена условиями использования X: применяйте этот скрипт умеренно и на свой страх и риск. Официальная альтернатива: удаление вручную или специализированные сервисы, работающие через API.
Как защитить некоторые конкретные твиты?
Две защиты действуют одновременно: KEEP_RECENT автоматически сохраняет ваши самые свежие твиты (15 по умолчанию), а KEEP_STATUS_IDS принимает список идентификаторов, которые никогда не будут удалены. Идентификатор твита: это большое число в конце его адреса: x.com/login/status/1234567890123456789.