Skip to content

Commit 8ebf65c

Browse files
committed
Make the show issues loading button re-parse the message
1 parent 941328e commit 8ebf65c

File tree

6 files changed

+168
-85
lines changed

6 files changed

+168
-85
lines changed
Lines changed: 7 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import logging
2-
import re
32
from dataclasses import dataclass
43
from types import MethodType
54
from typing import Any, Callable
@@ -10,10 +9,8 @@
109
from discord.utils import Coro
1110

1211
from ghutils.core.cog import GHUtilsCog
13-
from ghutils.utils.discord.components import RefreshIssueButton
14-
from ghutils.utils.discord.embeds import create_issue_embed
15-
from ghutils.utils.discord.references import IssueReference, IssueReferenceTransformer
16-
from ghutils.utils.discord.visibility import respond_with_visibility
12+
from ghutils.utils.discord.components import RefreshIssuesButton
13+
from ghutils.utils.discord.embeds import create_issue_embeds
1714

1815
logger = logging.getLogger(__name__)
1916

@@ -50,20 +47,6 @@ def builder(group: GroupT):
5047
return decorator
5148

5249

53-
_issue_pattern = re.compile(
54-
r"""
55-
(?<![a-zA-Z`</])
56-
(?P<value>
57-
(?P<repo>[\w-]+/[\w-]+)?
58-
\#
59-
(?P<reference>[0-9]+)
60-
)
61-
(?![a-zA-Z`>])
62-
""",
63-
flags=re.VERBOSE,
64-
)
65-
66-
6750
@dataclass(eq=False)
6851
class ContextMenusCog(GHUtilsCog):
6952
"""Context menu commands."""
@@ -90,51 +73,9 @@ async def cog_unload(self) -> None:
9073
async def show_issues(self, interaction: Interaction, message: Message):
9174
await interaction.response.defer()
9275

93-
seen = set[str]()
94-
issues = list[IssueReference]()
95-
transformer = IssueReferenceTransformer()
96-
9776
async with self.bot.github_app(interaction) as (github, _):
98-
for match in _issue_pattern.finditer(message.content):
99-
value = match.group("value")
100-
try:
101-
repo, issue = await transformer.transform_with_github(
102-
github, interaction, value
103-
)
104-
except Exception:
105-
logger.warning(
106-
f"Failed to transform issue reference: {value}", exc_info=True
107-
)
108-
continue
109-
110-
if issue.html_url in seen:
111-
continue
112-
113-
seen.add(issue.html_url)
114-
issues.append((repo, issue))
115-
116-
visibility = "public"
117-
match issues:
118-
case []:
119-
await respond_with_visibility(
120-
interaction,
121-
visibility,
122-
content="❌ No issue references found.",
123-
)
124-
case [reference]:
125-
button = await RefreshIssueButton.from_reference(github, reference)
126-
await respond_with_visibility(
127-
interaction,
128-
visibility,
129-
embed=create_issue_embed(*reference),
130-
items=[button],
131-
)
132-
case _:
133-
await respond_with_visibility(
134-
interaction,
135-
visibility,
136-
embeds=[
137-
create_issue_embed(*reference, add_body=False)
138-
for reference in issues
139-
],
140-
)
77+
contents = await create_issue_embeds(github, interaction, message)
78+
79+
contents.items.append(RefreshIssuesButton(message))
80+
81+
await contents.send(interaction, "public")

bot/src/ghutils/cogs/events.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55

66
from ghutils.core.cog import GHUtilsCog
77
from ghutils.utils.discord.commands import get_command, print_command
8-
from ghutils.utils.discord.components import RefreshCommitButton, RefreshIssueButton
8+
from ghutils.utils.discord.components import (
9+
RefreshCommitButton,
10+
RefreshIssueButton,
11+
RefreshIssuesButton,
12+
)
913
from ghutils.utils.discord.visibility import PermanentDeleteButton
1014

1115
logger = logging.getLogger(__name__)
@@ -21,6 +25,7 @@ async def on_ready(self):
2125
PermanentDeleteButton,
2226
RefreshCommitButton,
2327
RefreshIssueButton,
28+
RefreshIssuesButton,
2429
)
2530
await self.bot.fetch_custom_emojis()
2631

bot/src/ghutils/utils/discord/components.py

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
1+
from dataclasses import dataclass
12
from datetime import UTC, datetime, timedelta
23
from re import Match
34
from typing import Any, override
45

