Skip to content

Commit b8b4ebf

Browse files
Fix: Improve permission error handling, privacy, and notification deduplication
- Fix permission error messages for mentor-only commands (non-mentors now get clear error messages) - Make /issue-requests responses ephemeral (only visible to mentor who runs it) - Prevent duplicate notifications when mentors approve issue requests via /issue-requests - Fix README: remove incorrect social media links and fix TODO badge URL Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 82d3706 commit b8b4ebf

2 files changed

Lines changed: 152 additions & 28 deletions

File tree

README.md

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,12 @@
1212
<!-- Organization Name -->
1313
<div align="center">
1414

15-
[![Static Badge](https://img.shields.io/badge/aossie.org/TODO-228B22?style=for-the-badge&labelColor=FFC517)](https://TODO.aossie.org/)
16-
17-
<!-- Correct deployed url to be added -->
15+
[![Static Badge](https://img.shields.io/badge/aossie.org/Gitcord-228B22?style=for-the-badge&labelColor=FFC517)](https://github.com/AOSSIE-Org/Gitcord-GithubDiscordBot)
1816

1917
</div>
2018

2119
<!-- Organization/Project Social Handles -->
2220
<p align="center">
23-
<!-- Telegram -->
24-
<a href="https://t.me/StabilityNexus">
25-
<img src="https://img.shields.io/badge/Telegram-black?style=flat&logo=telegram&logoColor=white&logoSize=auto&color=24A1DE" alt="Telegram Badge"/></a>
26-
&nbsp;&nbsp;
2721
<!-- X (formerly Twitter) -->
2822
<a href="https://x.com/aossie_org">
2923
<img src="https://img.shields.io/twitter/follow/aossie_org" alt="X (formerly Twitter) Badge"/></a>
@@ -32,17 +26,13 @@
3226
<a href="https://discord.gg/hjUhu33uAn">
3327
<img src="https://img.shields.io/discord/1022871757289422898?style=flat&logo=discord&logoColor=white&logoSize=auto&label=Discord&labelColor=5865F2&color=57F287" alt="Discord Badge"/></a>
3428
&nbsp;&nbsp;
35-
<!-- Medium -->
36-
<a href="https://news.stability.nexus/">
37-
<img src="https://img.shields.io/badge/Medium-black?style=flat&logo=medium&logoColor=black&logoSize=auto&color=white" alt="Medium Badge"></a>
38-
&nbsp;&nbsp;
3929
<!-- LinkedIn -->
4030
<a href="https://www.linkedin.com/company/aossie/">
4131
<img src="https://img.shields.io/badge/LinkedIn-black?style=flat&logo=LinkedIn&logoColor=white&logoSize=auto&color=0A66C2" alt="LinkedIn Badge"></a>
4232
&nbsp;&nbsp;
43-
<!-- Youtube -->
44-
<a href="https://www.youtube.com/@StabilityNexus">
45-
<img src="https://img.shields.io/youtube/channel/subscribers/UCZOG4YhFQdlGaLugr_e5BKw?style=flat&logo=youtube&logoColor=white&logoSize=auto&labelColor=FF0000&color=FF0000" alt="Youtube Badge"></a>
33+
<!-- Website -->
34+
<a href="https://aossie.org/">
35+
<img src="https://img.shields.io/badge/Website-black?style=flat&logo=globe&logoColor=white&logoSize=auto&color=228B22" alt="AOSSIE Website Badge"></a>
4636
</p>
4737

4838
---

src/ghdcbot/bot.py

Lines changed: 148 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,12 @@
3636
get_merged_pr_count_and_last_time,
3737
group_pending_requests_by_repo,
3838
)
39-
from ghdcbot.engine.notifications import send_notification_for_event
39+
from ghdcbot.engine.notifications import (
40+
_build_dedupe_key,
41+
_mark_notification_sent,
42+
send_notification_for_event,
43+
)
44+
from ghdcbot.core.models import ContributionEvent
4045
from ghdcbot.engine.pr_context import (
4146
build_pr_embed,
4247
fetch_pr_context,
@@ -1280,6 +1285,57 @@ async def approve_assign(self, interaction: discord.Interaction, button: discord
12801285
f"**Link:** https://github.com/{self.owner}/{self.repo}/issues/{self.issue_number}\n\n"
12811286
f"💡 You're now responsible for this issue. Good luck!"
12821287
)
1288+
1289+
# Mark notification as sent to prevent duplicate when /sync runs
1290+
try:
1291+
mentor_github = resolve_discord_to_github(self.storage, str(interaction.user.id))
1292+
payload = {
1293+
"issue_number": self.issue_number,
1294+
"title": issue_title,
1295+
"state": issue.get("state", "open") if issue else "open",
1296+
"labels": [label.get("name") for label in issue.get("labels", [])] if issue else [],
1297+
}
1298+
if mentor_github:
1299+
payload["assigned_by"] = mentor_github
1300+
1301+
event = ContributionEvent(
1302+
github_user=self.requester_github,
1303+
event_type="issue_assigned",
1304+
repo=self.repo,
1305+
created_at=datetime.now(timezone.utc),
1306+
payload=payload,
1307+
)
1308+
1309+
dedupe_key = _build_dedupe_key(event, self.requester_github)
1310+
_mark_notification_sent(
1311+
self.storage,
1312+
dedupe_key,
1313+
event,
1314+
self.requester_discord_id,
1315+
None, # channel_id=None for DM
1316+
self.requester_github,
1317+
)
1318+
logger.debug(
1319+
"Marked issue assignment notification as sent to prevent duplicate",
1320+
extra={
1321+
"dedupe_key": dedupe_key,
1322+
"issue_number": self.issue_number,
1323+
"repo": self.repo,
1324+
"assignee": self.requester_github,
1325+
},
1326+
)
1327+
except Exception as e:
1328+
# Log error but don't fail the approval flow
1329+
logger.warning(
1330+
"Failed to mark notification as sent (may result in duplicate notification)",
1331+
exc_info=e,
1332+
extra={
1333+
"issue_number": self.issue_number,
1334+
"repo": self.repo,
1335+
"assignee": self.requester_github,
1336+
},
1337+
)
1338+
12831339
await interaction.followup.send("✅ Request approved and issue assigned.", ephemeral=True)
12841340
if hasattr(self, "message") and self.message:
12851341
try:
@@ -1321,6 +1377,57 @@ async def replace_assignee(self, interaction: discord.Interaction, button: disco
13211377
f"ℹ️ Note: The previous assignee was replaced.\n\n"
13221378
f"💡 You're now responsible for this issue. Good luck!"
13231379
)
1380+
1381+
# Mark notification as sent to prevent duplicate when /sync runs
1382+
try:
1383+
mentor_github = resolve_discord_to_github(self.storage, str(interaction.user.id))
1384+
payload = {
1385+
"issue_number": self.issue_number,
1386+
"title": issue_title,
1387+
"state": issue.get("state", "open") if issue else "open",
1388+
"labels": [label.get("name") for label in issue.get("labels", [])] if issue else [],
1389+
}
1390+
if mentor_github:
1391+
payload["assigned_by"] = mentor_github
1392+
1393+
event = ContributionEvent(
1394+
github_user=self.requester_github,
1395+
event_type="issue_assigned",
1396+
repo=self.repo,
1397+
created_at=datetime.now(timezone.utc),
1398+
payload=payload,
1399+
)
1400+
1401+
dedupe_key = _build_dedupe_key(event, self.requester_github)
1402+
_mark_notification_sent(
1403+
self.storage,
1404+
dedupe_key,
1405+
event,
1406+
self.requester_discord_id,
1407+
None, # channel_id=None for DM
1408+
self.requester_github,
1409+
)
1410+
logger.debug(
1411+
"Marked issue assignment notification as sent to prevent duplicate (replacement)",
1412+
extra={
1413+
"dedupe_key": dedupe_key,
1414+
"issue_number": self.issue_number,
1415+
"repo": self.repo,
1416+
"assignee": self.requester_github,
1417+
},
1418+
)
1419+
except Exception as e:
1420+
# Log error but don't fail the approval flow
1421+
logger.warning(
1422+
"Failed to mark notification as sent (may result in duplicate notification)",
1423+
exc_info=e,
1424+
extra={
1425+
"issue_number": self.issue_number,
1426+
"repo": self.repo,
1427+
"assignee": self.requester_github,
1428+
},
1429+
)
1430+
13241431
await interaction.followup.send("🔁 Replaced assignee and assigned contributor.", ephemeral=True)
13251432
if hasattr(self, "message") and self.message:
13261433
try:
@@ -1435,7 +1542,7 @@ async def request_issue_cmd(interaction: discord.Interaction, issue_url: str) ->
14351542
)
14361543
@app_commands.check(mentor_check)
14371544
async def issue_requests_cmd(interaction: discord.Interaction) -> None:
1438-
await interaction.response.defer(ephemeral=False)
1545+
await interaction.response.defer(ephemeral=True)
14391546
pending = getattr(storage, "list_pending_issue_requests", None)
14401547
if not callable(pending):
14411548
await interaction.followup.send("❌ Request list unavailable.", ephemeral=True)
@@ -1464,7 +1571,7 @@ async def issue_requests_cmd(interaction: discord.Interaction) -> None:
14641571
await interaction.followup.send(
14651572
embed=discord.Embed.from_dict(embed_dict),
14661573
view=view,
1467-
ephemeral=False,
1574+
ephemeral=True,
14681575
)
14691576

14701577
@client.event
@@ -1599,19 +1706,46 @@ async def sync_cmd(interaction: discord.Interaction) -> None:
15991706
async def on_app_command_error(interaction: discord.Interaction, error: app_commands.AppCommandError) -> None:
16001707
"""Handle app command errors, including check failures."""
16011708
if isinstance(error, app_commands.CheckFailure):
1602-
mentor_roles = getattr(config, "assignments", None)
1603-
issue_assignee_roles = getattr(mentor_roles, "issue_assignees", []) if mentor_roles else []
1604-
await interaction.response.send_message(
1605-
f"❌ Permission denied. Only mentors with roles {', '.join(issue_assignee_roles) or 'configure issue_assignees'} can use this command.",
1606-
ephemeral=True,
1607-
)
1709+
try:
1710+
mentor_roles = getattr(config, "assignments", None)
1711+
issue_assignee_roles = getattr(mentor_roles, "issue_assignees", []) if mentor_roles else []
1712+
role_list = ', '.join(issue_assignee_roles) if issue_assignee_roles else 'configure issue_assignees'
1713+
error_message = f"❌ Permission denied. Only mentors with roles **{role_list}** can use this command."
1714+
1715+
logger.info(
1716+
"Check failure for user %s (%s) on command %s. Required roles: %s",
1717+
interaction.user.name,
1718+
interaction.user.id,
1719+
interaction.command.name if interaction.command else "unknown",
1720+
role_list,
1721+
)
1722+
1723+
# Check if response is already sent (shouldn't happen for check failures, but be safe)
1724+
if interaction.response.is_done():
1725+
await interaction.followup.send(error_message, ephemeral=True)
1726+
else:
1727+
await interaction.response.send_message(error_message, ephemeral=True)
1728+
except Exception as e:
1729+
logger.exception("Failed to send permission denied message", exc_info=e)
1730+
# Try one more time with a simple message
1731+
try:
1732+
if not interaction.response.is_done():
1733+
await interaction.response.send_message(
1734+
"❌ Permission denied. Only mentors can use this command.",
1735+
ephemeral=True,
1736+
)
1737+
except Exception:
1738+
logger.error("Could not send any error message to user")
16081739
else:
16091740
logger.exception("App command error", exc_info=error)
1610-
if not interaction.response.is_done():
1611-
await interaction.response.send_message(
1612-
"❌ An error occurred while processing your command.",
1613-
ephemeral=True,
1614-
)
1741+
try:
1742+
if not interaction.response.is_done():
1743+
await interaction.response.send_message(
1744+
"❌ An error occurred while processing your command.",
1745+
ephemeral=True,
1746+
)
1747+
except Exception:
1748+
logger.error("Could not send error message to user")
16151749

16161750
@client.event
16171751
async def on_ready() -> None:

0 commit comments

Comments
 (0)