|
4 | 4 | import random |
5 | 5 | import re |
6 | 6 | from collections.abc import Mapping |
7 | | -from typing import Any |
| 7 | +from typing import Any, overload |
8 | 8 |
|
9 | 9 | import attrs |
10 | 10 | import cachingutils |
@@ -141,7 +141,38 @@ def _format_github_global_id(self, prefix: str, *ids: int, template: int = 0) -> |
141 | 141 | encoded = encoded.rstrip("=") # this isn't necessary, but github generates these IDs without padding |
142 | 142 | return f"{prefix}_{encoded}" |
143 | 143 |
|
144 | | - async def fetch_resource(self, obj: ghretos.GitHubResource) -> githubkit.GitHubModel | None: |
| 144 | + @overload |
| 145 | + async def fetch_resource(self, obj: ghretos.User) -> githubkit.rest.PublicUser: ... |
| 146 | + |
| 147 | + @overload |
| 148 | + async def fetch_resource(self, obj: ghretos.Repo) -> githubkit.rest.Repository: ... |
| 149 | + |
| 150 | + @overload |
| 151 | + async def fetch_resource(self, obj: ghretos.Issue) -> githubkit.rest.Issue: ... |
| 152 | + @overload |
| 153 | + async def fetch_resource(self, obj: ghretos.PullRequest) -> githubkit.rest.Issue: ... |
| 154 | + @overload |
| 155 | + async def fetch_resource(self, obj: ghretos.IssueComment) -> githubkit.rest.IssueComment: ... |
| 156 | + @overload |
| 157 | + async def fetch_resource(self, obj: ghretos.PullRequestComment) -> githubkit.rest.IssueComment: ... |
| 158 | + @overload |
| 159 | + async def fetch_resource( |
| 160 | + self, obj: ghretos.PullRequestReviewComment |
| 161 | + ) -> githubkit.rest.PullRequestReviewComment: ... |
| 162 | + @overload |
| 163 | + async def fetch_resource(self, obj: ghretos.Discussion) -> githubkit.rest.Discussion: ... |
| 164 | + @overload |
| 165 | + async def fetch_resource(self, obj: ghretos.DiscussionComment) -> graphql_models.DiscussionComment: ... |
| 166 | + @overload |
| 167 | + async def fetch_resource(self, obj: ghretos.IssueEvent) -> githubkit.rest.IssueEvent: ... |
| 168 | + @overload |
| 169 | + async def fetch_resource(self, obj: ghretos.PullRequestEvent) -> githubkit.rest.IssueEvent: ... |
| 170 | + @overload |
| 171 | + async def fetch_resource(self, obj: ghretos.Commit) -> githubkit.rest.Commit: ... |
| 172 | + @overload |
| 173 | + async def fetch_resource(self, obj: ghretos.Repo) -> githubkit.rest.Repository: ... |
| 174 | + |
| 175 | + async def fetch_resource(self, obj: ghretos.GitHubResource) -> githubkit.GitHubModel: |
145 | 176 | """Fetch a GitHub resource.""" |
146 | 177 | # TODO: add repo exists validation before fetching multiple resources from one repo? |
147 | 178 | # This would reduce wasted requests on private repos, and keep discussions from making many extra requests. |
@@ -218,7 +249,16 @@ async def fetch_resource(self, obj: ghretos.GitHubResource) -> githubkit.GitHubM |
218 | 249 | ) |
219 | 250 | return r.parsed_data |
220 | 251 |
|
221 | | - return None # Type is not yet supported |
| 252 | + if isinstance(obj, ghretos.User): |
| 253 | + r = await self.bot.github.rest.users.async_get_by_username(username=obj.login) |
| 254 | + data = r.parsed_data |
| 255 | + # Even though we use a token with no additional scopes, validate that we CERTAINLY only have public data. |
| 256 | + if data.user_view_type != "public": |
| 257 | + msg = "User is not public" |
| 258 | + raise ValueError(msg) |
| 259 | + return data |
| 260 | + |
| 261 | + raise NotImplementedError # Type is not yet supported |
222 | 262 |
|
223 | 263 | # @github_group.command(name="ratelimit", aliases=("rl",), hidden=True) |
224 | 264 |
|
@@ -366,12 +406,119 @@ async def get_matcher_settings(self, guild_id: int, config: GuildConfig) -> ghre |
366 | 406 |
|
367 | 407 | return matcher_settings |
368 | 408 |
|
| 409 | + @commands.group( |
| 410 | + name="github", description="Fetch GitHub information.", aliases=("gh",), invoke_without_command=True |
| 411 | + ) |
| 412 | + async def github_group(self, ctx: commands.Context, *args) -> None: |
| 413 | + """Group for GitHub related commands.""" |
| 414 | + # Shortcut user: |
| 415 | + # Intentionally match 2 on the args here |
| 416 | + # Allow messages such as !github username extra words |
| 417 | + # but also support !github username repo |
| 418 | + if len(args) != 2 and re.fullmatch(r"^[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,38})$", args[0]): |
| 419 | + await self.github_user(ctx, args[0]) |
| 420 | + return |
| 421 | + settings = ghretos.MatcherSettings.none() |
| 422 | + # enable what we have handlers for |
| 423 | + settings.shorthand = True |
| 424 | + settings.short_repo = True |
| 425 | + resource = ghretos.parse_shorthand(" ".join(args)) |
| 426 | + |
| 427 | + if isinstance(resource, ghretos.User): |
| 428 | + await self.github_user(ctx, resource.login) |
| 429 | + return |
| 430 | + if isinstance(resource, ghretos.Repo): |
| 431 | + await self.github_repo(ctx, resource.full_name) |
| 432 | + return |
| 433 | + if isinstance(resource, tuple(github_handlers.HANDLER_MAPPING.keys())): |
| 434 | + data = await self.get_reply({resource: github_handlers.InfoSize.OGP}, limit=850) |
| 435 | + if data: |
| 436 | + components: list[disnake.ui.Container | disnake.ui.ActionRow] = [] |
| 437 | + components.append( |
| 438 | + disnake.ui.ActionRow( |
| 439 | + DeleteButton( |
| 440 | + allow_manage_messages=True, |
| 441 | + user=ctx.author, |
| 442 | + initial_message=ctx.message, |
| 443 | + ) |
| 444 | + ) |
| 445 | + ) |
| 446 | + await ctx.reply( |
| 447 | + components=components, |
| 448 | + fail_if_not_exists=False, |
| 449 | + allowed_mentions=disnake.AllowedMentions.none(), |
| 450 | + **data, |
| 451 | + ) |
| 452 | + return |
| 453 | + |
| 454 | + await ctx.send( |
| 455 | + f"{constants.Emojis.decline} Could not parse GitHub resource from input.", |
| 456 | + components=DeleteButton( |
| 457 | + allow_manage_messages=True, |
| 458 | + user=ctx.author, |
| 459 | + initial_message=ctx.message, |
| 460 | + ), |
| 461 | + ) |
| 462 | + return |
| 463 | + |
| 464 | + @github_group.command(name="user", description="Fetch GitHub user information.") |
| 465 | + async def github_user(self, ctx: commands.Context, user: str) -> None: |
| 466 | + """Fetch GitHub user information.""" |
| 467 | + # validate the user |
| 468 | + if not re.fullmatch(r"^[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,38})$", user): |
| 469 | + await ctx.send( |
| 470 | + f"{constants.Emojis.decline} Invalid GitHub username.", |
| 471 | + components=[ |
| 472 | + DeleteButton( |
| 473 | + allow_manage_messages=True, |
| 474 | + user=ctx.author, |
| 475 | + initial_message=ctx.message, |
| 476 | + ) |
| 477 | + ], |
| 478 | + ) |
| 479 | + return |
| 480 | + context = ghretos.User(login=user) |
| 481 | + obj = await self.fetch_resource(context) |
| 482 | + components: list[disnake.ui.Container | disnake.ui.ActionRow] = [] |
| 483 | + components.append(github_handlers.UserRenderer().render_ogp_cv2(obj, context=context)) |
| 484 | + components.append( |
| 485 | + disnake.ui.ActionRow(DeleteButton(allow_manage_messages=True, user=ctx.author, initial_message=ctx.message)) |
| 486 | + ) |
| 487 | + await ctx.send(components=components) |
| 488 | + |
| 489 | + @github_group.command(name="repo", description="Fetch GitHub repository information.") |
| 490 | + async def github_repo(self, ctx: commands.Context, user_and_repo: str, repo: str = "") -> None: |
| 491 | + """Fetch GitHub repository information.""" |
| 492 | + # validate the repo |
| 493 | + if user_and_repo.count("/") == 1 and not repo: |
| 494 | + user, repo = user_and_repo.split("/", 1) |
| 495 | + else: |
| 496 | + user = user_and_repo |
| 497 | + if not repo: |
| 498 | + msg = "Repository name is required." |
| 499 | + raise commands.UserInputError(msg) |
| 500 | + if not re.fullmatch(r"^[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,38})$", user): |
| 501 | + msg = "Invalid GitHub username." |
| 502 | + raise commands.UserInputError(msg) |
| 503 | + if not re.fullmatch(r"^[\w\-\.]{1,100}$", repo): |
| 504 | + msg = "Invalid GitHub repository name." |
| 505 | + raise commands.UserInputError(msg) |
| 506 | + |
| 507 | + context = ghretos.Repo(owner=user, name=repo) |
| 508 | + obj = await self.fetch_resource(context) |
| 509 | + components: list[disnake.ui.Container | disnake.ui.ActionRow] = [] |
| 510 | + components.append(github_handlers.RepoRenderer().render_ogp_cv2(obj, context=context)) |
| 511 | + components.append( |
| 512 | + disnake.ui.ActionRow(DeleteButton(allow_manage_messages=True, user=ctx.author, initial_message=ctx.message)) |
| 513 | + ) |
| 514 | + await ctx.send(components=components) |
| 515 | + |
369 | 516 | @commands.slash_command(name="github", description="Fetch GitHub information.") |
370 | | - async def github_group(self, inter: disnake.ApplicationCommandInteraction) -> None: |
| 517 | + async def slash_github_group(self, inter: disnake.ApplicationCommandInteraction) -> None: |
371 | 518 | """Group for GitHub related commands.""" |
372 | 519 |
|
373 | | - @github_group.sub_command(name="info", description="Fetch GitHub information.") |
374 | | - async def github_info(self, inter: disnake.ApplicationCommandInteraction, arg: str) -> None: |
| 520 | + @slash_github_group.sub_command(name="info", description="Fetch GitHub information.") |
| 521 | + async def slash_github_info(self, inter: disnake.ApplicationCommandInteraction, arg: str) -> None: |
375 | 522 | """Fetch GitHub information. |
376 | 523 |
|
377 | 524 | Parameters |
@@ -474,6 +621,10 @@ async def on_message_automatic_issue_link( |
474 | 621 | """ |
475 | 622 | if not message.guild: |
476 | 623 | return |
| 624 | + command_context = await self.bot.get_context(message) |
| 625 | + if command_context.command and command_context.command.cog is self: |
| 626 | + return # do not auto-link in own commands |
| 627 | + |
477 | 628 | # in order to support shorthand, we need to check guild configuration |
478 | 629 | guild_config = await self.bot.ensure_guild_config(message.guild.id) |
479 | 630 |
|
|
0 commit comments