Skip to content

Commit 3707f83

Browse files
committed
Tried fixing discord ratelimits to countingcog
1 parent c20d4df commit 3707f83

1 file changed

Lines changed: 76 additions & 15 deletions

File tree

cogs/counting.py

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,47 @@ def __init__(self, bot):
1515
self.bot = bot
1616
# Cache for counting channels: guild_id -> channel_id
1717
self.counting_channels = {}
18+
# Protect against occasional duplicate MESSAGE_CREATE dispatches or accidental double-processing.
19+
# Key: message_id, Value: monotonic timestamp
20+
self._recent_message_ids: dict[int, float] = {}
21+
# Throttle reaction API calls to avoid Discord rate limits in fast counting channels.
22+
self._reaction_queue: asyncio.Queue[tuple[discord.Message, str]] = asyncio.Queue()
23+
self._pending_reactions: set[tuple[int, str]] = set()
24+
self._reaction_worker_task: Optional[asyncio.Task[None]] = None
25+
26+
async def cog_unload(self) -> None:
27+
if self._reaction_worker_task and not self._reaction_worker_task.done():
28+
self._reaction_worker_task.cancel()
29+
30+
async def _reaction_worker(self) -> None:
31+
# A small delay between reaction requests keeps us under the common reaction route limits.
32+
# Reactions may appear slightly delayed, but they will still be added.
33+
while True:
34+
message, emoji = await self._reaction_queue.get()
35+
try:
36+
try:
37+
await message.add_reaction(emoji)
38+
except Exception:
39+
pass
40+
await asyncio.sleep(0.35)
41+
finally:
42+
self._pending_reactions.discard((message.id, emoji))
43+
self._reaction_queue.task_done()
44+
45+
def _enqueue_reaction(self, message: discord.Message, emoji: str) -> None:
46+
key = (message.id, emoji)
47+
if key in self._pending_reactions:
48+
return
49+
self._pending_reactions.add(key)
50+
try:
51+
self._reaction_queue.put_nowait((message, emoji))
52+
except Exception:
53+
self._pending_reactions.discard(key)
1854

1955
async def cog_load(self):
2056
"""Load counting channels into memory on startup"""
57+
if self._reaction_worker_task is None or self._reaction_worker_task.done():
58+
self._reaction_worker_task = asyncio.create_task(self._reaction_worker())
2159
try:
2260
async with aiosqlite.connect(DB_PATH, timeout=30.0) as db:
2361
# Ensure auxiliary tables exist
@@ -153,10 +191,7 @@ async def _mark_highscore_message(
153191
# Only add the trophy here.
154192
# The ✅ reaction is added for all valid counts in the main handler;
155193
# adding it again here causes extra API calls and rate limits.
156-
try:
157-
await message.add_reaction("🏆")
158-
except Exception:
159-
pass
194+
self._enqueue_reaction(message, "🏆")
160195

161196
# Track the latest highscore/tie message ID for bookkeeping.
162197
# (We no longer remove reactions from older messages.)
@@ -316,6 +351,17 @@ async def on_message(self, message):
316351
if message.channel.id != self.counting_channels[message.guild.id]:
317352
return
318353

354+
# Deduplicate processing of the same message ID within this process.
355+
# This prevents duplicate warnings/messages if Discord or the bot dispatches the event twice.
356+
now = time.monotonic()
357+
last_seen = self._recent_message_ids.get(message.id)
358+
if last_seen is not None and (now - last_seen) < 30:
359+
return
360+
self._recent_message_ids[message.id] = now
361+
if len(self._recent_message_ids) > 5000:
362+
cutoff = now - 120
363+
self._recent_message_ids = {mid: ts for mid, ts in self._recent_message_ids.items() if ts >= cutoff}
364+
319365
# 2. Process the message logic
320366
# Wrap DB operations in retry loop for robustness
321367
retries = 3
@@ -350,19 +396,29 @@ async def on_message(self, message):
350396

351397
if message.author.id == last_user_id:
352398
# Warn instead of instant ruin. 3 warnings ruins the count.
353-
warnings = await self._get_warning_count(message.guild.id, message.author.id)
354-
warnings += 1
355-
await self._set_warning_count(message.guild.id, message.author.id, warnings)
356-
357-
try:
358-
await message.add_reaction("⚠️")
359-
except Exception:
360-
pass
399+
# Use an atomic increment in the SAME connection to avoid races and DB-lock retries.
400+
await db.execute(
401+
"""
402+
INSERT INTO counting_warnings (guild_id, user_id, warnings)
403+
VALUES (?, ?, 1)
404+
ON CONFLICT(guild_id, user_id) DO UPDATE SET warnings = warnings + 1
405+
""",
406+
(message.guild.id, message.author.id),
407+
)
408+
async with db.execute(
409+
"SELECT warnings FROM counting_warnings WHERE guild_id = ? AND user_id = ?",
410+
(message.guild.id, message.author.id),
411+
) as cursor:
412+
row = await cursor.fetchone()
413+
warnings = int(row[0]) if row else 1
414+
await db.commit()
361415

362416
if warnings >= 3:
363417
await self.fail_count(message, current_count, "Too many warnings (counted twice in a row 3 times)!")
364418
return
365419

420+
self._enqueue_reaction(message, "⚠️")
421+
366422
await message.channel.send(
367423
f"You can't count twice in a row, {message.author.mention}. "
368424
f"You have **{warnings}/3** warnings.",
@@ -371,7 +427,6 @@ async def on_message(self, message):
371427
return
372428

373429
# Valid count - Update DB
374-
await message.add_reaction("✅")
375430
new_high_score = max(high_score, next_count)
376431

377432
# Update configuration tables
@@ -387,11 +442,17 @@ async def on_message(self, message):
387442
VALUES (?, ?, 1, 0)
388443
ON CONFLICT(user_id, guild_id) DO UPDATE SET total_counts = total_counts + 1
389444
""", (message.author.id, message.guild.id))
445+
446+
# Reset warnings for this user on a valid count (in the same transaction).
447+
await db.execute(
448+
"DELETE FROM counting_warnings WHERE guild_id = ? AND user_id = ?",
449+
(message.guild.id, message.author.id),
450+
)
390451

391452
await db.commit()
392453

393-
# Reset warnings for this user on a valid count
394-
await self._set_warning_count(message.guild.id, message.author.id, 0)
454+
# Side effects after commit to avoid duplicate reactions on retries.
455+
self._enqueue_reaction(message, "✅")
395456

396457
# Highscore marker: react ✅+🏆 when reaching/topping the record
397458
if next_count >= high_score:

0 commit comments

Comments
 (0)