Skip to content

Commit 89d6967

Browse files
author
0xMett
committed
feat(db): persist welcome message map for ban-by-reply across restarts
Store welcome-message-to-user mappings in a new welcome_messages table so that /ban reply works even after a bot restart. The in-memory cache is still used as a fast path; the database is a fallback. Cleanup happens both in memory and in the DB when welcome messages are auto-deleted.
1 parent 2ff2273 commit 89d6967

7 files changed

Lines changed: 145 additions & 0 deletions

File tree

schema.sql

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,11 @@ ALTER TABLE group_settings
9292
-- Add title column to bot_chats for group name lookup (idempotent).
9393
ALTER TABLE bot_chats
9494
ADD COLUMN IF NOT EXISTS title TEXT;
95+
96+
CREATE TABLE IF NOT EXISTS welcome_messages (
97+
chat_id BIGINT NOT NULL,
98+
message_id BIGINT NOT NULL,
99+
user_id BIGINT NOT NULL,
100+
created_at TIMESTAMPTZ DEFAULT NOW(),
101+
PRIMARY KEY (chat_id, message_id)
102+
);

src/python_italy_bot/db/base.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,25 @@ def set_welcome_delay(self, chat_id: int, minutes: int | None) -> None:
195195
"""Set welcome message auto-delete delay. None to reset to default."""
196196
...
197197

198+
# -- Welcome message tracking (ban-by-reply) --
199+
200+
@abstractmethod
201+
def store_welcome_message(
202+
self, chat_id: int, message_id: int, user_id: int
203+
) -> None:
204+
"""Store mapping from a welcome message to the user who triggered it."""
205+
...
206+
207+
@abstractmethod
208+
def get_welcome_message_user(self, chat_id: int, message_id: int) -> int | None:
209+
"""Get the user_id associated with a welcome message, or None."""
210+
...
211+
212+
@abstractmethod
213+
def delete_welcome_message(self, chat_id: int, message_id: int) -> None:
214+
"""Remove a welcome message mapping."""
215+
...
216+
198217

199218
class AsyncRepository(ABC):
200219
"""Abstract interface for data persistence (async)."""
@@ -386,6 +405,27 @@ async def set_welcome_delay(self, chat_id: int, minutes: int | None) -> None:
386405
"""Set welcome message auto-delete delay. None to reset to default."""
387406
...
388407

408+
# -- Welcome message tracking (ban-by-reply) --
409+
410+
@abstractmethod
411+
async def store_welcome_message(
412+
self, chat_id: int, message_id: int, user_id: int
413+
) -> None:
414+
"""Store mapping from a welcome message to the user who triggered it."""
415+
...
416+
417+
@abstractmethod
418+
async def get_welcome_message_user(
419+
self, chat_id: int, message_id: int
420+
) -> int | None:
421+
"""Get the user_id associated with a welcome message, or None."""
422+
...
423+
424+
@abstractmethod
425+
async def delete_welcome_message(self, chat_id: int, message_id: int) -> None:
426+
"""Remove a welcome message mapping."""
427+
...
428+
389429
async def close(self) -> None:
390430
"""Close any resources (override if needed)."""
391431
pass

src/python_italy_bot/db/in_memory.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def __init__(self) -> None:
2323
self._username_to_user_id: dict[str, int] = {}
2424
self._welcomed: set[tuple[int, int]] = set()
2525
self._welcome_delays: dict[int, int] = {}
26+
self._welcome_message_map: dict[tuple[int, int], int] = {}
2627

2728
async def add_pending_verification(self, user_id: int, chat_id: int) -> None:
2829
self._pending.add((user_id, chat_id))
@@ -225,3 +226,18 @@ async def set_welcome_delay(self, chat_id: int, minutes: int | None) -> None:
225226
self._welcome_delays.pop(chat_id, None)
226227
else:
227228
self._welcome_delays[chat_id] = minutes
229+
230+
# -- Welcome message tracking --
231+
232+
async def store_welcome_message(
233+
self, chat_id: int, message_id: int, user_id: int
234+
) -> None:
235+
self._welcome_message_map[(chat_id, message_id)] = user_id
236+
237+
async def get_welcome_message_user(
238+
self, chat_id: int, message_id: int
239+
) -> int | None:
240+
return self._welcome_message_map.get((chat_id, message_id))
241+
242+
async def delete_welcome_message(self, chat_id: int, message_id: int) -> None:
243+
self._welcome_message_map.pop((chat_id, message_id), None)

src/python_italy_bot/db/postgres.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,5 +398,41 @@ async def set_welcome_delay(self, chat_id: int, minutes: int | None) -> None:
398398
(chat_id, minutes),
399399
)
400400

