#!/usr/bin/env python3 """Remove a member from the Spring 2026 Cohort. This script: 1. Removes from team_assignments.json (so activity checker stops tracking them) 2. Removes from discord_to_github.json 3. Removes from GitHub teams 4. Removes Discord roles 5. Sends a message to the team channel announcing the removal 6. Outputs notes to add to COHORT_NOTES.md Usage: python remove_member.py """ import asyncio import json import os import sys from datetime import datetime, timezone from pathlib import Path import aiohttp DATA_DIR = Path(__file__).parent.parent.parent / "data" DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"] DISCORD_API_BASE = "https://discord.com/api/v10" DISCORD_GUILD_ID = "692816967895220344" COHORT_ROLE_ID = "1464316447591985194" TEXT_CHANNEL_IDS = { "Jade Jasmine": "1464316501573107886", "Crimson Dahlia": "1464316744909852682", "Rose Camellia": "1464316751268286611", "Amber Wisteria": "1464316761410113641", "Ivory Orchid": "1464316770889240730", "Teal Iris": "1464316776459407448", "Peach Gardenia": "1464316785040953543", "Violet Carnation": "1464316805261824032", "Azure Lotus": "1464316814455472139", "Coral Sunflower": "1464316819711066263", "Indigo Tulip": "1464316826384072925", "Scarlet Hydrangea": "1464316839306985506", "Mint Narcissus": "1464316844251807952", "Sage Marigold": "1464316850669093040", } class MemberRemover: """Handles the complete removal of a cohort member.""" def __init__(self, discord_id: str) -> None: self.discord_id = discord_id self.headers = { "Authorization": f"Bot {DISCORD_BOT_TOKEN}", "Content-Type": "application/json", } self.session: aiohttp.ClientSession | None = None self.github_username: str | None = None self.team_name: str | None = None self.role: str | None = None self.team_role_id: str | None = None async def __aenter__(self) -> "MemberRemover": self.session = aiohttp.ClientSession() return self async def __aexit__( self, exc_type: object, exc_val: object, exc_tb: object ) -> None: if self.session: await self.session.close() def find_member_info(self) -> bool: """Find which team the member is on and their role.""" with open(DATA_DIR / "team_assignments.json") as f: teams = json.load(f) for team in teams: if self.discord_id in team["leaders"]: self.team_name = team["name"] self.role = "leader" return True if self.discord_id in team["participants"]: self.team_name = team["name"] self.role = "participant" return True return False def remove_from_team_assignments(self) -> None: """Remove member from team_assignments.json.""" with open(DATA_DIR / "team_assignments.json") as f: teams = json.load(f) for team in teams: if self.discord_id in team["leaders"]: team["leaders"].remove(self.discord_id) print( f"✅ Removed from {team['name']} leaders in team_assignments.json" ) elif self.discord_id in team["participants"]: team["participants"].remove(self.discord_id) print( f"✅ Removed from {team['name']} participants in team_assignments.json" ) with open(DATA_DIR / "team_assignments.json", "w") as f: json.dump(teams, f, indent=2) def remove_from_discord_to_github(self) -> None: """Remove member from discord_to_github.json.""" with open(DATA_DIR / "discord_to_github.json") as f: mappings = json.load(f) if self.discord_id in mappings: self.github_username = mappings[self.discord_id] del mappings[self.discord_id] with open(DATA_DIR / "discord_to_github.json", "w") as f: json.dump(mappings, f, indent=2) print(f"✅ Removed {self.github_username} from discord_to_github.json") else: print("⚠️ No GitHub username found in discord_to_github.json") async def remove_from_github_teams(self) -> None: """Print instructions to remove member from GitHub organization teams.""" if not self.github_username: print("⚠️ Skipping GitHub removal (no username)") return print(f"ℹ️ Please manually remove {self.github_username} from GitHub teams:") print(f" - Team: {self.team_name}") if self.role == "leader": print(f" - Team: {self.team_name} Leaders") async def remove_discord_roles(self) -> None: """Remove Discord roles from the member.""" with open(DATA_DIR / "team_message_ids.json") as f: team_message_data = json.load(f) if self.team_name not in team_message_data: print(f"⚠️ Could not find role ID for team {self.team_name}") return self.team_role_id = team_message_data[self.team_name]["role_id"] url = ( f"{DISCORD_API_BASE}/guilds/{DISCORD_GUILD_ID}" f"/members/{self.discord_id}/roles/{self.team_role_id}" ) async with self.session.delete(url, headers=self.headers) as response: if response.status == 204: print(f"✅ Removed {self.team_name} Discord role") else: print(f"❌ Failed to remove team role: {response.status}") url = ( f"{DISCORD_API_BASE}/guilds/{DISCORD_GUILD_ID}" f"/members/{self.discord_id}/roles/{COHORT_ROLE_ID}" ) async with self.session.delete(url, headers=self.headers) as response: if response.status == 204: print("✅ Removed Spring 2026 Cohort Discord role") else: print(f"❌ Failed to remove cohort role: {response.status}") async def send_team_message(self) -> None: """Send a message to the team channel announcing the removal.""" channel_id = TEXT_CHANNEL_IDS[self.team_name] message = ( f"<@{self.discord_id}> has been removed from the cohort for inactivity." ) url = f"{DISCORD_API_BASE}/channels/{channel_id}/messages" data = {"content": message} async with self.session.post(url, headers=self.headers, json=data) as response: if response.status == 200: print(f"✅ Sent removal message to {self.team_name} channel") else: print(f"❌ Failed to send message: {response.status}") def generate_cohort_notes(self) -> str: """Generate text to add to COHORT_NOTES.md.""" today = datetime.now(timezone.utc).strftime("%Y-%m-%d") notes = f"\n## {today} - Member Removal\n\n" notes += "| Discord ID | GitHub Username | Team | Role |\n" notes += "|------------|-----------------|------|------|\n" notes += ( f"| {self.discord_id} | {self.github_username or '-'} " f"| {self.team_name} | {self.role.capitalize()} |\n" ) return notes async def main() -> None: """Remove a member from the cohort.""" if len(sys.argv) != 2: print("Usage: python remove_member.py ") sys.exit(1) discord_id = sys.argv[1] async with MemberRemover(discord_id) as remover: if not remover.find_member_info(): print(f"❌ Discord ID {discord_id} not found in any team") sys.exit(1) print("\n📋 Found member:") print(f" Discord ID: {discord_id}") print(f" Team: {remover.team_name}") print(f" Role: {remover.role}") print() confirm = input("Proceed with removal? (yes/no): ") if confirm.lower() != "yes": print("❌ Removal cancelled") sys.exit(0) print("\n🚀 Starting removal process...\n") remover.remove_from_team_assignments() remover.remove_from_discord_to_github() await remover.remove_from_github_teams() await remover.remove_discord_roles() await remover.send_team_message() print("\n" + "=" * 80) print("📝 Add this to COHORT_NOTES.md:") print("=" * 80) print(remover.generate_cohort_notes()) print("=" * 80) print("\n✅ Member removal complete!") if __name__ == "__main__": asyncio.run(main())