echo-tutor / auth_module /telegram_verify.py
Nanny7's picture
feat: replace auth module with updated Nova bridge and wire auth UI
dbfeb5e
Raw History Blame Contribute Delete
18.5 kB
"""
๐Ÿ“ฑ Auth Module โ€” Telegram Verification Bot
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
Inline-button driven verification flow with email + phone support.
Usage:
from auth_module.telegram_verify import create_verify_telegram_app
telegram_app = create_verify_telegram_app()
"""
import os
import re
import random
import logging
from telegram import (
Update,
ReplyKeyboardMarkup,
KeyboardButton,
InlineKeyboardButton,
InlineKeyboardMarkup,
)
from telegram.ext import (
Application,
CommandHandler,
MessageHandler,
CallbackQueryHandler,
filters,
ContextTypes,
)
from .config import auth_settings
logger = logging.getLogger("auth_module.telegram")
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐Ÿ“Š STATE
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
VERIFY_STATE: dict = {}
# chat_id -> {"step": ..., "method": "email"|"phone", "email": ..., "user_id": ..., "otp": ...}
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# โŒจ๏ธ KEYBOARDS
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
def _main_keyboard():
return ReplyKeyboardMarkup(
[[KeyboardButton("๐Ÿ” ุชูุนูŠู„ ุงู„ุญุณุงุจ"), KeyboardButton("โ„น๏ธ ู…ุณุงุนุฏุฉ")]],
resize_keyboard=True,
is_persistent=True,
)
def _verify_method_keyboard():
return InlineKeyboardMarkup(
[
[InlineKeyboardButton("๐Ÿ“ง ุจุงู„ุฅูŠู…ูŠู„", callback_data="verify_email")],
[InlineKeyboardButton("๐Ÿ“ฑ ุจุฑู‚ู… ุงู„ู‡ุงุชู", callback_data="verify_phone")],
[InlineKeyboardButton("โŒ ุฅู„ุบุงุก", callback_data="verify_cancel")],
]
)
def _cancel_keyboard():
return InlineKeyboardMarkup(
[
[InlineKeyboardButton("โŒ ุฅู„ุบุงุก ุงู„ุชูุนูŠู„", callback_data="verify_cancel")],
]
)
def _confirm_otp_keyboard(otp: str):
return InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
f"๐Ÿ“‹ ู†ุณุฎ ุงู„ูƒูˆุฏ: {otp}", callback_data=f"copy_otp_{otp}"
)
],
[
InlineKeyboardButton(
f"โœ… ุชุฃูƒูŠุฏ ุงู„ูƒูˆุฏ ({otp})", callback_data=f"confirm_otp_{otp}"
)
],
[InlineKeyboardButton("๐Ÿ”„ ูƒูˆุฏ ุฌุฏูŠุฏ", callback_data="resend_otp")],
[InlineKeyboardButton("โŒ ุฅู„ุบุงุก", callback_data="verify_cancel")],
]
)
def _phone_share_keyboard():
return ReplyKeyboardMarkup(
[[KeyboardButton("๐Ÿ“ฑ ู…ุดุงุฑูƒุฉ ุฑู‚ู… ุงู„ู‡ุงุชู", request_contact=True)]],
resize_keyboard=True,
one_time_keyboard=True,
)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐Ÿ›ก๏ธ SAFE REPLY
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
async def safe_reply(update: Update, text: str, reply_markup=None, parse_mode="HTML"):
try:
await update.message.reply_text(
text, reply_markup=reply_markup, parse_mode=parse_mode
)
except Exception as e:
logger.warning(f"HTML reply failed: {e}")
try:
await update.message.reply_text(text, reply_markup=reply_markup)
except Exception as e2:
logger.error(f"Reply failed: {e2}")
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐ŸŽฏ COMMANDS
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_name = update.effective_user.first_name or "ู…ุณุชุฎุฏู…"
msg = f"""โœจ <b>ู…ุฑุญุจุงู‹ {user_name}!</b>
๐Ÿ” ู‡ุฐุง ุจูˆุช ุงู„ุชูุนูŠู„ ู„ู€ <b>RobovAI Nova</b>.
ุงุถุบุท ุงู„ุฒุฑ ู„ุชูุนูŠู„ ุญุณุงุจูƒ ๐Ÿ‘‡"""
inline_kb = InlineKeyboardMarkup(
[
[
InlineKeyboardButton("๐Ÿ” ุชูุนูŠู„ ุจุงู„ุฅูŠู…ูŠู„", callback_data="verify_email"),
InlineKeyboardButton("๐Ÿ“ฑ ุชูุนูŠู„ ุจุงู„ู‡ุงุชู", callback_data="verify_phone"),
],
]
)
await safe_reply(update, msg, reply_markup=_main_keyboard())
await update.message.reply_text(
"โšก ุงุฎุชุฑ ุทุฑูŠู‚ุฉ ุงู„ุชูุนูŠู„:", parse_mode="HTML", reply_markup=inline_kb
)
async def verify_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
msg = """๐Ÿ” <b>ุชูุนูŠู„ ุงู„ุญุณุงุจ</b>
ุงุฎุชุฑ ุทุฑูŠู‚ุฉ ุงู„ุชูุนูŠู„:
๐Ÿ“ง <b>ุจุงู„ุฅูŠู…ูŠู„</b> โ€” ุฃุฏุฎู„ ุจุฑูŠุฏูƒ ูˆุงุญุตู„ ุนู„ู‰ ูƒูˆุฏ
๐Ÿ“ฑ <b>ุจุฑู‚ู… ุงู„ู‡ุงุชู</b> โ€” ุดุงุฑูƒ ุฑู‚ู…ูƒ
ุงุถุบุท ุงู„ุฒุฑ ุงู„ู…ู†ุงุณุจ ๐Ÿ‘‡"""
await safe_reply(update, msg, reply_markup=_verify_method_keyboard())
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
msg = """๐Ÿ“– <b>ูƒูŠููŠุฉ ุงู„ุชูุนูŠู„</b>
1๏ธโƒฃ ุณุฌู‘ู„ ุญุณุงุจูƒ ู…ู† ุงู„ู…ูˆู‚ุน
2๏ธโƒฃ ุงุถุบุท <b>๐Ÿ” ุชูุนูŠู„ ุงู„ุญุณุงุจ</b>
3๏ธโƒฃ ุงุฎุชุฑ ุงู„ุทุฑูŠู‚ุฉ (ุฅูŠู…ูŠู„ ุฃูˆ ู‡ุงุชู)
4๏ธโƒฃ ุณุชุญุตู„ ุนู„ู‰ ูƒูˆุฏ โ€” ุงุถุบุท <b>ุชุฃูƒูŠุฏ</b>
5๏ธโƒฃ ุชู…! ุณุฌู‘ู„ ุฏุฎูˆู„ูƒ ู…ู† ุงู„ู…ูˆู‚ุน โœ…"""
await safe_reply(update, msg, reply_markup=_main_keyboard())
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐Ÿ”˜ CALLBACK QUERY HANDLER
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
chat_id = str(query.message.chat_id)
data = query.data
if data == "verify_email":
VERIFY_STATE[chat_id] = {"step": "awaiting_email", "method": "email"}
await query.message.reply_text(
"๐Ÿ“ง <b>ุฃุฏุฎู„ ุจุฑูŠุฏูƒ ุงู„ุฅู„ูƒุชุฑูˆู†ูŠ:</b>\n\n<i>ู…ุซุงู„: user@example.com</i>",
parse_mode="HTML",
reply_markup=_cancel_keyboard(),
)
elif data == "verify_phone":
VERIFY_STATE[chat_id] = {"step": "awaiting_phone", "method": "phone"}
await query.message.reply_text(
"๐Ÿ“ฑ ุงุถุบุท ุงู„ุฒุฑ ู„ู…ุดุงุฑูƒุฉ ุฑู‚ู…ูƒ ๐Ÿ‘‡\n\n<i>ุฃูˆ ุงูƒุชุจ ุจุฑูŠุฏูƒ ุงู„ุฅู„ูƒุชุฑูˆู†ูŠ</i>",
parse_mode="HTML",
reply_markup=_phone_share_keyboard(),
)
elif data == "verify_cancel":
VERIFY_STATE.pop(chat_id, None)
await query.message.reply_text("โŒ ุชู… ุงู„ุฅู„ุบุงุก.", reply_markup=_main_keyboard())
elif data.startswith("confirm_otp_"):
otp_code = data.replace("confirm_otp_", "")
state = VERIFY_STATE.get(chat_id)
if (
state
and state.get("step") == "awaiting_otp"
and state.get("otp") == otp_code
):
await _do_verify(query.message, chat_id, state, otp_code)
else:
await query.message.reply_text(
"โš ๏ธ ุงู†ุชู‡ุช ุงู„ุฌู„ุณุฉ. ุฃุนุฏ ุงู„ู…ุญุงูˆู„ุฉ ุจู€ /verify", reply_markup=_main_keyboard()
)
elif data.startswith("copy_otp_"):
otp_code = data.replace("copy_otp_", "")
await query.message.reply_text(
f"๐Ÿ”‘ <b>ุงู„ูƒูˆุฏ:</b> <code>{otp_code}</code>\n๐Ÿ“‹ ุงุถุบุท ู„ู†ุณุฎู‡",
parse_mode="HTML",
)
elif data == "resend_otp":
state = VERIFY_STATE.get(chat_id)
if state and state.get("user_id"):
await _send_otp(query.message, chat_id, state)
else:
await query.message.reply_text(
"โš ๏ธ ุงุจุฏุฃ ู…ู† ุฌุฏูŠุฏ ุจู€ /verify", reply_markup=_main_keyboard()
)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐Ÿ“ฒ CONTACT (Phone share)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
async def handle_contact(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = str(update.effective_chat.id)
state = VERIFY_STATE.get(chat_id)
if not state or state.get("method") != "phone":
return
contact = update.message.contact
phone = contact.phone_number if contact else None
if not phone:
await safe_reply(update, "โš ๏ธ ู„ู… ุฃุชู„ู‚ ุงู„ุฑู‚ู…. ุญุงูˆู„ ู…ุฑุฉ ุฃุฎุฑู‰.")
return
phone = re.sub(r"[\s\-()]", "", phone)
if not phone.startswith("+"):
phone = "+" + phone
try:
from .database import auth_db
user = await auth_db.get_user_by_telegram_or_phone(chat_id, phone)
if not user:
await safe_reply(
update,
f"โŒ ู„ู… ูŠุชู… ุงู„ุนุซูˆุฑ ุนู„ู‰ ุญุณุงุจ ุจุฑู‚ู… <code>{phone}</code>\n\nุฌุฑู‘ุจ ุจุงู„ุฅูŠู…ูŠู„ ๐Ÿ‘‡",
reply_markup=_verify_method_keyboard(),
)
VERIFY_STATE.pop(chat_id, None)
return
if user.get("is_verified"):
await safe_reply(
update, "โœ… ุญุณุงุจูƒ ู…ููุนู‘ู„ ุจุงู„ูุนู„!", reply_markup=_main_keyboard()
)
VERIFY_STATE.pop(chat_id, None)
return
state["user_id"] = user["id"]
state["email"] = user.get("email", "")
state["phone"] = phone
VERIFY_STATE[chat_id] = state
await _send_otp(update.message, chat_id, state)
except Exception as e:
logger.error(f"Phone verify error: {e}", exc_info=True)
await safe_reply(update, "โŒ ุฎุทุฃ ุชู‚ู†ูŠ. ุญุงูˆู„ ู…ุฑุฉ ุฃุฎุฑู‰.")
VERIFY_STATE.pop(chat_id, None)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐Ÿ’ฌ TEXT MESSAGE HANDLER
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = str(update.effective_chat.id)
state = VERIFY_STATE.get(chat_id)
message = (update.message.text or "").strip()
# Menu buttons
if message == "๐Ÿ” ุชูุนูŠู„ ุงู„ุญุณุงุจ":
await verify_command(update, context)
return
if message == "โ„น๏ธ ู…ุณุงุนุฏุฉ":
await help_command(update, context)
return
if not state:
await safe_reply(
update, "ุงุถุบุท ๐Ÿ” <b>ุชูุนูŠู„ ุงู„ุญุณุงุจ</b> ู„ู„ุจุฏุก", reply_markup=_main_keyboard()
)
return
# Cancel
if message in ("ุงู„ุบุงุก", "ุฅู„ุบุงุก", "/cancel", "cancel"):
VERIFY_STATE.pop(chat_id, None)
await safe_reply(update, "โŒ ุชู… ุงู„ุฅู„ุบุงุก.", reply_markup=_main_keyboard())
return
# Awaiting email
if state["step"] == "awaiting_email":
email = message.lower()
if "@" not in email or "." not in email:
await safe_reply(
update,
"โš ๏ธ ุจุฑูŠุฏ ุบูŠุฑ ุตุญูŠุญ. ุญุงูˆู„ ู…ุฑุฉ ุฃุฎุฑู‰:",
reply_markup=_cancel_keyboard(),
)
return
try:
from .database import auth_db
user = await auth_db.get_user_by_email_unverified(email)
if not user:
await safe_reply(
update,
"โŒ ู„ุง ูŠูˆุฌุฏ ุญุณุงุจ ุจู‡ุฐุง ุงู„ุจุฑูŠุฏ. ุณุฌู‘ู„ ู…ู† ุงู„ู…ูˆู‚ุน ุฃูˆู„ุงู‹.",
reply_markup=_verify_method_keyboard(),
)
VERIFY_STATE.pop(chat_id, None)
return
if user.get("is_verified"):
await safe_reply(
update, "โœ… ุญุณุงุจูƒ ู…ููุนู‘ู„ ุจุงู„ูุนู„!", reply_markup=_main_keyboard()
)
VERIFY_STATE.pop(chat_id, None)
return
state["user_id"] = user["id"]
state["email"] = email
VERIFY_STATE[chat_id] = state
await _send_otp(update.message, chat_id, state)
except Exception as e:
logger.error(f"Email verify error: {e}", exc_info=True)
await safe_reply(update, "โŒ ุฎุทุฃ ุชู‚ู†ูŠ.")
VERIFY_STATE.pop(chat_id, None)
return
# Awaiting phone but got text
if state["step"] == "awaiting_phone":
email = message.lower()
if "@" in email and "." in email:
state["step"] = "awaiting_email"
state["method"] = "email"
VERIFY_STATE[chat_id] = state
await handle_message(update, context)
else:
await safe_reply(
update,
"๐Ÿ“ฑ ุงุถุบุท ุฒุฑ ู…ุดุงุฑูƒุฉ ุงู„ุฑู‚ู… ุฃูˆ ุงูƒุชุจ ุจุฑูŠุฏูƒ.",
reply_markup=_phone_share_keyboard(),
)
return
# Awaiting OTP (manual entry)
if state["step"] == "awaiting_otp":
code = message.strip()
if not code.isdigit() or len(code) != 6:
await safe_reply(
update,
"โš ๏ธ ุงู„ูƒูˆุฏ 6 ุฃุฑู‚ุงู…. ุญุงูˆู„ ู…ุฑุฉ ุฃุฎุฑู‰:",
reply_markup=_cancel_keyboard(),
)
return
await _do_verify(update.message, chat_id, state, code)
return
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐Ÿ”ง HELPERS
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
async def _send_otp(message, chat_id: str, state: dict):
try:
from .database import auth_db
otp = str(random.randint(100000, 999999))
await auth_db.store_otp(state["user_id"], otp, "telegram_verify", minutes=10)
state["otp"] = otp
state["step"] = "awaiting_otp"
VERIFY_STATE[chat_id] = state
msg = f"""โœ… <b>ุชู… ุงู„ุนุซูˆุฑ ุนู„ู‰ ุงู„ุญุณุงุจ!</b>
๐Ÿ“ง {state.get('email', '')}
โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
๐Ÿ”‘ <b>ูƒูˆุฏ ุงู„ุชุญู‚ู‚:</b> <code>{otp}</code>
โฑ๏ธ ุตู„ุงุญูŠุฉ: <b>10 ุฏู‚ุงุฆู‚</b>
ุงุถุบุท <b>โœ… ุชุฃูƒูŠุฏ</b> ู„ู„ุชูุนูŠู„ ุงู„ููˆุฑูŠ ๐Ÿ‘‡"""
await message.reply_text(
msg, parse_mode="HTML", reply_markup=_confirm_otp_keyboard(otp)
)
except Exception as e:
logger.error(f"OTP error: {e}", exc_info=True)
await message.reply_text("โŒ ุฎุทุฃ ุชู‚ู†ูŠ.")
VERIFY_STATE.pop(chat_id, None)
async def _do_verify(message, chat_id: str, state: dict, code: str):
try:
from .database import auth_db
valid = await auth_db.verify_otp(state["user_id"], code, "telegram_verify")
if valid:
await auth_db.set_user_verified(state["user_id"], telegram_chat_id=chat_id)
VERIFY_STATE.pop(chat_id, None)
await message.reply_text(
"๐ŸŽ‰ <b>ุชู… ุชูุนูŠู„ ุญุณุงุจูƒ ุจู†ุฌุงุญ!</b>\n\nโœ… ุณุฌู‘ู„ ุฏุฎูˆู„ูƒ ู…ู† ุงู„ู…ูˆู‚ุน ุงู„ุขู†.",
parse_mode="HTML",
reply_markup=_main_keyboard(),
)
else:
VERIFY_STATE.pop(chat_id, None)
await message.reply_text(
"โŒ ูƒูˆุฏ ุบูŠุฑ ุตุญูŠุญ ุฃูˆ ู…ู†ุชู‡ูŠ. ุงุถุบุท /verify ู„ู„ู…ุญุงูˆู„ุฉ ู…ุฑุฉ ุฃุฎุฑู‰.",
reply_markup=_main_keyboard(),
)
except Exception as e:
logger.error(f"Verify error: {e}", exc_info=True)
await message.reply_text("โŒ ุฎุทุฃ ุชู‚ู†ูŠ.")
VERIFY_STATE.pop(chat_id, None)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐Ÿš€ APP FACTORY
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
def create_verify_telegram_app(token: str = None):
"""Create a standalone Telegram bot app for account verification."""
token = token or auth_settings.TELEGRAM_BOT_TOKEN
if not token:
logger.error("TELEGRAM_BOT_TOKEN not set")
return None
app = Application.builder().token(token).build()
app.add_handler(CommandHandler("start", start_command))
app.add_handler(CommandHandler("verify", verify_command))
app.add_handler(CommandHandler("help", help_command))
app.add_handler(CallbackQueryHandler(handle_callback))
app.add_handler(MessageHandler(filters.CONTACT, handle_contact))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
logger.info("โœ… Verification Telegram app created")
return app