401+
# -- Welcome message tracking (ban-by-reply) --
402+
403+
async def store_welcome_message(
404+
self, chat_id: int, message_id: int, user_id: int
405+
) -> None:
406+
async with self._pool.connection() as conn:
407+
await conn.execute(
408+
"""
409+
INSERT INTO welcome_messages (chat_id, message_id, user_id)
410+
VALUES (%s, %s, %s)
411+
ON CONFLICT (chat_id, message_id)
412+
DO UPDATE SET user_id = EXCLUDED.user_id
413+
""",
414+
(chat_id, message_id, user_id),
415+
)
416+
417+
async def get_welcome_message_user(
418+
self, chat_id: int, message_id: int
419+
) -> int | None:
420+
async with self._pool.connection() as conn:
421+
async with conn.cursor() as cur:
422+
await cur.execute(
423+
"SELECT user_id FROM welcome_messages "
424+
"WHERE chat_id = %s AND message_id = %s",
425+
(chat_id, message_id),
426+
)
427+
row = await cur.fetchone()
428+
return row[0] if row else None
429+
430+
async def delete_welcome_message(self, chat_id: int, message_id: int) -> None:
431+
async with self._pool.connection() as conn:
432+
await conn.execute(
433+
"DELETE FROM welcome_messages WHERE chat_id = %s AND message_id = %s",
434+
(chat_id, message_id),
435+
)
436+
401437
async def close(self) -> None:
402438
await self._pool.close()

src/python_italy_bot/handlers/moderation.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from .. import strings
1111
from ..db.base import AsyncRepository
12+
from ..services.captcha import CaptchaService
1213
from ..services.moderation import ModerationService
1314

1415
logger = logging.getLogger(__name__)
@@ -98,6 +99,12 @@ async def _handle_ban(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Non
9899
"welcome_message_map", {}
99100
)
100101
user_id = welcome_map.get((chat.id, reply.message_id))
102+
# Fall back to database if not in memory (e.g. after bot restart)
103+
if user_id is None:
104+
captcha_service: CaptchaService = context.bot_data["captcha_service"]
105+
user_id = await captcha_service.get_welcome_message_user(
106+
chat.id, reply.message_id
107+
)
101108
reason = " ".join(args) if args else None
102109
elif args:
103110
target = args[0]

src/python_italy_bot/handlers/welcome.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ async def _delete_welcome_message(context: ContextTypes.DEFAULT_TYPE) -> None:
5252
"Could not delete welcome message %s in chat %s: %s", message_id, chat_id, e
5353
)
5454

55+
# Clean up in-memory mapping
56+
welcome_map: dict[tuple[int, int], int] = context.bot_data.get(
57+
"welcome_message_map", {}
58+
)
59+
welcome_map.pop((chat_id, message_id), None)
60+
61+
# Clean up database mapping
62+
captcha_service: CaptchaService | None = context.bot_data.get("captcha_service")
63+
if captcha_service is not None:
64+
try:
65+
await captcha_service.delete_welcome_message(chat_id, message_id)
66+
except Exception as e:
67+
logger.debug("Could not delete welcome message mapping from DB: %s", e)
68+
5569

5670
async def _handle_new_member(
5771
update: Update,
@@ -149,6 +163,12 @@ async def _handle_new_member(
149163
welcome_map = context.bot_data.setdefault("welcome_message_map", {})
150164
welcome_map[(chat.id, sent.message_id)] = user.id
151165

166+
# Persist to database so the mapping survives bot restarts
167+
try:
168+
await captcha_service.store_welcome_message(chat.id, sent.message_id, user.id)
169+
except Exception as e:
170+
logger.warning("Could not persist welcome message mapping: %s", e)
171+
152172
# Schedule auto-deletion of the welcome message
153173
delay_minutes = await captcha_service.get_welcome_delay(chat.id)
154174
if delay_minutes is None:

src/python_italy_bot/services/captcha.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,21 @@ async def get_welcome_delay(self, chat_id: int) -> int | None:
186186
async def set_welcome_delay(self, chat_id: int, minutes: int | None) -> None:
187187
"""Set welcome message auto-delete delay. None to reset to default."""
188188
await self._repo.set_welcome_delay(chat_id, minutes)
189+
190+
# -- Welcome message tracking (ban-by-reply) --
191+
192+
async def store_welcome_message(
193+
self, chat_id: int, message_id: int, user_id: int
194+
) -> None:
195+
"""Persist mapping from a welcome message to the user who triggered it."""
196+
await self._repo.store_welcome_message(chat_id, message_id, user_id)
197+
198+
async def get_welcome_message_user(
199+
self, chat_id: int, message_id: int
200+
) -> int | None:
201+
"""Get the user_id associated with a welcome message, or None."""
202+
return await self._repo.get_welcome_message_user(chat_id, message_id)
203+
204+
async def delete_welcome_message(self, chat_id: int, message_id: int) -> None:
205+
"""Remove a welcome message mapping."""
206+
await self._repo.delete_welcome_message(chat_id, message_id)

0 commit comments

Comments
 (0)