From 6184801fed0dc391e3a12f6a40eaaa725085838d Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Fri, 23 Jan 2026 18:26:39 -0800 Subject: [PATCH] feat: cohort scripts --- prod.env | 1 + python/cohort/assign_cohort_role.py | 86 ++++++++++ python/cohort/assign_team_roles.py | 104 ++++++++++++ python/cohort/generate_member_files.py | 216 +++++++++++++++++++++++++ python/cohort/generate_timeslots.py | 24 +++ python/cohort/send_team_messages.py | 197 ++++++++++++++++++++++ python/cohort/verify_discord.py | 108 +++++++++++++ 7 files changed, 736 insertions(+) create mode 100644 python/cohort/assign_cohort_role.py create mode 100644 python/cohort/assign_team_roles.py create mode 100644 python/cohort/generate_member_files.py create mode 100644 python/cohort/generate_timeslots.py create mode 100644 python/cohort/send_team_messages.py create mode 100644 python/cohort/verify_discord.py diff --git a/prod.env b/prod.env index 8ad11cb..c794742 100644 --- a/prod.env +++ b/prod.env @@ -10,6 +10,7 @@ GITHUB_TOKEN="op://Environment Variables - Development/Ephemere/GitHub Token" DISCORD_TOKEN="op://Environment Variables - Development/Ephemere/Discord Token" DISCORD_CLIENT_ID="op://Private/Guild Counter/client id" DISCORD_CLIENT_SECRET="op://Private/Guild Counter/client secret" +DISCORD_BOT_TOKEN="op://Private/Amari Bot/Token" # AWS AWS_ACCESS_KEY_ID="op://Private/Hetzner/S3 Access Key ID" diff --git a/python/cohort/assign_cohort_role.py b/python/cohort/assign_cohort_role.py new file mode 100644 index 0000000..e4bfce6 --- /dev/null +++ b/python/cohort/assign_cohort_role.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +""" +Assign the Cohort role to all 155 participants. +Respects Discord rate limits with proper backoff and retry logic. +""" + +import json +import os +import time +import requests + +BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"] +GUILD_ID = "692816967895220344" +COHORT_ROLE_ID = "1464314780935258112" + +BASE_URL = "https://discord.com/api/v10" +HEADERS = { + "Authorization": f"Bot {BOT_TOKEN}", + "Content-Length": "0" +} + +def assign_role_with_retry(user_id: str, role_id: str, max_retries: int = 5) -> bool: + url = f"{BASE_URL}/guilds/{GUILD_ID}/members/{user_id}/roles/{role_id}" + + for attempt in range(max_retries): + response = requests.put(url, headers=HEADERS) + + if response.status_code == 204: + return True + elif response.status_code == 429: + # Check headers first, fall back to JSON body + retry_after = response.headers.get("Retry-After") + if retry_after is None: + retry_after = response.headers.get("X-RateLimit-Reset-After") + if retry_after is None: + try: + retry_after = response.json().get("retry_after", 1) + except: + retry_after = 1 + retry_after = float(retry_after) + print(f" Rate limited! Waiting {retry_after:.2f}s before retry...") + time.sleep(retry_after) + else: + print(f" Error {response.status_code}: {response.text}") + backoff_time = (2 ** attempt) * 0.5 + print(f" Retrying in {backoff_time:.2f}s...") + time.sleep(backoff_time) + + return False + +def main(): + with open("team_assignments.json", "r") as f: + teams = json.load(f) + + all_users = [] + for team in teams: + all_users.extend(team["leaders"]) + all_users.extend(team["participants"]) + + unique_users = list(dict.fromkeys(all_users)) + + print(f"Assigning Cohort role to {len(unique_users)} users...") + print(f"Role ID: {COHORT_ROLE_ID}") + print("-" * 50) + + success_count = 0 + fail_count = 0 + + for i, user_id in enumerate(unique_users, 1): + print(f"[{i}/{len(unique_users)}] Assigning to {user_id}...", end=" ") + + if assign_role_with_retry(user_id, COHORT_ROLE_ID): + print("✓") + success_count += 1 + else: + print("✗ FAILED") + fail_count += 1 + + # Small delay between requests to be nice to the API + time.sleep(0.1) + + print("-" * 50) + print(f"Complete! Success: {success_count}, Failed: {fail_count}") + +if __name__ == "__main__": + main() diff --git a/python/cohort/assign_team_roles.py b/python/cohort/assign_team_roles.py new file mode 100644 index 0000000..2280d2f --- /dev/null +++ b/python/cohort/assign_team_roles.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +""" +Assign team-specific roles to all 155 participants. +Respects Discord rate limits with proper backoff and retry logic. +""" + +import json +import os +import time +import requests + +BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"] +GUILD_ID = "692816967895220344" + +BASE_URL = "https://discord.com/api/v10" +HEADERS = { + "Authorization": f"Bot {BOT_TOKEN}", + "Content-Length": "0" +} + +TEAM_ROLE_IDS = { + "Jade Jasmine": "1464314923780931677", + "Crimson Dahlia": "1464315093402784015", + "Rose Camellia": "1464315098452726106", + "Amber Wisteria": "1464315105264275600", + "Ivory Orchid": "1464315109873684593", + "Teal Iris": "1464315114378498152", + "Peach Gardenia": "1464315118904152107", + "Violet Carnation": "1464315124251754559", + "Azure Lotus": "1464315128437801177", + "Coral Sunflower": "1464315132896088168", + "Indigo Tulip": "1464315138428633241", + "Scarlet Hydrangea": "1464315142710890520", + "Mint Narcissus": "1464315149203804405", + "Sage Marigold": "1464315153599299803", +} + + +def assign_role_with_retry(user_id: str, role_id: str, max_retries: int = 5) -> bool: + url = f"{BASE_URL}/guilds/{GUILD_ID}/members/{user_id}/roles/{role_id}" + + for attempt in range(max_retries): + response = requests.put(url, headers=HEADERS) + + if response.status_code == 204: + return True + elif response.status_code == 429: + # Check headers first, fall back to JSON body + retry_after = response.headers.get("Retry-After") + if retry_after is None: + retry_after = response.headers.get("X-RateLimit-Reset-After") + if retry_after is None: + try: + retry_after = response.json().get("retry_after", 1) + except: + retry_after = 1 + retry_after = float(retry_after) + print(f" Rate limited! Waiting {retry_after:.2f}s before retry...") + time.sleep(retry_after) + else: + print(f" Error {response.status_code}: {response.text}") + backoff_time = (2 ** attempt) * 0.5 + print(f" Retrying in {backoff_time:.2f}s...") + time.sleep(backoff_time) + + return False + + +def main(): + with open("team_assignments.json", "r") as f: + teams = json.load(f) + + print(f"Assigning team roles to {len(teams)} teams...") + print("-" * 50) + + total_success = 0 + total_fail = 0 + + for team in teams: + team_name = team["name"] + role_id = TEAM_ROLE_IDS[team_name] + all_members = team["leaders"] + team["participants"] + + print(f"\n[{team_name}] Assigning role to {len(all_members)} members...") + + for user_id in all_members: + print(f" {user_id}...", end=" ") + + if assign_role_with_retry(user_id, role_id): + print("✓") + total_success += 1 + else: + print("✗ FAILED") + total_fail += 1 + + # Small delay between requests to be nice to the API + time.sleep(0.1) + + print("-" * 50) + print(f"Complete! Success: {total_success}, Failed: {total_fail}") + + +if __name__ == "__main__": + main() diff --git a/python/cohort/generate_member_files.py b/python/cohort/generate_member_files.py new file mode 100644 index 0000000..03042eb --- /dev/null +++ b/python/cohort/generate_member_files.py @@ -0,0 +1,216 @@ +import json + +BLOCK_EMOJIS = { + 'mornings': '🌅', + 'afternoons': '☀️', + 'evenings': '🌆', + 'nights': '🌙' +} + +def load_all_data(): + """Load all evaluation data files""" + with open('discord_verification.json', 'r') as f: + verification = json.load(f) + + with open('proficiency_evaluations.json', 'r') as f: + proficiency = json.load(f) + + with open('availability_analysis.json', 'r') as f: + availability = json.load(f) + + with open('leadership_candidates.json', 'r') as f: + candidates = json.load(f) + + with open('leadership_evaluations.json', 'r') as f: + leadership = json.load(f) + + return verification, proficiency, availability, candidates, leadership + +def build_lookup_dicts(verification, proficiency, availability, leadership): + """Build lookup dictionaries by discord_id""" + verified_usernames = {v[0]: v[1] for v in verification['verified']} + + prof_by_id = {p['discord_id']: p for p in proficiency} + + avail_by_id = {a['discord_id']: a for a in availability} + + lead_by_id = {l['discord_id']: l for l in leadership} + + return verified_usernames, prof_by_id, avail_by_id, lead_by_id + +def format_availability_blocks(blocks): + """Format availability blocks with emojis""" + if not blocks: + return "No consistent availability" + + formatted = [] + for block in ['mornings', 'afternoons', 'evenings', 'nights']: + if block in blocks: + formatted.append(f"{BLOCK_EMOJIS[block]} {block.capitalize()}") + return ", ".join(formatted) + +def format_tech_stack(tech_stack): + """Format tech stack list""" + if not tech_stack: + return "Not specified" + return ", ".join(sorted(tech_stack)) + +def generate_participants_md(non_leader_ids, verified_usernames, prof_by_id, avail_by_id): + """Generate participants.md for non-leaders""" + lines = [ + "# Cohort Participants", + "", + f"**Total Participants**: {len(non_leader_ids)}", + "", + "---", + "" + ] + + beginner_count = 0 + intermediate_count = 0 + advanced_count = 0 + + for discord_id in sorted(non_leader_ids): + if discord_id not in verified_usernames: + continue + + username = verified_usernames.get(discord_id, "Unknown") + prof = prof_by_id.get(discord_id, {}) + avail = avail_by_id.get(discord_id, {}) + + proficiency = prof.get('final_proficiency', 'unknown') + tech_stack = prof.get('tech_stack', []) + blocks = avail.get('available_blocks', []) + notes = prof.get('notes', []) + + if proficiency == 'beginner': + beginner_count += 1 + elif proficiency == 'intermediate': + intermediate_count += 1 + elif proficiency == 'advanced': + advanced_count += 1 + + lines.append(f"## {discord_id}") + lines.append(f"**Username**: @{username}") + lines.append(f"**Technical Proficiency**: {proficiency.capitalize()}") + lines.append(f"**Tech Stack**: {format_tech_stack(tech_stack)}") + lines.append(f"**Availability**: {format_availability_blocks(blocks)}") + if notes: + lines.append(f"**Notes**: {', '.join(notes)}") + lines.append("") + + summary = [ + "# Cohort Participants", + "", + f"**Total Participants**: {len([id for id in non_leader_ids if id in verified_usernames])}", + "", + "### Proficiency Breakdown", + f"- Beginner: {beginner_count}", + f"- Intermediate: {intermediate_count}", + f"- Advanced: {advanced_count}", + "", + "---", + "" + ] + + return "\n".join(summary + lines[6:]) + +def leadership_fit_label(score): + """Convert leadership score to label""" + if score >= 6: + return "Excellent" + elif score >= 4: + return "Good" + elif score >= 2: + return "Adequate" + else: + return "Needs Review" + +def generate_leaders_md(leader_ids, verified_usernames, prof_by_id, avail_by_id, lead_by_id): + """Generate leaders.md for leadership candidates""" + verified_leaders = [id for id in leader_ids if id in verified_usernames] + + lines = [ + "# Cohort Leaders", + "", + f"**Total Leaders**: {len(verified_leaders)}", + "", + "---", + "" + ] + + sorted_leaders = sorted(verified_leaders, key=lambda x: lead_by_id.get(x, {}).get('leadership_score', 0), reverse=True) + + for discord_id in sorted_leaders: + username = verified_usernames.get(discord_id, "Unknown") + prof = prof_by_id.get(discord_id, {}) + avail = avail_by_id.get(discord_id, {}) + lead = lead_by_id.get(discord_id, {}) + + proficiency = prof.get('final_proficiency', 'unknown') + tech_stack = prof.get('tech_stack', []) + blocks = avail.get('available_blocks', []) + + leadership_score = lead.get('leadership_score', 0) + leadership_fit = lead.get('leadership_fit', 'unknown') + leadership_notes = lead.get('notes', []) + prof_notes = prof.get('notes', []) + + lines.append(f"## {discord_id}") + lines.append(f"**Username**: @{username}") + lines.append(f"**Leadership Fit**: {leadership_fit.capitalize()} (Score: {leadership_score})") + lines.append(f"**Technical Proficiency**: {proficiency.capitalize()}") + lines.append(f"**Tech Stack**: {format_tech_stack(tech_stack)}") + lines.append(f"**Availability**: {format_availability_blocks(blocks)}") + if leadership_notes: + lines.append(f"**Leadership Notes**: {', '.join(leadership_notes)}") + if prof_notes: + lines.append(f"**Technical Notes**: {', '.join(prof_notes)}") + lines.append("") + + excellent = sum(1 for id in verified_leaders if lead_by_id.get(id, {}).get('leadership_fit') == 'excellent') + good = sum(1 for id in verified_leaders if lead_by_id.get(id, {}).get('leadership_fit') == 'good') + adequate = sum(1 for id in verified_leaders if lead_by_id.get(id, {}).get('leadership_fit') == 'adequate') + + summary = [ + "# Cohort Leaders", + "", + f"**Total Leaders**: {len(verified_leaders)}", + "", + "### Leadership Fit Breakdown", + f"- Excellent: {excellent}", + f"- Good: {good}", + f"- Adequate: {adequate}", + "", + "---", + "" + ] + + return "\n".join(summary + lines[6:]) + +def main(): + verification, proficiency, availability, candidates, leadership = load_all_data() + + verified_usernames, prof_by_id, avail_by_id, lead_by_id = build_lookup_dicts( + verification, proficiency, availability, leadership + ) + + leader_ids = set(candidates['leaders']) + non_leader_ids = set(candidates['non_leaders']) + + verified_ids = set(verified_usernames.keys()) + leader_ids = leader_ids & verified_ids + non_leader_ids = non_leader_ids & verified_ids + + participants_md = generate_participants_md(non_leader_ids, verified_usernames, prof_by_id, avail_by_id) + with open('participants.md', 'w') as f: + f.write(participants_md) + print(f"Generated participants.md with {len(non_leader_ids)} participants") + + leaders_md = generate_leaders_md(leader_ids, verified_usernames, prof_by_id, avail_by_id, lead_by_id) + with open('leaders.md', 'w') as f: + f.write(leaders_md) + print(f"Generated leaders.md with {len(leader_ids)} leaders") + +if __name__ == "__main__": + main() diff --git a/python/cohort/generate_timeslots.py b/python/cohort/generate_timeslots.py new file mode 100644 index 0000000..e34d4bc --- /dev/null +++ b/python/cohort/generate_timeslots.py @@ -0,0 +1,24 @@ +from datetime import datetime, timedelta +import json + +# Generate hourly time slots from Feb 1 to March 3, 2026 +# 24 hours a day, America/Los_Angeles timezone +start_date = datetime(2026, 2, 1, 0, 0) # Feb 1, 2026, midnight +end_date = datetime(2026, 3, 3, 23, 0) # March 3, 2026, 11pm + +times = [] +current = start_date +while current <= end_date: + # Format: YYYY-MM-DDTHH:MM + times.append(current.strftime("%Y-%m-%dT%H:%M")) + current += timedelta(hours=1) + +print(f"Generated {len(times)} time slots") +print(f"First: {times[0]}") +print(f"Last: {times[-1]}") + +# Save to file for use +with open('/home/naomi/docs/cohort/crabfit_timeslots.json', 'w') as f: + json.dump(times, f) + +print("Saved to crabfit_timeslots.json") diff --git a/python/cohort/send_team_messages.py b/python/cohort/send_team_messages.py new file mode 100644 index 0000000..a5f2ee9 --- /dev/null +++ b/python/cohort/send_team_messages.py @@ -0,0 +1,197 @@ +import json +import os +import time +import requests + +# Amari's bot token +TOKEN = os.environ["DISCORD_BOT_TOKEN"] +GUILD_ID = "692816967895220344" + +# File to save message IDs +MESSAGE_IDS_FILE = "team_message_ids.json" + +# Team channel IDs and role IDs +TEAMS = { + "Jade Jasmine": {"channel_id": "1464316501573107886", "role_id": "1464314923780931677"}, + "Crimson Dahlia": {"channel_id": "1464316744909852682", "role_id": "1464315093402784015"}, + "Rose Camellia": {"channel_id": "1464316751268286611", "role_id": "1464315098452726106"}, + "Amber Wisteria": {"channel_id": "1464316761410113641", "role_id": "1464315105264275600"}, + "Ivory Orchid": {"channel_id": "1464316770889240730", "role_id": "1464315109873684593"}, + "Teal Iris": {"channel_id": "1464316776459407448", "role_id": "1464315114378498152"}, + "Peach Gardenia": {"channel_id": "1464316785040953543", "role_id": "1464315118904152107"}, + "Violet Carnation": {"channel_id": "1464316805261824032", "role_id": "1464315124251754559"}, + "Azure Lotus": {"channel_id": "1464316814455472139", "role_id": "1464315128437801177"}, + "Coral Sunflower": {"channel_id": "1464316819711066263", "role_id": "1464315132896088168"}, + "Indigo Tulip": {"channel_id": "1464316826384072925", "role_id": "1464315138428633241"}, + "Scarlet Hydrangea": {"channel_id": "1464316839306985506", "role_id": "1464315142710890520"}, + "Mint Narcissus": {"channel_id": "1464316844251807952", "role_id": "1464315149203804405"}, + "Sage Marigold": {"channel_id": "1464316850669093040", "role_id": "1464315153599299803"}, +} + +# Load team assignments and convert to dict by team name +with open("team_assignments.json", "r") as f: + team_list = json.load(f) + team_data = {team["name"]: team for team in team_list} + +# Load applicants to get project_url by discord_id +with open("applicants_to_evaluate.json", "r") as f: + applicants = json.load(f) + applicant_lookup = {str(a["discord_id"]): a for a in applicants} + +def extract_github_username(url): + """Extract GitHub username from various URL formats""" + if not url: + return "unknown" + + url = url.strip() + + # Handle GitLab special case (RashiqAzhan) + if "gitlab.com" in url: + # We know this is RashiqAzhan from earlier confirmation + return "RashiqAzhan" + + # Handle GitHub Pages URLs + if ".github.io" in url: + # Extract username from username.github.io format + parts = url.replace("https://", "").replace("http://", "").split(".") + if parts: + return parts[0] + + # Handle plain usernames (no URL) + if not url.startswith("http"): + return url + + # Handle standard GitHub URLs + if "github.com" in url: + # Remove protocol and github.com + path = url.replace("https://", "").replace("http://", "").replace("github.com/", "") + # Get just the username (first part of path) + username = path.split("/")[0] + return username + + return url + +def build_message(team_name, role_id, leader_ids, participant_ids): + """Build the welcome message for a team""" + lines = [ + f"# {team_name}", + "", + f"Welcome, <@&{role_id}>. This is your private team channel — a space for you to collaborate, support one another, and build something meaningful together.", + "", + "## Roster", + "", + "**Leadership**", + ] + + for discord_id in leader_ids: + applicant = applicant_lookup.get(str(discord_id), {}) + project_url = applicant.get("project_url", "") + github_username = extract_github_username(project_url) + lines.append(f"- <@{discord_id}>: https://github.com/{github_username}") + + lines.append("") + lines.append("**Participants**") + + for discord_id in participant_ids: + applicant = applicant_lookup.get(str(discord_id), {}) + project_url = applicant.get("project_url", "") + github_username = extract_github_username(project_url) + lines.append(f"- <@{discord_id}>: https://github.com/{github_username}") + + lines.append("") + lines.append("## Project Info") + lines.append("") + lines.append("Coming soon. 💜") + + return "\n".join(lines) + +def send_message(channel_id, content): + """Send a message to a channel""" + url = f"https://discord.com/api/v10/channels/{channel_id}/messages" + headers = { + "Authorization": f"Bot {TOKEN}", + "Content-Type": "application/json" + } + data = {"content": content} + + response = requests.post(url, headers=headers, json=data) + + if response.status_code == 429: + retry_after = float(response.headers.get("Retry-After", response.headers.get("X-RateLimit-Reset-After", 1))) + print(f"Rate limited, waiting {retry_after}s...") + time.sleep(retry_after) + return send_message(channel_id, content) + + if response.status_code == 200: + return response.json() + else: + print(f"Error sending message: {response.status_code} - {response.text}") + return None + +def pin_message(channel_id, message_id): + """Pin a message in a channel""" + url = f"https://discord.com/api/v10/channels/{channel_id}/pins/{message_id}" + headers = { + "Authorization": f"Bot {TOKEN}", + } + + response = requests.put(url, headers=headers) + + if response.status_code == 429: + retry_after = float(response.headers.get("Retry-After", response.headers.get("X-RateLimit-Reset-After", 1))) + print(f"Rate limited, waiting {retry_after}s...") + time.sleep(retry_after) + return pin_message(channel_id, message_id) + + return response.status_code == 204 + +def main(): + message_ids = {} + + for team_name, team_info in TEAMS.items(): + channel_id = team_info["channel_id"] + role_id = team_info["role_id"] + + # Get team members from team_data + team = team_data.get(team_name, {"leaders": [], "participants": []}) + leaders = team.get("leaders", []) + participants = team.get("participants", []) + + # Build the message + message_content = build_message(team_name, role_id, leaders, participants) + + print(f"Sending message to {team_name}...") + + # Send the message + result = send_message(channel_id, message_content) + + if result: + message_id = result["id"] + message_ids[team_name] = { + "channel_id": channel_id, + "message_id": message_id, + "role_id": role_id + } + print(f" Message sent! ID: {message_id}") + + # Pin the message + print(f" Pinning message...") + if pin_message(channel_id, message_id): + print(f" Pinned!") + else: + print(f" Failed to pin") + else: + print(f" Failed to send message") + + # Small delay between teams + time.sleep(0.2) + + # Save message IDs to file + with open(MESSAGE_IDS_FILE, "w") as f: + json.dump(message_ids, f, indent=2) + + print(f"\nDone! Message IDs saved to {MESSAGE_IDS_FILE}") + print(f"Successfully sent and pinned messages for {len(message_ids)} teams") + +if __name__ == "__main__": + main() diff --git a/python/cohort/verify_discord.py b/python/cohort/verify_discord.py new file mode 100644 index 0000000..15812b0 --- /dev/null +++ b/python/cohort/verify_discord.py @@ -0,0 +1,108 @@ +import json +import os +import time +import urllib.request +import urllib.error + +# Configuration +BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"] +GUILD_ID = "692816967895220344" +BASE_URL = "https://discord.com/api/v10" + +# Read Discord IDs from table.md +with open("table.md", "r") as f: + content = f.read() + +lines = content.strip().split("\n") + +# Find the table header line (starts with |) +header_line = None +header_idx = 0 +for i, line in enumerate(lines): + if line.startswith("| Discord"): + header_line = line + header_idx = i + break + +if not header_line: + print("Could not find table header!") + exit(1) + +headers = [h.strip() for h in header_line.split("|")[1:-1]] + +discord_idx = 0 # Discord ID is the first column + +discord_ids = [] +for line in lines[header_idx + 2:]: # Skip header and separator + if not line.startswith("|"): + continue + cols = [c.strip() for c in line.split("|")[1:-1]] + if len(cols) > discord_idx: + discord_id = cols[discord_idx].strip() + if discord_id and discord_id.isdigit(): + discord_ids.append(discord_id) + +print(f"Found {len(discord_ids)} Discord IDs to verify") + +# Verify each ID against the guild +verified = [] +missing = [] +errors = [] + +for i, discord_id in enumerate(discord_ids): + url = f"{BASE_URL}/guilds/{GUILD_ID}/members/{discord_id}" + req = urllib.request.Request(url) + req.add_header("Authorization", f"Bot {BOT_TOKEN}") + + try: + response = urllib.request.urlopen(req) + data = json.loads(response.read().decode()) + username = data.get("user", {}).get("username", "Unknown") + verified.append((discord_id, username)) + print(f"[{i+1}/{len(discord_ids)}] ✓ {discord_id} - {username}") + except urllib.error.HTTPError as e: + if e.code == 404: + missing.append(discord_id) + print(f"[{i+1}/{len(discord_ids)}] ✗ {discord_id} - NOT IN SERVER") + elif e.code == 429: + # Rate limited - wait and retry + retry_after = json.loads(e.read().decode()).get("retry_after", 1) + print(f"[{i+1}/{len(discord_ids)}] Rate limited, waiting {retry_after}s...") + time.sleep(retry_after + 0.5) + # Retry + try: + req2 = urllib.request.Request(url) + req2.add_header("Authorization", f"Bot {BOT_TOKEN}") + response = urllib.request.urlopen(req2) + data = json.loads(response.read().decode()) + username = data.get("user", {}).get("username", "Unknown") + verified.append((discord_id, username)) + print(f"[{i+1}/{len(discord_ids)}] ✓ {discord_id} - {username} (after retry)") + except urllib.error.HTTPError as e2: + if e2.code == 404: + missing.append(discord_id) + print(f"[{i+1}/{len(discord_ids)}] ✗ {discord_id} - NOT IN SERVER (after retry)") + else: + errors.append((discord_id, f"HTTP {e2.code}")) + print(f"[{i+1}/{len(discord_ids)}] ? {discord_id} - Error {e2.code}") + else: + errors.append((discord_id, f"HTTP {e.code}")) + print(f"[{i+1}/{len(discord_ids)}] ? {discord_id} - Error {e.code}") + + # Small delay to avoid rate limits + time.sleep(0.1) + +print(f"\n=== SUMMARY ===") +print(f"Verified: {len(verified)}") +print(f"Missing: {len(missing)}") +print(f"Errors: {len(errors)}") + +# Save results +with open("discord_verification.json", "w") as f: + json.dump({ + "verified": verified, + "missing": missing, + "errors": errors + }, f, indent=2) + +print("\nResults saved to discord_verification.json")