Skip to content

Commit e189e4f

Browse files
committed
refactor: Extract verify/unverify logic and add inline button support for forwarded messages
- Extract core verification logic into reusable verify_user() and unverify_user() functions - Add handle_forwarded_message() to allow admins to forward user messages in DM with inline buttons - Add handle_verify_callback() and handle_unverify_callback() for inline button interactions - Support both new (MessageOrigin) and legacy (forward_from) Telegram APIs for forwarded messages - Register CallbackQueryHandler for verify/unverify button patterns in main.py - Add FORWARD_VERIFY_PROMPT constant for forwarded message prompts - Update handler ordering with descriptive comments (handlers 1-8) - Add comprehensive tests for all new callback and forwarded message handlers - Update README test statistics: 244 tests, 792 statements, 100% coverage
1 parent 78f48f3 commit e189e4f

5 files changed

Lines changed: 676 additions & 66 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,9 @@ uv run pytest -v
135135
### Test Coverage
136136

137137
The project maintains comprehensive test coverage:
138-
- **Coverage**: 100% across all modules (702 statements, 0 missed)
139-
- **Tests**: 224 total
140-
- **Pass Rate**: 100% (224/224 passed)
138+
- **Coverage**: 100% across all modules (792 statements, 0 missed)
139+
- **Tests**: 244 total
140+
- **Pass Rate**: 100% (244/244 passed)
141141
- **All modules**: 100% coverage including JobQueue scheduler integration and captcha verification
142142
- Services: `bot_info.py`, `scheduler.py`, `user_checker.py`, `telegram_utils.py`, `captcha_recovery.py`
143143
- Handlers: `captcha.py`, `dm.py`, `message.py`, `topic_guard.py`, `verify.py`

src/bot/constants.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,8 @@ def format_threshold_display(threshold_minutes: int) -> str:
134134
VERIFICATION_CLEARANCE_MESSAGE = (
135135
"✅ {user_mention} telah diverifikasi oleh admin. Silakan berdiskusi kembali."
136136
)
137+
138+
FORWARD_VERIFY_PROMPT = (
139+
"📋 User: {user_mention} (ID: {user_id})\n\n"
140+
"Pilih aksi untuk user ini:"
141+
)

src/bot/handlers/verify.py

Lines changed: 284 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,108 @@
88

99
import logging
1010

11-
from telegram import Update
11+
from telegram import Bot, InlineKeyboardButton, InlineKeyboardMarkup, Update
1212
from telegram.error import BadRequest
1313
from telegram.ext import ContextTypes
1414

15-
from bot.config import get_settings
16-
from bot.constants import VERIFICATION_CLEARANCE_MESSAGE
17-
from bot.database.service import get_database
15+
from bot.config import Settings, get_settings
16+
from bot.constants import FORWARD_VERIFY_PROMPT, VERIFICATION_CLEARANCE_MESSAGE
17+
from bot.database.service import DatabaseService, get_database
1818
from bot.services.telegram_utils import get_user_mention_by_id, unrestrict_user
1919

2020
logger = logging.getLogger(__name__)
2121

2222

