Skip to content

Commit 865cd77

Browse files
committed
Fix Discord UX: /sync single ephemeral, issue-requests async load, replace/cancel buttons
1 parent eaa7219 commit 865cd77

3 files changed

Lines changed: 117 additions & 56 deletions

File tree

config/aussie.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ github:
2727
mode: allow
2828
names:
2929
- "Gitcord-GithubDiscordBot"
30-
# Add more repos as needed, e.g.:
31-
# - "another-repo"
30+
- "Gitcord-Test"
3231

3332
discord:
3433
guild_id: "1022871757289422898"
@@ -63,7 +62,8 @@ discord:
6362
channel_id: null # null = DM; set to a channel ID to post there instead
6463

6564
scoring:
66-
period_days: 30
65+
# Main planning/audit window; /summary still shows last 7 and 30 days from stored events.
66+
period_days: 7
6767
weights:
6868
issue_opened: 3
6969
pr_opened: 5

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
services:
77
init_data:
8+
build: .
89
image: gitcord:latest
910
user: "0"
1011
volumes:

src/ghdcbot/bot.py

Lines changed: 113 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,21 +1117,6 @@ async def _on_repo_chosen(self, interaction: discord.Interaction, repo_value: st
11171117
if r.get("owner") == owner and r.get("repo") == repo
11181118
]
11191119
repo_requests.sort(key=lambda r: (_request_created_at(r), r.get("request_id", "")))
1120-
if hasattr(self.storage, "append_audit_event"):
1121-
self.storage.append_audit_event({
1122-
"event_type": "issue_request_viewed_repo",
1123-
"context": {
1124-
"repo": repo_value,
1125-
"mentor_discord_id": str(interaction.user.id),
1126-
"timestamp": datetime.now(timezone.utc).isoformat(),
1127-
},
1128-
})
1129-
period_days = self.config.scoring.period_days
1130-
period_end = datetime.now(timezone.utc)
1131-
period_start = period_end - timedelta(days=period_days)
1132-
mentor_roles = getattr(self.config, "assignments", None)
1133-
eligible_roles_config = getattr(mentor_roles, "issue_request_eligible_roles", []) if mentor_roles else []
1134-
member_roles_map = self.discord_reader.list_member_roles()
11351120

11361121
async def send_repo_list_back(interaction_or_channel: Any) -> None:
11371122
pending = self.storage.list_pending_issue_requests()
@@ -1153,33 +1138,78 @@ async def send_repo_list_back(interaction_or_channel: Any) -> None:
11531138
else:
11541139
await interaction_or_channel.send(embed=emb, view=v)
11551140

