File: //workspace/karateka_nuke.py
#!/usr/bin/env python3
"""Delete tweets from @Karateka using search window strategy."""
from __future__ import annotations
import json
import subprocess
import time
from datetime import date
from pathlib import Path
WRAPPER = "/root/clawd/skills/x-twitter/scripts/x_twitter.py"
PROFILE = "Karateka"
USER = "Karateka"
START = date(2006, 1, 1)
CUTOFF = date(2026, 3, 13)
DELAY = 1.0
DELAY_429 = 90.0
DELAY_DELETE = 1.5
PROGRESS_PATH = Path("/workspace/karateka_progress.json")
LOG_PATH = Path("/workspace/karateka_nuke.log")
def log(msg: str) -> None:
from datetime import datetime
ts = datetime.utcnow().isoformat(timespec="seconds")
line = f"[{ts}] {msg}"
print(line, flush=True)
with LOG_PATH.open("a", encoding="utf-8") as f:
f.write(line + "\n")
def load_progress() -> dict:
if PROGRESS_PATH.exists():
try:
with PROGRESS_PATH.open("r", encoding="utf-8") as f:
return json.load(f)
except Exception:
pass
return {
"next_day": START.isoformat(),
"found": 0,
"deleted": 0,
"failed": 0,
"seen_ids": [],
}
def save_progress(state: dict, seen: set[int | str]) -> None:
state["seen_ids"] = list(seen)[-5000:]
with PROGRESS_PATH.open("w", encoding="utf-8") as f:
json.dump(state, f)
def next_month(d: date) -> date:
return date(d.year + (d.month // 12), d.month % 12 + 1, 1)
def run_search(query: str):
attempts = 0
while True:
attempts += 1
proc = subprocess.run(
[
"python3",
WRAPPER,
"--profile-key",
PROFILE,
"search",
query,
"--count",
"100",
"--all",
"--max-pages",
"12",
],
capture_output=True,
text=True,
timeout=120,
)
if proc.returncode == 0:
raw = (proc.stdout or "").strip()
if not raw:
return []
try:
data = json.loads(raw)
except Exception:
log(f"WARN json parse failed for query={query}")
return []
return [t for t in (data.get("tweets", []) if isinstance(data, dict) else data) if isinstance(t, dict)]
err = (proc.stderr or "") + (proc.stdout or "")
if "429" in err and attempts <= 6:
log(f"429 on {query}, attempt {attempts}, wait {DELAY_429}s")
time.sleep(DELAY_429)
continue
log(f"QUERY ERROR rc={proc.returncode} query={query}: {err[:240]}")
return []
def delete_tweet(tweet: dict) -> bool:
tid = tweet.get("id")
if not tid:
return False
text = tweet.get("text", "")
cmd = "unretweet" if str(text).startswith("RT @") else "delete"
url = f"https://x.com/{USER}/status/{tid}"
proc = subprocess.run(
["python3", WRAPPER, "--profile-key", PROFILE, cmd, url],
capture_output=True,
text=True,
timeout=120,
)
if proc.returncode != 0:
low = (proc.stderr + proc.stdout).lower()
if "not found" in low or "does not exist" in low:
return True
if "429" in low:
time.sleep(DELAY_429)
return delete_tweet(tweet)
return False
try:
data = json.loads(proc.stdout)
if isinstance(data, dict) and data.get("ok"):
return True
except Exception:
pass
return '"status_code": 200' in proc.stdout
def main() -> None:
state = load_progress()
cur = date.fromisoformat(state.get("next_day", START.isoformat()))
if cur < START:
cur = START
found = state.get("found", 0)
deleted = state.get("deleted", 0)
failed = state.get("failed", 0)
seen = set(state.get("seen_ids", []))
log(f"=== START Karateka | from {cur} ===")
while cur < CUTOFF:
nxt = next_month(cur)
q = f"from:{USER} since:{cur.isoformat()} until:{nxt.isoformat()}"
tweets = run_search(q)
added = 0
for t in tweets:
tid = t.get("id")
if not tid or tid in seen:
continue
seen.add(tid)
added += 1
found += 1
if delete_tweet(t):
deleted += 1
log(f"DELETE OK {tid}")
else:
failed += 1
log(f"DELETE FAIL {tid}")
time.sleep(DELAY_DELETE)
if added:
log(f"{cur:%Y-%m}: +{added} new")
state["next_day"] = nxt.isoformat()
state["found"] = found
state["deleted"] = deleted
state["failed"] = failed
save_progress(state, seen)
cur = nxt
time.sleep(DELAY)
log(f"=== DONE Karateka | found={found} deleted={deleted} failed={failed} ===")
if __name__ == "__main__":
main()