generated from nhcarrigan/template
a40188413a
All Python cohort scripts now use DATA_DIR = Path(__file__).parent.parent.parent / "data" to correctly resolve the repo-root data/ directory regardless of the working directory set by run.sh. All TypeScript scripts have expanded JSDoc headers documenting data file requirements and environment variables.
248 lines
8.5 KiB
Python
248 lines
8.5 KiB
Python
#!/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 <discord_id>
|
||
"""
|
||
|
||
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 <discord_id>")
|
||
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())
|