Skip to content

Commit cef5858

Browse files
author
0xMett
committed
feat(db): implement user tracking, welcome-once, and welcome delay in repositories
InMemoryRepository: in-memory dicts with secondary username index. PostgresRepository: SQL against known_users, welcomed_users tables and welcome_delay_minutes column on group_settings.
1 parent 3650c2a commit cef5858

2 files changed

Lines changed: 174 additions & 1 deletion

File tree

src/python_italy_bot/db/in_memory.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from datetime import datetime, timezone
44

55
from .base import AsyncRepository
6-
from .models import Ban, Mute, Report
6+
from .models import Ban, KnownUser, Mute, Report
77

88

99
class InMemoryRepository(AsyncRepository):
@@ -19,6 +19,10 @@ def __init__(self) -> None:
1919
self._globally_verified: set[int] = set()
2020
self._bot_chats: set[int] = set()
2121
self._global_bans: dict[int, tuple[int, str | None]] = {}
22+
self._known_users: dict[int, KnownUser] = {}
23+
self._username_to_user_id: dict[str, int] = {}
24+
self._welcomed: set[tuple[int, int]] = set()
25+
self._welcome_delays: dict[int, int] = {}
2226

2327
async def add_pending_verification(self, user_id: int, chat_id: int) -> None:
2428
self._pending.add((user_id, chat_id))
@@ -159,3 +163,54 @@ async def remove_global_ban(self, user_id: int) -> bool:
159163

160164
async def is_globally_banned(self, user_id: int) -> bool:
161165
return user_id in self._global_bans
166+
167+
# -- Known users --
168+
169+
async def upsert_known_user(
170+
self,
171+
user_id: int,
172+
username: str | None,
173+
first_name: str | None,
174+
last_name: str | None,
175+
) -> None:
176+
old = self._known_users.get(user_id)
177+
if old and old.username:
178+
self._username_to_user_id.pop(old.username.lower(), None)
179+
user = KnownUser(
180+
user_id=user_id,
181+
username=username,
182+
first_name=first_name,
183+
last_name=last_name,
184+
updated_at=datetime.now(timezone.utc),
185+
)
186+
self._known_users[user_id] = user
187+
if username:
188+
self._username_to_user_id[username.lower()] = user_id
189+
190+
async def get_known_user(self, user_id: int) -> KnownUser | None:
191+
return self._known_users.get(user_id)
192+
193+
async def get_known_user_by_username(self, username: str) -> KnownUser | None:
194+
uid = self._username_to_user_id.get(username.lower())
195+
if uid is not None:
196+
return self._known_users.get(uid)
197+
return None
198+
199+
# -- Welcomed users --
200+
201+
async def has_been_welcomed(self, user_id: int, chat_id: int) -> bool:
202+
return (user_id, chat_id) in self._welcomed
203+
204+
async def mark_welcomed(self, user_id: int, chat_id: int) -> None:
205+
self._welcomed.add((user_id, chat_id))
206+
207+
# -- Welcome delay --
208+
209+
async def get_welcome_delay(self, chat_id: int) -> int | None:
210+
return self._welcome_delays.get(chat_id)
211+
212+
async def set_welcome_delay(self, chat_id: int, minutes: int | None) -> None:
213+
if minutes is None:
214+
self._welcome_delays.pop(chat_id, None)
215+
else:
216+
self._welcome_delays[chat_id] = minutes

src/python_italy_bot/db/postgres.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from psycopg_pool import AsyncConnectionPool
44

55
from .base import AsyncRepository
6+
from .models import KnownUser
67

78

89
class PostgresRepository(AsyncRepository):
@@ -261,5 +262,122 @@ async def is_globally_banned(self, user_id: int) -> bool:
261262
)
262263
return await cur.fetchone() is not None
263264