1156-
for req in repo_requests:
1157-
issue = fetch_issue_context(
1158-
self.github_adapter, req["owner"], req["repo"], req["issue_number"]
1159-
)
1160-
if not issue:
1161-
continue
1162-
contributor_roles = member_roles_map.get(req["discord_user_id"], [])
1163-
merged_count, last_merged_at = get_merged_pr_count_and_last_time(
1164-
self.storage, req["github_user"], period_start, period_end
1165-
)
1166-
now = datetime.now(timezone.utc)
1167-
verdict, reason = compute_eligibility(
1168-
eligible_roles_config, contributor_roles, merged_count, last_merged_at, now
1141+
mentor_discord_id = str(interaction.user.id)
1142+
1143+
def _collect_mentor_review_rows() -> list[dict[str, Any]]:
1144+
if hasattr(self.storage, "append_audit_event"):
1145+
self.storage.append_audit_event({
1146+
"event_type": "issue_request_viewed_repo",
1147+
"context": {
1148+
"repo": repo_value,
1149+
"mentor_discord_id": mentor_discord_id,
1150+
"timestamp": datetime.now(timezone.utc).isoformat(),
1151+
},
1152+
})
1153+
period_days = self.config.scoring.period_days
1154+
period_end = datetime.now(timezone.utc)
1155+
period_start = period_end - timedelta(days=period_days)
1156+
mentor_roles = getattr(self.config, "assignments", None)
1157+
eligible_roles_config = (
1158+
getattr(mentor_roles, "issue_request_eligible_roles", []) if mentor_roles else []
11691159
)
1170-
embed_dict = build_mentor_request_embed(
1171-
request=req,
1172-
issue=issue,
1173-
contributor_discord_mention=f"<@{req['discord_user_id']}>",
1174-
contributor_roles=contributor_roles,
1175-
merged_count=merged_count,
1176-
last_merged_at=last_merged_at,
1177-
eligibility_verdict=verdict,
1178-
eligibility_reason=reason,
1179-
eligible_roles_config=eligible_roles_config,
1180-
period_days=period_days,
1181-
now=now,
1160+
member_roles_map = self.discord_reader.list_member_roles()
1161+
rows: list[dict[str, Any]] = []
1162+
for req in repo_requests:
1163+
issue = fetch_issue_context(
1164+
self.github_adapter, req["owner"], req["repo"], req["issue_number"]
1165+
)
1166+
if not issue:
1167+
continue
1168+
contributor_roles = member_roles_map.get(req["discord_user_id"], [])
1169+
merged_count, last_merged_at = get_merged_pr_count_and_last_time(
1170+
self.storage, req["github_user"], period_start, period_end
1171+
)
1172+
now = datetime.now(timezone.utc)
1173+
verdict, reason = compute_eligibility(
1174+
eligible_roles_config, contributor_roles, merged_count, last_merged_at, now
1175+
)
1176+
embed_dict = build_mentor_request_embed(
1177+
request=req,
1178+
issue=issue,
1179+
contributor_discord_mention=f"<@{req['discord_user_id']}>",
1180+
contributor_roles=contributor_roles,
1181+
merged_count=merged_count,
1182+
last_merged_at=last_merged_at,
1183+
eligibility_verdict=verdict,
1184+
eligibility_reason=reason,
1185+
eligible_roles_config=eligible_roles_config,
1186+
period_days=period_days,
1187+
now=now,
1188+
)
1189+
assignees = issue.get("assignees") or []
1190+
has_existing_assignee = any(
1191+
isinstance(a, dict) and bool(a.get("login")) for a in assignees
1192+
)
1193+
rows.append({
1194+
"req": req,
1195+
"embed_dict": embed_dict,
1196+
"has_existing_assignee": has_existing_assignee,
1197+
})
1198+
return rows
1199+
1200+
try:
1201+
review_rows = await asyncio.to_thread(_collect_mentor_review_rows)
1202+
except Exception as exc:
1203+
logger.exception("issue-requests: failed after repo select")
1204+
await interaction.followup.send(
1205+
f"❌ Failed to load issue request details: {exc}",
1206+
ephemeral=True,
11821207
)
1208+
return
1209+
1210+
for row in review_rows:
1211+
req = row["req"]
1212+
embed_dict = row["embed_dict"]
11831213
view = IssueRequestReviewView(
11841214
request_id=req["request_id"],
11851215
owner=req["owner"],
@@ -1192,8 +1222,8 @@ async def send_repo_list_back(interaction_or_channel: Any) -> None:
11921222
policy=self.policy,
11931223
discord_sender=self.discord_reader,
11941224
back_callback=send_repo_list_back,
1225+
has_existing_assignee=row["has_existing_assignee"],
11951226
)
1196-
# Send as ephemeral followup so only mentor can see it
11971227
msg = await interaction.followup.send(
11981228
embed=discord.Embed.from_dict(embed_dict),
11991229
view=view,
@@ -1221,6 +1251,7 @@ def __init__(
12211251
policy: MutationPolicy,
12221252
discord_sender: Any,
12231253
back_callback: Any = None,
1254+
has_existing_assignee: bool = False,
12241255
timeout: float = 300.0,
12251256
) -> None:
12261257
super().__init__(timeout=timeout)
@@ -1235,6 +1266,9 @@ def __init__(
12351266
self.policy = policy
12361267
self.discord_sender = discord_sender
12371268
self.back_callback = back_callback
1269+
if not has_existing_assignee:
1270+
self.replace_assignee.disabled = True
1271+
self.replace_assignee.style = discord.ButtonStyle.secondary
12381272
if back_callback:
12391273
back_btn = discord.ui.Button(
12401274
label="Back to Repo List",
@@ -1518,8 +1552,23 @@ async def reject_request(self, interaction: discord.Interaction, button: discord
15181552

15191553
@discord.ui.button(label="Cancel", style=discord.ButtonStyle.secondary, emoji="🚫")
15201554
async def cancel_action(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
1521-
await interaction.response.defer(ephemeral=True)
1522-
await interaction.followup.send("No action taken.", ephemeral=True)
1555+
# Replace the card with plain text and remove components (same pattern as reject).
1556+
# Do not use only a zero-width placeholder or delete_after here: Discord often rejects "empty"
1557+
# MESSAGE_UPDATE payloads, and delete_original_response after UPDATE is unreliable
1558+
# for ephemeral follow-up messages.
1559+
try:
1560+
await interaction.response.edit_message(
1561+
content="Cancelled — no changes made.",
1562+
embed=None,
1563+
view=None,
1564+
)
1565+
except Exception:
1566+
logger.warning("Could not edit message on cancel", exc_info=True)
1567+
try:
1568+
await interaction.response.defer(ephemeral=True)
1569+
await interaction.followup.send("Cancelled — no changes made.", ephemeral=True)
1570+
except Exception:
1571+
logger.warning("Could not defer/followup on cancel", exc_info=True)
15231572

15241573
@tree.command(
15251574
name="request-issue",
@@ -1693,11 +1742,12 @@ async def on_message(message: discord.Message) -> None:
16931742
async def sync_cmd(interaction: discord.Interaction) -> None:
16941743
"""Manually trigger run-once to sync GitHub events and send notifications."""
16951744
await interaction.response.defer(ephemeral=True)
1696-
1745+
1746+
status_msg = None
16971747
try:
16981748
# Build orchestrator and run once
16991749
from ghdcbot.engine.orchestrator import Orchestrator
1700-
1750+
17011751
github_adapter_for_sync = build_adapter(
17021752
config.runtime.github_adapter,
17031753
token=config.github.token,
@@ -1720,7 +1770,7 @@ async def sync_cmd(interaction: discord.Interaction) -> None:
17201770
token=config.discord.token,
17211771
guild_id=config.discord.guild_id,
17221772
)
1723-
1773+
17241774
orchestrator = Orchestrator(
17251775
github_reader=github_adapter_for_sync,
17261776
github_writer=github_writer_for_sync,
@@ -1729,8 +1779,12 @@ async def sync_cmd(interaction: discord.Interaction) -> None:
17291779
storage=storage,
17301780
config=config,
17311781
)
1732-
1733-
await interaction.followup.send("🔄 Syncing GitHub events and sending notifications...", ephemeral=True)
1782+
1783+
status_msg = await interaction.followup.send(
1784+
"🔄 Syncing GitHub events and sending notifications...",
1785+
ephemeral=True,
1786+
wait=True,
1787+
)
17341788

17351789
# run_once() is synchronous and can take many minutes for large orgs; running it on the
17361790
# event loop blocks Discord heartbeats and delays other slash commands (they then hit
@@ -1743,13 +1797,19 @@ def _run_sync_and_close() -> None:
17431797

17441798
await asyncio.to_thread(_run_sync_and_close)
17451799

1746-
await interaction.followup.send("✅ Sync complete! Notifications sent for new GitHub events.", ephemeral=True)
1800+
await status_msg.edit(
1801+
content="✅ Sync complete! Notifications sent for new GitHub events.",
1802+
)
17471803
except Exception as exc:
17481804
logger.exception("Sync failed", exc_info=True)
1749-
await interaction.followup.send(
1750-
f"❌ Sync failed: {exc}",
1751-
ephemeral=True,
1752-
)
1805+
err_text = f"❌ Sync failed: {exc}"
1806+
if status_msg is not None:
1807+
try:
1808+
await status_msg.edit(content=err_text)
1809+
except Exception:
1810+
await interaction.followup.send(err_text, ephemeral=True)
1811+
else:
1812+
await interaction.followup.send(err_text, ephemeral=True)
17531813

17541814
@tree.error
17551815
async def on_app_command_error(interaction: discord.Interaction, error: app_commands.AppCommandError) -> None:

0 commit comments

Comments
 (0)