Skip to content

Commit f8f4db3

Browse files
committed
Fix mention safety, counting parsing, quiz reactions, and bump detection
1 parent 9a85cfb commit f8f4db3

9 files changed

Lines changed: 203 additions & 196 deletions

File tree

bot.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ def __init__(self, config: Config):
4343
super().__init__(
4444
command_prefix='?',
4545
intents=intents,
46-
help_command=None
46+
help_command=None,
47+
# SECURITY: Prevent mass-mention exploits caused by echoing user content.
48+
# Even if the bot has Administrator / Mention Everyone, this blocks @everyone/@here and role pings.
49+
allowed_mentions=discord.AllowedMentions(everyone=False, roles=False, users=True, replied_user=False),
4750
)
4851

4952
self.start_time = discord.utils.utcnow()

cogs/bump_leaderboard.py

Lines changed: 73 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@
44
55
What this does
66
-------------
7-
Counts bumps done via the **Disboard bot** in a configured bump channel.
7+
Counts bumps done via a **Bump Reminder** embed in a configured bump channel.
88
99
Important technical note
1010
------------------------
11-
Discord does not expose *another bot's* slash-command interactions to your bot,
12-
so we cannot directly "listen to /bump" when Disboard handles it.
13-
Instead, we listen for Disboard's **confirmation message** (e.g. "Bump done")
14-
in the bump channel and attribute the bump to the mentioned user.
11+
Discord does not expose *another bot/app's* slash-command interactions to your bot.
12+
Instead, we listen for a bump confirmation embed ("Bump Reminder") and attribute
13+
the bump to the user name stored in the embed title.
1514
1615
Data is stored in the SQLite database (`botdata.db`).
1716
@@ -42,11 +41,8 @@
4241
DATA_VERSION = 1
4342
DEFAULT_COOLDOWN_SECONDS = 60 # basic anti-spam; adjust as needed
4443

45-
# Default Disboard bot user id. If your server uses a different bump bot,
46-
# you can change this constant.
47-
DISBOARD_BOT_ID = 302050872383242240
48-
49-
_MENTION_RE = re.compile(r"<@!?(\d{15,25})>")
44+
# Minimal signal to identify a "bump" embed.
45+
_BUMP_CMD_RE = re.compile(r"\b/bump\b", re.IGNORECASE)
5046

5147

5248
def _utcnow() -> datetime:
@@ -103,72 +99,65 @@ def __init__(self, bot: commands.Bot):
10399
# In-memory cache to avoid repeatedly parsing ISO strings for cooldown checks.
104100
self._last_bump_cache: Dict[int, Dict[int, datetime]] = {}
105101

106-
# Prevent double counting when Disboard edits the same message.
102+
# Prevent double counting when the same bump message is edited/reposted.
107103
self._processed_message_ids: Dict[int, float] = {}
108104

109-
# Remember who invoked /bump most recently per (guild, channel).
110-
# Disboard's confirmation embed often does not mention the user.
111-
self._recent_bump_invoker: Dict[Tuple[int, int], Tuple[int, float]] = {}
112-
113105
# Ensure DB tables exist (no JSON migration).
114106
self.bot.loop.create_task(self.load_data())
115107

116108
# ----------------------------
117-
# Disboard message parsing
109+
# Bump Reminder embed parsing
118110
# ----------------------------
119111

120-
def _is_disboard_message(self, message: discord.Message) -> bool:
121-
return message.author is not None and message.author.id == DISBOARD_BOT_ID
112+
def _looks_like_bump_reminder_embed(self, embed: discord.Embed) -> bool:
113+
"""Return True if an embed looks like a bump confirmation."""
114+
# The screenshot shows a field like "Command ran: /bump".
115+
for f in embed.fields or []:
116+
name = (f.name or "").strip()
117+
value = (f.value or "").strip()
118+
if _BUMP_CMD_RE.search(name) or _BUMP_CMD_RE.search(value):
119+
return True
122120