265+
# -- Known users --
266+
267+
async def upsert_known_user(
268+
self,
269+
user_id: int,
270+
username: str | None,
271+
first_name: str | None,
272+
last_name: str | None,
273+
) -> None:
274+
async with self._pool.connection() as conn:
275+
await conn.execute(
276+
"""
277+
INSERT INTO known_users (user_id, username, first_name, last_name, updated_at)
278+
VALUES (%s, %s, %s, %s, NOW())
279+
ON CONFLICT (user_id)
280+
DO UPDATE SET username = EXCLUDED.username,
281+
first_name = EXCLUDED.first_name,
282+
last_name = EXCLUDED.last_name,
283+
updated_at = NOW()
284+
""",
285+
(user_id, username, first_name, last_name),
286+
)
287+
288+
async def get_known_user(self, user_id: int) -> KnownUser | None:
289+
async with self._pool.connection() as conn:
290+
async with conn.cursor() as cur:
291+
await cur.execute(
292+
"SELECT user_id, username, first_name, last_name, updated_at "
293+
"FROM known_users WHERE user_id = %s",
294+
(user_id,),
295+
)
296+
row = await cur.fetchone()
297+
if row is None:
298+
return None
299+
return KnownUser(
300+
user_id=row[0],
301+
username=row[1],
302+
first_name=row[2],
303+
last_name=row[3],
304+
updated_at=row[4],
305+
)
306+
307+
async def get_known_user_by_username(self, username: str) -> KnownUser | None:
308+
async with self._pool.connection() as conn:
309+
async with conn.cursor() as cur:
310+
await cur.execute(
311+
"SELECT user_id, username, first_name, last_name, updated_at "
312+
"FROM known_users WHERE LOWER(username) = LOWER(%s)",
313+
(username,),
314+
)
315+
row = await cur.fetchone()
316+
if row is None:
317+
return None
318+
return KnownUser(
319+
user_id=row[0],
320+
username=row[1],
321+
first_name=row[2],
322+
last_name=row[3],
323+
updated_at=row[4],
324+
)
325+
326+
# -- Welcomed users --
327+
328+
async def has_been_welcomed(self, user_id: int, chat_id: int) -> bool:
329+
async with self._pool.connection() as conn:
330+
async with conn.cursor() as cur:
331+
await cur.execute(
332+
"SELECT 1 FROM welcomed_users WHERE user_id = %s AND chat_id = %s",
333+
(user_id, chat_id),
334+
)
335+
return await cur.fetchone() is not None
336+
337+
async def mark_welcomed(self, user_id: int, chat_id: int) -> None:
338+
async with self._pool.connection() as conn:
339+
await conn.execute(
340+
"""
341+
INSERT INTO welcomed_users (user_id, chat_id)
342+
VALUES (%s, %s)
343+
ON CONFLICT (user_id, chat_id) DO NOTHING
344+
""",
345+
(user_id, chat_id),
346+
)
347+
348+
# -- Welcome delay --
349+
350+
async def get_welcome_delay(self, chat_id: int) -> int | None:
351+
async with self._pool.connection() as conn:
352+
async with conn.cursor() as cur:
353+
await cur.execute(
354+
"SELECT welcome_delay_minutes FROM group_settings WHERE chat_id = %s",
355+
(chat_id,),
356+
)
357+
row = await cur.fetchone()
358+
return row[0] if row else None
359+
360+
async def set_welcome_delay(self, chat_id: int, minutes: int | None) -> None:
361+
async with self._pool.connection() as conn:
362+
if minutes is None:
363+
await conn.execute(
364+
"""
365+
UPDATE group_settings SET welcome_delay_minutes = NULL, updated_at = NOW()
366+
WHERE chat_id = %s
367+
""",
368+
(chat_id,),
369+
)
370+
else:
371+
await conn.execute(
372+
"""
373+
INSERT INTO group_settings (chat_id, welcome_delay_minutes, updated_at)
374+
VALUES (%s, %s, NOW())
375+
ON CONFLICT (chat_id)
376+
DO UPDATE SET welcome_delay_minutes = EXCLUDED.welcome_delay_minutes,
377+
updated_at = NOW()
378+
""",
379+
(chat_id, minutes),
380+
)
381+
264382
async def close(self) -> None:
265383
await self._pool.close()

0 commit comments

Comments
 (0)