File: //workspace/datathrop_nuke.py
#!/usr/bin/env python3
"""
datathrop_nuke.py — Extract and delete all @datathrop tweets before 2026-03-13.
Day-windowed SearchTimeline -> collect -> delete/unretweet.
Saves progress to /workspace/datathrop_progress.json after every day.
"""
import json, subprocess, time, sys, os
from datetime import datetime, timedelta, date
WRAPPER = "/root/clawd/skills/x-twitter/scripts/x_twitter.py"
PROFILE = "datathrop"
CUTOFF = date(2026, 3, 13)
START = date(2011, 7, 1)
PROGRESS_FILE = "/workspace/datathrop_progress.json"
LOG_FILE = "/workspace/datathrop_nuke.log"
DELAY_OK = 2.5
DELAY_429 = 90.0
MAX_429_RETRIES = 8
def log(msg):
ts = datetime.utcnow().isoformat(timespec='seconds')
line = f"[{ts}] {msg}"
print(line, flush=True)
try:
with open(LOG_FILE, 'a') as f:
f.write(line + '\n')
except:
pass
def load_progress():
if os.path.exists(PROGRESS_FILE):
try:
with open(PROGRESS_FILE) as f:
return json.load(f)
except:
pass
return {
'next_day': START.isoformat(),
'found': 0,
'deleted': 0,
'failed': 0,
'seen_ids': [],
}
def save_progress(p):
tmp = PROGRESS_FILE + '.tmp'
with open(tmp, 'w') as f:
json.dump(p, f)
os.replace(tmp, PROGRESS_FILE)
def search_day(day):
next_day = day + timedelta(days=1)
q = f"from:datathrop since:{day.isoformat()} until:{next_day.isoformat()}"
for attempt in range(MAX_429_RETRIES):
r = subprocess.run(
['python3', WRAPPER, '--profile-key', PROFILE, 'search', q, '--count', '100', '--all'],
capture_output=True, text=True, timeout=120
)
if r.returncode != 0:
if '429' in r.stderr or 'Rate limit' in r.stderr:
log(f" 429 on {day}, attempt {attempt+1}/{MAX_429_RETRIES}, wait {DELAY_429}s")
time.sleep(DELAY_429)
continue
log(f" ERROR {day}: rc={r.returncode} stderr={r.stderr[:300]}")
return []
try:
data = json.loads(r.stdout)
# Handle both list and dict format
if isinstance(data, list):
tweets = data
elif isinstance(data, dict):
tweets = data.get('tweets', [])
else:
tweets = []
return [t for t in tweets if isinstance(t, dict) and t.get('author', {}).get('username') == 'datathrop']
except Exception as e:
log(f" PARSE ERROR {day}: {e}")
return []
log(f" GAVE UP {day} after {MAX_429_RETRIES} retries")
return []
def delete_tweet(tweet_id, is_rt):
url = f"https://x.com/datathrop/status/{tweet_id}"
cmd = 'unretweet' if is_rt else 'delete'
r = subprocess.run(
['python3', WRAPPER, '--profile-key', PROFILE, cmd, url],
capture_output=True, text=True, timeout=60
)
if r.returncode == 0:
try:
data = json.loads(r.stdout)
if isinstance(data, dict) and data.get('ok'):
return True
except:
pass
if 'already' in r.stderr.lower() or 'not found' in r.stderr.lower() or 'not found' in r.stdout.lower():
return True
return False
def main():
progress = load_progress()
seen = set(progress.get('seen_ids', []))
current_day = date.fromisoformat(progress['next_day'])
log(f"=== RESUME {current_day} | found={progress['found']} del={progress['deleted']} fail={progress['failed']} seen={len(seen)} ===")
while current_day < CUTOFF:
tweets = search_day(current_day)
new_count = 0
for t in tweets:
tid = t.get('id')
if not tid or tid in seen:
continue
seen.add(tid)
new_count += 1
text = t.get('text', '')
is_rt = text.startswith('RT @')
ok = delete_tweet(tid, is_rt)
if ok:
progress['deleted'] += 1
if progress['deleted'] % 25 == 0:
log(f" MILESTONE: {progress['deleted']} deleted")
else:
progress['failed'] += 1
log(f" FAIL DELETE {tid} (rt={is_rt}) stderr=...")
time.sleep(1.5)
progress['found'] += new_count
if new_count > 0:
log(f"{current_day}: +{new_count} new, del={progress['deleted']} fail={progress['failed']}")
current_day += timedelta(days=1)
progress['next_day'] = current_day.isoformat()
# Keep seen IDs manageable: last 5000
if len(seen) > 5000:
seen = set(list(seen)[-5000:])
progress['seen_ids'] = list(seen)
save_progress(progress)
time.sleep(DELAY_OK)
log(f"=== DONE | found={progress['found']} del={progress['deleted']} fail={progress['failed']} ===")
if __name__ == '__main__':
main()