diff --git a/python/cohort/add_github_team_members.py b/python/cohort/add_github_team_members.py index dafda8a..175385f 100755 --- a/python/cohort/add_github_team_members.py +++ b/python/cohort/add_github_team_members.py @@ -1,15 +1,29 @@ #!/usr/bin/env python3 -"""Add GitHub users to their appropriate teams in nhcarrigan-spring-2026-cohort org""" +"""Add GitHub users to their appropriate teams in the cohort GitHub organisation. + +Uses the GitHub CLI to add each member to their corresponding team and, for +leaders, to the team's leaders sub-team. + +Data files (place in data/): + - team_assignments.json Team rosters with leaders and participants per team + - discord_to_github.json Mapping of Discord IDs to GitHub usernames + +Env vars: + - None (uses `gh` CLI for authentication) +""" import json import subprocess import time +from pathlib import Path + +DATA_DIR = Path(__file__).parent.parent.parent / "data" # Load team assignments and Discord to GitHub mappings -with open("team_assignments.json") as f: +with open(DATA_DIR / "team_assignments.json") as f: teams = json.load(f) -with open("discord_to_github.json") as f: +with open(DATA_DIR / "discord_to_github.json") as f: discord_to_github = json.load(f) # Map team names to GitHub team slugs diff --git a/python/cohort/analyse_availability.py b/python/cohort/analyse_availability.py index 438ee78..723aa98 100644 --- a/python/cohort/analyse_availability.py +++ b/python/cohort/analyse_availability.py @@ -1,6 +1,26 @@ +"""Analyse applicant availability from a markdown table and produce UTC block stats. + +Reads a markdown table of availability responses and a Discord verification file, +then produces a JSON summary of coverage across morning/afternoon/evening UTC blocks +for each day of the week. + +Data files (place in data/): + - table.md Markdown table of applicant availability responses + - discord_verification.json Discord ID verification results (from verify_discord.py) + +Outputs (written to data/): + - availability_analysis.json UTC block distribution per applicant + +Env vars: + - None +""" + import json import re from collections import defaultdict +from pathlib import Path + +DATA_DIR = Path(__file__).parent.parent.parent / "data" DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] @@ -99,7 +119,7 @@ def analyze_applicant_availability(timezone_str: str, day_slots: dict) -> dict: def parse_table_md() -> list[dict]: """Parse table.md and extract availability data""" - with open("table.md") as f: + with open(DATA_DIR / "table.md") as f: content = f.read() lines = content.strip().split("\n") @@ -131,7 +151,7 @@ def parse_table_md() -> list[dict]: def main(): - with open("discord_verification.json") as f: + with open(DATA_DIR / "discord_verification.json") as f: verification = json.load(f) verified_ids = {v[0] for v in verification["verified"]} @@ -167,7 +187,7 @@ def main(): } ) - with open("availability_analysis.json", "w") as f: + with open(DATA_DIR / "availability_analysis.json", "w") as f: json.dump(availability_results, f, indent=2) block_distribution = defaultdict(int) diff --git a/python/cohort/assign_cohort_role.py b/python/cohort/assign_cohort_role.py index 97e0ba7..0a9e26f 100644 --- a/python/cohort/assign_cohort_role.py +++ b/python/cohort/assign_cohort_role.py @@ -6,9 +6,12 @@ Respects Discord rate limits with proper backoff and retry logic. import json import os import time +from pathlib import Path import requests +DATA_DIR = Path(__file__).parent.parent.parent / "data" + BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"] GUILD_ID = "692816967895220344" COHORT_ROLE_ID = "1464314780935258112" @@ -48,7 +51,7 @@ def assign_role_with_retry(user_id: str, role_id: str, max_retries: int = 5) -> def main(): - with open("team_assignments.json") as f: + with open(DATA_DIR / "team_assignments.json") as f: teams = json.load(f) all_users = [] diff --git a/python/cohort/assign_team_roles.py b/python/cohort/assign_team_roles.py index 91bf134..de162d2 100644 --- a/python/cohort/assign_team_roles.py +++ b/python/cohort/assign_team_roles.py @@ -6,9 +6,12 @@ Respects Discord rate limits with proper backoff and retry logic. import json import os import time +from pathlib import Path import requests +DATA_DIR = Path(__file__).parent.parent.parent / "data" + BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"] GUILD_ID = "692816967895220344" @@ -64,7 +67,7 @@ def assign_role_with_retry(user_id: str, role_id: str, max_retries: int = 5) -> def main(): - with open("team_assignments.json") as f: + with open(DATA_DIR / "team_assignments.json") as f: teams = json.load(f) print(f"Assigning team roles to {len(teams)} teams...") diff --git a/python/cohort/catch_up_report.py b/python/cohort/catch_up_report.py index 2eb948a..41a0d98 100644 --- a/python/cohort/catch_up_report.py +++ b/python/cohort/catch_up_report.py @@ -11,9 +11,12 @@ import json import os import subprocess from datetime import datetime, timezone +from pathlib import Path import aiohttp +DATA_DIR = Path(__file__).parent.parent.parent / "data" + DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"] DISCORD_API_BASE = "https://discord.com/api/v10" GITHUB_API_BASE = "https://api.github.com" @@ -440,10 +443,10 @@ def build_report( async def main() -> None: """Run the catch-up activity report.""" print("Loading data files...") - with open("team_assignments.json") as f: + with open(DATA_DIR / "team_assignments.json") as f: team_data: list[dict] = json.load(f) - with open("discord_to_github.json") as f: + with open(DATA_DIR / "discord_to_github.json") as f: discord_to_github: dict[str, str] = json.load(f) print("Getting GitHub token via gh CLI...") diff --git a/python/cohort/check_lengths.py b/python/cohort/check_lengths.py index a4a39f6..9ef5343 100644 --- a/python/cohort/check_lengths.py +++ b/python/cohort/check_lengths.py @@ -1,4 +1,15 @@ -"""Quick dry-run to check Discord message lengths before sending.""" +"""Dry-run check of Discord message lengths before sending the activity report. + +Parses the catch_up_report.md table, formats each team's data into a monospace +Discord table, and reports whether any message would exceed Discord's 2000-char limit. +Run this before send_activity_report.py to catch length issues early. + +Data files (place in data/): + - catch_up_report.md Activity report generated by catch_up_report.py + +Env vars: + - None +""" FIELDS = [ ("Discord Username", "Name", 18), @@ -11,7 +22,7 @@ FIELDS = [ ("Commits", "Commits", 7), ] -REPORT_PATH = "catch_up_report.md" +REPORT_PATH = "data/catch_up_report.md" def parse_report(path: str) -> dict[str, list[dict]]: diff --git a/python/cohort/discord_activity_checker.py b/python/cohort/discord_activity_checker.py index c23184f..e6cda78 100755 --- a/python/cohort/discord_activity_checker.py +++ b/python/cohort/discord_activity_checker.py @@ -1,6 +1,17 @@ #!/usr/bin/env python3 -"""Discord Team Activity Checker -Checks for team members who haven't sent messages in their channels within 36 hours +"""Check for team members who have not sent a message in their channel within 36 hours. + +Scans each team's Discord channel and flags members with no recent activity. +Optionally sends a direct mention message to inactive members. + +Data files (place in data/): + - team_assignments.json Team rosters with leaders and participants per team + +Outputs (written to data/): + - discord_activity_report.json Inactive members per team with last-seen timestamps + +Env vars: + - DISCORD_BOT_TOKEN Bot token for the Discord API """ import asyncio @@ -8,16 +19,19 @@ import json import os import sys from datetime import datetime, timedelta, timezone +from pathlib import Path import aiohttp +DATA_DIR = Path(__file__).parent.parent.parent / "data" + # Configuration DISCORD_TOKEN = os.environ["DISCORD_BOT_TOKEN"] DISCORD_API_BASE = "https://discord.com/api/v10" INACTIVE_THRESHOLD_HOURS = 36 # Load team assignments from file -with open("team_assignments.json") as f: +with open(DATA_DIR / "team_assignments.json") as f: team_data = json.load(f) # Build TEAMS dictionary with channel IDs and member lists @@ -233,7 +247,7 @@ async def main(): print("\n" + "=" * 80) # Save results to JSON - with open("discord_activity_report.json", "w") as f: + with open(DATA_DIR / "discord_activity_report.json", "w") as f: json.dump( { "generated_at": datetime.now(timezone.utc).isoformat(), diff --git a/python/cohort/evaluate_technical_proficiency.py b/python/cohort/evaluate_technical_proficiency.py index b31d50b..86393ae 100644 --- a/python/cohort/evaluate_technical_proficiency.py +++ b/python/cohort/evaluate_technical_proficiency.py @@ -1,8 +1,27 @@ +"""Evaluate the technical proficiency of cohort applicants using their GitHub profiles. + +Fetches each applicant's public GitHub repositories and scores their proficiency as +Beginner, Intermediate, or Advanced based on language variety, repo count, commit +activity, and presence of certain technologies. + +Data files (place in data/): + - applicants_to_evaluate.json List of applicants with GitHub usernames + +Outputs (written to data/): + - proficiency_evaluations.json Proficiency scores and tech stacks per applicant + +Env vars: + - None (uses public GitHub API; may be rate-limited without authentication) +""" + import json import re import time import urllib.error import urllib.request +from pathlib import Path + +DATA_DIR = Path(__file__).parent.parent.parent / "data" # GitHub API (no auth needed for public repos, but rate limited) GITHUB_API = "https://api.github.com" @@ -234,7 +253,7 @@ def evaluate_applicant(applicant: dict, index: int, total: int) -> dict: def main(): # Load applicants - with open("applicants_to_evaluate.json") as f: + with open(DATA_DIR / "applicants_to_evaluate.json") as f: applicants = json.load(f) print(f"Evaluating {len(applicants)} applicants...\n") @@ -249,7 +268,7 @@ def main(): print(f" Progress: {i + 1}/{len(applicants)} complete") # Save results - with open("proficiency_evaluations.json", "w") as f: + with open(DATA_DIR / "proficiency_evaluations.json", "w") as f: json.dump(evaluations, f, indent=2) # Summary diff --git a/python/cohort/fix_channel_permissions.py b/python/cohort/fix_channel_permissions.py index f159d7b..c257609 100644 --- a/python/cohort/fix_channel_permissions.py +++ b/python/cohort/fix_channel_permissions.py @@ -1,7 +1,15 @@ #!/usr/bin/env python3 -"""Fix permissions for cohort-team-michael-and-yoon channel. +"""Fix Send Messages / Send Messages in Threads permissions for a Discord channel. -Deny Send Messages in Threads for @everyone and @cohort. +Denies Send Messages and Send Messages in Threads for both @everyone and the +@cohort role on the target channel. Update CHANNEL_ID and COHORT_ROLE_ID before +running. + +Data files (place in data/): + - None + +Env vars: + - DISCORD_BOT_TOKEN Bot token for the Discord API """ import asyncio diff --git a/python/cohort/generate_member_files.py b/python/cohort/generate_member_files.py index 8ce6749..663eccb 100644 --- a/python/cohort/generate_member_files.py +++ b/python/cohort/generate_member_files.py @@ -1,23 +1,46 @@ +"""Generate markdown participant and leader profile files for the cohort. + +Reads all evaluation data files and produces two markdown files summarising +each member's tech stack, availability, proficiency, and leadership assessment. + +Data files (place in data/): + - discord_verification.json Discord ID verification results (from verify_discord.py) + - proficiency_evaluations.json Proficiency scores (from evaluate_technical_proficiency.py) + - availability_analysis.json Availability UTC blocks (from analyse_availability.py) + - leadership_candidates.json List of applicants who expressed interest in leading + - leadership_evaluations.json Leadership assessment scores + +Outputs (written to data/): + - participants.md Markdown profile for each participant + - leaders.md Markdown profile for each leader candidate + +Env vars: + - None +""" + import json +from pathlib import Path + +DATA_DIR = Path(__file__).parent.parent.parent / "data" BLOCK_EMOJIS = {"mornings": "🌅", "afternoons": "☀️", "evenings": "🌆", "nights": "🌙"} def load_all_data(): """Load all evaluation data files""" - with open("discord_verification.json") as f: + with open(DATA_DIR / "discord_verification.json") as f: verification = json.load(f) - with open("proficiency_evaluations.json") as f: + with open(DATA_DIR / "proficiency_evaluations.json") as f: proficiency = json.load(f) - with open("availability_analysis.json") as f: + with open(DATA_DIR / "availability_analysis.json") as f: availability = json.load(f) - with open("leadership_candidates.json") as f: + with open(DATA_DIR / "leadership_candidates.json") as f: candidates = json.load(f) - with open("leadership_evaluations.json") as f: + with open(DATA_DIR / "leadership_evaluations.json") as f: leadership = json.load(f) return verification, proficiency, availability, candidates, leadership @@ -230,14 +253,14 @@ def main(): participants_md = generate_participants_md( non_leader_ids, verified_usernames, prof_by_id, avail_by_id ) - with open("participants.md", "w") as f: + with open(DATA_DIR / "participants.md", "w") as f: f.write(participants_md) print(f"Generated participants.md with {len(non_leader_ids)} participants") leaders_md = generate_leaders_md( leader_ids, verified_usernames, prof_by_id, avail_by_id, lead_by_id ) - with open("leaders.md", "w") as f: + with open(DATA_DIR / "leaders.md", "w") as f: f.write(leaders_md) print(f"Generated leaders.md with {len(leader_ids)} leaders") diff --git a/python/cohort/generate_timeslots.py b/python/cohort/generate_timeslots.py index 9492877..2b44116 100644 --- a/python/cohort/generate_timeslots.py +++ b/python/cohort/generate_timeslots.py @@ -1,5 +1,20 @@ +"""Generate hourly timeslot JSON for use with Crabfit scheduling tool. + +Produces a list of ISO-format datetime strings covering every hour across the +scheduling window. Update the start_date and end_date constants before running. + +Outputs (written to data/): + - crabfit_timeslots.json List of hourly timeslot strings + +Env vars: + - None +""" + import json from datetime import datetime, timedelta +from pathlib import Path + +DATA_DIR = Path(__file__).parent.parent.parent / "data" # Generate hourly time slots from Feb 1 to March 3, 2026 # 24 hours a day, America/Los_Angeles timezone @@ -18,7 +33,7 @@ print(f"First: {times[0]}") print(f"Last: {times[-1]}") # Save to file for use -with open("/home/naomi/docs/cohort/crabfit_timeslots.json", "w") as f: +with open(DATA_DIR / "crabfit_timeslots.json", "w") as f: json.dump(times, f) print("Saved to crabfit_timeslots.json") diff --git a/python/cohort/get_cohort_members.py b/python/cohort/get_cohort_members.py index 4ad2940..0b588c8 100644 --- a/python/cohort/get_cohort_members.py +++ b/python/cohort/get_cohort_members.py @@ -4,9 +4,12 @@ import asyncio import json import os +from pathlib import Path import aiohttp +DATA_DIR = Path(__file__).parent.parent.parent / "data" + DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"] GUILD_ID = "692816967895220344" COHORT_ROLE_ID = "1464314780935258112" @@ -72,7 +75,7 @@ async def main() -> None: f"{i}. {member['display_name']} (@{member['username']}) - ID: {member['id']}" # noqa: E501 ) - with open("active_cohort_members.json", "w") as f: + with open(DATA_DIR / "active_cohort_members.json", "w") as f: json.dump(cohort_members, f, indent=2) print("\nSaved to active_cohort_members.json") diff --git a/python/cohort/remove_discord_roles.py b/python/cohort/remove_discord_roles.py index b0149a6..6d2b15b 100644 --- a/python/cohort/remove_discord_roles.py +++ b/python/cohort/remove_discord_roles.py @@ -1,5 +1,15 @@ #!/usr/bin/env python3 -"""Remove cohort and team roles from inactive Discord members.""" +"""Remove the Cohort and team-specific Discord roles from a list of members. + +Update INACTIVE_MEMBERS and MEMBER_TO_TEAM before running to target the correct +members. Removes both the cohort-wide role and the member's team role. + +Data files (place in data/): + - None (member IDs and team mappings are defined as constants in the script) + +Env vars: + - DISCORD_BOT_TOKEN Bot token for the Discord API +""" import asyncio import os diff --git a/python/cohort/remove_member.py b/python/cohort/remove_member.py index acccb45..3eb27e0 100644 --- a/python/cohort/remove_member.py +++ b/python/cohort/remove_member.py @@ -18,9 +18,12 @@ import json import os import sys from datetime import datetime, timezone +from pathlib import Path import aiohttp +DATA_DIR = Path(__file__).parent.parent.parent / "data" + DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"] DISCORD_API_BASE = "https://discord.com/api/v10" DISCORD_GUILD_ID = "692816967895220344" @@ -71,7 +74,7 @@ class MemberRemover: def find_member_info(self) -> bool: """Find which team the member is on and their role.""" - with open("team_assignments.json") as f: + with open(DATA_DIR / "team_assignments.json") as f: teams = json.load(f) for team in teams: @@ -88,7 +91,7 @@ class MemberRemover: def remove_from_team_assignments(self) -> None: """Remove member from team_assignments.json.""" - with open("team_assignments.json") as f: + with open(DATA_DIR / "team_assignments.json") as f: teams = json.load(f) for team in teams: @@ -103,19 +106,19 @@ class MemberRemover: f"✅ Removed from {team['name']} participants in team_assignments.json" ) - with open("team_assignments.json", "w") as f: + with open(DATA_DIR / "team_assignments.json", "w") as f: json.dump(teams, f, indent=2) def remove_from_discord_to_github(self) -> None: """Remove member from discord_to_github.json.""" - with open("discord_to_github.json") as f: + with open(DATA_DIR / "discord_to_github.json") as f: mappings = json.load(f) if self.discord_id in mappings: self.github_username = mappings[self.discord_id] del mappings[self.discord_id] - with open("discord_to_github.json", "w") as f: + with open(DATA_DIR / "discord_to_github.json", "w") as f: json.dump(mappings, f, indent=2) print(f"✅ Removed {self.github_username} from discord_to_github.json") @@ -135,7 +138,7 @@ class MemberRemover: async def remove_discord_roles(self) -> None: """Remove Discord roles from the member.""" - with open("team_message_ids.json") as f: + with open(DATA_DIR / "team_message_ids.json") as f: team_message_data = json.load(f) if self.team_name not in team_message_data: diff --git a/python/cohort/remove_resigned_members.py b/python/cohort/remove_resigned_members.py index 8c7a029..5f79cf1 100644 --- a/python/cohort/remove_resigned_members.py +++ b/python/cohort/remove_resigned_members.py @@ -6,6 +6,9 @@ before running this script. """ import json +from pathlib import Path + +DATA_DIR = Path(__file__).parent.parent.parent / "data" # Discord IDs of members who have resigned - update before running RESIGNED_IDS: list[str] = [] @@ -13,7 +16,7 @@ RESIGNED_IDS: list[str] = [] def main() -> None: """Remove resigned members from team assignments.""" - with open("team_assignments.json") as f: + with open(DATA_DIR / "team_assignments.json") as f: teams = json.load(f) changes_made = [] @@ -36,7 +39,7 @@ def main() -> None: 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: + with open(DATA_DIR / "team_assignments.json", "w") as f: json.dump(teams, f, indent=2) if changes_made: diff --git a/python/cohort/send_activity_report.py b/python/cohort/send_activity_report.py index 01b2914..4832e19 100644 --- a/python/cohort/send_activity_report.py +++ b/python/cohort/send_activity_report.py @@ -1,4 +1,14 @@ -"""Send formatted activity report tables to each team channel via Amari bot.""" +"""Send formatted activity report tables to each team's Discord channel. + +Parses catch_up_report.md and posts a monospace table of each member's Discord +and GitHub activity stats to their respective team channel. + +Data files (place in data/): + - catch_up_report.md Activity report generated by catch_up_report.py + +Env vars: + - DISCORD_BOT_TOKEN Bot token for the Discord API +""" import asyncio import os @@ -25,7 +35,9 @@ CHANNEL_IDS = { "Sage Marigold": "1464316850669093040", } -REPORT_PATH = os.path.join(os.path.dirname(__file__), "catch_up_report.md") +REPORT_PATH = os.path.join( + os.path.dirname(__file__), "..", "..", "data", "catch_up_report.md" +) FIELDS = [ ("Discord Username", "Name", 18), diff --git a/python/cohort/send_checkin.py b/python/cohort/send_checkin.py index 26b142c..9515381 100644 --- a/python/cohort/send_checkin.py +++ b/python/cohort/send_checkin.py @@ -1,11 +1,25 @@ -"""Send check-in messages to all team channels.""" +"""Send biweekly check-in messages to all team Discord channels. + +Posts a check-in prompt to each team channel and automatically creates a thread +for responses. Members who do not respond by the deadline may be removed for +inactivity. + +Data files (place in data/): + - team_message_ids.json Channel IDs per team (generated by send_team_messages.py) + +Env vars: + - DISCORD_BOT_TOKEN Bot token for the Discord API +""" import asyncio import json import os +from pathlib import Path import aiohttp +DATA_DIR = Path(__file__).parent.parent.parent / "data" + DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"] DISCORD_API = "https://discord.com/api/v10" GUILD_ID = "692816967895220344" @@ -22,7 +36,7 @@ CHECK_IN_MESSAGE = """@everyone it is time for Naomi to do a check in. I need ** 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: + with open(DATA_DIR / "team_message_ids.json") as f: team_data = json.load(f) headers = { diff --git a/python/cohort/send_team_checkin.py b/python/cohort/send_team_checkin.py index dc66071..0fc6724 100644 --- a/python/cohort/send_team_checkin.py +++ b/python/cohort/send_team_checkin.py @@ -1,12 +1,26 @@ #!/usr/bin/env python3 -"""Send team check-in messages to all 14 teams.""" +"""Send a capacity check-in message to each team channel. + +Posts a message asking whether the team feels able to complete their project +given their current member count, and invites them to request support if needed. + +Data files (place in data/): + - team_message_ids.json Channel and role IDs per team (from send_team_messages.py) + - team_assignments.json Team rosters used to report current member counts + +Env vars: + - DISCORD_BOT_TOKEN Bot token for the Discord API +""" import asyncio import json import os +from pathlib import Path import aiohttp +DATA_DIR = Path(__file__).parent.parent.parent / "data" + DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"] NAOMI_ID = "465650873650118659" @@ -58,10 +72,10 @@ async def send_message( async def main() -> None: """Send check-in messages to all teams.""" - with open("team_message_ids.json") as f: + with open(DATA_DIR / "team_message_ids.json") as f: team_data = json.load(f) - with open("team_assignments.json") as f: + with open(DATA_DIR / "team_assignments.json") as f: teams = json.load(f) async with aiohttp.ClientSession() as session: diff --git a/python/cohort/send_team_messages.py b/python/cohort/send_team_messages.py index 0ee6bb0..c9b69bb 100644 --- a/python/cohort/send_team_messages.py +++ b/python/cohort/send_team_messages.py @@ -1,10 +1,29 @@ +"""Send initial welcome and roster messages to all team Discord channels. + +Creates a pinned roster message in each team channel and stores the resulting +message ID, channel ID, and role ID in team_message_ids.json for use by +other scripts (send_checkin.py, update_roster_messages.py, etc.). + +Data files (place in data/): + - team_assignments.json Team rosters with leaders and participants per team + - applicants_to_evaluate.json Applicant data including Discord channel/role IDs + +Outputs (written to data/): + - team_message_ids.json Channel ID, message ID, and role ID per team + +Env vars: + - DISCORD_BOT_TOKEN Bot token for the Discord API +""" + import json import os import time +from pathlib import Path import requests -# Amari's bot token +DATA_DIR = Path(__file__).parent.parent.parent / "data" + TOKEN = os.environ["DISCORD_BOT_TOKEN"] GUILD_ID = "692816967895220344" @@ -72,12 +91,12 @@ TEAMS = { } # Load team assignments and convert to dict by team name -with open("team_assignments.json") as f: +with open(DATA_DIR / "team_assignments.json") as f: team_list = json.load(f) team_data = {team["name"]: team for team in team_list} # Load applicants to get project_url by discord_id -with open("applicants_to_evaluate.json") as f: +with open(DATA_DIR / "applicants_to_evaluate.json") as f: applicants = json.load(f) applicant_lookup = {str(a["discord_id"]): a for a in applicants} diff --git a/python/cohort/update_roster_messages.py b/python/cohort/update_roster_messages.py index d181ba9..378ee41 100644 --- a/python/cohort/update_roster_messages.py +++ b/python/cohort/update_roster_messages.py @@ -3,9 +3,12 @@ import asyncio import json import os +from pathlib import Path import aiohttp +DATA_DIR = Path(__file__).parent.parent.parent / "data" + DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"] GUILD_ID = "692816967895220344" @@ -62,13 +65,13 @@ def generate_roster(team: dict, discord_to_github: dict) -> str: async def main() -> None: """Update roster messages for all teams.""" - with open("team_message_ids.json") as f: + with open(DATA_DIR / "team_message_ids.json") as f: team_data = json.load(f) - with open("team_assignments.json") as f: + with open(DATA_DIR / "team_assignments.json") as f: teams = json.load(f) - with open("discord_to_github.json") as f: + with open(DATA_DIR / "discord_to_github.json") as f: discord_to_github = json.load(f) async with aiohttp.ClientSession() as session: diff --git a/python/cohort/verify_discord.py b/python/cohort/verify_discord.py index e9894c8..d4bd09a 100644 --- a/python/cohort/verify_discord.py +++ b/python/cohort/verify_discord.py @@ -1,8 +1,26 @@ +"""Verify Discord user IDs from a markdown table of applicant data. + +Reads a markdown table containing Discord IDs and checks each one against the +Discord API to confirm the user exists. Handles rate limits automatically. + +Data files (place in data/): + - table.md Markdown table of applicants including a Discord ID column + +Outputs (written to data/): + - discord_verification.json Verification result (valid/invalid) per Discord ID + +Env vars: + - DISCORD_BOT_TOKEN Bot token for the Discord API +""" + import json import os import time import urllib.error import urllib.request +from pathlib import Path + +DATA_DIR = Path(__file__).parent.parent.parent / "data" # Configuration BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"] @@ -10,7 +28,7 @@ GUILD_ID = "692816967895220344" BASE_URL = "https://discord.com/api/v10" # Read Discord IDs from table.md -with open("table.md") as f: +with open(DATA_DIR / "table.md") as f: content = f.read() lines = content.strip().split("\n") @@ -104,7 +122,7 @@ print(f"Missing: {len(missing)}") print(f"Errors: {len(errors)}") # Save results -with open("discord_verification.json", "w") as f: +with open(DATA_DIR / "discord_verification.json", "w") as f: json.dump({"verified": verified, "missing": missing, "errors": errors}, f, indent=2) print("\nResults saved to discord_verification.json") diff --git a/typescript/src/crowdin/clearHiddenTranslations.ts b/typescript/src/crowdin/clearHiddenTranslations.ts index 325dff4..efb32f7 100644 --- a/typescript/src/crowdin/clearHiddenTranslations.ts +++ b/typescript/src/crowdin/clearHiddenTranslations.ts @@ -1,4 +1,15 @@ /** + * @file Delete hidden/outdated translations from a Crowdin project. + * Reads a list of string IDs from data/crowdin-strings.json, checks which + * have already been processed via the log file, and deletes translations + * for unprocessed strings across all active languages in parallel. + * Data files (place in data/): + * - crowdin-strings.json String IDs to clear (from crowdin/writeData.ts) + * - crowdin-strings-hidden.txt Log of already-processed string IDs + * Env vars: + * CROWDIN_PROJECT_ID - Crowdin project numeric ID + * CROWDIN_API_URL - Base URL of the Crowdin API + * CROWDIN_TOKEN - Crowdin personal access token. * @copyright NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan diff --git a/typescript/src/crowdin/reapplyTranslations.ts b/typescript/src/crowdin/reapplyTranslations.ts index 52468a9..581a014 100644 --- a/typescript/src/crowdin/reapplyTranslations.ts +++ b/typescript/src/crowdin/reapplyTranslations.ts @@ -1,4 +1,12 @@ /** + * @file Trigger a pre-translation run on a Crowdin project and wait for completion. + * Fetches all active languages and files, submits a pre-translation request, + * and polls for progress every 5 seconds until it reaches 100%%. + * Data files: None + * Env vars: + * CROWDIN_PROJECT_ID - Crowdin project numeric ID + * CROWDIN_API_URL - Base URL of the Crowdin API + * CROWDIN_TOKEN - Crowdin personal access token. * @copyright NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan diff --git a/typescript/src/discord/cycThreads.ts b/typescript/src/discord/cycThreads.ts index 6a752b7..8eb08fb 100644 --- a/typescript/src/discord/cycThreads.ts +++ b/typescript/src/discord/cycThreads.ts @@ -1,4 +1,10 @@ /** + * @file Create discussion threads for CYC talk submissions in a Discord channel. + * Iterates over a hardcoded list of talk titles and speakers and creates a + * Discord forum thread for each one. Update the data array and CHANNEL_ID before running. + * Data files: None (talk data is hardcoded in the script) + * Env vars: + * DISCORD_TOKEN - Bot token for the Discord API. * @copyright NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan diff --git a/typescript/src/discord/guildCount.ts b/typescript/src/discord/guildCount.ts index 668dd6b..36db36b 100644 --- a/typescript/src/discord/guildCount.ts +++ b/typescript/src/discord/guildCount.ts @@ -1,4 +1,14 @@ /** + * @file Count and categorise all Discord servers a user belongs to. + * Uses the Discord OAuth2 PKCE flow with a local callback server to authenticate, + * then fetches and categorises all guilds (owned, admin, moderated, partnered, + * verified, community, discoverable) and serves an HTML dashboard on localhost. + * Data files: None + * Env vars: + * DISCORD_CLIENT_ID - OAuth2 application client ID + * DISCORD_CLIENT_SECRET - OAuth2 application client secret (optional) + * DISCORD_REDIRECT_URI - OAuth2 redirect URI (default: http://127.0.0.1:8721/callback) + * DISCORD_SCOPES - OAuth2 scopes (default: identify guilds). * @copyright NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan diff --git a/typescript/src/discourse/bulkUpdateCategories.ts b/typescript/src/discourse/bulkUpdateCategories.ts index 0110298..c9d5f05 100644 --- a/typescript/src/discourse/bulkUpdateCategories.ts +++ b/typescript/src/discourse/bulkUpdateCategories.ts @@ -1,4 +1,12 @@ /** + * @file Set auto-close settings on all Discourse forum categories. + * Fetches all categories and subcategories and enables auto-close based on + * last post (672 hours / 28 days) for each one. + * Data files: None + * Env vars: + * DISCOURSE_URL - Base URL of the Discourse instance + * DISCOURSE_API_KEY - Discourse API key + * DISCOURSE_API_USERNAME - Discourse API username. * @copyright NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan diff --git a/typescript/src/discourse/closeOldTopics.ts b/typescript/src/discourse/closeOldTopics.ts index daea1a1..410b98c 100644 --- a/typescript/src/discourse/closeOldTopics.ts +++ b/typescript/src/discourse/closeOldTopics.ts @@ -1,4 +1,11 @@ /** + * @file Close inactive Discourse topics that have had no activity for 28+ days. + * Skips topics older than 6 months (already archived) and any that are already closed. + * Data files: None + * Env vars: + * DISCOURSE_URL - Base URL of the Discourse instance + * DISCOURSE_API_KEY - Discourse API key + * DISCOURSE_API_USERNAME - Discourse API username. * @copyright NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan diff --git a/typescript/src/gitea/deleteFromAllRepos.ts b/typescript/src/gitea/deleteFromAllRepos.ts index 0814539..421e959 100644 --- a/typescript/src/gitea/deleteFromAllRepos.ts +++ b/typescript/src/gitea/deleteFromAllRepos.ts @@ -1,4 +1,10 @@ /** + * @file Delete a file from every repository across all nhcarrigan Gitea orgs. + * Prompts for the file path to delete, then removes it from every repo + * across nhcarrigan, nhcarrigan-private, and nhcarrigan-games. + * Data files: None + * Env vars: + * GITEA_TOKEN - Gitea personal access token with repo write permissions. * @copyright NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan diff --git a/typescript/src/gitea/uploadToAllRepos.ts b/typescript/src/gitea/uploadToAllRepos.ts index 74a0dc6..8d0f304 100644 --- a/typescript/src/gitea/uploadToAllRepos.ts +++ b/typescript/src/gitea/uploadToAllRepos.ts @@ -1,4 +1,11 @@ /** + * @file Upload a file from data/ to every repository across all nhcarrigan Gitea orgs. + * Prompts for the local filename and destination path, then creates or updates + * the file in every repo across nhcarrigan, nhcarrigan-private, and nhcarrigan-games. + * Data files (place in data/): + * - Any file to upload (prompted interactively) + * Env vars: + * GITEA_TOKEN - Gitea personal access token with repo write permissions. * @copyright NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan diff --git a/typescript/src/gitea/uploadToReposConditionally.ts b/typescript/src/gitea/uploadToReposConditionally.ts index 90f540c..a93d3b8 100644 --- a/typescript/src/gitea/uploadToReposConditionally.ts +++ b/typescript/src/gitea/uploadToReposConditionally.ts @@ -1,4 +1,11 @@ /** + * @file Conditionally upload a file to Gitea repos based on whether another file exists. + * Prompts for the file to upload, a condition file path, and whether to upload + * when the condition file exists or does not exist. + * Data files (place in data/): + * - Any file to upload (prompted interactively) + * Env vars: + * GITEA_TOKEN - Gitea personal access token with repo write permissions. * @copyright NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan diff --git a/typescript/src/github/auditNpmPackages.ts b/typescript/src/github/auditNpmPackages.ts index 5a65d06..dda69f5 100644 --- a/typescript/src/github/auditNpmPackages.ts +++ b/typescript/src/github/auditNpmPackages.ts @@ -1,4 +1,12 @@ /** + * @file Audit npm packages across a GitHub organisation for known vulnerabilities. + * Lists all repositories in the target org, fetches each package.json, and checks + * for usage of packages in the hardcoded vulnerable package list. + * Update the vulnerable package list and orgName constant before running. + * Outputs (written to data/): + * - npm-vulnerabilities.txt Repos and packages with vulnerability findings + * Env vars: + * GITHUB_TOKEN - GitHub personal access token with org read permissions. * @copyright NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan diff --git a/typescript/src/github/onboardMentee.ts b/typescript/src/github/onboardMentee.ts index 0c704ef..5129925 100644 --- a/typescript/src/github/onboardMentee.ts +++ b/typescript/src/github/onboardMentee.ts @@ -1,4 +1,12 @@ /** + * @file Onboard a new mentee to the nhcarrigan-mentorship GitHub organisation. + * Prompts for the mentee's Discord ID, full name, and GitHub username, creates + * a personal repository in the org, adds the mentee as a collaborator with + * maintain permissions, and posts a welcome message to the Discord channel. + * Data files: None + * Env vars: + * GITHUB_TOKEN - GitHub personal access token with org and repo permissions + * DISCORD_TOKEN - Bot token for the Discord API. * @copyright NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan diff --git a/typescript/src/github/postUserStories.ts b/typescript/src/github/postUserStories.ts index c2cc618..4e85f64 100644 --- a/typescript/src/github/postUserStories.ts +++ b/typescript/src/github/postUserStories.ts @@ -1,4 +1,11 @@ /** + * @file Post user story markdown files as GitHub issue descriptions. + * Reads all markdown files from data/stories/ and updates the corresponding + * GitHub issue with that content. Filename format: {repo-name}-{issue-number}.md. + * Data files (place in data/stories/): + * - {repo-name}-{issue-number}.md User story content for each issue + * Env vars: + * GITHUB_TOKEN - GitHub personal access token with repo write permissions. * @copyright NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan diff --git a/typescript/src/music/id3v2.ts b/typescript/src/music/id3v2.ts index 1055f89..d06392b 100644 --- a/typescript/src/music/id3v2.ts +++ b/typescript/src/music/id3v2.ts @@ -1,4 +1,10 @@ /** + * @file Tag MP3 files with ID3v2 metadata and album art for Neuro-sama tracks. + * Reads all MP3 files from the download directory, extracts the title from + * the filename, sets the artist to "Neuro-sama", and applies the cover image. + * Update the directory and cover constants before running. + * Data files: None (reads from a local directory path defined in the script) + * Env vars: None (uses eyeD3 and id3v2 CLI tools). * @copyright NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan diff --git a/typescript/src/s3/bulkUpload.ts b/typescript/src/s3/bulkUpload.ts index c7ef695..d9341fb 100644 --- a/typescript/src/s3/bulkUpload.ts +++ b/typescript/src/s3/bulkUpload.ts @@ -1,4 +1,13 @@ /** + * @file Upload all files in the data/ directory to an S3-compatible bucket. + * Displays a tree of files to upload, prompts for confirmation, then uploads + * with a progress bar showing per-file status. + * Data files (place in data/): + * - All files to upload are read from the data/ directory recursively + * Env vars: + * AWS_ACCESS_KEY_ID - S3 access key + * AWS_SECRET_ACCESS_KEY - S3 secret key + * S3_ENDPOINT - S3-compatible endpoint URL. * @copyright NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan diff --git a/typescript/src/s3/correctContentType.ts b/typescript/src/s3/correctContentType.ts index 29a8824..a97ebfc 100644 --- a/typescript/src/s3/correctContentType.ts +++ b/typescript/src/s3/correctContentType.ts @@ -1,4 +1,12 @@ /** + * @file Fix incorrect Content-Type metadata on objects in an S3-compatible bucket. + * Lists all objects, detects MIME type from extension, and re-uploads metadata + * for any file whose stored Content-Type does not match the expected type. + * Data files: None + * Env vars: + * AWS_ACCESS_KEY_ID - S3 access key + * AWS_SECRET_ACCESS_KEY - S3 secret key + * S3_ENDPOINT - S3-compatible endpoint URL. * @copyright NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan diff --git a/typescript/src/s3/deleteContents.ts b/typescript/src/s3/deleteContents.ts index a145600..cd30454 100644 --- a/typescript/src/s3/deleteContents.ts +++ b/typescript/src/s3/deleteContents.ts @@ -1,4 +1,12 @@ /** + * @file Delete all objects from an S3-compatible bucket. + * Prompts for the bucket name, requires double confirmation (type name + yes/no), + * then lists and deletes all objects in batches with a progress bar. + * Data files: None + * Env vars: + * AWS_ACCESS_KEY_ID - S3 access key + * AWS_SECRET_ACCESS_KEY - S3 secret key + * S3_ENDPOINT - S3-compatible endpoint URL. * @copyright NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan diff --git a/typescript/src/s3/upload.ts b/typescript/src/s3/upload.ts index d046285..233c417 100644 --- a/typescript/src/s3/upload.ts +++ b/typescript/src/s3/upload.ts @@ -1,4 +1,12 @@ /** + * @file Upload a single file from the data/ directory to an S3-compatible bucket. + * Prompts for the local filename and the destination path in the bucket. + * Data files (place in data/): + * - Any file to upload (prompted interactively) + * Env vars: + * AWS_ACCESS_KEY_ID - S3 access key + * AWS_SECRET_ACCESS_KEY - S3 secret key + * S3_ENDPOINT - S3-compatible endpoint URL. * @copyright NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan diff --git a/typescript/src/security/generateReport.ts b/typescript/src/security/generateReport.ts index f5aa469..8dbac63 100644 --- a/typescript/src/security/generateReport.ts +++ b/typescript/src/security/generateReport.ts @@ -1,5 +1,12 @@ /* eslint-disable max-lines -- Necessary for all of the HTML templating. */ /** + * @file Generate a public HTML security report from DefectDojo findings. + * Fetches all active, verified findings from DefectDojo and produces a styled + * HTML dashboard grouped by project with severity counts. + * Outputs (written to data/): + * - public_security_report.html HTML security report + * Env vars: + * DOJO_TOKEN - DefectDojo API token. * @copyright NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan