Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
19a9cf7
Add Support for aliases repositories & default org in .gh repo command
mhaxanali Nov 15, 2025
5f38cda
run pre-commit hooks
mhaxanali Nov 15, 2025
c87abee
Merge branch 'main' into feature/gh-repo-enhancements
mhaxanali Nov 15, 2025
b91d8e6
remove unnecessary for-loop
mhaxanali Nov 16, 2025
6d5f315
add cog unload function
mhaxanali Nov 16, 2025
79268d6
create build_embed function to keep the code DRY and make suggested c…
mhaxanali Nov 17, 2025
a8cded7
remove unnecessary return
mhaxanali Nov 17, 2025
9713cfe
add comment for clarity
mhaxanali Nov 17, 2025
9e7735c
add comment for clarity
mhaxanali Nov 17, 2025
abfef99
add peps and cpython to stored repos
mhaxanali Nov 18, 2025
c118a1b
implemet refactoring suggestions
mhaxanali Nov 18, 2025
453cf83
run pre-commit hooks
mhaxanali Nov 18, 2025
2ce40dc
add comment for clarity
mhaxanali Nov 18, 2025
f20f2fa
implement suggested changes
mhaxanali Nov 19, 2025
cc41fea
implement suggested changes
mhaxanali Nov 19, 2025
0b2d182
run pre-commit hooks
mhaxanali Nov 19, 2025
93499fb
implement suggested changes
mhaxanali Nov 19, 2025
e31ccca
Update bot/exts/utilities/githubinfo.py to keep the code style consis…
mhaxanali Nov 19, 2025
a5a0a44
Add more descriptive docstrings for cog_load and cog_unload methods
mhaxanali Nov 19, 2025
a33038f
Remove default for repo description
mhaxanali Nov 19, 2025
7266dcc
change the most starred scenario to only return repo if name matches …
mhaxanali Nov 22, 2025
deeae9b
change most starred scenario to only return a repo if name matches query
mhaxanali Nov 22, 2025
075ae8e
casefold for consistency
mhaxanali Nov 22, 2025
3a9826a
bugfix: move repo_query_casefold below repo_query definition
mhaxanali Nov 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 97 additions & 40 deletions bot/exts/utilities/githubinfo.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import json
import random
import re
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from urllib.parse import quote

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
Expand All @@ -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}"

Expand Down Expand Up @@ -78,6 +81,21 @@ def __init__(self, bot: Bot):
self.bot = bot
self.repos = []

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:
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:
"""Remove any codeblock in a message."""
Expand Down Expand Up @@ -293,54 +311,28 @@ async def github_user_info(self, ctx: commands.Context, username: str) -> None:

await ctx.send(embed=embed)

@github_group.command(name="repository", aliases=("repo",))
async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None:
"""
Fetches a repositories' GitHub information.

The repository should look like `user/reponame` or `user reponame`.
"""
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

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:
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
@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.")

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"],
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 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_owner = repo_data["owner"]

embed.set_author(
name=repo_owner["login"],
url=repo_owner["html_url"],
Expand All @@ -362,9 +354,74 @@ async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None:
f"• Last Commit {last_pushed}"
)
)
return embed

await ctx.send(embed=embed)

@github_group.command(name="repository", aliases=("repo",))
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.
"""
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
is_pydis = True
break
else:
fetch_most_starred = True

async with ctx.typing():
# Case 1: PyDis repo
if is_pydis:
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:
repos, _ = await self.fetch_data(FETCH_MOST_STARRED_ENDPOINT.format(name=quote(repo_query)))

if not repos["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_data = repos["items"][0] # Top result
embed = self.build_embed(repo_data)

await ctx.send(embed=embed)
return

# 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 = self.build_embed(repo_data)
await ctx.send(embed=embed)

async def setup(bot: Bot) -> None:
"""Load the GithubInfo cog."""
Expand Down
4 changes: 4 additions & 0 deletions bot/resources/utilities/stored_repos.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"kubernetes": "kubernetes/kubernetes",
"discord.py": "Rapptz/discord.py"
}