Skip to content

Commit c20d4df

Browse files
committed
fixed counting bug from issue #38
1 parent a22251b commit c20d4df

2 files changed

Lines changed: 87 additions & 22 deletions

File tree

cogs/counting.py

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -140,27 +140,26 @@ async def _mark_highscore_message(
140140
new_count: int,
141141
previous_high_score: int,
142142
) -> None:
143-
"""Add ✅+🏆 to the message and keep only one active highscore marker per guild."""
143+
"""Add ✅+🏆 to the message.
144+
145+
Note: Reactions, once added by the bot, should never be removed.
146+
"""
144147
if not message.guild or not isinstance(message.channel, discord.TextChannel):
145148
return
146149

147150
guild_id = message.guild.id
148151
channel = message.channel
149152

150-
previous_marker = await self._get_active_highscore_message_id(guild_id)
151-
if previous_marker and previous_marker != message.id:
152-
await self._remove_bot_reactions(channel, previous_marker)
153-
154-
# Ensure reactions exist (✅ may already be there)
155-
try:
156-
await message.add_reaction("✅")
157-
except Exception:
158-
pass
153+
# Only add the trophy here.
154+
# The ✅ reaction is added for all valid counts in the main handler;
155+
# adding it again here causes extra API calls and rate limits.
159156
try:
160157
await message.add_reaction("🏆")
161158
except Exception:
162159
pass
163160

161+
# Track the latest highscore/tie message ID for bookkeeping.
162+
# (We no longer remove reactions from older messages.)
164163
await self._set_active_highscore_message_id(guild_id, message.id)
165164

166165
# Record history only if it is a NEW record
@@ -178,24 +177,48 @@ async def _mark_highscore_message(
178177
async def _clear_highscore_marker_if_any(self, guild_id: int, channel: discord.TextChannel) -> None:
179178
marker_id = await self._get_active_highscore_message_id(guild_id)
180179
if marker_id:
181-
await self._remove_bot_reactions(channel, marker_id)
182180
await self._set_active_highscore_message_id(guild_id, None)
183181

184182
@app_commands.command(name="setcountingchannel", description="Set the channel for the counting game")
185183
@app_commands.checks.has_permissions(administrator=True)
186184
async def setcountingchannel(self, interaction: discord.Interaction, channel: discord.TextChannel):
187-
async with aiosqlite.connect(DB_PATH, timeout=30.0) as db:
188-
await db.execute("""
189-
INSERT INTO counting_config (guild_id, channel_id)
190-
VALUES (?, ?)
191-
ON CONFLICT(guild_id) DO UPDATE SET channel_id = excluded.channel_id
192-
""", (interaction.guild_id, channel.id))
193-
await db.commit()
194-
185+
# Slash command interactions must be acknowledged quickly.
186+
# DB operations can take >3s (locks, slow disks), so defer immediately.
187+
if interaction.response.is_done():
188+
# Extremely defensive; normally false here.
189+
pass
190+
else:
191+
await interaction.response.defer(ephemeral=True)
192+
193+
if interaction.guild_id is None:
194+
await interaction.followup.send("This command can only be used in a server.", ephemeral=True)
195+
return
196+
197+
retries = 3
198+
while retries > 0:
199+
try:
200+
async with aiosqlite.connect(DB_PATH, timeout=30.0) as db:
201+
await db.execute(
202+
"""
203+
INSERT INTO counting_config (guild_id, channel_id)
204+
VALUES (?, ?)
205+
ON CONFLICT(guild_id) DO UPDATE SET channel_id = excluded.channel_id
206+
""",
207+
(interaction.guild_id, channel.id),
208+
)
209+
await db.commit()
210+
break
211+
except aiosqlite.OperationalError as e:
212+
if "locked" in str(e).lower():
213+
retries -= 1
214+
await asyncio.sleep(0.5)
215+
continue
216+
raise
217+
195218
# Update cache
196219
self.counting_channels[interaction.guild_id] = channel.id
197-
198-
await interaction.response.send_message(f"Counting channel set to {channel.mention}", ephemeral=True)
220+
221+
await interaction.followup.send(f"Counting channel set to {channel.mention}", ephemeral=True)
199222

200223
def safe_eval(self, expr):
201224
operators = {

utils/config.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import os
56
from typing import Any, Optional
67

78
from pydantic import Field, field_validator, model_validator
@@ -31,6 +32,42 @@ class Config(BaseSettings):
3132
extra='ignore' # Ignore extra fields from .env
3233
)
3334

35+
@field_validator('guild_id', mode='before')
36+
@classmethod
37+
def _parse_guild_id(cls, v: Any) -> Optional[int]:
38+
"""Parse legacy single guild id.
39+
40+
Some deployments mistakenly set `GUILD_ID` as a CSV / JSON list.
41+
To stay backwards compatible and avoid startup failures, we accept:
42+
- int
43+
- numeric string
44+
- CSV: "1,2,3" (uses the first value)
45+
- JSON list: "[1,2]" (uses the first value)
46+
"""
47+
if v is None:
48+
return None
49+
if isinstance(v, int):
50+
return v
51+
if isinstance(v, str):
52+
s = v.strip()
53+
if not s:
54+
return None
55+
if s.startswith('[') and s.endswith(']'):
56+
try:
57+
import json
58+
59+
parsed = json.loads(s)
60+
if isinstance(parsed, list) and parsed:
61+
return int(parsed[0])
62+
except Exception:
63+
# Fall back to CSV/int parsing
64+
pass
65+
if ',' in s:
66+
first = s.split(',', 1)[0].strip()
67+
return int(first) if first else None
68+
return int(s)
69+
return int(v)
70+
3471
@field_validator('guild_ids', mode='before')
3572
@classmethod
3673
def _parse_guild_ids(cls, v: Any) -> list[int]:
@@ -42,7 +79,12 @@ def _parse_guild_ids(cls, v: Any) -> list[int]:
4279
- CSV: "1,2,3"
4380
"""
4481
if v is None:
45-
return []
82+
# If GUILD_IDS isn't set, allow legacy GUILD_ID to behave like a list.
83+
legacy = os.getenv('GUILD_ID')
84+
if legacy and str(legacy).strip():
85+
v = legacy
86+
else:
87+
return []
4688
if isinstance(v, list):
4789
return [int(x) for x in v if str(x).strip()]
4890
if isinstance(v, (int, str)):

0 commit comments

Comments
 (0)