5-
from discord import Color, Embed, Interaction
6+
from discord import Color, Embed, Interaction, Message
67
from discord.ui import Button, DynamicItem, Item
78
from githubkit import GitHub
89
from githubkit.rest import Commit, Issue
910
from pydantic import TypeAdapter
10-
from pydantic.dataclasses import dataclass
11+
from pydantic.dataclasses import dataclass as pydantic_dataclass
1112

1213
from ghutils.core.bot import GHUtilsBot
1314
from ghutils.core.types import LoginState
14-
from ghutils.utils.discord.references import (
15+
16+
from ..github import RepositoryName, gh_request
17+
from .embeds import create_commit_embed, create_issue_embed, create_issue_embeds
18+
from .references import (
1519
CommitReference,
1620
IssueReference,
1721
PRReference,
1822
)
19-
from ghutils.utils.github import RepositoryName, gh_request
20-
21-
from .embeds import create_commit_embed, create_issue_embed
2223

2324

24-
@dataclass
25+
@pydantic_dataclass
2526
class RefreshIssueButton(
2627
DynamicItem[Button[Any]],
2728
template=r"RefreshIssue:(?P<repo_id>[0-9]+):(?P<issue>[0-9]+)",
@@ -86,6 +87,54 @@ async def callback(self, interaction: Interaction):
8687

8788

8889
@dataclass
90+
class RefreshIssuesButton(
91+
DynamicItem[Button[Any]],
92+
template=r"RefreshIssues:(?P<message_id>[0-9]+)",
93+
):
94+
message: Message
95+
96+
def __post_init__(self):
97+
super().__init__(
98+
Button(
99+
emoji="🔄",
100+
custom_id=f"RefreshIssues:{self.message.id}",
101+
)
102+
)
103+
104+
@classmethod
105+
@override
106+
async def from_custom_id(
107+
cls,
108+
interaction: Interaction,
109+
item: Item[Any],
110+
match: Match[str],
111+
):
112+
assert interaction.message is not None
113+
message_id = int(match["message_id"])
114+
return cls(
115+
message=await interaction.message.channel.fetch_message(message_id),
116+
)
117+
118+
@override
119+
async def callback(self, interaction: Interaction):
120+
async with GHUtilsBot.github_app_of(interaction) as (github, state):
121+
if not await _check_ratelimit(interaction, state):
122+
return
123+
124+
# disable the button while we're working to give a loading indication
125+
self.item.disabled = True
126+
await interaction.response.edit_message(view=self.view)
127+
128+
self.item.disabled = False
129+
try:
130+
contents = await create_issue_embeds(github, interaction, self.message)
131+
await contents.edit_original_response(interaction, view=self.view)
132+
except Exception:
133+
await interaction.response.edit_message(view=self.view)
134+
raise
135+
136+
137+
@pydantic_dataclass
89138
class RefreshCommitButton(
90139
DynamicItem[Button[Any]],
91140
template=r"RefreshCommit:(?P<repo_id>[0-9]+):(?P<sha>[^:]+)",

bot/src/ghutils/utils/discord/embeds.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
from __future__ import annotations
22

3+
import logging
4+
import re
35
from datetime import datetime
46
from typing import Any
57

6-
from discord import Embed
8+
from discord import Embed, Interaction, Message
79
from githubkit import GitHub
810
from githubkit.exception import GitHubException
911
from githubkit.rest import Commit, Issue, IssuePropPullRequest, PullRequest, SimpleUser
1012

13+
from ghutils.utils.discord.visibility import MessageContents
14+
1115
from ..github import (
1216
CommitCheckState,
1317
IssueState,
@@ -18,6 +22,9 @@
1822
shorten_sha,
1923
)
2024
from ..strings import truncate_str
25+
from .references import IssueReference, IssueReferenceTransformer
26+
27+
logger = logging.getLogger(__name__)
2128

2229

2330
def set_embed_author(embed: Embed, user: SimpleUser):
@@ -62,6 +69,65 @@ def create_issue_embed(
6269
return embed
6370

6471

72+
_ISSUE_PATTERN = re.compile(
73+
r"""
74+
(?<![a-zA-Z`</])
75+
(?P<value>
76+
(?P<repo>[\w-]+/[\w-]+)?
77+
\#
78+
(?P<reference>[0-9]+)
79+
)
80+
(?![a-zA-Z`>])
81+
""",
82+
flags=re.VERBOSE,
83+
)
84+
85+
86+
async def create_issue_embeds(
87+
github: GitHub[Any],
88+
interaction: Interaction,
89+
message: Message,
90+
):
91+
seen = set[str]()
92+
issues = list[IssueReference]()
93+
transformer = IssueReferenceTransformer()
94+
95+
for match in _ISSUE_PATTERN.finditer(message.content):
96+
value = match.group("value")
97+
try:
98+
repo, issue = await transformer.transform_with_github(
99+
github, interaction, value
100+
)
101+
except Exception:
102+
logger.warning(
103+
f"Failed to transform issue reference: {value}", exc_info=True
104+
)
105+
continue
106+
107+
if issue.html_url in seen:
108+
continue
109+
110+
seen.add(issue.html_url)
111+
issues.append((repo, issue))
112+
113+
content = None
114+
embeds = list[Embed]()
115+
match issues:
116+
case []:
117+
content = "❌ No issue references found."
118+
case [reference]:
119+
embeds.append(create_issue_embed(*reference))
120+
case _:
121+
embeds.extend(
122+
create_issue_embed(*reference, add_body=False) for reference in issues
123+
)
124+
return MessageContents(
125+
command=interaction.command,
126+
content=content,
127+
embeds=embeds,
128+
)
129+
130+
65131
async def create_commit_embed(
66132
github: GitHub[Any],
67133
repo: RepositoryName,

bot/src/ghutils/utils/discord/visibility.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,28 +23,35 @@ async def respond_with_visibility(
2323
content: Any | None = None,
2424
embed: Embed = MISSING,
2525
embeds: Sequence[Embed] = MISSING,
26-
items: Sequence[Item[Any]] = [],
26+
items: list[Item[Any]] | None = None,
2727
):
2828
data = MessageContents(
2929
command=interaction.command,
3030
content=content,
3131
embed=embed,
3232
embeds=embeds,
33-
items=items,
33+
items=items if items is not None else [],
3434
)
35-
if interaction.response.is_done():
36-
await data.send_followup(interaction, visibility)
37-
else:
38-
await data.send_response(interaction, visibility)
35+
await data.send(interaction, visibility)
3936

4037

4138
@dataclass(kw_only=True)
4239
class MessageContents:
4340
command: AnyCommand | ContextMenu | None
44-
content: Any | None = None
41+
content: Any | None = MISSING
4542
embed: Embed = MISSING
4643
embeds: Sequence[Embed] = MISSING
47-
items: Sequence[Item[Any]] = field(default_factory=lambda: list())
44+
items: list[Item[Any]] = field(default_factory=lambda: list())
45+
46+
async def send(
47+
self,
48+
interaction: Interaction,
49+
visibility: MessageVisibility,
50+
):
51+
if interaction.response.is_done():
52+
await self.send_followup(interaction, visibility)
53+
else:
54+
await self.send_response(interaction, visibility)
4855

4956
async def send_response(
5057
self,
@@ -74,6 +81,19 @@ async def send_followup(
7481
view=self._get_view(interaction, visibility, show_user),
7582
)
7683

84+
async def edit_original_response(
85+
self,
86+
interaction: Interaction,
87+
*,
88+
view: View | None = MISSING,
89+
):
90+
await interaction.edit_original_response(
91+
content=self.content,
92+
embed=self.embed,
93+
embeds=self.embeds,
94+
view=view,
95+
)
96+
7797
def _get_view(
7898
self,
7999
interaction: Interaction,

bot/src/ghutils/utils/github.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ def __init__(self, color: Color | None):
8484
self.color = color
8585

8686

87-
REPOSITORY_NAME_URL_PATTERN = re.compile(r"github.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)")
87+
_REPOSITORY_NAME_URL_PATTERN = re.compile(
88+
r"github.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)"
89+
)
8890

8991

9092
@dataclass
@@ -112,7 +114,7 @@ def try_parse(cls, value: str) -> Self | None:
112114

113115
@classmethod
114116
def from_url(cls, url: str) -> Self:
115-
match = REPOSITORY_NAME_URL_PATTERN.search(url)
117+
match = _REPOSITORY_NAME_URL_PATTERN.search(url)
116118
if not match:
117119
raise ValueError("GitHub URL not found")
118120
return cls(owner=match["owner"], repo=match["repo"])

0 commit comments

Comments
 (0)