"""
ð± 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"""âš Ù
Ø±ØØšØ§Ù {user_name}!
ð ÙØ°Ø§ ØšÙØª Ø§ÙØªÙعÙÙ ÙÙ RobovAI Nova.
اضغط Ø§ÙØ²Ø± ÙØªÙعÙÙ ØØ³Ø§ØšÙ ð"""
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 = """ð ØªÙØ¹ÙÙ Ø§ÙØØ³Ø§Øš
اختر طرÙÙØ© Ø§ÙØªÙعÙÙ:
ð§ ØšØ§ÙØ¥ÙÙ
ÙÙ â Ø£Ø¯Ø®Ù ØšØ±ÙØ¯Ù ÙØ§ØØµÙ عÙÙ ÙÙØ¯
ð± ØšØ±ÙÙ
اÙÙØ§ØªÙ â ØŽØ§Ø±Ù رÙÙ
Ù
اضغط Ø§ÙØ²Ø± اÙÙ
ÙØ§Ø³Øš ð"""
await safe_reply(update, msg, reply_markup=_verify_method_keyboard())
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
msg = """ð ÙÙÙÙØ© Ø§ÙØªÙعÙÙ
1ïžâ£ سجÙÙ ØØ³Ø§ØšÙ Ù
٠اÙÙ
ÙÙØ¹
2ïžâ£ اضغط ð ØªÙØ¹ÙÙ Ø§ÙØØ³Ø§Øš
3ïžâ£ اختر Ø§ÙØ·Ø±ÙÙØ© (Ø¥ÙÙ
ÙÙ Ø£Ù ÙØ§ØªÙ)
4ïžâ£ Ø³ØªØØµÙ عÙÙ ÙÙØ¯ â Ø§Ø¶ØºØ· تأÙÙØ¯
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(
"ð§ Ø£Ø¯Ø®Ù ØšØ±ÙØ¯Ù Ø§ÙØ¥ÙÙØªØ±ÙÙÙ:\n\nÙ
ثاÙ: user@example.com",
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Ø£Ù Ø§ÙØªØš ØšØ±ÙØ¯Ù Ø§ÙØ¥ÙÙØªØ±ÙÙÙ",
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"ð Ø§ÙÙÙØ¯: {otp_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"â ÙÙ
ÙØªÙ
Ø§ÙØ¹Ø«Ùر عÙÙ ØØ³Ø§Øš ؚرÙÙ
{phone}\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, "اضغط ð ØªÙØ¹ÙÙ Ø§ÙØØ³Ø§Øš ÙÙØšØ¯Ø¡", 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"""â
تÙ
Ø§ÙØ¹Ø«Ùر عÙÙ Ø§ÙØØ³Ø§Øš!
ð§ {state.get('email', '')}
ââââââââââââââââââââ
ð ÙÙØ¯ Ø§ÙØªØÙÙ: {otp}
â±ïž ØµÙØ§ØÙØ©: 10 Ø¯ÙØ§ØŠÙ
اضغط â
تأÙÙØ¯ ÙÙØªÙعÙ٠اÙÙÙØ±Ù ð"""
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(
"ð ØªÙ
ØªÙØ¹ÙÙ ØØ³Ø§ØšÙ ØšÙØ¬Ø§Ø!\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