diff --git a/scripts/leaderboard.py b/scripts/leaderboard.py index ef54f57..eddcde4 100644 --- a/scripts/leaderboard.py +++ b/scripts/leaderboard.py @@ -1,9 +1,11 @@ """Fetch contributor stats from all NextCommunity repos and update the leaderboard.""" +import html import os import re import sys import urllib.error +import urllib.parse import urllib.request import json from bisect import bisect_right @@ -16,6 +18,9 @@ LEADERBOARD_START = "" LEADERBOARD_END = "" SITE_REPO_NAME = "NextCommunity.github.io" + +# GitHub usernames: 1-39 alphanumeric chars, interior single hyphens only (no consecutive hyphens). +_GITHUB_LOGIN_RE = re.compile(r"^(?=.{1,39}$)[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$") DOTGITHUB_REPO_NAME = ".github" # Self-documenting record for each commit entry collected across all repos. @@ -707,6 +712,26 @@ def build_leaderboard(token=None): return sorted_contributors, had_errors, levels_data +def _contributor_cell(login): + """Return a pure-HTML table cell with avatar and username link. + + ``login`` is validated against GitHub's username pattern before use. + Returns ``None`` and logs a warning if the login contains unexpected + characters, so callers can skip the row rather than crashing. + """ + if not _GITHUB_LOGIN_RE.match(login): + print(f"WARNING: skipping contributor with invalid GitHub login: {login!r}") + return None + safe_login = urllib.parse.quote(login, safe="") + escaped_login = html.escape(login) + return ( + f'' + f'{escaped_login}\'s avatar
' + f"@{escaped_login}
" + ) + + def generate_markdown(contributors, levels_data): """Generate a gamified markdown leaderboard from contributor data.""" rank_badges = {1: "๐Ÿฅ‡", 2: "๐Ÿฅˆ", 3: "๐Ÿฅ‰"} @@ -767,8 +792,11 @@ def generate_markdown(contributors, levels_data): commits_display += f" ยท ๐Ÿค {coauthored}" commits_display += f" ยท ๐Ÿ“ฆ {repos_count}" + contributor_cell = _contributor_cell(login) + if contributor_cell is None: + continue lines.append( - f"| {rank} | [@{login}](https://github.com/{login})" + f"| {rank} | {contributor_cell}" f" | {level} | {rarity_display} | {commits_display}" f" | {prog} | {streak_display}" f" | {badges} | {points_display} |" @@ -812,8 +840,11 @@ def generate_markdown(contributors, levels_data): breakdown_parts.append(f"๐Ÿ“ {other_c}") breakdown = " ยท ".join(breakdown_parts) if breakdown_parts else "โ€”" + contributor_cell = _contributor_cell(login) + if contributor_cell is None: + continue lines.append( - f"| {i} | [@{login}](https://github.com/{login})" + f"| {i} | {contributor_cell}" f" | {first_date} | {last_date}" f" | {days_active} | {cpd}" f" | {breakdown} | Top {pctile}% |"