23+
async def verify_user(
24+
bot: Bot, db: DatabaseService, settings: Settings, target_user_id: int, admin_user_id: int
25+
) -> str:
26+
"""
27+
Verify a user by adding them to the photo verification whitelist.
28+
29+
This function handles the core verification logic: adds user to whitelist,
30+
unrestricts them, deletes warnings, and sends clearance notification if needed.
31+
32+
Args:
33+
bot: Telegram bot instance.
34+
db: Database service instance.
35+
settings: Bot settings instance.
36+
target_user_id: ID of the user to verify.
37+
admin_user_id: ID of the admin performing the verification.
38+
39+
Returns:
40+
Success message string.
41+
42+
Raises:
43+
ValueError: If user is already whitelisted.
44+
"""
45+
db.add_photo_verification_whitelist(
46+
user_id=target_user_id,
47+
verified_by_admin_id=admin_user_id,
48+
)
49+
50+
# Unrestrict user if they are restricted
51+
try:
52+
await unrestrict_user(bot, settings.group_id, target_user_id)
53+
logger.info(f"Unrestricted user {target_user_id} during verification")
54+
except BadRequest as e:
55+
# User might not be restricted or not in group - that's okay
56+
logger.debug(f"Could not unrestrict user {target_user_id}: {e}")
57+
58+
# Delete all warning records for this user
59+
deleted_count = db.delete_user_warnings(target_user_id, settings.group_id)
60+
61+
# Send notification to warning topic if user had previous warnings
62+
if deleted_count > 0:
63+
# Get user info for proper mention
64+
user_info = await bot.get_chat(target_user_id)
65+
user_mention = get_user_mention_by_id(target_user_id, user_info.full_name)
66+
67+
# Send clearance message to warning topic
68+
clearance_message = VERIFICATION_CLEARANCE_MESSAGE.format(
69+
user_mention=user_mention
70+
)
71+
await bot.send_message(
72+
chat_id=settings.group_id,
73+
message_thread_id=settings.warning_topic_id,
74+
text=clearance_message,
75+
parse_mode="Markdown"
76+
)
77+
logger.info(f"Sent clearance notification to warning topic for user {target_user_id}")
78+
logger.info(f"Deleted {deleted_count} warning record(s) for user {target_user_id}")
79+
80+
return (
81+
f"✅ User dengan ID {target_user_id} telah diverifikasi:\n"
82+
f"• Ditambahkan ke whitelist foto profil\n"
83+
f"• Pembatasan dicabut (jika ada)\n"
84+
f"• Riwayat warning dihapus\n\n"
85+
f"User ini tidak akan dicek foto profil lagi."
86+
)
87+
88+
89+
async def unverify_user(
90+
db: DatabaseService, target_user_id: int, admin_user_id: int
91+
) -> str:
92+
"""
93+
Unverify a user by removing them from the photo verification whitelist.
94+
95+
Args:
96+
db: Database service instance.
97+
target_user_id: ID of the user to unverify.
98+
admin_user_id: ID of the admin performing the unverification.
99+
100+
Returns:
101+
Success message string.
102+
103+
Raises:
104+
ValueError: If user is not in whitelist.
105+
"""
106+
db.remove_photo_verification_whitelist(user_id=target_user_id)
107+
logger.info(
108+
f"Admin {admin_user_id} removed user {target_user_id} from photo verification whitelist"
109+
)
110+
return f"✅ User dengan ID {target_user_id} telah dihapus dari whitelist verifikasi foto."
111+
112+
23113
async def handle_verify_command(
24114
update: Update, context: ContextTypes.DEFAULT_TYPE
25115
) -> None:
@@ -68,51 +158,9 @@ async def handle_verify_command(
68158
db = get_database()
69159

70160
try:
71-
db.add_photo_verification_whitelist(
72-
user_id=target_user_id,
73-
verified_by_admin_id=admin_user_id,
74-
)
75-
76-
# Get settings for group_id
77161
settings = get_settings()
78-
79-
# Unrestrict user if they are restricted
80-
try:
81-
await unrestrict_user(context.bot, settings.group_id, target_user_id)
82-
logger.info(f"Unrestricted user {target_user_id} during verification")
83-
except BadRequest as e:
84-
# User might not be restricted or not in group - that's okay
85-
logger.debug(f"Could not unrestrict user {target_user_id}: {e}")
86-
87-
# Delete all warning records for this user
88-
deleted_count = db.delete_user_warnings(target_user_id, settings.group_id)
89-
90-
# Send notification to warning topic if user had previous warnings
91-
if deleted_count > 0:
92-
# Get user info for proper mention
93-
user_info = await context.bot.get_chat(target_user_id)
94-
user_mention = get_user_mention_by_id(target_user_id, user_info.full_name)
95-
96-
# Send clearance message to warning topic
97-
clearance_message = VERIFICATION_CLEARANCE_MESSAGE.format(
98-
user_mention=user_mention
99-
)
100-
await context.bot.send_message(
101-
chat_id=settings.group_id,
102-
message_thread_id=settings.warning_topic_id,
103-
text=clearance_message,
104-
parse_mode="Markdown"
105-
)
106-
logger.info(f"Sent clearance notification to warning topic for user {target_user_id}")
107-
logger.info(f"Deleted {deleted_count} warning record(s) for user {target_user_id}")
108-
109-
await update.message.reply_text(
110-
f"✅ User dengan ID {target_user_id} telah diverifikasi:\n"
111-
f"• Ditambahkan ke whitelist foto profil\n"
112-
f"• Pembatasan dicabut (jika ada)\n"
113-
f"• Riwayat warning dihapus\n\n"
114-
f"User ini tidak akan dicek foto profil lagi."
115-
)
162+
message = await verify_user(context.bot, db, settings, target_user_id, admin_user_id)
163+
await update.message.reply_text(message)
116164
logger.info(
117165
f"Admin {admin_user_id} ({update.message.from_user.full_name}) "
118166
f"whitelisted user {target_user_id} for photo verification"
@@ -172,16 +220,198 @@ async def handle_unverify_command(
172220
db = get_database()
173221

174222
try:
175-
db.remove_photo_verification_whitelist(user_id=target_user_id)
223+
message = await unverify_user(db, target_user_id, admin_user_id)
224+
await update.message.reply_text(message)
225+
except ValueError as e:
226+
await update.message.reply_text(f"ℹ️ User dengan ID {target_user_id} tidak ada di whitelist.")
227+
logger.info(
228+
f"Admin {admin_user_id} tried to remove {target_user_id} but not in whitelist: {e}"
229+
)
230+
231+
232+
async def handle_forwarded_message(
233+
update: Update, context: ContextTypes.DEFAULT_TYPE
234+
) -> None:
235+
"""
236+
Handle forwarded messages from admins to create verify/unverify buttons.
237+
238+
When an admin forwards a user's message to the bot in DM, this handler
239+
creates inline buttons to verify or unverify that user.
240+
241+
Args:
242+
update: Telegram update containing the forwarded message.
243+
context: Bot context with helper methods.
244+
"""
245+
if not update.message or not update.message.from_user:
246+
return
247+
248+
admin_user_id = update.message.from_user.id
249+
admin_ids = context.bot_data.get("admin_ids", [])
250+
251+
if admin_user_id not in admin_ids:
252+
await update.message.reply_text("❌ Kamu tidak memiliki izin untuk menggunakan fitur ini.")
253+
logger.warning(
254+
f"Non-admin user {admin_user_id} ({update.message.from_user.full_name}) "
255+
f"attempted to forward message for verification"
256+
)
257+
return
258+
259+
# Extract user info from forwarded message
260+
forwarded_user = None
261+
if update.message.forward_origin:
262+
# New API (MessageOrigin)
263+
if hasattr(update.message.forward_origin, 'sender_user'):
264+
forwarded_user = update.message.forward_origin.sender_user
265+
elif update.message.forward_from:
266+
# Legacy API
267+
forwarded_user = update.message.forward_from
268+
269+
if not forwarded_user:
176270
await update.message.reply_text(
177-
f"✅ User dengan ID {target_user_id} telah dihapus dari whitelist verifikasi foto."
271+
"❌ Tidak dapat mengekstrak informasi user dari pesan yang diteruskan.\n"
272+
"Pastikan user tidak menyembunyikan status forward di pengaturan privasi."
178273
)
274+
return
275+
276+
user_id = forwarded_user.id
277+
user_name = forwarded_user.full_name if hasattr(forwarded_user, 'full_name') else forwarded_user.first_name
278+
user_mention = get_user_mention_by_id(user_id, user_name)
279+
280+
# Create inline keyboard with verify/unverify buttons
281+
keyboard = [
282+
[
283+
InlineKeyboardButton("✅ Verify User", callback_data=f"verify:{user_id}"),
284+
InlineKeyboardButton("❌ Unverify User", callback_data=f"unverify:{user_id}"),
285+
]
286+
]
287+
reply_markup = InlineKeyboardMarkup(keyboard)
288+
289+
# Send prompt message with buttons
290+
prompt_message = FORWARD_VERIFY_PROMPT.format(
291+
user_mention=user_mention,
292+
user_id=user_id
293+
)
294+
await update.message.reply_text(
295+
prompt_message,
296+
reply_markup=reply_markup,
297+
parse_mode="Markdown"
298+
)
299+
logger.info(
300+
f"Admin {admin_user_id} ({update.message.from_user.full_name}) "
301+
f"forwarded message from user {user_id} for verification options"
302+
)
303+
304+
305+
async def handle_verify_callback(
306+
update: Update, context: ContextTypes.DEFAULT_TYPE
307+
) -> None:
308+
"""
309+
Handle callback query for verify button.
310+
311+
Processes the inline button click to verify a user and updates the message
312+
with the result.
313+
314+
Args:
315+
update: Telegram update containing the callback query.
316+
context: Bot context with helper methods.
317+
"""
318+
query = update.callback_query
319+
if not query or not query.from_user or not query.data:
320+
return
321+
322+
await query.answer()
323+
324+
admin_user_id = query.from_user.id
325+
admin_ids = context.bot_data.get("admin_ids", [])
326+
327+
if admin_user_id not in admin_ids:
328+
await query.edit_message_text("❌ Kamu tidak memiliki izin untuk menggunakan perintah ini.")
329+
logger.warning(
330+
f"Non-admin user {admin_user_id} ({query.from_user.full_name}) "
331+
f"attempted to use verify callback"
332+
)
333+
return
334+
335+
# Extract user_id from callback_data
336+
try:
337+
target_user_id = int(query.data.split(":")[1])
338+
except (IndexError, ValueError):
339+
await query.edit_message_text("❌ Data callback tidak valid.")
340+
logger.error(f"Invalid callback_data format: {query.data}")
341+
return
342+
343+
db = get_database()
344+
345+
try:
346+
settings = get_settings()
347+
message = await verify_user(context.bot, db, settings, target_user_id, admin_user_id)
348+
await query.edit_message_text(message)
179349
logger.info(
180-
f"Admin {admin_user_id} ({update.message.from_user.full_name}) "
181-
f"removed user {target_user_id} from photo verification whitelist"
350+
f"Admin {admin_user_id} ({query.from_user.full_name}) "
351+
f"verified user {target_user_id} via callback"
182352
)
183353
except ValueError as e:
184-
await update.message.reply_text(f"ℹ️ User dengan ID {target_user_id} tidak ada di whitelist.")
354+
await query.edit_message_text(f"ℹ️ User dengan ID {target_user_id} sudah ada di whitelist.")
185355
logger.info(
186-
f"Admin {admin_user_id} tried to remove {target_user_id} but not in whitelist: {e}"
356+
f"Admin {admin_user_id} tried to verify {target_user_id} via callback but already exists: {e}"
357+
)
358+
except Exception as e:
359+
await query.edit_message_text(f"❌ Terjadi kesalahan: {str(e)}")
360+
logger.error(f"Error during verify callback: {e}", exc_info=True)
361+
362+
363+
async def handle_unverify_callback(
364+
update: Update, context: ContextTypes.DEFAULT_TYPE
365+
) -> None:
366+
"""
367+
Handle callback query for unverify button.
368+
369+
Processes the inline button click to unverify a user and updates the message
370+
with the result.
371+
372+
Args:
373+
update: Telegram update containing the callback query.
374+
context: Bot context with helper methods.
375+
"""
376+
query = update.callback_query
377+
if not query or not query.from_user or not query.data:
378+
return
379+
380+
await query.answer()
381+
382+
admin_user_id = query.from_user.id
383+
admin_ids = context.bot_data.get("admin_ids", [])
384+
385+
if admin_user_id not in admin_ids:
386+
await query.edit_message_text("❌ Kamu tidak memiliki izin untuk menggunakan perintah ini.")
387+
logger.warning(
388+
f"Non-admin user {admin_user_id} ({query.from_user.full_name}) "
389+
f"attempted to use unverify callback"
390+
)
391+
return
392+
393+
# Extract user_id from callback_data
394+
try:
395+
target_user_id = int(query.data.split(":")[1])
396+
except (IndexError, ValueError):
397+
await query.edit_message_text("❌ Data callback tidak valid.")
398+
logger.error(f"Invalid callback_data format: {query.data}")
399+
return
400+
401+
db = get_database()
402+
403+
try:
404+
message = await unverify_user(db, target_user_id, admin_user_id)
405+
await query.edit_message_text(message)
406+
logger.info(
407+
f"Admin {admin_user_id} ({query.from_user.full_name}) "
408+
f"unverified user {target_user_id} via callback"
409+
)
410+
except ValueError as e:
411+
await query.edit_message_text(f"ℹ️ User dengan ID {target_user_id} tidak ada di whitelist.")
412+
logger.info(
413+
f"Admin {admin_user_id} tried to unverify {target_user_id} via callback but not in whitelist: {e}"
187414
)
415+
except Exception as e:
416+
await query.edit_message_text(f"❌ Terjadi kesalahan: {str(e)}")
417+
logger.error(f"Error during unverify callback: {e}", exc_info=True)

0 commit comments

Comments
 (0)