123-
def _message_text_blob(self, message: discord.Message) -> str:
121+
# Fallback: search embed text blob.
124122
parts: List[str] = []
125-
if message.content:
126-
parts.append(message.content)
127-
123+
if embed.title:
124+
parts.append(str(embed.title))
125+
if embed.description:
126+
parts.append(str(embed.description))
127+
for f in embed.fields or []:
128+
if f.name:
129+
parts.append(str(f.name))
130+
if f.value:
131+
parts.append(str(f.value))
132+
return _BUMP_CMD_RE.search("\n".join(parts) or "") is not None
133+
134+
def _extract_bumper_name_from_embeds(self, message: discord.Message) -> Optional[str]:
135+
"""Return bumper username from the bump embed title."""
128136
for emb in message.embeds or []:
129-
if emb.title:
130-
parts.append(str(emb.title))
131-
if emb.description:
132-
parts.append(str(emb.description))
133-
for f in emb.fields or []:
134-
if f.name:
135-
parts.append(str(f.name))
136-
if f.value:
137-
parts.append(str(f.value))
138-
139-
return "\n".join(parts)
140-
141-
def _looks_like_bump_success(self, message: discord.Message) -> bool:
142-
blob = self._message_text_blob(message).lower()
143-
# Common Disboard phrases.
144-
return (
145-
"bump done" in blob
146-
or "bumped" in blob and "done" in blob
147-
or "successful" in blob and "bump" in blob
148-
)
137+
if not emb or not emb.title:
138+
continue
139+
if not self._looks_like_bump_reminder_embed(emb):
140+
continue
141+
name = str(emb.title).strip()
142+
if name:
143+
return name
144+
return None
149145

150-
async def _extract_bumper_user(self, message: discord.Message) -> Optional[discord.abc.User]:
151-
# Prefer real resolved mentions.
152-
for m in message.mentions or []:
153-
if not m.bot:
146+
def _resolve_member_by_name(self, guild: discord.Guild, name: str) -> Optional[discord.Member]:
147+
"""Resolve a guild member by display name / username (best-effort)."""
148+
# discord.py helper: matches nick / name / name#discrim.
149+
try:
150+
m = guild.get_member_named(name)
151+
if m is not None:
154152
return m
153+
except Exception:
154+
pass
155155

156-
# Fall back to parsing mention tags in text.
157-
blob = self._message_text_blob(message)
158-
m = _MENTION_RE.search(blob)
159-
if not m:
160-
return None
161-
162-
user_id = int(m.group(1))
163-
if message.guild:
164-
member = message.guild.get_member(user_id)
165-
if member:
156+
needle = name.casefold()
157+
for member in guild.members:
158+
if member.display_name.casefold() == needle or member.name.casefold() == needle:
166159
return member
167-
168-
try:
169-
return await self.bot.fetch_user(user_id)
170-
except Exception:
171-
return None
160+
return None
172161

173162
def _cleanup_processed_cache(self) -> None:
174163
# Keep ~10 minutes of ids; enough to cover edits/reposts.
@@ -177,62 +166,7 @@ def _cleanup_processed_cache(self) -> None:
177166
for mid in stale:
178167
self._processed_message_ids.pop(mid, None)
179168

180-
# Keep ~2 minutes of recent invokers.
181-
inv_cutoff = time.monotonic() - 120
182-
stale_keys = [k for k, (_, ts) in self._recent_bump_invoker.items() if ts < inv_cutoff]
183-
for k in stale_keys:
184-
self._recent_bump_invoker.pop(k, None)
185-
186-
def _record_bump_invocation(self, message: discord.Message) -> None:
187-
"""Record a visible '/bump' invocation message so we can attribute Disboard's confirmation."""
188-
if message.guild is None:
189-
return
190-
191-
# This is the "<user> used /bump" system message shown in Discord.
192-
# In discord.py it comes through as MessageType.chat_input_command with a MessageInteraction.
193-
if message.type != discord.MessageType.chat_input_command:
194-
return
195-
196-
# discord.py 2.4+: message.interaction_metadata (preferred)
197-
# Older versions: message.interaction (deprecated)
198-
meta = getattr(message, "interaction_metadata", None)
199-
mi = meta if meta is not None else getattr(message, "interaction", None)
200-
if mi is None:
201-
return
202-
203-
name = getattr(mi, "name", None)
204-
user = getattr(mi, "user", None)
205-
if name != "bump" or user is None:
206-
return
207-
208-
# Only store humans.
209-
if getattr(user, "bot", False):
210-
return
211-
212-
key = (message.guild.id, message.channel.id)
213-
self._recent_bump_invoker[key] = (int(user.id), time.monotonic())
214-
215-
async def _get_recent_invoker(self, guild: discord.Guild, channel_id: int) -> Optional[discord.abc.User]:
216-
key = (guild.id, channel_id)
217-
rec = self._recent_bump_invoker.get(key)
218-
if not rec:
219-
return None
220-
221-
user_id, ts = rec
222-
# Disboard replies quickly; allow a generous window.
223-
if time.monotonic() - ts > 45:
224-
return None
225-
226-
m = guild.get_member(user_id)
227-
if m:
228-
return m
229-
230-
try:
231-
return await self.bot.fetch_user(user_id)
232-
except Exception:
233-
return None
234-
235-
async def _handle_possible_disboard_bump(self, message: discord.Message) -> None:
169+
async def _handle_possible_bump_reminder_bump(self, message: discord.Message) -> None:
236170
if message.guild is None:
237171
return
238172

