From 19a9cf712cb741298b09a1cbb80304dcfba81996 Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Sat, 15 Nov 2025 11:40:40 +0500 Subject: [PATCH 01/22] Add Support for aliases repositories & default org in .gh repo command --- bot/exts/utilities/githubinfo.py | 189 +++++++++++++++------- bot/resources/utilities/stored_repos.json | 4 + 2 files changed, 138 insertions(+), 55 deletions(-) create mode 100644 bot/resources/utilities/stored_repos.json diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index 4847451176..1c9065c507 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -3,10 +3,12 @@ from dataclasses import dataclass from datetime import UTC, datetime from urllib.parse import quote +from pathlib import Path +import json import discord from aiohttp import ClientResponse -from discord.ext import commands +from discord.ext import commands, tasks from pydis_core.utils.logging import get_logger from bot.bot import Bot @@ -21,6 +23,7 @@ } REPOSITORY_ENDPOINT = "https://api.github.com/orgs/{org}/repos?per_page=100&type=public" +FETCH_MOST_STARRED_ENDPOINT = "https://api.github.com/search/repositories?q={name}&sort=stars&order=desc&per_page=1" ISSUE_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/issues/{number}" PR_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/pulls/{number}" @@ -78,6 +81,16 @@ def __init__(self, bot: Bot): self.bot = bot self.repos = [] + async def cog_load(self): + self.refresh_repos.start() + + self.stored_repos_json = Path(__file__).parent.parent.parent / "resources" / "utilities" / "stored_repos.json" + + with open(self.stored_repos_json, "r") as f: + self.stored_repos = json.load(f) + log.info("Loaded stored repos in memory.") + + @staticmethod def remove_codeblocks(message: str) -> str: """Remove any codeblock in a message.""" @@ -293,78 +306,144 @@ async def github_user_info(self, ctx: commands.Context, username: str) -> None: await ctx.send(embed=embed) + @tasks.loop(hours=24) + async def refresh_repos(self): + self.repos, _ = await self.fetch_data(REPOSITORY_ENDPOINT.format(org="python-discord")) + log.info(f"Loaded {len(self.repos)} repos from Python Discord org into memory.") + @github_group.command(name="repository", aliases=("repo",)) async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: """ - Fetches a repositories' GitHub information. + Fetches a repository's GitHub information. The repository should look like `user/reponame` or `user reponame`. + If it's not a stored repo or PyDis repo, it will fetch the most-starred repo + matching the search query from GitHub. """ - repo = "/".join(repo) - if repo.count("/") != 1: - embed = discord.Embed( - title=random.choice(NEGATIVE_REPLIES), - description="The repository should look like `user/reponame` or `user reponame`.", - colour=Colours.soft_red - ) - - await ctx.send(embed=embed) - return + is_pydis = False + fetch_most_starred = False + repo_query = "/".join(repo) + + # Determine type of repo + if repo_query.count("/") != 1: + if repo_query in self.stored_repos: + repo_query = self.stored_repos[repo_query] + else: + for each in self.repos: + if repo_query == each['name']: + repo_query = each['full_name'] + is_pydis = True + break + else: + fetch_most_starred = True async with ctx.typing(): - repo_data, _ = await self.fetch_data(f"{GITHUB_API_URL}/repos/{quote(repo)}") - - # There won't be a message key if this repo exists - if "message" in repo_data: + # Case 1: PyDis repo + if is_pydis: + for each in self.repos: + if repo_query == each['full_name']: + repo_data = each + break + + # Case 2: Not stored or PyDis, fetch most-starred matching repo + elif fetch_most_starred: + repo_data, _ = await self.fetch_data(FETCH_MOST_STARRED_ENDPOINT.format(name=quote(repo_query))) + + if not repo_data['items']: + embed = discord.Embed( + title=random.choice(NEGATIVE_REPLIES), + description=f"No repositories found matching `{repo_query}`.", + colour=Colours.soft_red + ) + await ctx.send(embed=embed) + return + + repo_item = repo_data['items'][0] # Top result embed = discord.Embed( - title=random.choice(NEGATIVE_REPLIES), - description="The requested repository was not found.", - colour=Colours.soft_red + title=repo_item["name"], + description=repo_item["description"] or "No description provided.", + colour=discord.Colour.og_blurple(), + url=repo_item["html_url"] ) - await ctx.send(embed=embed) - return - - embed = discord.Embed( - title=repo_data["name"], - description=repo_data["description"], - colour=discord.Colour.og_blurple(), - url=repo_data["html_url"] - ) + repo_owner = repo_item["owner"] + embed.set_author( + name=repo_owner["login"], + url=repo_owner["html_url"], + icon_url=repo_owner["avatar_url"] + ) - # If it's a fork, then it will have a parent key - try: - parent = repo_data["parent"] - embed.description += f"\n\nForked from [{parent['full_name']}]({parent['html_url']})" - except KeyError: - log.debug("Repository is not a fork.") + repo_created_at = datetime.strptime( + repo_item["created_at"], "%Y-%m-%dT%H:%M:%SZ" + ).replace(tzinfo=UTC).strftime("%d/%m/%Y") + last_pushed = datetime.strptime( + repo_item["pushed_at"], "%Y-%m-%dT%H:%M:%SZ" + ).replace(tzinfo=UTC).strftime("%d/%m/%Y at %H:%M") + + embed.set_footer( + text=( + f"{repo_item['forks_count']} ⑂ " + f"• {repo_item['stargazers_count']} ⭐ " + f"• Created At {repo_created_at} " + f"• Last Commit {last_pushed}" + ) + ) - repo_owner = repo_data["owner"] + await ctx.send(embed=embed) + return - embed.set_author( - name=repo_owner["login"], - url=repo_owner["html_url"], - icon_url=repo_owner["avatar_url"] - ) + # Case 3: Regular GitHub repo + else: + repo_data, _ = await self.fetch_data(f"{GITHUB_API_URL}/repos/{quote(repo_query)}") + + if "message" in repo_data: + embed = discord.Embed( + title=random.choice(NEGATIVE_REPLIES), + description="The requested repository was not found.", + colour=Colours.soft_red + ) + await ctx.send(embed=embed) + return + + # Embed creation for PyDis or regular GitHub repo + embed = discord.Embed( + title=repo_data["name"], + description=repo_data["description"] or "No description provided.", + colour=discord.Colour.og_blurple(), + url=repo_data["html_url"] + ) - repo_created_at = datetime.strptime( - repo_data["created_at"], "%Y-%m-%dT%H:%M:%SZ" - ).replace(tzinfo=UTC).strftime("%d/%m/%Y") - last_pushed = datetime.strptime( - repo_data["pushed_at"], "%Y-%m-%dT%H:%M:%SZ" - ).replace(tzinfo=UTC).strftime("%d/%m/%Y at %H:%M") - - embed.set_footer( - text=( - f"{repo_data['forks_count']} ⑂ " - f"• {repo_data['stargazers_count']} ⭐ " - f"• Created At {repo_created_at} " - f"• Last Commit {last_pushed}" + # Fork info + try: + parent = repo_data["parent"] + embed.description += f"\n\nForked from [{parent['full_name']}]({parent['html_url']})" + except KeyError: + log.debug("Repository is not a fork.") + + repo_owner = repo_data["owner"] + embed.set_author( + name=repo_owner["login"], + url=repo_owner["html_url"], + icon_url=repo_owner["avatar_url"] ) - ) - await ctx.send(embed=embed) + repo_created_at = datetime.strptime( + repo_data["created_at"], "%Y-%m-%dT%H:%M:%SZ" + ).replace(tzinfo=UTC).strftime("%d/%m/%Y") + last_pushed = datetime.strptime( + repo_data["pushed_at"], "%Y-%m-%dT%H:%M:%SZ" + ).replace(tzinfo=UTC).strftime("%d/%m/%Y at %H:%M") + + embed.set_footer( + text=( + f"{repo_data['forks_count']} ⑂ " + f"• {repo_data['stargazers_count']} ⭐ " + f"• Created At {repo_created_at} " + f"• Last Commit {last_pushed}" + ) + ) + await ctx.send(embed=embed) async def setup(bot: Bot) -> None: """Load the GithubInfo cog.""" diff --git a/bot/resources/utilities/stored_repos.json b/bot/resources/utilities/stored_repos.json new file mode 100644 index 0000000000..32ce384308 --- /dev/null +++ b/bot/resources/utilities/stored_repos.json @@ -0,0 +1,4 @@ +{ + "kubernetes": "kubernetes/kubernetes", + "discord.py": "Rapptz/discord.py" +} \ No newline at end of file From 5f38cda1cc49558895ebed1d81091eb414fc505c Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Sat, 15 Nov 2025 11:52:01 +0500 Subject: [PATCH 02/22] run pre-commit hooks --- bot/exts/utilities/githubinfo.py | 22 ++++++++++++---------- bot/resources/utilities/stored_repos.json | 2 +- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index 1c9065c507..92db5afa6b 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -1,10 +1,10 @@ +import json import random import re from dataclasses import dataclass from datetime import UTC, datetime -from urllib.parse import quote from pathlib import Path -import json +from urllib.parse import quote import discord from aiohttp import ClientResponse @@ -81,12 +81,13 @@ def __init__(self, bot: Bot): self.bot = bot self.repos = [] - async def cog_load(self): + async def cog_load(self) -> None: + """Function to be run at cog load.""" self.refresh_repos.start() self.stored_repos_json = Path(__file__).parent.parent.parent / "resources" / "utilities" / "stored_repos.json" - with open(self.stored_repos_json, "r") as f: + with open(self.stored_repos_json) as f: self.stored_repos = json.load(f) log.info("Loaded stored repos in memory.") @@ -307,7 +308,8 @@ async def github_user_info(self, ctx: commands.Context, username: str) -> None: await ctx.send(embed=embed) @tasks.loop(hours=24) - async def refresh_repos(self): + async def refresh_repos(self) -> None: + """Refresh self.repos with latest PyDis repos.""" self.repos, _ = await self.fetch_data(REPOSITORY_ENDPOINT.format(org="python-discord")) log.info(f"Loaded {len(self.repos)} repos from Python Discord org into memory.") @@ -330,8 +332,8 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: repo_query = self.stored_repos[repo_query] else: for each in self.repos: - if repo_query == each['name']: - repo_query = each['full_name'] + if repo_query == each["name"]: + repo_query = each["full_name"] is_pydis = True break else: @@ -341,7 +343,7 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: # Case 1: PyDis repo if is_pydis: for each in self.repos: - if repo_query == each['full_name']: + if repo_query == each["full_name"]: repo_data = each break @@ -349,7 +351,7 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: elif fetch_most_starred: repo_data, _ = await self.fetch_data(FETCH_MOST_STARRED_ENDPOINT.format(name=quote(repo_query))) - if not repo_data['items']: + if not repo_data["items"]: embed = discord.Embed( title=random.choice(NEGATIVE_REPLIES), description=f"No repositories found matching `{repo_query}`.", @@ -358,7 +360,7 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: await ctx.send(embed=embed) return - repo_item = repo_data['items'][0] # Top result + repo_item = repo_data["items"][0] # Top result embed = discord.Embed( title=repo_item["name"], description=repo_item["description"] or "No description provided.", diff --git a/bot/resources/utilities/stored_repos.json b/bot/resources/utilities/stored_repos.json index 32ce384308..7764d26e29 100644 --- a/bot/resources/utilities/stored_repos.json +++ b/bot/resources/utilities/stored_repos.json @@ -1,4 +1,4 @@ { "kubernetes": "kubernetes/kubernetes", "discord.py": "Rapptz/discord.py" -} \ No newline at end of file +} From b91d8e63e9f8fc5dfaf03a8588c99d2b74e582cf Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Sun, 16 Nov 2025 09:31:02 +0500 Subject: [PATCH 03/22] remove unnecessary for-loop --- bot/exts/utilities/githubinfo.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index 92db5afa6b..09a11f8afa 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -333,7 +333,7 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: else: for each in self.repos: if repo_query == each["name"]: - repo_query = each["full_name"] + repo_query = each is_pydis = True break else: @@ -342,10 +342,7 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: async with ctx.typing(): # Case 1: PyDis repo if is_pydis: - for each in self.repos: - if repo_query == each["full_name"]: - repo_data = each - break + repo_data = repo_query # repo_query already contains the matched repo # Case 2: Not stored or PyDis, fetch most-starred matching repo elif fetch_most_starred: From 6d5f3158238c4b02defd592272dbf2d87b930ebf Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Sun, 16 Nov 2025 19:55:12 +0500 Subject: [PATCH 04/22] add cog unload function --- bot/exts/utilities/githubinfo.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index 09a11f8afa..a4daba37e8 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -91,6 +91,10 @@ async def cog_load(self) -> None: self.stored_repos = json.load(f) log.info("Loaded stored repos in memory.") + async def cog_unload(self) -> None: + """Function to be run at cog unload.""" + self.refresh_repos.cancel() + @staticmethod def remove_codeblocks(message: str) -> str: From 79268d6e9dd506fe0a166b80842433ef2c4990fe Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Mon, 17 Nov 2025 12:22:06 +0500 Subject: [PATCH 05/22] create build_embed function to keep the code DRY and make suggested changes --- bot/exts/utilities/githubinfo.py | 115 ++++++++++++------------------- 1 file changed, 45 insertions(+), 70 deletions(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index a4daba37e8..ee15e7215e 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -317,6 +317,46 @@ async def refresh_repos(self) -> None: self.repos, _ = await self.fetch_data(REPOSITORY_ENDPOINT.format(org="python-discord")) log.info(f"Loaded {len(self.repos)} repos from Python Discord org into memory.") + def build_embed(self, repo_data: dict) -> discord.Embed: + """Create a clean discord embed to show repo data.""" + embed = discord.Embed( + title=repo_data["name"], + description=repo_data["description"] or "No description provided.", + colour=discord.Colour.og_blurple(), + url=repo_data["html_url"] + ) + + try: + parent = repo_data["parent"] + embed.description += f"\n\nForked from [{parent['full_name']}]({parent['html_url']})" + except KeyError: + log.debug("Repository is not a fork.") + + repo_owner = repo_data["owner"] + embed.set_author( + name=repo_owner["login"], + url=repo_owner["html_url"], + icon_url=repo_owner["avatar_url"] + ) + + repo_created_at = datetime.strptime( + repo_data["created_at"], "%Y-%m-%dT%H:%M:%SZ" + ).replace(tzinfo=UTC).strftime("%d/%m/%Y") + last_pushed = datetime.strptime( + repo_data["pushed_at"], "%Y-%m-%dT%H:%M:%SZ" + ).replace(tzinfo=UTC).strftime("%d/%m/%Y at %H:%M") + + embed.set_footer( + text=( + f"{repo_data['forks_count']} ⑂ " + f"• {repo_data['stargazers_count']} ⭐ " + f"• Created At {repo_created_at} " + f"• Last Commit {last_pushed}" + ) + ) + return embed + + @github_group.command(name="repository", aliases=("repo",)) async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: """ @@ -350,9 +390,9 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: # Case 2: Not stored or PyDis, fetch most-starred matching repo elif fetch_most_starred: - repo_data, _ = await self.fetch_data(FETCH_MOST_STARRED_ENDPOINT.format(name=quote(repo_query))) + repos, _ = await self.fetch_data(FETCH_MOST_STARRED_ENDPOINT.format(name=quote(repo_query))) - if not repo_data["items"]: + if not repos["items"]: embed = discord.Embed( title=random.choice(NEGATIVE_REPLIES), description=f"No repositories found matching `{repo_query}`.", @@ -361,36 +401,8 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: await ctx.send(embed=embed) return - repo_item = repo_data["items"][0] # Top result - embed = discord.Embed( - title=repo_item["name"], - description=repo_item["description"] or "No description provided.", - colour=discord.Colour.og_blurple(), - url=repo_item["html_url"] - ) - - repo_owner = repo_item["owner"] - embed.set_author( - name=repo_owner["login"], - url=repo_owner["html_url"], - icon_url=repo_owner["avatar_url"] - ) - - repo_created_at = datetime.strptime( - repo_item["created_at"], "%Y-%m-%dT%H:%M:%SZ" - ).replace(tzinfo=UTC).strftime("%d/%m/%Y") - last_pushed = datetime.strptime( - repo_item["pushed_at"], "%Y-%m-%dT%H:%M:%SZ" - ).replace(tzinfo=UTC).strftime("%d/%m/%Y at %H:%M") - - embed.set_footer( - text=( - f"{repo_item['forks_count']} ⑂ " - f"• {repo_item['stargazers_count']} ⭐ " - f"• Created At {repo_created_at} " - f"• Last Commit {last_pushed}" - ) - ) + repo_data = repos["items"][0] # Top result + embed = self.build_embed(repo_data) await ctx.send(embed=embed) return @@ -408,44 +420,7 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: await ctx.send(embed=embed) return - # Embed creation for PyDis or regular GitHub repo - embed = discord.Embed( - title=repo_data["name"], - description=repo_data["description"] or "No description provided.", - colour=discord.Colour.og_blurple(), - url=repo_data["html_url"] - ) - - # Fork info - try: - parent = repo_data["parent"] - embed.description += f"\n\nForked from [{parent['full_name']}]({parent['html_url']})" - except KeyError: - log.debug("Repository is not a fork.") - - repo_owner = repo_data["owner"] - embed.set_author( - name=repo_owner["login"], - url=repo_owner["html_url"], - icon_url=repo_owner["avatar_url"] - ) - - repo_created_at = datetime.strptime( - repo_data["created_at"], "%Y-%m-%dT%H:%M:%SZ" - ).replace(tzinfo=UTC).strftime("%d/%m/%Y") - last_pushed = datetime.strptime( - repo_data["pushed_at"], "%Y-%m-%dT%H:%M:%SZ" - ).replace(tzinfo=UTC).strftime("%d/%m/%Y at %H:%M") - - embed.set_footer( - text=( - f"{repo_data['forks_count']} ⑂ " - f"• {repo_data['stargazers_count']} ⭐ " - f"• Created At {repo_created_at} " - f"• Last Commit {last_pushed}" - ) - ) - + embed = self.build_embed(repo_data) await ctx.send(embed=embed) async def setup(bot: Bot) -> None: From a8cded78bac171c9fe67566949224472bd7f024d Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Mon, 17 Nov 2025 13:54:55 +0500 Subject: [PATCH 06/22] remove unnecessary return --- bot/exts/utilities/githubinfo.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index ee15e7215e..ee9385e52d 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -402,10 +402,6 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: return repo_data = repos["items"][0] # Top result - embed = self.build_embed(repo_data) - - await ctx.send(embed=embed) - return # Case 3: Regular GitHub repo else: From 9713cfef77e73b88304b262f84e6538239e56248 Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Mon, 17 Nov 2025 21:29:53 +0500 Subject: [PATCH 07/22] add comment for clarity --- bot/exts/utilities/githubinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index ee9385e52d..81c17742af 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -325,7 +325,7 @@ def build_embed(self, repo_data: dict) -> discord.Embed: colour=discord.Colour.og_blurple(), url=repo_data["html_url"] ) - + # if its a fork it will have a parent key try: parent = repo_data["parent"] embed.description += f"\n\nForked from [{parent['full_name']}]({parent['html_url']})" From 9e7735cd0534b8a6462dc605f734b2bdaa055dbb Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Mon, 17 Nov 2025 21:31:41 +0500 Subject: [PATCH 08/22] add comment for clarity --- bot/exts/utilities/githubinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index 81c17742af..03975dcdb2 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -406,7 +406,7 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: # Case 3: Regular GitHub repo else: repo_data, _ = await self.fetch_data(f"{GITHUB_API_URL}/repos/{quote(repo_query)}") - + # There won't be a message key if this repo exists if "message" in repo_data: embed = discord.Embed( title=random.choice(NEGATIVE_REPLIES), From abfef9961f41f5ad447ebe2858c5b6ec0f5a080b Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Tue, 18 Nov 2025 09:52:06 +0500 Subject: [PATCH 09/22] add peps and cpython to stored repos --- bot/resources/utilities/stored_repos.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/resources/utilities/stored_repos.json b/bot/resources/utilities/stored_repos.json index 7764d26e29..3505d4fb84 100644 --- a/bot/resources/utilities/stored_repos.json +++ b/bot/resources/utilities/stored_repos.json @@ -1,4 +1,6 @@ { "kubernetes": "kubernetes/kubernetes", - "discord.py": "Rapptz/discord.py" + "discord.py": "Rapptz/discord.py", + "peps": "python/peps", + "cpython": "python/cpython" } From c118a1ba991a53740ef3d3d25a2b10ce34573385 Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Tue, 18 Nov 2025 10:17:45 +0500 Subject: [PATCH 10/22] implemet refactoring suggestions --- bot/exts/utilities/githubinfo.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index 03975dcdb2..08296902a5 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -23,7 +23,7 @@ } REPOSITORY_ENDPOINT = "https://api.github.com/orgs/{org}/repos?per_page=100&type=public" -FETCH_MOST_STARRED_ENDPOINT = "https://api.github.com/search/repositories?q={name}&sort=stars&order=desc&per_page=1" +MOST_STARRED_ENDPOINT = "https://api.github.com/search/repositories?q={name}&sort=stars&order=desc&per_page=1" ISSUE_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/issues/{number}" PR_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/pulls/{number}" @@ -79,7 +79,7 @@ class GithubInfo(commands.Cog): def __init__(self, bot: Bot): self.bot = bot - self.repos = [] + self.pydis_repos: dict = {} async def cog_load(self) -> None: """Function to be run at cog load.""" @@ -313,9 +313,11 @@ async def github_user_info(self, ctx: commands.Context, username: str) -> None: @tasks.loop(hours=24) async def refresh_repos(self) -> None: - """Refresh self.repos with latest PyDis repos.""" - self.repos, _ = await self.fetch_data(REPOSITORY_ENDPOINT.format(org="python-discord")) - log.info(f"Loaded {len(self.repos)} repos from Python Discord org into memory.") + """Refresh self.pydis_repos with latest PyDis repos.""" + fetched_repos, _ = await self.fetch_data(REPOSITORY_ENDPOINT.format(org="python-discord")) + for each in fetched_repos: + self.pydis_repos.update({each['name']: each}) + log.info(f"Loaded {len(self.pydis_repos)} repos from Python Discord org into memory.") def build_embed(self, repo_data: dict) -> discord.Embed: """Create a clean discord embed to show repo data.""" @@ -375,11 +377,9 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: if repo_query in self.stored_repos: repo_query = self.stored_repos[repo_query] else: - for each in self.repos: - if repo_query == each["name"]: - repo_query = each - is_pydis = True - break + if repo_query in self.pydis_repos: + repo_query = self.pydis_repos[repo_query] + is_pydis = True else: fetch_most_starred = True @@ -390,7 +390,7 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: # Case 2: Not stored or PyDis, fetch most-starred matching repo elif fetch_most_starred: - repos, _ = await self.fetch_data(FETCH_MOST_STARRED_ENDPOINT.format(name=quote(repo_query))) + repos, _ = await self.fetch_data(MOST_STARRED_ENDPOINT.format(name=quote(repo_query))) if not repos["items"]: embed = discord.Embed( From 453cf8323b68ef89016b8ab1ec4621ea1e006f03 Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Tue, 18 Nov 2025 10:19:20 +0500 Subject: [PATCH 11/22] run pre-commit hooks --- bot/exts/utilities/githubinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index 08296902a5..31d28be842 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -316,7 +316,7 @@ async def refresh_repos(self) -> None: """Refresh self.pydis_repos with latest PyDis repos.""" fetched_repos, _ = await self.fetch_data(REPOSITORY_ENDPOINT.format(org="python-discord")) for each in fetched_repos: - self.pydis_repos.update({each['name']: each}) + self.pydis_repos.update({each["name"]: each}) log.info(f"Loaded {len(self.pydis_repos)} repos from Python Discord org into memory.") def build_embed(self, repo_data: dict) -> discord.Embed: From 2ce40dc8e1465826b6331f3215ba5d181af9f698 Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Tue, 18 Nov 2025 21:34:53 +0500 Subject: [PATCH 12/22] add comment for clarity Co-authored-by: Sacul <183588943+Sacul0457@users.noreply.github.com> --- bot/exts/utilities/githubinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index 31d28be842..6ab29d54a9 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -391,7 +391,7 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: # Case 2: Not stored or PyDis, fetch most-starred matching repo elif fetch_most_starred: repos, _ = await self.fetch_data(MOST_STARRED_ENDPOINT.format(name=quote(repo_query))) - + # 'items' is a list of repos, if it is empty, no repos were found if not repos["items"]: embed = discord.Embed( title=random.choice(NEGATIVE_REPLIES), From f20f2fad1e5a4c241814c1027d24a87cf55e9be5 Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Wed, 19 Nov 2025 12:33:33 +0500 Subject: [PATCH 13/22] implement suggested changes --- bot/exts/utilities/githubinfo.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index 6ab29d54a9..d467621525 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -27,6 +27,9 @@ ISSUE_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/issues/{number}" PR_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/pulls/{number}" +STORED_REPOS_FILE = Path(__file__).parent.parent.parent / "resources" / "utilities" / "stored_repos.json" + + if Tokens.github: REQUEST_HEADERS["Authorization"] = f"token {Tokens.github.get_secret_value()}" @@ -85,9 +88,7 @@ async def cog_load(self) -> None: """Function to be run at cog load.""" self.refresh_repos.start() - self.stored_repos_json = Path(__file__).parent.parent.parent / "resources" / "utilities" / "stored_repos.json" - - with open(self.stored_repos_json) as f: + with open(STORED_REPOS_FILE) as f: self.stored_repos = json.load(f) log.info("Loaded stored repos in memory.") @@ -315,18 +316,17 @@ async def github_user_info(self, ctx: commands.Context, username: str) -> None: async def refresh_repos(self) -> None: """Refresh self.pydis_repos with latest PyDis repos.""" fetched_repos, _ = await self.fetch_data(REPOSITORY_ENDPOINT.format(org="python-discord")) - for each in fetched_repos: - self.pydis_repos.update({each["name"]: each}) + self.pydis_repos = {repo["name"].casefold(): repo for repo in fetched_repos} log.info(f"Loaded {len(self.pydis_repos)} repos from Python Discord org into memory.") def build_embed(self, repo_data: dict) -> discord.Embed: """Create a clean discord embed to show repo data.""" embed = discord.Embed( - title=repo_data["name"], - description=repo_data["description"] or "No description provided.", - colour=discord.Colour.og_blurple(), - url=repo_data["html_url"] - ) + title=repo_data["name"], + description=repo_data["description"] or "No description provided.", + colour=discord.Colour.og_blurple(), + url=repo_data["html_url"] + ) # if its a fork it will have a parent key try: parent = repo_data["parent"] @@ -374,10 +374,10 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: # Determine type of repo if repo_query.count("/") != 1: - if repo_query in self.stored_repos: + if repo_query.casefold() in self.stored_repos: repo_query = self.stored_repos[repo_query] else: - if repo_query in self.pydis_repos: + if repo_query.casefold() in self.pydis_repos: repo_query = self.pydis_repos[repo_query] is_pydis = True else: @@ -391,7 +391,7 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: # Case 2: Not stored or PyDis, fetch most-starred matching repo elif fetch_most_starred: repos, _ = await self.fetch_data(MOST_STARRED_ENDPOINT.format(name=quote(repo_query))) - # 'items' is a list of repos, if it is empty, no repos were found + if not repos["items"]: embed = discord.Embed( title=random.choice(NEGATIVE_REPLIES), From cc41fea9b788a3443e340aa1c2da8ba372cbcb81 Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Wed, 19 Nov 2025 15:11:00 +0500 Subject: [PATCH 14/22] implement suggested changes --- bot/exts/utilities/githubinfo.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index d467621525..a16339163c 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -372,15 +372,23 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: fetch_most_starred = False repo_query = "/".join(repo) + if repo_query.count("/") > 1: + embed = discord.Embed( + colour=Colours.soft_red, + title=random.choice(NEGATIVE_REPLIES), + description="There cannot be more than one `/` in the repository." + ) + await ctx.send(embed=embed) + return + # Determine type of repo - if repo_query.count("/") != 1: + if repo_query.count("/") == 0: if repo_query.casefold() in self.stored_repos: - repo_query = self.stored_repos[repo_query] - else: - if repo_query.casefold() in self.pydis_repos: - repo_query = self.pydis_repos[repo_query] + repo_query = self.stored_repos[repo_query.casefold()] + elif repo_query.casefold() in self.pydis_repos: + repo_query = self.pydis_repos[repo_query.casefold()] is_pydis = True - else: + else: fetch_most_starred = True async with ctx.typing(): From 0b2d1823792de3a009508e254b9d982d1148cde7 Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Wed, 19 Nov 2025 15:12:45 +0500 Subject: [PATCH 15/22] run pre-commit hooks --- bot/exts/utilities/githubinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index a16339163c..58b5c431bf 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -380,7 +380,7 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: ) await ctx.send(embed=embed) return - + # Determine type of repo if repo_query.count("/") == 0: if repo_query.casefold() in self.stored_repos: From 93499fb75b32df06a3a579d08cb45f65c0d3470f Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Wed, 19 Nov 2025 15:57:07 +0500 Subject: [PATCH 16/22] implement suggested changes --- bot/exts/utilities/githubinfo.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index 58b5c431bf..ab9726bbae 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -364,9 +364,10 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: """ Fetches a repository's GitHub information. - The repository should look like `user/reponame` or `user reponame`. - If it's not a stored repo or PyDis repo, it will fetch the most-starred repo - matching the search query from GitHub. + If the repository looks like `user/reponame` or `user reponame` then it will fetch it from github. + Otherwise, if it's a stored repo or PyDis repo, it will fetch the stored repo or use the PyDis repo + stored inside self.pydis_repos. + Otherwise it will fetch the most starred repo matching the search query from GitHub. """ is_pydis = False fetch_most_starred = False @@ -383,13 +384,14 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: # Determine type of repo if repo_query.count("/") == 0: - if repo_query.casefold() in self.stored_repos: - repo_query = self.stored_repos[repo_query.casefold()] - elif repo_query.casefold() in self.pydis_repos: - repo_query = self.pydis_repos[repo_query.casefold()] - is_pydis = True + repo_query_casefold = repo_query.casefold() + if repo_query_casefold in self.stored_repos: + repo_query = self.stored_repos[repo_query_casefold] + elif repo_query_casefold in self.pydis_repos: + repo_query = self.pydis_repos[repo_query_casefold] + is_pydis = True else: - fetch_most_starred = True + fetch_most_starred = True async with ctx.typing(): # Case 1: PyDis repo From e31ccca2fda70b68e5bd0b311ba0e705ca3d4923 Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Wed, 19 Nov 2025 16:29:08 +0500 Subject: [PATCH 17/22] Update bot/exts/utilities/githubinfo.py to keep the code style consistent throughout Co-authored-by: Sacul <183588943+Sacul0457@users.noreply.github.com> --- bot/exts/utilities/githubinfo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index ab9726bbae..59026c80c5 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -375,9 +375,9 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: if repo_query.count("/") > 1: embed = discord.Embed( - colour=Colours.soft_red, title=random.choice(NEGATIVE_REPLIES), - description="There cannot be more than one `/` in the repository." + description="There cannot be more than one `/` in the repository.", + colour=Colours.soft_red ) await ctx.send(embed=embed) return From a5a0a4417878b2b6c4d746ad1a565777e3f8d583 Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Wed, 19 Nov 2025 20:14:33 +0500 Subject: [PATCH 18/22] Add more descriptive docstrings for cog_load and cog_unload methods --- bot/exts/utilities/githubinfo.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index 59026c80c5..53becb78a5 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -85,7 +85,11 @@ def __init__(self, bot: Bot): self.pydis_repos: dict = {} async def cog_load(self) -> None: - """Function to be run at cog load.""" + """ + Function to be run at cog load. + + Starts the refresh_repos tasks.loop that runs every 24 hours. + """ self.refresh_repos.start() with open(STORED_REPOS_FILE) as f: @@ -93,7 +97,11 @@ async def cog_load(self) -> None: log.info("Loaded stored repos in memory.") async def cog_unload(self) -> None: - """Function to be run at cog unload.""" + """ + Function to be run at cog unload. + + Cancels the execution of refresh_repos tasks.loop. + """ self.refresh_repos.cancel() From a33038f3d38dbccffc9f27edaaeb4df3a99f7103 Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Wed, 19 Nov 2025 21:12:05 +0500 Subject: [PATCH 19/22] Remove default for repo description Co-authored-by: z --- bot/exts/utilities/githubinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index 53becb78a5..21588ee394 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -331,7 +331,7 @@ def build_embed(self, repo_data: dict) -> discord.Embed: """Create a clean discord embed to show repo data.""" embed = discord.Embed( title=repo_data["name"], - description=repo_data["description"] or "No description provided.", + description=repo_data["description"], colour=discord.Colour.og_blurple(), url=repo_data["html_url"] ) From 7266dcc34369ebde2c91ed79ebb36e286c858558 Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Sat, 22 Nov 2025 10:46:36 +0500 Subject: [PATCH 20/22] change the most starred scenario to only return repo if name matches the query --- bot/exts/utilities/githubinfo.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index 53becb78a5..35de49b716 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -23,7 +23,7 @@ } REPOSITORY_ENDPOINT = "https://api.github.com/orgs/{org}/repos?per_page=100&type=public" -MOST_STARRED_ENDPOINT = "https://api.github.com/search/repositories?q={name}&sort=stars&order=desc&per_page=1" +MOST_STARRED_ENDPOINT = "https://api.github.com/search/repositories?q={name}&sort=stars&order=desc&per_page=100" ISSUE_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/issues/{number}" PR_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/pulls/{number}" @@ -419,7 +419,19 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: await ctx.send(embed=embed) return - repo_data = repos["items"][0] # Top result + for repo in repos["items"]: + if repo["name"] == repo_query: + repo_data = repo + break + else: + embed = discord.Embed( + title=random.choice(NEGATIVE_REPLIES), + description=f"No repositories found matching `{repo_query}`.", + colour=Colours.soft_red + ) + await ctx.send(embed=embed) + return + # Case 3: Regular GitHub repo else: From 075ae8ed741109ab4bb191cd832ad2dbd7d96b21 Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Sat, 22 Nov 2025 11:09:07 +0500 Subject: [PATCH 21/22] casefold for consistency --- bot/exts/utilities/githubinfo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index 89f35d4787..dcf3addeb0 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -379,6 +379,7 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: """ is_pydis = False fetch_most_starred = False + repo_query_casefold = repo_query.casefold() repo_query = "/".join(repo) if repo_query.count("/") > 1: @@ -392,7 +393,6 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: # Determine type of repo if repo_query.count("/") == 0: - repo_query_casefold = repo_query.casefold() if repo_query_casefold in self.stored_repos: repo_query = self.stored_repos[repo_query_casefold] elif repo_query_casefold in self.pydis_repos: @@ -420,7 +420,7 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: return for repo in repos["items"]: - if repo["name"] == repo_query: + if repo["name"] == repo_query_casefold: repo_data = repo break else: From 3a9826aaf2dca98e35803cd1c4c03b5d664637c6 Mon Sep 17 00:00:00 2001 From: Muhammad Hasan Date: Sat, 22 Nov 2025 11:26:17 +0500 Subject: [PATCH 22/22] bugfix: move repo_query_casefold below repo_query definition --- bot/exts/utilities/githubinfo.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index dcf3addeb0..e7774680d1 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -379,8 +379,9 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None: """ is_pydis = False fetch_most_starred = False - repo_query_casefold = repo_query.casefold() repo_query = "/".join(repo) + repo_query_casefold = repo_query.casefold() + if repo_query.count("/") > 1: embed = discord.Embed(