generated from nhcarrigan/template
feat: port remaining cohort scripts and make reusable
- 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
This commit is contained in:
@@ -0,0 +1,516 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Catch-Up Activity Report.
|
||||||
|
|
||||||
|
Generates a markdown report of Discord and GitHub activity since Feb 15, 2026.
|
||||||
|
Covers Discord messages in team channels (+ threads) and GitHub activity
|
||||||
|
(PRs opened, issues opened, issue comments, PR comments, PR reviews, commits).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]
|
||||||
|
DISCORD_API_BASE = "https://discord.com/api/v10"
|
||||||
|
GITHUB_API_BASE = "https://api.github.com"
|
||||||
|
GITHUB_ORG = "nhcarrigan-spring-2026-cohort"
|
||||||
|
|
||||||
|
CUTOFF = datetime(2026, 2, 15, 0, 0, 0, tzinfo=timezone.utc)
|
||||||
|
CUTOFF_ISO = CUTOFF.isoformat().replace("+00:00", "Z")
|
||||||
|
|
||||||
|
OUTPUT_FILE = "catch_up_report.md"
|
||||||
|
|
||||||
|
TEXT_CHANNEL_IDS: dict[str, str] = {
|
||||||
|
"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",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def team_repo_slug(team_name: str) -> str:
|
||||||
|
"""Convert a team name to its repository slug."""
|
||||||
|
return team_name.lower().replace(" ", "-")
|
||||||
|
|
||||||
|
|
||||||
|
def get_github_token() -> str:
|
||||||
|
"""Retrieve the GitHub token via the gh CLI."""
|
||||||
|
result = subprocess.run(
|
||||||
|
["gh", "auth", "token"], capture_output=True, text=True, check=True
|
||||||
|
)
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityCollector:
|
||||||
|
"""Collects Discord and GitHub activity for the catch-up report."""
|
||||||
|
|
||||||
|
def __init__(self, discord_token: str, github_token: str) -> None:
|
||||||
|
self.discord_headers = {
|
||||||
|
"Authorization": f"Bot {discord_token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
self.github_headers = {
|
||||||
|
"Authorization": f"Bearer {github_token}",
|
||||||
|
"Accept": "application/vnd.github+json",
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
}
|
||||||
|
self.session: aiohttp.ClientSession | None = None
|
||||||
|
|
||||||
|
async def __aenter__(self) -> "ActivityCollector":
|
||||||
|
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()
|
||||||
|
|
||||||
|
async def get_discord_username(self, user_id: str) -> str:
|
||||||
|
"""Fetch a Discord user's display name or username."""
|
||||||
|
url = f"{DISCORD_API_BASE}/users/{user_id}"
|
||||||
|
async with self.session.get(url, headers=self.discord_headers) as response:
|
||||||
|
if response.status == 429:
|
||||||
|
retry_after = float((await response.json()).get("retry_after", 1))
|
||||||
|
await asyncio.sleep(retry_after)
|
||||||
|
return await self.get_discord_username(user_id)
|
||||||
|
if response.status != 200:
|
||||||
|
return "*(unknown)*"
|
||||||
|
data = await response.json()
|
||||||
|
return data.get("global_name") or data.get("username") or "*(unknown)*"
|
||||||
|
|
||||||
|
async def _get_discord_thread_ids(self, channel_id: str) -> list[str]:
|
||||||
|
"""Return IDs of all active and archived threads in a channel."""
|
||||||
|
thread_ids: list[str] = []
|
||||||
|
|
||||||
|
url = f"{DISCORD_API_BASE}/channels/{channel_id}/threads/active"
|
||||||
|
async with self.session.get(url, headers=self.discord_headers) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
data = await response.json()
|
||||||
|
thread_ids.extend(t["id"] for t in data.get("threads", []))
|
||||||
|
|
||||||
|
for archive_type in ("public", "private"):
|
||||||
|
url = (
|
||||||
|
f"{DISCORD_API_BASE}/channels/{channel_id}"
|
||||||
|
f"/threads/archived/{archive_type}"
|
||||||
|
)
|
||||||
|
async with self.session.get(url, headers=self.discord_headers) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
data = await response.json()
|
||||||
|
thread_ids.extend(t["id"] for t in data.get("threads", []))
|
||||||
|
|
||||||
|
return thread_ids
|
||||||
|
|
||||||
|
async def _count_messages_in_channel(
|
||||||
|
self, channel_id: str, label: str = ""
|
||||||
|
) -> dict[str, int]:
|
||||||
|
"""Count messages per Discord user ID since CUTOFF."""
|
||||||
|
counts: dict[str, int] = {}
|
||||||
|
before_id: str | None = None
|
||||||
|
page = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
url = f"{DISCORD_API_BASE}/channels/{channel_id}/messages?limit=100"
|
||||||
|
if before_id:
|
||||||
|
url += f"&before={before_id}"
|
||||||
|
|
||||||
|
async with self.session.get(url, headers=self.discord_headers) as response:
|
||||||
|
if response.status == 429:
|
||||||
|
retry_after = float((await response.json()).get("retry_after", 1))
|
||||||
|
print(f" [Discord] rate limited, waiting {retry_after:.1f}s...")
|
||||||
|
await asyncio.sleep(retry_after)
|
||||||
|
continue
|
||||||
|
if response.status != 200:
|
||||||
|
print(f" [Discord] channel {channel_id} → HTTP {response.status}")
|
||||||
|
break
|
||||||
|
|
||||||
|
messages: list[dict] = await response.json()
|
||||||
|
if not messages:
|
||||||
|
break
|
||||||
|
|
||||||
|
page += 1
|
||||||
|
prefix = f" ({label})" if label else ""
|
||||||
|
print(
|
||||||
|
f" [Discord]{prefix} page {page} — {len(messages)} messages fetched", # noqa: E501
|
||||||
|
end="\r",
|
||||||
|
)
|
||||||
|
|
||||||
|
reached_cutoff = False
|
||||||
|
for message in messages:
|
||||||
|
ts = datetime.fromisoformat(
|
||||||
|
message["timestamp"].replace("Z", "+00:00")
|
||||||
|
)
|
||||||
|
if ts < CUTOFF:
|
||||||
|
reached_cutoff = True
|
||||||
|
break
|
||||||
|
if message["author"].get("bot", False):
|
||||||
|
continue
|
||||||
|
author_id = message["author"]["id"]
|
||||||
|
counts[author_id] = counts.get(author_id, 0) + 1
|
||||||
|
|
||||||
|
if reached_cutoff or len(messages) < 100:
|
||||||
|
print()
|
||||||
|
break
|
||||||
|
|
||||||
|
before_id = messages[-1]["id"]
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
return counts
|
||||||
|
|
||||||
|
async def collect_discord_counts(
|
||||||
|
self, team_name: str, channel_id: str, member_ids: list[str]
|
||||||
|
) -> dict[str, int]:
|
||||||
|
"""Return message counts per member for a team's channel and threads."""
|
||||||
|
print(" [Discord] Scanning main channel...")
|
||||||
|
totals: dict[str, int] = await self._count_messages_in_channel(
|
||||||
|
channel_id, label="main"
|
||||||
|
)
|
||||||
|
|
||||||
|
thread_ids = await self._get_discord_thread_ids(channel_id)
|
||||||
|
total_threads = len(thread_ids)
|
||||||
|
for i, thread_id in enumerate(thread_ids, start=1):
|
||||||
|
print(f" [Discord] Scanning thread {i}/{total_threads}...")
|
||||||
|
thread_counts = await self._count_messages_in_channel(
|
||||||
|
thread_id, label=f"thread {i}/{total_threads}"
|
||||||
|
)
|
||||||
|
for user_id, count in thread_counts.items():
|
||||||
|
totals[user_id] = totals.get(user_id, 0) + count
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
|
||||||
|
if total_threads == 0:
|
||||||
|
print(" [Discord] No threads found.")
|
||||||
|
|
||||||
|
return {member_id: totals.get(member_id, 0) for member_id in member_ids}
|
||||||
|
|
||||||
|
async def _github_get_all_pages(self, url: str, params: dict) -> list[dict]:
|
||||||
|
"""Fetch all pages from a paginated GitHub REST API endpoint."""
|
||||||
|
results: list[dict] = []
|
||||||
|
page = 1
|
||||||
|
|
||||||
|
while True:
|
||||||
|
paged_params = {**params, "per_page": 100, "page": page}
|
||||||
|
async with self.session.get(
|
||||||
|
url, headers=self.github_headers, params=paged_params
|
||||||
|
) as response:
|
||||||
|
if response.status in (404, 422):
|
||||||
|
break
|
||||||
|
if response.status == 403:
|
||||||
|
print(f" [GitHub] rate limited on {url}, waiting 60s...")
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
continue
|
||||||
|
if response.status != 200:
|
||||||
|
print(f" [GitHub] {url} → HTTP {response.status}")
|
||||||
|
break
|
||||||
|
|
||||||
|
data: list[dict] = await response.json()
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
|
||||||
|
results.extend(data)
|
||||||
|
|
||||||
|
if len(data) < 100:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
await asyncio.sleep(0.2)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
async def collect_github_counts(
|
||||||
|
self, team_name: str, github_usernames: list[str]
|
||||||
|
) -> dict[str, dict[str, int]]:
|
||||||
|
"""Return activity counts per member for a team's GitHub repository."""
|
||||||
|
repo_slug = team_repo_slug(team_name)
|
||||||
|
repo = f"{GITHUB_ORG}/{repo_slug}"
|
||||||
|
print(f" [GitHub] repo: {repo}")
|
||||||
|
|
||||||
|
counts: dict[str, dict[str, int]] = {
|
||||||
|
username: {
|
||||||
|
"prs_opened": 0,
|
||||||
|
"issues_opened": 0,
|
||||||
|
"issue_comments": 0,
|
||||||
|
"pr_comments": 0,
|
||||||
|
"pr_reviews": 0,
|
||||||
|
"commits": 0,
|
||||||
|
}
|
||||||
|
for username in github_usernames
|
||||||
|
if username
|
||||||
|
}
|
||||||
|
|
||||||
|
def resolve_username(login: str) -> str | None:
|
||||||
|
lower = login.lower()
|
||||||
|
for u in github_usernames:
|
||||||
|
if u and u.lower() == lower:
|
||||||
|
return u
|
||||||
|
return None
|
||||||
|
|
||||||
|
print(" [GitHub] Fetching PRs...")
|
||||||
|
prs = await self._github_get_all_pages(
|
||||||
|
f"{GITHUB_API_BASE}/repos/{repo}/pulls",
|
||||||
|
{"state": "all", "sort": "created", "direction": "desc"},
|
||||||
|
)
|
||||||
|
print(f" [GitHub] {len(prs)} PRs fetched — counting opens since cutoff...")
|
||||||
|
for pr in prs:
|
||||||
|
created_at = datetime.fromisoformat(pr["created_at"].replace("Z", "+00:00"))
|
||||||
|
if created_at < CUTOFF:
|
||||||
|
break
|
||||||
|
login = pr["user"]["login"]
|
||||||
|
username = resolve_username(login)
|
||||||
|
if username:
|
||||||
|
counts[username]["prs_opened"] += 1
|
||||||
|
|
||||||
|
print(" [GitHub] Fetching issues...")
|
||||||
|
issues = await self._github_get_all_pages(
|
||||||
|
f"{GITHUB_API_BASE}/repos/{repo}/issues",
|
||||||
|
{
|
||||||
|
"state": "all",
|
||||||
|
"sort": "created",
|
||||||
|
"direction": "desc",
|
||||||
|
"since": CUTOFF_ISO,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
print(f" [GitHub] {len(issues)} issues/PRs fetched — counting issue opens...")
|
||||||
|
for issue in issues:
|
||||||
|
if "pull_request" in issue:
|
||||||
|
continue
|
||||||
|
created_at = datetime.fromisoformat(
|
||||||
|
issue["created_at"].replace("Z", "+00:00")
|
||||||
|
)
|
||||||
|
if created_at < CUTOFF:
|
||||||
|
continue
|
||||||
|
login = issue["user"]["login"]
|
||||||
|
username = resolve_username(login)
|
||||||
|
if username:
|
||||||
|
counts[username]["issues_opened"] += 1
|
||||||
|
|
||||||
|
print(" [GitHub] Fetching issue comments...")
|
||||||
|
issue_comments = await self._github_get_all_pages(
|
||||||
|
f"{GITHUB_API_BASE}/repos/{repo}/issues/comments",
|
||||||
|
{"sort": "created", "direction": "desc", "since": CUTOFF_ISO},
|
||||||
|
)
|
||||||
|
print(f" [GitHub] {len(issue_comments)} issue comments fetched.")
|
||||||
|
for comment in issue_comments:
|
||||||
|
created_at = datetime.fromisoformat(
|
||||||
|
comment["created_at"].replace("Z", "+00:00")
|
||||||
|
)
|
||||||
|
if created_at < CUTOFF:
|
||||||
|
continue
|
||||||
|
login = comment["user"]["login"]
|
||||||
|
username = resolve_username(login)
|
||||||
|
if username:
|
||||||
|
counts[username]["issue_comments"] += 1
|
||||||
|
|
||||||
|
print(" [GitHub] Fetching PR review comments...")
|
||||||
|
pr_comments = await self._github_get_all_pages(
|
||||||
|
f"{GITHUB_API_BASE}/repos/{repo}/pulls/comments",
|
||||||
|
{"sort": "created", "direction": "desc", "since": CUTOFF_ISO},
|
||||||
|
)
|
||||||
|
print(f" [GitHub] {len(pr_comments)} PR review comments fetched.")
|
||||||
|
for comment in pr_comments:
|
||||||
|
created_at = datetime.fromisoformat(
|
||||||
|
comment["created_at"].replace("Z", "+00:00")
|
||||||
|
)
|
||||||
|
if created_at < CUTOFF:
|
||||||
|
continue
|
||||||
|
login = comment["user"]["login"]
|
||||||
|
username = resolve_username(login)
|
||||||
|
if username:
|
||||||
|
counts[username]["pr_comments"] += 1
|
||||||
|
|
||||||
|
all_pr_numbers = [pr["number"] for pr in prs]
|
||||||
|
total_prs = len(all_pr_numbers)
|
||||||
|
print(f" [GitHub] Fetching reviews for {total_prs} PRs...")
|
||||||
|
for i, pr_number in enumerate(all_pr_numbers, start=1):
|
||||||
|
print(f" [GitHub] PR reviews: {i}/{total_prs}", end="\r")
|
||||||
|
reviews = await self._github_get_all_pages(
|
||||||
|
f"{GITHUB_API_BASE}/repos/{repo}/pulls/{pr_number}/reviews",
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
for review in reviews:
|
||||||
|
submitted_at_raw = review.get("submitted_at")
|
||||||
|
if not submitted_at_raw:
|
||||||
|
continue
|
||||||
|
submitted_at = datetime.fromisoformat(
|
||||||
|
submitted_at_raw.replace("Z", "+00:00")
|
||||||
|
)
|
||||||
|
if submitted_at < CUTOFF:
|
||||||
|
continue
|
||||||
|
login = review["user"]["login"]
|
||||||
|
username = resolve_username(login)
|
||||||
|
if username:
|
||||||
|
counts[username]["pr_reviews"] += 1
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
if total_prs > 0:
|
||||||
|
print()
|
||||||
|
|
||||||
|
member_list = list(counts.keys())
|
||||||
|
total_members = len(member_list)
|
||||||
|
print(f" [GitHub] Fetching commits for {total_members} members...")
|
||||||
|
for i, username in enumerate(member_list, start=1):
|
||||||
|
print(f" [GitHub] Commits: {i}/{total_members} ({username})", end="\r")
|
||||||
|
commits = await self._github_get_all_pages(
|
||||||
|
f"{GITHUB_API_BASE}/repos/{repo}/commits",
|
||||||
|
{"author": username, "since": CUTOFF_ISO},
|
||||||
|
)
|
||||||
|
counts[username]["commits"] = len(commits)
|
||||||
|
await asyncio.sleep(0.2)
|
||||||
|
if total_members > 0:
|
||||||
|
print()
|
||||||
|
|
||||||
|
return counts
|
||||||
|
|
||||||
|
|
||||||
|
def build_report(
|
||||||
|
team_data: list[dict],
|
||||||
|
discord_to_github: dict[str, str],
|
||||||
|
discord_usernames: dict[str, str],
|
||||||
|
discord_results: dict[str, dict[str, int]],
|
||||||
|
github_results: dict[str, dict[str, dict[str, int]]],
|
||||||
|
) -> str:
|
||||||
|
"""Build the markdown activity report."""
|
||||||
|
lines = [
|
||||||
|
"# Catch-Up Activity Report",
|
||||||
|
"",
|
||||||
|
f"**Period:** 2026-02-15 00:00 UTC → "
|
||||||
|
f"{datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M')} UTC",
|
||||||
|
"",
|
||||||
|
"## Activity by Team",
|
||||||
|
"",
|
||||||
|
"| Discord ID | Discord Username | GitHub Username | Team | "
|
||||||
|
"Discord Messages | PRs Opened | Issues Opened | Issue Comments | "
|
||||||
|
"PR Comments | PR Reviews | Commits |",
|
||||||
|
"|------------|-----------------|-----------------|------|"
|
||||||
|
"-----------------|------------|---------------|----------------|"
|
||||||
|
"-------------|------------|---------|",
|
||||||
|
]
|
||||||
|
|
||||||
|
for team in team_data:
|
||||||
|
team_name = team["name"]
|
||||||
|
if team_name == "Jade Jasmine":
|
||||||
|
continue
|
||||||
|
|
||||||
|
member_ids = team["leaders"] + team["participants"]
|
||||||
|
team_discord_counts = discord_results.get(team_name, {})
|
||||||
|
team_github_counts = github_results.get(team_name, {})
|
||||||
|
|
||||||
|
for member_id in member_ids:
|
||||||
|
github_username = discord_to_github.get(member_id, "")
|
||||||
|
discord_username = discord_usernames.get(member_id, "*(unknown)*")
|
||||||
|
discord_msg_count = team_discord_counts.get(member_id, 0)
|
||||||
|
|
||||||
|
if github_username:
|
||||||
|
gh = team_github_counts.get(github_username, {})
|
||||||
|
prs = gh.get("prs_opened", 0)
|
||||||
|
issues = gh.get("issues_opened", 0)
|
||||||
|
issue_comments = gh.get("issue_comments", 0)
|
||||||
|
pr_comments = gh.get("pr_comments", 0)
|
||||||
|
pr_reviews = gh.get("pr_reviews", 0)
|
||||||
|
commits = gh.get("commits", 0)
|
||||||
|
else:
|
||||||
|
prs = issues = issue_comments = pr_comments = pr_reviews = commits = (
|
||||||
|
"N/A"
|
||||||
|
)
|
||||||
|
|
||||||
|
lines.append(
|
||||||
|
f"| {member_id} | {discord_username} | {github_username or 'N/A'} "
|
||||||
|
f"| {team_name} | {discord_msg_count} | {prs} | {issues} "
|
||||||
|
f"| {issue_comments} | {pr_comments} | {pr_reviews} | {commits} |"
|
||||||
|
)
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
lines.append(
|
||||||
|
f"*Generated at {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} UTC*"
|
||||||
|
)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
"""Run the catch-up activity report."""
|
||||||
|
print("Loading data files...")
|
||||||
|
with open("team_assignments.json") as f:
|
||||||
|
team_data: list[dict] = json.load(f)
|
||||||
|
|
||||||
|
with open("discord_to_github.json") as f:
|
||||||
|
discord_to_github: dict[str, str] = json.load(f)
|
||||||
|
|
||||||
|
print("Getting GitHub token via gh CLI...")
|
||||||
|
github_token = get_github_token()
|
||||||
|
|
||||||
|
print(f"\nCollecting activity since {CUTOFF.isoformat()}...\n")
|
||||||
|
|
||||||
|
discord_results: dict[str, dict[str, int]] = {}
|
||||||
|
github_results: dict[str, dict[str, dict[str, int]]] = {}
|
||||||
|
discord_usernames: dict[str, str] = {}
|
||||||
|
|
||||||
|
async with ActivityCollector(DISCORD_BOT_TOKEN, github_token) as collector:
|
||||||
|
all_member_ids: list[str] = []
|
||||||
|
for team in team_data:
|
||||||
|
if team["name"] == "Jade Jasmine":
|
||||||
|
continue
|
||||||
|
all_member_ids.extend(team["leaders"] + team["participants"])
|
||||||
|
|
||||||
|
unique_member_ids = list(dict.fromkeys(all_member_ids))
|
||||||
|
total_members = len(unique_member_ids)
|
||||||
|
print(f"Fetching Discord usernames for {total_members} members...")
|
||||||
|
for i, member_id in enumerate(unique_member_ids, start=1):
|
||||||
|
if member_id not in discord_usernames:
|
||||||
|
print(f" username {i}/{total_members}...", end="\r")
|
||||||
|
discord_usernames[member_id] = await collector.get_discord_username(
|
||||||
|
member_id
|
||||||
|
)
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
print(f" Done — {total_members} usernames fetched. ")
|
||||||
|
|
||||||
|
for team in team_data:
|
||||||
|
team_name = team["name"]
|
||||||
|
if team_name == "Jade Jasmine":
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"\n=== {team_name} ===")
|
||||||
|
channel_id = TEXT_CHANNEL_IDS[team_name]
|
||||||
|
member_ids = team["leaders"] + team["participants"]
|
||||||
|
|
||||||
|
discord_results[team_name] = await collector.collect_discord_counts(
|
||||||
|
team_name, channel_id, member_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
github_usernames_for_team = [
|
||||||
|
discord_to_github[mid]
|
||||||
|
for mid in member_ids
|
||||||
|
if mid in discord_to_github and discord_to_github[mid]
|
||||||
|
]
|
||||||
|
|
||||||
|
github_results[team_name] = await collector.collect_github_counts(
|
||||||
|
team_name, github_usernames_for_team
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nBuilding report...")
|
||||||
|
report = build_report(
|
||||||
|
team_data,
|
||||||
|
discord_to_github,
|
||||||
|
discord_usernames,
|
||||||
|
discord_results,
|
||||||
|
github_results,
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(OUTPUT_FILE, "w") as f:
|
||||||
|
f.write(report)
|
||||||
|
|
||||||
|
print(f"\n✅ Report saved to {OUTPUT_FILE}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Check cohort-team-* channels for incorrect @everyone or @cohort permissions.
|
||||||
|
|
||||||
|
Find channels where @everyone or @cohort has Send Messages or
|
||||||
|
Send Messages in Threads enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]
|
||||||
|
BASE_URL = "https://discord.com/api/v10"
|
||||||
|
GUILD_ID = "739845668582981683"
|
||||||
|
|
||||||
|
SEND_MESSAGES = 0x0000000000000800
|
||||||
|
SEND_MESSAGES_IN_THREADS = 0x0000004000000000
|
||||||
|
|
||||||
|
|
||||||
|
async def check_permissions() -> None:
|
||||||
|
"""Check all cohort-team-* channels for permission issues."""
|
||||||
|
headers = {"Authorization": f"Bot {DISCORD_BOT_TOKEN}"}
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
print("Fetching channels...")
|
||||||
|
async with session.get(
|
||||||
|
f"{BASE_URL}/guilds/{GUILD_ID}/channels", headers=headers
|
||||||
|
) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
error = await resp.text()
|
||||||
|
print(f"Error fetching channels: {resp.status} - {error}")
|
||||||
|
return
|
||||||
|
channels = await resp.json()
|
||||||
|
|
||||||
|
print("Fetching roles...")
|
||||||
|
async with session.get(
|
||||||
|
f"{BASE_URL}/guilds/{GUILD_ID}/roles", headers=headers
|
||||||
|
) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
error = await resp.text()
|
||||||
|
print(f"Error fetching roles: {resp.status} - {error}")
|
||||||
|
return
|
||||||
|
roles = await resp.json()
|
||||||
|
|
||||||
|
everyone_role_id = GUILD_ID
|
||||||
|
|
||||||
|
cohort_role_id = None
|
||||||
|
for role in roles:
|
||||||
|
if "cohort" in role["name"].lower():
|
||||||
|
cohort_role_id = role["id"]
|
||||||
|
print(f"Found cohort role: {role['name']} ({role['id']})")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not cohort_role_id:
|
||||||
|
print("Warning: Could not find @cohort role!")
|
||||||
|
|
||||||
|
cohort_channels = [
|
||||||
|
ch
|
||||||
|
for ch in channels
|
||||||
|
if ch["name"].startswith("cohort-team-") and ch["type"] == 0
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"\nFound {len(cohort_channels)} cohort-team-* channels\n")
|
||||||
|
|
||||||
|
problematic_channels = []
|
||||||
|
|
||||||
|
for channel in sorted(cohort_channels, key=lambda x: x["name"]):
|
||||||
|
channel_name = channel["name"]
|
||||||
|
channel_id = channel["id"]
|
||||||
|
permission_overwrites = channel.get("permission_overwrites", [])
|
||||||
|
|
||||||
|
everyone_perms = None
|
||||||
|
cohort_perms = None
|
||||||
|
|
||||||
|
for overwrite in permission_overwrites:
|
||||||
|
if overwrite["id"] == everyone_role_id:
|
||||||
|
everyone_perms = overwrite
|
||||||
|
elif cohort_role_id and overwrite["id"] == cohort_role_id:
|
||||||
|
cohort_perms = overwrite
|
||||||
|
|
||||||
|
issues = []
|
||||||
|
|
||||||
|
if everyone_perms:
|
||||||
|
deny = int(everyone_perms.get("deny", "0"))
|
||||||
|
allow = int(everyone_perms.get("allow", "0"))
|
||||||
|
|
||||||
|
if (allow & SEND_MESSAGES) or not (deny & SEND_MESSAGES):
|
||||||
|
issues.append("@everyone can send messages")
|
||||||
|
|
||||||
|
if (allow & SEND_MESSAGES_IN_THREADS) or not (
|
||||||
|
deny & SEND_MESSAGES_IN_THREADS
|
||||||
|
):
|
||||||
|
issues.append("@everyone can send messages in threads")
|
||||||
|
else:
|
||||||
|
issues.append(
|
||||||
|
"@everyone has no permission overwrite (inheriting server perms)"
|
||||||
|
)
|
||||||
|
|
||||||
|
if cohort_perms and cohort_role_id:
|
||||||
|
deny = int(cohort_perms.get("deny", "0"))
|
||||||
|
allow = int(cohort_perms.get("allow", "0"))
|
||||||
|
|
||||||
|
if (allow & SEND_MESSAGES) or not (deny & SEND_MESSAGES):
|
||||||
|
issues.append("@cohort can send messages")
|
||||||
|
|
||||||
|
if (allow & SEND_MESSAGES_IN_THREADS) or not (
|
||||||
|
deny & SEND_MESSAGES_IN_THREADS
|
||||||
|
):
|
||||||
|
issues.append("@cohort can send messages in threads")
|
||||||
|
elif cohort_role_id:
|
||||||
|
issues.append(
|
||||||
|
"@cohort has no permission overwrite (inheriting server perms)"
|
||||||
|
)
|
||||||
|
|
||||||
|
if issues:
|
||||||
|
problematic_channels.append(
|
||||||
|
{"name": channel_name, "id": channel_id, "issues": issues}
|
||||||
|
)
|
||||||
|
print(f"❌ {channel_name}")
|
||||||
|
for issue in issues:
|
||||||
|
print(f" - {issue}")
|
||||||
|
else:
|
||||||
|
print(f"✅ {channel_name}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print(
|
||||||
|
f"\nSummary: {len(problematic_channels)} channels with permission issues\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
if problematic_channels:
|
||||||
|
print("Problematic channels:")
|
||||||
|
for ch in problematic_channels:
|
||||||
|
print(f"\n{ch['name']} (ID: {ch['id']})")
|
||||||
|
for issue in ch["issues"]:
|
||||||
|
print(f" • {issue}")
|
||||||
|
else:
|
||||||
|
print("All channels have correct permissions! 🎉")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(check_permissions())
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"""Quick dry-run to check Discord message lengths before sending."""
|
||||||
|
|
||||||
|
FIELDS = [
|
||||||
|
("Discord Username", "Name", 18),
|
||||||
|
("Discord Messages", "Msgs", 5),
|
||||||
|
("PRs Opened", "PRs", 4),
|
||||||
|
("Issues Opened", "Issues", 6),
|
||||||
|
("Issue Comments", "Issue♟", 7),
|
||||||
|
("PR Comments", "PR♟", 5),
|
||||||
|
("PR Reviews", "Reviews", 7),
|
||||||
|
("Commits", "Commits", 7),
|
||||||
|
]
|
||||||
|
|
||||||
|
REPORT_PATH = "catch_up_report.md"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_report(path: str) -> dict[str, list[dict]]:
|
||||||
|
"""Parse the markdown table from catch_up_report.md into team → rows."""
|
||||||
|
teams: dict[str, list[dict]] = {}
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
header_line = None
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.startswith("| Discord ID |"):
|
||||||
|
header_line = i
|
||||||
|
break
|
||||||
|
headers = [h.strip() for h in lines[header_line].strip().strip("|").split("|")]
|
||||||
|
for line in lines[header_line + 2 :]:
|
||||||
|
line = line.strip()
|
||||||
|
if not line.startswith("|"):
|
||||||
|
break
|
||||||
|
vals = [v.strip() for v in line.strip().strip("|").split("|")]
|
||||||
|
row = dict(zip(headers, vals))
|
||||||
|
teams.setdefault(row["Team"], []).append(row)
|
||||||
|
return teams
|
||||||
|
|
||||||
|
|
||||||
|
def format_table(members: list[dict]) -> str:
|
||||||
|
"""Format a team's member list as a monospace table for Discord."""
|
||||||
|
members = sorted(members, key=lambda r: int(r["Discord Messages"]), reverse=True)
|
||||||
|
col_widths = [w for _, _, w in FIELDS]
|
||||||
|
col_headers = [h for _, h, _ in FIELDS]
|
||||||
|
max_name = max(len(m["Discord Username"]) for m in members)
|
||||||
|
col_widths[0] = max(col_widths[0], max_name)
|
||||||
|
|
||||||
|
def pad(val: str, width: int, right_align: bool = False) -> str:
|
||||||
|
return val.rjust(width) if right_align else val.ljust(width)
|
||||||
|
|
||||||
|
header_row = " ".join(
|
||||||
|
pad(col_headers[i], col_widths[i], right_align=(i > 0))
|
||||||
|
for i in range(len(FIELDS))
|
||||||
|
)
|
||||||
|
separator = " ".join("-" * w for w in col_widths)
|
||||||
|
rows = []
|
||||||
|
for m in members:
|
||||||
|
vals = [m[key] for key, _, _ in FIELDS]
|
||||||
|
row = " ".join(
|
||||||
|
pad(vals[i], col_widths[i], right_align=(i > 0)) for i in range(len(FIELDS))
|
||||||
|
)
|
||||||
|
rows.append(row)
|
||||||
|
return "\n".join([header_row, separator] + rows)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Check Discord message lengths for all teams."""
|
||||||
|
teams = parse_report(REPORT_PATH)
|
||||||
|
for team, members in teams.items():
|
||||||
|
table = format_table(members)
|
||||||
|
msg = f"**{team} — Activity Report (Feb 15–23)**\n```\n{table}\n```"
|
||||||
|
status = "OK" if len(msg) <= 2000 else f"OVER by {len(msg) - 2000}"
|
||||||
|
print(f"{team}: {len(msg)} chars — {status}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Check if removed members are still in the Discord server."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]
|
||||||
|
BASE_URL = "https://discord.com/api/v10"
|
||||||
|
GUILD_ID = "1354624415861833870"
|
||||||
|
|
||||||
|
SAMPLE_MEMBERS = [
|
||||||
|
"899092786802987069",
|
||||||
|
"1318882254365397032",
|
||||||
|
"799293680799711273",
|
||||||
|
"237793557992308736",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def check_member(session: aiohttp.ClientSession, user_id: str) -> bool | None:
|
||||||
|
"""Check if a member is in the server."""
|
||||||
|
url = f"{BASE_URL}/guilds/{GUILD_ID}/members/{user_id}"
|
||||||
|
headers = {"Authorization": f"Bot {DISCORD_BOT_TOKEN}"}
|
||||||
|
|
||||||
|
async with session.get(url, headers=headers) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
data = await resp.json()
|
||||||
|
roles = data.get("roles", [])
|
||||||
|
print(f"✅ User {user_id} IS in server - has {len(roles)} roles: {roles}")
|
||||||
|
return True
|
||||||
|
if resp.status == 404:
|
||||||
|
print(f"❌ User {user_id} NOT in server (left or was kicked)")
|
||||||
|
return False
|
||||||
|
error = await resp.text()
|
||||||
|
print(f"⚠️ Error checking {user_id}: {resp.status} - {error}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
"""Check if sample members are still in the server."""
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
for user_id in SAMPLE_MEMBERS:
|
||||||
|
await check_member(session, user_id)
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fetch pinned messages from Ivory Orchid channel to find the roster."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]
|
||||||
|
CHANNEL_ID = "1464316770889240730"
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
"""Fetch and print pinned messages from the Ivory Orchid channel."""
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bot {DISCORD_BOT_TOKEN}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
async with (
|
||||||
|
aiohttp.ClientSession() as session,
|
||||||
|
session.get(
|
||||||
|
f"https://discord.com/api/v10/channels/{CHANNEL_ID}/pins",
|
||||||
|
headers=headers,
|
||||||
|
) as response,
|
||||||
|
):
|
||||||
|
pins = await response.json()
|
||||||
|
print(json.dumps(pins, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fix permissions for cohort-team-michael-and-yoon channel.
|
||||||
|
|
||||||
|
Deny Send Messages in Threads for @everyone and @cohort.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]
|
||||||
|
BASE_URL = "https://discord.com/api/v10"
|
||||||
|
GUILD_ID = "739845668582981683"
|
||||||
|
CHANNEL_ID = "1467964405646885237"
|
||||||
|
|
||||||
|
SEND_MESSAGES = 0x0000000000000800
|
||||||
|
SEND_MESSAGES_IN_THREADS = 0x0000004000000000
|
||||||
|
|
||||||
|
EVERYONE_ROLE_ID = GUILD_ID
|
||||||
|
COHORT_ROLE_ID = "1390925253102010521"
|
||||||
|
|
||||||
|
|
||||||
|
async def fix_permissions() -> None:
|
||||||
|
"""Fix the channel permissions."""
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bot {DISCORD_BOT_TOKEN}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
print("Fetching current channel permissions...")
|
||||||
|
async with session.get(
|
||||||
|
f"{BASE_URL}/channels/{CHANNEL_ID}", headers=headers
|
||||||
|
) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
error = await resp.text()
|
||||||
|
print(f"Error fetching channel: {resp.status} - {error}")
|
||||||
|
return
|
||||||
|
|
||||||
|
channel = await resp.json()
|
||||||
|
print(f"Channel: {channel['name']}")
|
||||||
|
|
||||||
|
permission_overwrites = channel.get("permission_overwrites", [])
|
||||||
|
|
||||||
|
everyone_overwrite = None
|
||||||
|
cohort_overwrite = None
|
||||||
|
|
||||||
|
for overwrite in permission_overwrites:
|
||||||
|
if overwrite["id"] == EVERYONE_ROLE_ID:
|
||||||
|
everyone_overwrite = overwrite
|
||||||
|
elif overwrite["id"] == COHORT_ROLE_ID:
|
||||||
|
cohort_overwrite = overwrite
|
||||||
|
|
||||||
|
print("\nFixing @everyone permissions...")
|
||||||
|
if everyone_overwrite:
|
||||||
|
current_deny = int(everyone_overwrite.get("deny", "0"))
|
||||||
|
current_allow = int(everyone_overwrite.get("allow", "0"))
|
||||||
|
|
||||||
|
new_deny = current_deny | SEND_MESSAGES | SEND_MESSAGES_IN_THREADS
|
||||||
|
new_allow = current_allow & ~SEND_MESSAGES & ~SEND_MESSAGES_IN_THREADS
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"type": 0,
|
||||||
|
"deny": str(new_deny),
|
||||||
|
"allow": str(new_allow),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
payload = {
|
||||||
|
"type": 0,
|
||||||
|
"deny": str(SEND_MESSAGES | SEND_MESSAGES_IN_THREADS),
|
||||||
|
"allow": "0",
|
||||||
|
}
|
||||||
|
|
||||||
|
async with session.put(
|
||||||
|
f"{BASE_URL}/channels/{CHANNEL_ID}/permissions/{EVERYONE_ROLE_ID}",
|
||||||
|
headers=headers,
|
||||||
|
json=payload,
|
||||||
|
) as resp:
|
||||||
|
if resp.status == 204:
|
||||||
|
print("✅ @everyone permissions fixed!")
|
||||||
|
else:
|
||||||
|
error = await resp.text()
|
||||||
|
print(f"❌ Error fixing @everyone: {resp.status} - {error}")
|
||||||
|
|
||||||
|
print("\nFixing @cohort permissions...")
|
||||||
|
if cohort_overwrite:
|
||||||
|
current_deny = int(cohort_overwrite.get("deny", "0"))
|
||||||
|
current_allow = int(cohort_overwrite.get("allow", "0"))
|
||||||
|
|
||||||
|
new_deny = current_deny | SEND_MESSAGES | SEND_MESSAGES_IN_THREADS
|
||||||
|
new_allow = current_allow & ~SEND_MESSAGES & ~SEND_MESSAGES_IN_THREADS
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"type": 0,
|
||||||
|
"deny": str(new_deny),
|
||||||
|
"allow": str(new_allow),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
payload = {
|
||||||
|
"type": 0,
|
||||||
|
"deny": str(SEND_MESSAGES | SEND_MESSAGES_IN_THREADS),
|
||||||
|
"allow": "0",
|
||||||
|
}
|
||||||
|
|
||||||
|
async with session.put(
|
||||||
|
f"{BASE_URL}/channels/{CHANNEL_ID}/permissions/{COHORT_ROLE_ID}",
|
||||||
|
headers=headers,
|
||||||
|
json=payload,
|
||||||
|
) as resp:
|
||||||
|
if resp.status == 204:
|
||||||
|
print("✅ @cohort permissions fixed!")
|
||||||
|
else:
|
||||||
|
error = await resp.text()
|
||||||
|
print(f"❌ Error fixing @cohort: {resp.status} - {error}")
|
||||||
|
|
||||||
|
print("\n✨ Done! Permissions have been fixed.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(fix_permissions())
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Get all members who currently have the Cohort role."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]
|
||||||
|
GUILD_ID = "692816967895220344"
|
||||||
|
COHORT_ROLE_ID = "1464314780935258112"
|
||||||
|
|
||||||
|
|
||||||
|
async def get_all_members_with_role(
|
||||||
|
session: aiohttp.ClientSession,
|
||||||
|
) -> list[dict[str, str]]:
|
||||||
|
"""Get all members who have the Cohort role."""
|
||||||
|
members = []
|
||||||
|
headers = {"Authorization": f"Bot {DISCORD_BOT_TOKEN}"}
|
||||||
|
|
||||||
|
after = None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
url = f"https://discord.com/api/v10/guilds/{GUILD_ID}/members"
|
||||||
|
params: dict[str, str | int] = {"limit": 1000}
|
||||||
|
if after:
|
||||||
|
params["after"] = after
|
||||||
|
|
||||||
|
async with session.get(url, headers=headers, params=params) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
error_text = await resp.text()
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Failed to fetch members: {resp.status} - {error_text}"
|
||||||
|
)
|
||||||
|
|
||||||
|
data = await resp.json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
|
||||||
|
for member in data:
|
||||||
|
if COHORT_ROLE_ID in member.get("roles", []):
|
||||||
|
members.append(
|
||||||
|
{
|
||||||
|
"id": member["user"]["id"],
|
||||||
|
"username": member["user"]["username"],
|
||||||
|
"display_name": member.get("nick")
|
||||||
|
or member["user"]["username"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(data) < 1000:
|
||||||
|
break
|
||||||
|
|
||||||
|
after = data[-1]["user"]["id"]
|
||||||
|
|
||||||
|
return members
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
"""Get all cohort members with the Cohort role."""
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
print("Fetching all members with Cohort role...")
|
||||||
|
|
||||||
|
cohort_members = await get_all_members_with_role(session)
|
||||||
|
|
||||||
|
print(f"\n✨ Found {len(cohort_members)} members with the Cohort role:\n")
|
||||||
|
|
||||||
|
for i, member in enumerate(cohort_members, 1):
|
||||||
|
print(
|
||||||
|
f"{i}. {member['display_name']} (@{member['username']}) - ID: {member['id']}" # noqa: E501
|
||||||
|
)
|
||||||
|
|
||||||
|
with open("active_cohort_members.json", "w") as f:
|
||||||
|
json.dump(cohort_members, f, indent=2)
|
||||||
|
|
||||||
|
print("\nSaved to active_cohort_members.json")
|
||||||
|
|
||||||
|
print("\nMember IDs only:")
|
||||||
|
print(json.dumps([m["id"] for m in cohort_members], indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""List all roles in the guild to find the correct team role IDs."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]
|
||||||
|
BASE_URL = "https://discord.com/api/v10"
|
||||||
|
GUILD_ID = "1354624415861833870"
|
||||||
|
|
||||||
|
|
||||||
|
async def list_guild_roles() -> None:
|
||||||
|
"""List all roles in the guild, highlighting team-related ones."""
|
||||||
|
url = f"{BASE_URL}/guilds/{GUILD_ID}/roles"
|
||||||
|
headers = {"Authorization": f"Bot {DISCORD_BOT_TOKEN}"}
|
||||||
|
|
||||||
|
async with (
|
||||||
|
aiohttp.ClientSession() as session,
|
||||||
|
session.get(url, headers=headers) as resp,
|
||||||
|
):
|
||||||
|
if resp.status == 200:
|
||||||
|
roles = await resp.json()
|
||||||
|
print(f"Found {len(roles)} roles in the server:\n")
|
||||||
|
|
||||||
|
team_names = [
|
||||||
|
"Jade Jasmine",
|
||||||
|
"Crimson Dahlia",
|
||||||
|
"Rose Camellia",
|
||||||
|
"Amber Wisteria",
|
||||||
|
"Ivory Orchid",
|
||||||
|
"Teal Iris",
|
||||||
|
"Peach Gardenia",
|
||||||
|
"Violet Carnation",
|
||||||
|
"Azure Lotus",
|
||||||
|
"Coral Sunflower",
|
||||||
|
"Indigo Tulip",
|
||||||
|
"Scarlet Hydrangea",
|
||||||
|
"Mint Narcissus",
|
||||||
|
"Sage Marigold",
|
||||||
|
"Cohort",
|
||||||
|
]
|
||||||
|
|
||||||
|
for role in sorted(roles, key=lambda r: r["name"].lower()):
|
||||||
|
name = role["name"]
|
||||||
|
is_team = any(team in name for team in team_names)
|
||||||
|
if is_team or "spring" in name.lower() or "2026" in name:
|
||||||
|
print(f"✅ {role['id']}: {name}")
|
||||||
|
|
||||||
|
print("\n\nAll roles (sorted by name):")
|
||||||
|
for role in sorted(roles, key=lambda r: r["name"].lower()):
|
||||||
|
print(f"{role['id']}: {role['name']}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
error = await resp.text()
|
||||||
|
print(f"Error: {resp.status} - {error}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(list_guild_roles())
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"""List all Discord roles in the freeCodeCamp server."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]
|
||||||
|
GUILD_ID = "692816967895220344"
|
||||||
|
|
||||||
|
API_BASE = "https://discord.com/api/v10"
|
||||||
|
|
||||||
|
|
||||||
|
async def get_guild_roles(session: aiohttp.ClientSession) -> list[dict]:
|
||||||
|
"""Get all guild roles."""
|
||||||
|
url = f"{API_BASE}/guilds/{GUILD_ID}/roles"
|
||||||
|
headers = {"Authorization": f"Bot {DISCORD_BOT_TOKEN}"}
|
||||||
|
|
||||||
|
async with session.get(url, headers=headers) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
return await resp.json()
|
||||||
|
print(f"Failed to get roles: {resp.status}")
|
||||||
|
text = await resp.text()
|
||||||
|
print(text)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
"""List all roles from freeCodeCamp Discord, highlighting cohort-related ones."""
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
print("Fetching all roles from freeCodeCamp Discord...\n")
|
||||||
|
roles = await get_guild_roles(session)
|
||||||
|
|
||||||
|
print(f"Found {len(roles)} roles:\n")
|
||||||
|
|
||||||
|
cohort_roles = [
|
||||||
|
r
|
||||||
|
for r in roles
|
||||||
|
if "2026" in r["name"] or "Cohort" in r["name"] or "Leader" in r["name"]
|
||||||
|
]
|
||||||
|
|
||||||
|
print("=== Cohort/Leader Roles ===")
|
||||||
|
for role in cohort_roles:
|
||||||
|
print(f" {role['name']}: {role['id']}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Remove cohort and team roles from inactive Discord members."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]
|
||||||
|
GUILD_ID = "1354624415861833870"
|
||||||
|
BASE_URL = "https://discord.com/api/v10"
|
||||||
|
|
||||||
|
INACTIVE_MEMBERS = [
|
||||||
|
"1177481351490981889",
|
||||||
|
"899092786802987069",
|
||||||
|
"1318882254365397032",
|
||||||
|
"680429718511943770",
|
||||||
|
"1195902319976521748",
|
||||||
|
"1424001797072359576",
|
||||||
|
"1333378962289590365",
|
||||||
|
"799293680799711273",
|
||||||
|
"1183395404293869662",
|
||||||
|
"1325958873831440566",
|
||||||
|
"717028253633871965",
|
||||||
|
"847789364217905182",
|
||||||
|
"746703502369095700",
|
||||||
|
"192541018908786690",
|
||||||
|
"1017761694514163712",
|
||||||
|
]
|
||||||
|
|
||||||
|
COHORT_ROLE_ID = "1464314780935258112"
|
||||||
|
TEAM_ROLE_IDS = {
|
||||||
|
1: "1464314923780931677",
|
||||||
|
2: "1464315093402784015",
|
||||||
|
3: "1464315098452726106",
|
||||||
|
4: "1464315105264275600",
|
||||||
|
5: "1464315109873684593",
|
||||||
|
6: "1464315114378498152",
|
||||||
|
7: "1464315118904152107",
|
||||||
|
8: "1464315124251754559",
|
||||||
|
9: "1464315128437801177",
|
||||||
|
10: "1464315132896088168",
|
||||||
|
11: "1464315138428633241",
|
||||||
|
12: "1464315142710890520",
|
||||||
|
13: "1464315149203804405",
|
||||||
|
14: "1464315153599299803",
|
||||||
|
}
|
||||||
|
|
||||||
|
MEMBER_TO_TEAM = {
|
||||||
|
"1177481351490981889": 1,
|
||||||
|
"899092786802987069": 1,
|
||||||
|
"1318882254365397032": 2,
|
||||||
|
"680429718511943770": 2,
|
||||||
|
"1195902319976521748": 3,
|
||||||
|
"1424001797072359576": 4,
|
||||||
|
"1333378962289590365": 4,
|
||||||
|
"799293680799711273": 5,
|
||||||
|
"1183395404293869662": 7,
|
||||||
|
"1325958873831440566": 8,
|
||||||
|
"717028253633871965": 8,
|
||||||
|
"847789364217905182": 10,
|
||||||
|
"746703502369095700": 10,
|
||||||
|
"192541018908786690": 11,
|
||||||
|
"1017761694514163712": 13,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def remove_role(
|
||||||
|
session: aiohttp.ClientSession, user_id: str, role_id: str
|
||||||
|
) -> bool:
|
||||||
|
"""Remove a role from a user."""
|
||||||
|
url = f"{BASE_URL}/guilds/{GUILD_ID}/members/{user_id}/roles/{role_id}"
|
||||||
|
headers = {"Authorization": f"Bot {DISCORD_BOT_TOKEN}"}
|
||||||
|
|
||||||
|
async with session.delete(url, headers=headers) as resp:
|
||||||
|
if resp.status == 204:
|
||||||
|
return True
|
||||||
|
text = await resp.text()
|
||||||
|
print(f" Error removing role {role_id} from {user_id}: {resp.status} - {text}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
"""Remove roles from all inactive members."""
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
print("Removing roles from inactive members...\n")
|
||||||
|
|
||||||
|
for member_id in INACTIVE_MEMBERS:
|
||||||
|
print(f"Processing {member_id}...")
|
||||||
|
|
||||||
|
if await remove_role(session, member_id, COHORT_ROLE_ID):
|
||||||
|
print(" ✓ Removed cohort role")
|
||||||
|
|
||||||
|
if member_id in MEMBER_TO_TEAM:
|
||||||
|
team_id = MEMBER_TO_TEAM[member_id]
|
||||||
|
team_role_id = TEAM_ROLE_IDS[team_id]
|
||||||
|
if await remove_role(session, member_id, team_role_id):
|
||||||
|
print(f" ✓ Removed Team {team_id} role")
|
||||||
|
|
||||||
|
await asyncio.sleep(1.5)
|
||||||
|
|
||||||
|
print("\nDone!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Script to remove inactive members from GitHub organization teams
|
||||||
|
# Date: 2026-02-12
|
||||||
|
|
||||||
|
ORG="nhcarrigan-spring-2026-cohort"
|
||||||
|
|
||||||
|
# Team 1 (Jade Jasmine) - Remove leader and participant
|
||||||
|
echo "Removing from Jade Jasmine..."
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/jade-jasmine-leaders/memberships/Mista-Log" || true
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/jade-jasmine/memberships/A-normal-programmer" || true
|
||||||
|
|
||||||
|
# Team 2 (Crimson Dahlia) - Remove 2 participants
|
||||||
|
echo "Removing from Crimson Dahlia..."
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/crimson-dahlia/memberships/1s-crypto" || true
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/crimson-dahlia/memberships/emlanis" || true
|
||||||
|
|
||||||
|
# Team 3 (Rose Camellia) - Remove leader
|
||||||
|
echo "Removing from Rose Camellia..."
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/rose-camellia-leaders/memberships/michaelboateng1" || true
|
||||||
|
|
||||||
|
# Team 4 (Amber Wisteria) - Remove leader and participant
|
||||||
|
echo "Removing from Amber Wisteria..."
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/amber-wisteria-leaders/memberships/neonbit101" || true
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/amber-wisteria/memberships/avanishchandra" || true
|
||||||
|
|
||||||
|
# Team 5 (Ivory Orchid) - Remove participant
|
||||||
|
echo "Removing from Ivory Orchid..."
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/ivory-orchid/memberships/VuBui217" || true
|
||||||
|
|
||||||
|
# Team 7 (Peach Gardenia) - Remove participant
|
||||||
|
echo "Removing from Peach Gardenia..."
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/peach-gardenia/memberships/TabsOO7" || true
|
||||||
|
|
||||||
|
# Team 8 (Violet Carnation) - Remove 2 participants
|
||||||
|
echo "Removing from Violet Carnation..."
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/violet-carnation/memberships/masudulalam" || true
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/violet-carnation/memberships/urmilbhatt" || true
|
||||||
|
|
||||||
|
# Team 10 (Coral Sunflower) - Remove leader and participant
|
||||||
|
echo "Removing from Coral Sunflower..."
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/coral-sunflower-leaders/memberships/AjayTheWizard" || true
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/coral-sunflower/memberships/Hritikhh" || true
|
||||||
|
|
||||||
|
# Team 11 (Indigo Tulip) - Remove participant
|
||||||
|
echo "Removing from Indigo Tulip..."
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/indigo-tulip/memberships/SiAust" || true
|
||||||
|
|
||||||
|
# Team 13 (Mint Narcissus) - Remove participant
|
||||||
|
echo "Removing from Mint Narcissus..."
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/mint-narcissus/memberships/SergioPardoSanchez" || true
|
||||||
|
|
||||||
|
echo "Done removing members from GitHub teams!"
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
#!/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())
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Remove resigned members from team_assignments.json.
|
||||||
|
|
||||||
|
Update RESIGNED_IDS with the Discord IDs of members who have resigned
|
||||||
|
before running this script.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Discord IDs of members who have resigned - update before running
|
||||||
|
RESIGNED_IDS: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Remove resigned members from team assignments."""
|
||||||
|
with open("team_assignments.json") as f:
|
||||||
|
teams = json.load(f)
|
||||||
|
|
||||||
|
changes_made = []
|
||||||
|
|
||||||
|
for team in teams:
|
||||||
|
team_name = team["name"]
|
||||||
|
|
||||||
|
original_leaders = len(team["leaders"])
|
||||||
|
team["leaders"] = [lid for lid in team["leaders"] if lid not in RESIGNED_IDS]
|
||||||
|
if len(team["leaders"]) < original_leaders:
|
||||||
|
removed = original_leaders - len(team["leaders"])
|
||||||
|
changes_made.append(f"Removed {removed} leader(s) from {team_name}")
|
||||||
|
|
||||||
|
original_participants = len(team["participants"])
|
||||||
|
team["participants"] = [
|
||||||
|
pid for pid in team["participants"] if pid not in RESIGNED_IDS
|
||||||
|
]
|
||||||
|
if len(team["participants"]) < original_participants:
|
||||||
|
removed = original_participants - len(team["participants"])
|
||||||
|
changes_made.append(f"Removed {removed} participant(s) from {team_name}")
|
||||||
|
print(f"⚠️ {team_name}: Proficiency distribution may need updating")
|
||||||
|
|
||||||
|
with open("team_assignments.json", "w") as f:
|
||||||
|
json.dump(teams, f, indent=2)
|
||||||
|
|
||||||
|
if changes_made:
|
||||||
|
print("✅ Successfully removed resigned members from team_assignments.json")
|
||||||
|
print("\nChanges made:")
|
||||||
|
for change in changes_made:
|
||||||
|
print(f" - {change}")
|
||||||
|
else:
|
||||||
|
print("No changes needed - resigned members were not found in team assignments")
|
||||||
|
|
||||||
|
print("\nUpdated team sizes:")
|
||||||
|
for team in teams:
|
||||||
|
total_members = len(team["leaders"]) + len(team["participants"])
|
||||||
|
print(
|
||||||
|
f" {team['name']}: {total_members} members "
|
||||||
|
f"({len(team['leaders'])} leaders, {len(team['participants'])} participants)" # noqa: E501
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
"""Send formatted activity report tables to each team channel via Amari bot."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]
|
||||||
|
GUILD_ID = "692816967895220344"
|
||||||
|
API_BASE = "https://discord.com/api/v10"
|
||||||
|
|
||||||
|
CHANNEL_IDS = {
|
||||||
|
"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",
|
||||||
|
}
|
||||||
|
|
||||||
|
REPORT_PATH = os.path.join(os.path.dirname(__file__), "catch_up_report.md")
|
||||||
|
|
||||||
|
FIELDS = [
|
||||||
|
("Discord Username", "Name", 18),
|
||||||
|
("Discord Messages", "Msgs", 5),
|
||||||
|
("PRs Opened", "PRs", 4),
|
||||||
|
("Issues Opened", "Issues", 6),
|
||||||
|
("Issue Comments", "Issue♟", 7),
|
||||||
|
("PR Comments", "PR♟", 5),
|
||||||
|
("PR Reviews", "Reviews", 7),
|
||||||
|
("Commits", "Commits", 7),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_report(path: str) -> dict[str, list[dict]]:
|
||||||
|
"""Parse the markdown table from catch_up_report.md into team → rows."""
|
||||||
|
teams: dict[str, list[dict]] = {}
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
header_line = None
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.startswith("| Discord ID |"):
|
||||||
|
header_line = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if header_line is None:
|
||||||
|
raise ValueError("Could not find table header in report")
|
||||||
|
|
||||||
|
headers = [h.strip() for h in lines[header_line].strip().strip("|").split("|")]
|
||||||
|
for line in lines[header_line + 2 :]:
|
||||||
|
line = line.strip()
|
||||||
|
if not line.startswith("|"):
|
||||||
|
break
|
||||||
|
row_values = [v.strip() for v in line.strip().strip("|").split("|")]
|
||||||
|
row = dict(zip(headers, row_values))
|
||||||
|
team = row["Team"]
|
||||||
|
teams.setdefault(team, []).append(row)
|
||||||
|
|
||||||
|
return teams
|
||||||
|
|
||||||
|
|
||||||
|
def format_table(members: list[dict]) -> str:
|
||||||
|
"""Format a team's member list as a monospace table for Discord."""
|
||||||
|
members = sorted(members, key=lambda r: int(r["Discord Messages"]), reverse=True)
|
||||||
|
|
||||||
|
col_widths = [width for _, _, width in FIELDS]
|
||||||
|
col_headers = [header for _, header, _ in FIELDS]
|
||||||
|
|
||||||
|
name_col_index = 0
|
||||||
|
max_name = max(len(m["Discord Username"]) for m in members)
|
||||||
|
col_widths[name_col_index] = max(col_widths[name_col_index], max_name)
|
||||||
|
|
||||||
|
def pad(val: str, width: int, right_align: bool = False) -> str:
|
||||||
|
return val.rjust(width) if right_align else val.ljust(width)
|
||||||
|
|
||||||
|
header_row = " ".join(
|
||||||
|
pad(col_headers[i], col_widths[i], right_align=(i > 0))
|
||||||
|
for i in range(len(FIELDS))
|
||||||
|
)
|
||||||
|
separator = " ".join("-" * w for w in col_widths)
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for m in members:
|
||||||
|
source_keys = [key for key, _, _ in FIELDS]
|
||||||
|
values = [m[key] for key in source_keys]
|
||||||
|
row = " ".join(
|
||||||
|
pad(values[i], col_widths[i], right_align=(i > 0))
|
||||||
|
for i in range(len(FIELDS))
|
||||||
|
)
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
return "\n".join([header_row, separator] + rows)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_message(
|
||||||
|
session: aiohttp.ClientSession, channel_id: str, content: str
|
||||||
|
) -> None:
|
||||||
|
"""Send a message to a Discord channel."""
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bot {DISCORD_BOT_TOKEN}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
url = f"{API_BASE}/channels/{channel_id}/messages"
|
||||||
|
while True:
|
||||||
|
async with session.post(
|
||||||
|
url, json={"content": content}, headers=headers
|
||||||
|
) as resp:
|
||||||
|
if resp.status == 429:
|
||||||
|
data = await resp.json()
|
||||||
|
retry_after = data.get("retry_after", 5)
|
||||||
|
print(f" Rate limited — sleeping {retry_after}s...")
|
||||||
|
await asyncio.sleep(retry_after)
|
||||||
|
continue
|
||||||
|
if resp.status not in (200, 201):
|
||||||
|
text = await resp.text()
|
||||||
|
print(f" ERROR {resp.status}: {text}")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
"""Send activity tables to all team channels."""
|
||||||
|
teams = parse_report(REPORT_PATH)
|
||||||
|
team_names = list(CHANNEL_IDS.keys())
|
||||||
|
print(f"Sending activity tables to {len(team_names)} channels...\n")
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
for i, team_name in enumerate(team_names, 1):
|
||||||
|
channel_id = CHANNEL_IDS[team_name]
|
||||||
|
members = teams.get(team_name, [])
|
||||||
|
if not members:
|
||||||
|
print(f" [{i}/{len(team_names)}] {team_name} — no data, skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
table = format_table(members)
|
||||||
|
message = (
|
||||||
|
f"**{team_name} — Activity Report (Feb 15–23)**\n```\n{table}\n```"
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(message) > 2000:
|
||||||
|
print(
|
||||||
|
f" [{i}/{len(team_names)}] {team_name} — WARNING: "
|
||||||
|
f"message is {len(message)} chars (over 2000!)"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f" [{i}/{len(team_names)}] Sending to {team_name}... ",
|
||||||
|
end="",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
await send_message(session, channel_id, message)
|
||||||
|
print("sent!")
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
print("\nAll done! 🌸")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
"""Send check-in messages to all team channels."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]
|
||||||
|
DISCORD_API = "https://discord.com/api/v10"
|
||||||
|
GUILD_ID = "692816967895220344"
|
||||||
|
|
||||||
|
CHECK_IN_MESSAGE = """@everyone it is time for Naomi to do a check in. I need **each and every one of you** to respond to this message **in the thread** by Monday, or you **will be removed** for inactivity.
|
||||||
|
|
||||||
|
1. What have you achieved over the last two weeks?
|
||||||
|
2. What is your focus for the next two weeks?
|
||||||
|
3. How much time can you **reliably** commit to your team over the next two weeks?
|
||||||
|
4. Are you facing any **blockers** or **challenges** that are preventing you from contributing?
|
||||||
|
5. Do you need **help** from your team leaders or from Naomi to get unstuck?
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
async def send_checkin_to_teams() -> None:
|
||||||
|
"""Send check-in message to all team channels (except Jade Jasmine)."""
|
||||||
|
with open("team_message_ids.json") as f:
|
||||||
|
team_data = json.load(f)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bot {DISCORD_BOT_TOKEN}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
for team_name, data in team_data.items():
|
||||||
|
if team_name == "jade-jasmine":
|
||||||
|
print(f"⏭️ Skipping {team_name} (dissolved)")
|
||||||
|
continue
|
||||||
|
|
||||||
|
channel_id = data["channel_id"]
|
||||||
|
print(f"📤 Sending check-in to {team_name} (channel {channel_id})...")
|
||||||
|
|
||||||
|
async with session.post(
|
||||||
|
f"{DISCORD_API}/channels/{channel_id}/messages",
|
||||||
|
headers=headers,
|
||||||
|
json={"content": CHECK_IN_MESSAGE},
|
||||||
|
) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
message_data = await resp.json()
|
||||||
|
message_id = message_data["id"]
|
||||||
|
print(f" ✅ Message sent! Message ID: {message_id}")
|
||||||
|
|
||||||
|
thread_name = (
|
||||||
|
f"Check-in Responses - {team_name.replace('-', ' ').title()}"
|
||||||
|
)
|
||||||
|
async with session.post(
|
||||||
|
f"{DISCORD_API}/channels/{channel_id}/messages/{message_id}/threads",
|
||||||
|
headers=headers,
|
||||||
|
json={
|
||||||
|
"name": thread_name,
|
||||||
|
"auto_archive_duration": 10080,
|
||||||
|
},
|
||||||
|
) as thread_resp:
|
||||||
|
if thread_resp.status == 201:
|
||||||
|
thread_data = await thread_resp.json()
|
||||||
|
thread_id = thread_data["id"]
|
||||||
|
print(f" 🧵 Thread created! Thread ID: {thread_id}")
|
||||||
|
else:
|
||||||
|
error_text = await thread_resp.text()
|
||||||
|
print(
|
||||||
|
f" ❌ Failed to create thread: "
|
||||||
|
f"{thread_resp.status} - {error_text}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
error_text = await resp.text()
|
||||||
|
print(f" ❌ Failed to send message: {resp.status} - {error_text}")
|
||||||
|
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
print("\n✅ Check-in messages sent to all teams!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(send_checkin_to_teams())
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Send team check-in messages to all 14 teams."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]
|
||||||
|
NAOMI_ID = "465650873650118659"
|
||||||
|
|
||||||
|
|
||||||
|
def build_checkin_message(team_name: str, team: dict, role_id: str) -> str:
|
||||||
|
"""Build the team check-in message."""
|
||||||
|
total_members = len(team["leaders"]) + len(team["participants"])
|
||||||
|
num_leaders = len(team["leaders"])
|
||||||
|
num_participants = len(team["participants"])
|
||||||
|
|
||||||
|
leader_text = "leader" if num_leaders == 1 else "leaders"
|
||||||
|
participant_text = "participant" if num_participants == 1 else "participants"
|
||||||
|
|
||||||
|
return f"""## Team Check-In
|
||||||
|
|
||||||
|
Your team currently has **{total_members} members** ({num_leaders} {leader_text} + {num_participants} {participant_text}).
|
||||||
|
|
||||||
|
Given the recent changes to team size, we want to make sure you feel confident moving forward with your project. Please discuss as a team and let us know:
|
||||||
|
|
||||||
|
**Do you feel you can still complete your project with your current team size?**
|
||||||
|
|
||||||
|
If you have concerns about capacity, need additional support, or would like to discuss options (such as combining with another team or adjusting project scope), please ping <@{NAOMI_ID}> and we'll work together to find a solution.
|
||||||
|
|
||||||
|
Your success is the priority here - we want to make sure every team has what they need to build something amazing! 💜
|
||||||
|
|
||||||
|
-# <@&{role_id}>"""
|
||||||
|
|
||||||
|
|
||||||
|
async def send_message(
|
||||||
|
session: aiohttp.ClientSession, channel_id: str, content: str
|
||||||
|
) -> dict | None:
|
||||||
|
"""Send a message to a Discord channel."""
|
||||||
|
url = f"https://discord.com/api/v10/channels/{channel_id}/messages"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bot {DISCORD_BOT_TOKEN}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
payload = {"content": content}
|
||||||
|
|
||||||
|
async with session.post(url, headers=headers, json=payload) as resp:
|
||||||
|
if resp.status in [200, 201]:
|
||||||
|
return await resp.json()
|
||||||
|
error_text = await resp.text()
|
||||||
|
print(
|
||||||
|
f"❌ Failed to send message to channel {channel_id}: {resp.status} - {error_text}" # noqa: E501
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
"""Send check-in messages to all teams."""
|
||||||
|
with open("team_message_ids.json") as f:
|
||||||
|
team_data = json.load(f)
|
||||||
|
|
||||||
|
with open("team_assignments.json") as f:
|
||||||
|
teams = json.load(f)
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
print("📢 Sending team check-in messages...\n")
|
||||||
|
|
||||||
|
for team in teams:
|
||||||
|
team_name = team["name"]
|
||||||
|
channel_id = team_data[team_name]["channel_id"]
|
||||||
|
role_id = team_data[team_name]["role_id"]
|
||||||
|
|
||||||
|
print(f"Processing {team_name}...")
|
||||||
|
|
||||||
|
checkin_msg = build_checkin_message(team_name, team, role_id)
|
||||||
|
result = await send_message(session, channel_id, checkin_msg)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
total = len(team["leaders"]) + len(team["participants"])
|
||||||
|
print(f" ✅ Sent check-in ({total} members)")
|
||||||
|
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
print("\n✨ Done sending all team check-in messages!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
Executable
+73
@@ -0,0 +1,73 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Update GitHub teams for Jade Jasmine dissolution
|
||||||
|
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
ORG="nhcarrigan-spring-2026-cohort"
|
||||||
|
|
||||||
|
echo "=== Phase 2: GitHub Team Changes ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 1: Remove all members from jade-jasmine team
|
||||||
|
echo "Step 1: Removing members from jade-jasmine team..."
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/jade-jasmine/memberships/Nikil-D-Gr8" || echo " - Nikil-D-Gr8 already removed or not found"
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/jade-jasmine/memberships/amandaw800" || echo " - amandaw800 already removed or not found"
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/jade-jasmine/memberships/hayden-html" || echo " - hayden-html already removed or not found"
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/jade-jasmine/memberships/hbar1st" || echo " - hbar1st already removed or not found"
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/jade-jasmine/memberships/rosacabrerac" || echo " - rosacabrerac already removed or not found"
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/jade-jasmine/memberships/ArbitraryPie" || echo " - ArbitraryPie already removed or not found"
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/jade-jasmine/memberships/builtbykabir" || echo " - builtbykabir already removed or not found"
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/jade-jasmine/memberships/Mista-Log" || echo " - Mista-Log already removed or not found"
|
||||||
|
echo "✅ jade-jasmine team cleared"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 2: Remove leaders from jade-jasmine-leaders team
|
||||||
|
echo "Step 2: Removing leaders from jade-jasmine-leaders team..."
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/jade-jasmine-leaders/memberships/hayden-html" || echo " - hayden-html already removed or not found"
|
||||||
|
gh api --method DELETE "/orgs/$ORG/teams/jade-jasmine-leaders/memberships/Mista-Log" || echo " - Mista-Log already removed or not found"
|
||||||
|
echo "✅ jade-jasmine-leaders team cleared"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 3: Add members to new teams
|
||||||
|
echo "Step 3: Adding members to new teams..."
|
||||||
|
|
||||||
|
echo " - Adding Nikil-D-Gr8 to crimson-dahlia..."
|
||||||
|
gh api --method PUT "/orgs/$ORG/teams/crimson-dahlia/memberships/Nikil-D-Gr8" -f role=member
|
||||||
|
|
||||||
|
echo " - Adding amandaw800 to violet-carnation..."
|
||||||
|
gh api --method PUT "/orgs/$ORG/teams/violet-carnation/memberships/amandaw800" -f role=member
|
||||||
|
|
||||||
|
echo " - Adding hayden-html to teal-iris..."
|
||||||
|
gh api --method PUT "/orgs/$ORG/teams/teal-iris/memberships/hayden-html" -f role=member
|
||||||
|
|
||||||
|
echo " - Adding hbar1st to indigo-tulip..."
|
||||||
|
gh api --method PUT "/orgs/$ORG/teams/indigo-tulip/memberships/hbar1st" -f role=member
|
||||||
|
|
||||||
|
echo " - Adding rosacabrerac to scarlet-hydrangea..."
|
||||||
|
gh api --method PUT "/orgs/$ORG/teams/scarlet-hydrangea/memberships/rosacabrerac" -f role=member
|
||||||
|
|
||||||
|
echo " - Adding ArbitraryPie to peach-gardenia..."
|
||||||
|
gh api --method PUT "/orgs/$ORG/teams/peach-gardenia/memberships/ArbitraryPie" -f role=member
|
||||||
|
|
||||||
|
echo " - Adding builtbykabir to azure-lotus..."
|
||||||
|
gh api --method PUT "/orgs/$ORG/teams/azure-lotus/memberships/builtbykabir" -f role=member
|
||||||
|
|
||||||
|
echo " - Adding Mista-Log to ivory-orchid..."
|
||||||
|
gh api --method PUT "/orgs/$ORG/teams/ivory-orchid/memberships/Mista-Log" -f role=member
|
||||||
|
|
||||||
|
echo "✅ All members added to new teams"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 4: Add Mista-Log to ivory-orchid-leaders
|
||||||
|
echo "Step 4: Adding Mista-Log to ivory-orchid-leaders..."
|
||||||
|
gh api --method PUT "/orgs/$ORG/teams/ivory-orchid-leaders/memberships/Mista-Log" -f role=member
|
||||||
|
echo "✅ Mista-Log promoted to leader in Ivory Orchid"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "=== Phase 2 Complete! ==="
|
||||||
|
echo ""
|
||||||
|
echo "Summary:"
|
||||||
|
echo "- ✅ jade-jasmine team cleared (8 members removed)"
|
||||||
|
echo "- ✅ jade-jasmine-leaders team cleared (2 leaders removed)"
|
||||||
|
echo "- ✅ 8 members added to their new teams"
|
||||||
|
echo "- ✅ Mista-Log promoted to leader in ivory-orchid"
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
"""Update team roster messages in Discord from team_assignments.json."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]
|
||||||
|
GUILD_ID = "692816967895220344"
|
||||||
|
|
||||||
|
API_BASE = "https://discord.com/api/v10"
|
||||||
|
|
||||||
|
|
||||||
|
async def edit_message(
|
||||||
|
session: aiohttp.ClientSession,
|
||||||
|
channel_id: str,
|
||||||
|
message_id: str,
|
||||||
|
content: str,
|
||||||
|
) -> bool:
|
||||||
|
"""Edit a Discord message."""
|
||||||
|
url = f"{API_BASE}/channels/{channel_id}/messages/{message_id}"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bot {DISCORD_BOT_TOKEN}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
payload = {"content": content}
|
||||||
|
|
||||||
|
async with session.patch(url, headers=headers, json=payload) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
return True
|
||||||
|
text = await resp.text()
|
||||||
|
print(f" ❌ Failed to edit message: {resp.status} - {text}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def generate_roster(team: dict, discord_to_github: dict) -> str:
|
||||||
|
"""Generate roster message for a team."""
|
||||||
|
team_name = team["name"]
|
||||||
|
|
||||||
|
leader_lines = [
|
||||||
|
f"- <@{discord_id}> ({discord_to_github.get(discord_id, 'Unknown')})"
|
||||||
|
for discord_id in team["leaders"]
|
||||||
|
]
|
||||||
|
|
||||||
|
participant_lines = [
|
||||||
|
f"- <@{discord_id}> ({discord_to_github.get(discord_id, 'Unknown')})"
|
||||||
|
for discord_id in team["participants"]
|
||||||
|
]
|
||||||
|
|
||||||
|
leaders_text = "\n".join(leader_lines) if leader_lines else "None"
|
||||||
|
participants_text = "\n".join(participant_lines) if participant_lines else "None"
|
||||||
|
|
||||||
|
return f"""# Team {team_name}
|
||||||
|
|
||||||
|
**Leaders:**
|
||||||
|
{leaders_text}
|
||||||
|
|
||||||
|
**Participants:**
|
||||||
|
{participants_text}"""
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
"""Update roster messages for all teams."""
|
||||||
|
with open("team_message_ids.json") as f:
|
||||||
|
team_data = json.load(f)
|
||||||
|
|
||||||
|
with open("team_assignments.json") as f:
|
||||||
|
teams = json.load(f)
|
||||||
|
|
||||||
|
with open("discord_to_github.json") as f:
|
||||||
|
discord_to_github = json.load(f)
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
for team in teams:
|
||||||
|
team_name = team["name"]
|
||||||
|
print(f"Updating roster for {team_name}...")
|
||||||
|
|
||||||
|
if team_name not in team_data:
|
||||||
|
print(" ⚠️ Team not found in team_message_ids.json")
|
||||||
|
continue
|
||||||
|
|
||||||
|
channel_id = team_data[team_name]["channel_id"]
|
||||||
|
message_id = team_data[team_name]["message_id"]
|
||||||
|
|
||||||
|
roster_content = generate_roster(team, discord_to_github)
|
||||||
|
|
||||||
|
success = await edit_message(
|
||||||
|
session, channel_id, message_id, roster_content
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
print(f" ✅ Updated (Message ID: {message_id})")
|
||||||
|
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
print("\n✅ All roster messages updated!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -79,6 +79,8 @@ ignore = [
|
|||||||
"DTZ001",
|
"DTZ001",
|
||||||
# Ambiguous variable names - context makes it clear
|
# Ambiguous variable names - context makes it clear
|
||||||
"E741",
|
"E741",
|
||||||
|
# Long lines in string literals (Discord messages, URLs)
|
||||||
|
"E501",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff.lint.pydocstyle]
|
[tool.ruff.lint.pydocstyle]
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ ruff==0.14.14
|
|||||||
# Runtime dependencies
|
# Runtime dependencies
|
||||||
requests==2.32.3
|
requests==2.32.3
|
||||||
aiohttp==3.11.12
|
aiohttp==3.11.12
|
||||||
|
pandas==3.0.1
|
||||||
Reference in New Issue
Block a user