@@ -245,10 +179,13 @@ async def _handle_possible_disboard_bump(self, message: discord.Message) -> None
245179
if message.channel.id != int(bump_channel_id):
246180
return
247181

248-
if not self._is_disboard_message(message):
182+
bumper_name = self._extract_bumper_name_from_embeds(message)
183+
if not bumper_name:
249184
return
250185

251-
if not self._looks_like_bump_success(message):
186+
bumper_member = self._resolve_member_by_name(message.guild, bumper_name)
187+
if bumper_member is None:
188+
# If we can't resolve the member, do not guess.
252189
return
253190

254191
# Deduplicate message id (Disboard often edits the same message).
@@ -257,16 +194,19 @@ async def _handle_possible_disboard_bump(self, message: discord.Message) -> None
257194
return
258195
self._processed_message_ids[message.id] = time.monotonic()
259196

260-
bumper = await self._extract_bumper_user(message)
261-
if bumper is None:
262-
# Disboard embed often doesn't mention the user; fall back to the
263-
# last '/bump' invoker message in the channel.
264-
bumper = await self._get_recent_invoker(message.guild, message.channel.id)
265-
if bumper is None:
266-
return
197+
# Count the bump (+1) and thank the user.
198+
await self.update_bump_count(
199+
message.guild,
200+
bumper_member,
201+
now=message.created_at or _utcnow(),
202+
amount=1,
203+
bypass_cooldown=True,
204+
)
267205

268-
# Count the bump. Disboard enforces ~2h cooldown, so we bypass our own.
269-
await self.update_bump_count(message.guild, bumper, now=message.created_at or _utcnow(), amount=1, bypass_cooldown=True)
206+
try:
207+
await message.channel.send(f"Thanks {bumper_member.mention} for bump")
208+
except Exception:
209+
pass
270210

271211
# ----------------------------
272212
# Event listeners
@@ -278,22 +218,19 @@ async def on_message(self, message: discord.Message) -> None:
278218
if message.author and message.author.id == getattr(self.bot.user, "id", None):
279219
return
280220
try:
281-
# Track who invoked /bump (system message).
282-
self._record_bump_invocation(message)
283-
await self._handle_possible_disboard_bump(message)
221+
await self._handle_possible_bump_reminder_bump(message)
284222
except Exception:
285-
logger.exception("Failed handling possible Disboard bump message")
223+
logger.exception("Failed handling possible bump reminder message")
286224

287225
@commands.Cog.listener()
288226
async def on_message_edit(self, before: discord.Message, after: discord.Message) -> None:
289-
# Disboard frequently edits the confirmation message; handle edits too.
227+
# Some apps may edit the confirmation message; handle edits too.
290228
if after.author and after.author.id == getattr(self.bot.user, "id", None):
291229
return
292230
try:
293-
self._record_bump_invocation(after)
294-
await self._handle_possible_disboard_bump(after)
231+
await self._handle_possible_bump_reminder_bump(after)
295232
except Exception:
296-
logger.exception("Failed handling edited Disboard bump message")
233+
logger.exception("Failed handling edited bump reminder message")
297234

298235
# ----------------------------
299236
# Persistence helpers

cogs/codebuddy_quiz.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ async def on_message(self, message: discord.Message):
113113

114114
# Richtige Antwort
115115
if content == self.current_answer:
116+
try:
117+
await message.add_reaction("✅")
118+
except Exception:
119+
pass
120+
116121
points = 2 if self.bonus_active else 1
117122
extra_bonus = 0
118123

@@ -180,6 +185,11 @@ async def on_message(self, message: discord.Message):
180185
# Falsche Antwort
181186
else:
182187
self.ignored_users.add(user_id)
188+
189+
try:
190+
await message.add_reaction("❌")
191+
except Exception:
192+
pass
183193

184194
# Try to use streak freeze first
185195
freeze_used = False

0 commit comments

Comments
 (0)