generated from nhcarrigan/template
4fdb5d06f1
- Port 19 cohort scripts from /home/naomi/docs/cohort/ - Replace all hardcoded tokens and dotenv usage with os.environ - Add pandas==3.0.1 dependency - Add E501 to ruff ignore list for Discord message string content - Make remove_resigned_members.py reusable (empty RESIGNED_IDS constant) - Make update_roster_messages.py reusable (iterates all teams from JSON) - Exclude 12 one-off/event-specific scripts as non-reusable
245 lines
8.3 KiB
Python
245 lines
8.3 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
|
||
|
||
import aiohttp
|
||
|
||
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("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("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("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("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("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("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())
|