HEX
Server: LiteSpeed
System: Linux houston.panomity.com 6.8.0-100-generic #100-Ubuntu SMP PREEMPT_DYNAMIC Tue Jan 13 16:40:06 UTC 2026 x86_64
User: nudepix (1011)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
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()