Skip to content

Commit 46b5967

Browse files
feat: use github search for issue and repo linking
1 parent c82adae commit 46b5967

4 files changed

Lines changed: 113 additions & 11 deletions

File tree

monty/exts/info/github/_handlers.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -259,17 +259,24 @@ def render_full(
259259
return obj.name or obj.login, [text_display]
260260

261261

262-
class RepoRenderer(GitHubRenderer[githubkit.rest.Repository | githubkit.rest.FullRepository, ghretos.Repo]):
262+
class RepoRenderer(
263+
GitHubRenderer[
264+
githubkit.rest.Repository | githubkit.rest.FullRepository | githubkit.rest.RepoSearchResultItem, ghretos.Repo
265+
]
266+
):
263267
def render_tiny(
264268
self,
265-
obj: githubkit.rest.Repository | githubkit.rest.FullRepository,
269+
obj: githubkit.rest.Repository | githubkit.rest.FullRepository | githubkit.rest.RepoSearchResultItem,
266270
*,
267271
context: ghretos.Repo,
268272
) -> str:
269273
return f"📦 [{obj.name}](<{obj.html_url}>)"
270274

271275
def render_ogp_cv2(
272-
self, obj: githubkit.rest.Repository | githubkit.rest.FullRepository, *, context: ghretos.Repo
276+
self,
277+
obj: githubkit.rest.Repository | githubkit.rest.FullRepository | githubkit.rest.RepoSearchResultItem,
278+
*,
279+
context: ghretos.Repo,
273280
) -> disnake.ui.Container:
274281
container = disnake.ui.Container()
275282
text_display = disnake.ui.TextDisplay("")
@@ -290,7 +297,7 @@ def render_ogp_cv2(
290297

291298
def render_full(
292299
self,
293-
obj: githubkit.rest.Repository | githubkit.rest.FullRepository,
300+
obj: githubkit.rest.Repository | githubkit.rest.FullRepository | githubkit.rest.RepoSearchResultItem,
294301
*,
295302
context: ghretos.Repo,
296303
) -> tuple[str, list[disnake.ui.TextDisplay]]:

monty/exts/info/github/cog.py

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
import asyncio
22
import functools
3+
import json
34
import operator
5+
import pathlib
46
import random
57
import re
68
from collections.abc import Mapping
7-
from typing import Any
9+
from typing import Any, TypedDict
810

911
import cachingutils
1012
import disnake
1113
import ghretos
1214
import githubkit
1315
import githubkit.exception
16+
import githubkit.rest
1417
from disnake.ext import commands
18+
from typing_extensions import Required
1519

1620
import monty.utils.services
1721
from monty import constants
@@ -46,6 +50,13 @@
4650
log = get_logger(__name__)
4751

4852

53+
class GitHubShorthandAliases(TypedDict, total=False):
54+
owner: Required[str]
55+
repo: Required[str]
56+
alias_for_issues: bool
57+
"""Whether this shorthand can be used for issues/PRs in addition to repos."""
58+
59+
4960
class GithubInfo(
5061
commands.Cog,
5162
name="GitHub Information",
@@ -59,6 +70,15 @@ class GithubInfo(
5970
def __init__(self, bot: Monty) -> None:
6071
self.bot = bot
6172
self.client: GitHubFetcher = GitHubFetcher(bot.github)
73+
self.short_repos: dict[str, GitHubShorthandAliases] = json.loads(
74+
pathlib.Path("monty/resources/repo_aliases.json").read_text()
75+
)
76+
77+
# Validate casefold.
78+
assert all(
79+
repo == repo.casefold() and value.get("repo") and value.get("owner")
80+
for repo, value in self.short_repos.items()
81+
), "Repository shorthand keys must be casefolded and include exactly one `/` in the data file."
6282

6383
self.autolink_cache: cachingutils.MemoryCache[
6484
int, tuple[disnake.Message, dict[ghretos.GitHubResource, github_handlers.InfoSize]]
@@ -81,6 +101,40 @@ async def fetch_resource(
81101
"""Fetch a GitHub resource."""
82102
return await self.client.fetch_resource(resource)
83103

104+
async def resolve_repo(
105+
self,
106+
repo: ghretos.Repo,
107+
*,
108+
default_user: str | None = None,
109+
) -> ghretos.Repo:
110+
"""Resolve the owner of a GitHub repository."""
111+
if repo.owner:
112+
return repo
113+
if repo.name in self.short_repos:
114+
return ghretos.Repo(
115+
owner=self.short_repos[repo.name]["owner"],
116+
name=self.short_repos[repo.name]["repo"],
117+
)
118+
r = await self.bot.github.rest.search.async_repos(q=(repo.name + " is:public"), per_page=20, order="desc")
119+
for repo_data in r.parsed_data.items:
120+
if repo_data.name.casefold() == repo.name.casefold():
121+
break
122+
123+
else:
124+
# TODO: Check if the repository belongs to the default user before returning
125+
# Fallback to the default user if provided
126+
if default_user:
127+
return ghretos.Repo(owner=default_user, name=repo.name)
128+
msg = "GitHub repository not found."
129+
raise commands.UserInputError(msg)
130+
131+
user, repo_name = repo_data.full_name.split("/", 1)
132+
133+
if not isinstance(repo_data, (githubkit.rest.RepoSearchResultItem)):
134+
msg = "Could not resolve repository owner."
135+
raise ValueError(msg)
136+
return ghretos.Repo(owner=user, name=repo_name)
137+
84138
async def get_full_reply(
85139
self,
86140
resource: ghretos.GitHubResource,
@@ -287,11 +341,22 @@ async def parse_contents(
287341
for segment in context.text.split():
288342
match = ghretos.parse_shorthand(
289343
segment,
290-
default_user=default_user,
344+
allow_optional_user=True,
291345
settings=settings,
292346
)
293-
if match is not None:
294-
matches[match] = github_handlers.InfoSize.TINY
347+
if match is None:
348+
continue
349+
# resolve the repo owner if needed
350+
try:
351+
if isinstance(match, ghretos.Repo) and not match.owner:
352+
match = await self.resolve_repo(match, default_user=default_user)
353+
elif getattr(match, "repo", None) and not match.repo.owner:
354+
object.__setattr__(match, "repo", await self.resolve_repo(match.repo, default_user=default_user))
355+
except commands.UserInputError:
356+
continue
357+
358+
matches[match] = github_handlers.InfoSize.TINY
359+
295360
for url in context.urls:
296361
match = ghretos.parse_url(
297362
url,
@@ -403,14 +468,21 @@ async def github_user(self, ctx: commands.Context, user: str) -> None:
403468
)
404469
await ctx.send(components=components)
405470

406-
@github_group.command(name="repo", description="Fetch GitHub repository information.")
471+
@github_group.command(name="repo", aliases=("repository", "repo_info"))
407472
async def github_repo(self, ctx: commands.Context, user_and_repo: str, repo: str = "") -> None:
408473
"""Fetch GitHub repository information."""
409474
# validate the repo
475+
await ctx.trigger_typing()
476+
obj: githubkit.rest.FullRepository | githubkit.rest.RepoSearchResultItem | None = None
410477
if user_and_repo.count("/") == 1 and not repo:
411478
user, repo = user_and_repo.split("/", 1)
412479
else:
413-
user = user_and_repo
480+
repo = user_and_repo
481+
# Resolve the user from the repo name when possible
482+
repo_shorthand = await self.resolve_repo(ghretos.Repo(owner="", name=repo))
483+
user = repo_shorthand.owner
484+
repo = repo_shorthand.name
485+
414486
if not repo:
415487
msg = "Repository name is required."
416488
raise commands.UserInputError(msg)
@@ -429,6 +501,7 @@ async def github_repo(self, ctx: commands.Context, user_and_repo: str, repo: str
429501
msg = "GitHub repository not found."
430502
raise commands.UserInputError(msg) from e
431503
raise
504+
432505
components: list[disnake.ui.Container | disnake.ui.ActionRow] = []
433506
components.append(github_handlers.RepoRenderer().render_ogp_cv2(obj, context=context))
434507
components.append(

monty/resources/repo_aliases.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"k8s": {
3+
"owner": "kubernetes",
4+
"repo": "kubernetes",
5+
"alias_for_issues": true
6+
},
7+
"python": {
8+
"owner": "python",
9+
"repo": "cpython",
10+
"alias_for_issues": true
11+
},
12+
"monty": {
13+
"owner": "onerandomusername",
14+
"repo": "monty",
15+
"alias_for_issues": false
16+
},
17+
"monty-python": {
18+
"owner": "onerandomusername",
19+
"repo": "monty-python",
20+
"alias_for_issues": true
21+
}
22+
}

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)