Skip to content

Commit b71731b

Browse files
author
0xMett
committed
feat(welcome): add user tracking, welcome-once-per-group, and auto-delete
Track users on every interaction via known_users table. Skip welcome message if user was already welcomed in the group. Schedule auto-delete of welcome messages via job_queue with configurable per-group delay. Store welcome message-to-user mapping for ban-by-reply support.
1 parent fcb394b commit b71731b

1 file changed

Lines changed: 68 additions & 2 deletions

File tree

src/python_italy_bot/handlers/welcome.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,24 @@
1313
)
1414

1515
from .. import strings
16+
from ..db.base import AsyncRepository
1617
from ..services.captcha import CaptchaService
1718
from ..services.moderation import ModerationService
1819

1920
logger = logging.getLogger(__name__)
2021

22+
DEFAULT_WELCOME_DELAY_MINUTES = 5
23+
24+
25+
async def _track_user(repo: AsyncRepository, user: object) -> None:
26+
"""Track a Telegram user in the known_users table."""
27+
await repo.upsert_known_user(
28+
user_id=user.id, # type: ignore[attr-defined]
29+
username=getattr(user, "username", None),
30+
first_name=getattr(user, "first_name", None),
31+
last_name=getattr(user, "last_name", None),
32+
)
33+
2134

2235
def create_welcome_handlers(captcha_service: CaptchaService) -> list:
2336
"""Create welcome and captcha handlers."""
@@ -34,13 +47,29 @@ def create_welcome_handlers(captcha_service: CaptchaService) -> list:
3447
]
3548

3649

50+
async def _delete_welcome_message(context: ContextTypes.DEFAULT_TYPE) -> None:
51+
"""Job callback: delete a welcome message after the configured delay."""
52+
job = context.job
53+
if job is None or job.data is None:
54+
return
55+
data: tuple[int, int] = job.data # type: ignore[assignment]
56+
chat_id, message_id = data
57+
try:
58+
await context.bot.delete_message(chat_id=chat_id, message_id=message_id)
59+
except Exception as e:
60+
logger.debug(
61+
"Could not delete welcome message %s in chat %s: %s", message_id, chat_id, e
62+
)
63+
64+
3765
async def _handle_new_member(
3866
update: Update,
3967
context: ContextTypes.DEFAULT_TYPE,
4068
) -> None:
4169
"""Handle new chat members: restrict and send welcome with captcha instructions."""
4270
captcha_service: CaptchaService = context.bot_data["captcha_service"]
4371
moderation_service: ModerationService = context.bot_data["moderation_service"]
72+
repository: AsyncRepository = context.bot_data["repository"]
4473
result = update.chat_member
4574
if result is None:
4675
return
@@ -64,6 +93,9 @@ async def _handle_new_member(
6493

6594
await moderation_service.register_chat(chat.id)
6695

96+
# Track the user
97+
await _track_user(repository, user)
98+
6799
if user.is_bot:
68100
return
69101

@@ -90,6 +122,10 @@ async def _handle_new_member(
90122

91123
await captcha_service.add_pending(user.id, chat.id)
92124

125+
# Skip welcome if user was already welcomed in this chat
126+
if await captcha_service.has_been_welcomed(user.id, chat.id):
127+
return
128+
93129
bot_me = await context.bot.get_me()
94130
bot_username = bot_me.username or "bot"
95131

@@ -99,17 +135,39 @@ async def _handle_new_member(
99135
else:
100136
template = captcha_service.get_default_welcome_template(bot_username)
101137

102-
formatted = captcha_service.format_welcome_message(template, user, chat, bot_username)
138+
formatted = captcha_service.format_welcome_message(
139+
template, user, chat, bot_username
140+
)
103141
text, keyboard = captcha_service.parse_button_urls(formatted)
104142

105143
try:
106-
await context.bot.send_message(
144+
sent = await context.bot.send_message(
107145
chat_id=chat.id,
108146
text=text,
109147
reply_markup=keyboard,
110148
)
111149
except Exception as e:
112150
logger.warning("Could not send welcome to chat %s: %s", chat.id, e)
151+
return
152+
153+
# Mark user as welcomed in this chat
154+
await captcha_service.mark_welcomed(user.id, chat.id)
155+
156+
# Store welcome message -> user mapping for ban-by-reply
157+
welcome_map = context.bot_data.setdefault("welcome_message_map", {})
158+
welcome_map[(chat.id, sent.message_id)] = user.id
159+
160+
# Schedule auto-deletion of the welcome message
161+
delay_minutes = await captcha_service.get_welcome_delay(chat.id)
162+
if delay_minutes is None:
163+
delay_minutes = DEFAULT_WELCOME_DELAY_MINUTES
164+
if delay_minutes > 0 and context.job_queue is not None:
165+
context.job_queue.run_once(
166+
_delete_welcome_message,
167+
when=delay_minutes * 60,
168+
data=(chat.id, sent.message_id),
169+
name=f"del_welcome_{chat.id}_{sent.message_id}",
170+
)
113171

114172

115173
async def _handle_start(
@@ -118,6 +176,7 @@ async def _handle_start(
118176
) -> None:
119177
"""Handle /start command, including deep link for verification."""
120178
captcha_service: CaptchaService = context.bot_data["captcha_service"]
179+
repository: AsyncRepository = context.bot_data["repository"]
121180
message = update.message
122181
if message is None:
123182
return
@@ -130,6 +189,9 @@ async def _handle_start(
130189
if user is None:
131190
return
132191

192+
# Track the user
193+
await _track_user(repository, user)
194+
133195
args = context.args
134196
if args and args[0] == "verify":
135197
rules_url = captcha_service.get_rules_url()
@@ -192,6 +254,7 @@ async def _handle_private_message(
192254
) -> None:
193255
"""Handle private messages: check for secret command and verify user globally."""
194256
captcha_service: CaptchaService = context.bot_data["captcha_service"]
257+
repository: AsyncRepository = context.bot_data["repository"]
195258
message = update.message
196259
if message is None or message.text is None:
197260
return
@@ -200,6 +263,9 @@ async def _handle_private_message(
200263
if user is None:
201264
return
202265

266+
# Track the user
267+
await _track_user(repository, user)
268+
203269
if not captcha_service.is_secret_command(message.text):
204270
await message.reply_text(strings.VERIFY_UNKNOWN_COMMAND)
205271
return

0 commit comments

Comments
 (0)