Files
TG_autoposter/app/handlers/userbot_auth.py
Andrew K. Choi 9d116a91ab 🔐 Улучшена обработка облачного пароля (Cloud Password)
 УЛУЧШЕНИЯ:
  • Более подробное сообщение при запросе облачного пароля
  • Ясное объяснение, что это именно облачный пароль (2FA)
  • Разъяснение что это НЕ SMS-код и НЕ пароль от почты
  • Инструкции где найти облачный пароль
  • Советы по вводу (учитывается регистр)

📝 СООБЩЕНИЯ:
   Запрос пароля - 15 строк с полным объяснением
   Неверный пароль - рекомендации и способ восстановления
   Ошибка при проверке - сообщение об ошибке

💡 ПОДДЕРЖКА:
  • Восстановительный код (если забыли пароль)
  • Инструкции для мобильного Telegram
  • Чек-лист перед вводом пароля

🚀 ГОТОВНОСТЬ: Полная поддержка 2FA с облачным паролем
2025-12-22 05:17:40 +09:00

628 lines
23 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Интерактивная авторизация UserBot через интерфейс бота
"""
import os
import logging
from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton
from telegram.ext import ContextTypes, ConversationHandler
from telethon import TelegramClient
from telethon.errors import SessionPasswordNeededError, PhoneNumberInvalidError
from app.database import AsyncSessionLocal
from app.utils.keyboards import CallbackType
import asyncio
logger = logging.getLogger(__name__)
# Состояния для авторизации
AUTH_START = 1
AUTH_PHONE = 2
AUTH_CODE = 3
AUTH_PASSWORD = 4
AUTH_WAITING = 5
# Хранилище для временных данных авторизации
auth_sessions = {}
async def auth_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Меню авторизации UserBot"""
query = update.callback_query
if query:
await query.answer()
user_id = update.effective_user.id
# Проверяем статус
session_file = f"app/sessions/userbot_{user_id}.session"
is_authorized = os.path.exists(session_file)
text = """🔐 <b>Авторизация UserBot</b>
UserBot использует ваш личный аккаунт Telegram для сбора информации о группах и участниках.
<b>Важно:</b>
• Авторизация безопасна и происходит локально
• Ваши данные не передаются никому
• UserBot будет работать 24/7 на сервере
"""
if is_authorized:
text += "✅ <b>Статус: Авторизован</b>\n\nВы можете использовать все функции UserBot."
keyboard = [
[InlineKeyboardButton("🔄 Переавторизоваться", callback_data="auth_start_phone")],
[InlineKeyboardButton(" Информация", callback_data="auth_info")],
[InlineKeyboardButton("⬅️ Назад", callback_data="userbot_menu")],
]
else:
text += "❌ <b>Статус: Не авторизован</b>\n\nНажмите кнопку ниже, чтобы начать авторизацию."
keyboard = [
[InlineKeyboardButton("🚀 Начать авторизацию", callback_data="auth_start_phone")],
[InlineKeyboardButton(" Информация", callback_data="auth_info")],
[InlineKeyboardButton("⬅️ Назад", callback_data="userbot_menu")],
]
if query:
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
else:
await update.message.reply_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
return AUTH_START
async def auth_info(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Информация об авторизации"""
query = update.callback_query
await query.answer()
text = """ <b>Информация об авторизации UserBot</b>
<b>Как это работает:</b>
1. Введите номер телефона вашего аккаунта Telegram
2. Получите SMS-код подтверждения (5 цифр)
3. Введите код в бот
4. Если включена 2FA - введите пароль
5. Готово! UserBot авторизован
<b>Что такое SMS-код?</b>
Это 5-значный код, который Telegram отправляет на ваш номер.
Действителен ~5 минут. Нужно ввести быстро.
<b>Что такое 2FA пароль?</b>
Это пароль, который ВЫ установили в Telegram на случай потери телефона.
<i>НЕ SMS-код, НЕ пароль от почты!</i>
📍 Где его найти?
Telegram → Настройки → Приватность и безопасность → Двухфакторная аутентификация
<b>Безопасность:</b>
✓ Авторизация происходит локально на сервере
✓ Пароли НЕ сохраняются в базе (обработаны Telethon)
✓ SMS-коды НЕ логируются
✓ Сессия сохраняется в зашифрованном виде
✓ Доступ имеет только этот бот
<b>Что может делать UserBot:</b>
✓ Собирать информацию о ваших группах
✓ Получать список участников группы
✓ Сохранять данные в базу данных
✓ Работать 24/7 без вашего участия
<b>Что НЕ может делать:</b>
✗ Отправлять сообщения от вашего имени
✗ Удалять или изменять ваши сообщения
✗ Изменять настройки групп
✗ Получать доступ к приватным чатам других пользователей
<b>Справка по ошибкам:</b>
"Неверный номер" - проверьте формат (+XX...)
"Код истек" - повторите авторизацию
"Требуется пароль" - введите пароль 2FA
"Неверный пароль" - проверьте регистр и опечатки
📖 Полное руководство: смотрите 2FA_GUIDE.md
"""
keyboard = [
[InlineKeyboardButton("⬅️ Назад", callback_data="auth_menu")],
]
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
return AUTH_START
async def start_phone_input(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Начало ввода номера телефона"""
query = update.callback_query
if query:
await query.answer()
await query.delete_message()
user_id = update.effective_user.id
text = """📱 <b>Введите номер телефона</b>
Введите номер телефона вашего аккаунта Telegram в формате:
<code>+7 (XXX) XXX-XX-XX</code>
Примеры:
• +79991234567 (Россия)
• +82101234567 (Южная Корея)
• +11234567890 (США)
<i>После ввода номера вам будет отправлен SMS-код</i>
"""
await update.effective_message.reply_text(
text,
parse_mode='HTML'
)
# Инициализируем хранилище для этого пользователя
if user_id not in auth_sessions:
auth_sessions[user_id] = {}
return AUTH_PHONE
async def handle_phone(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Обработка номера телефона"""
user_id = update.effective_user.id
phone = update.message.text.strip()
# Очищаем от спецсимволов
phone_clean = ''.join(c for c in phone if c.isdigit())
if not phone_clean or len(phone_clean) < 10:
await update.message.reply_text(
"❌ Некорректный номер телефона. Пожалуйста, введите номер в международном формате (с кодом страны).",
parse_mode='HTML'
)
return AUTH_PHONE
# Восстанавливаем номер с + спереди если его нет
if not phone_clean.startswith('1') and not phone_clean[0].isdigit():
phone_clean = phone_clean
if not phone.startswith('+'):
phone = '+' + phone_clean
else:
phone = '+' + phone_clean
# Сохраняем номер
auth_sessions[user_id]['phone'] = phone
text = f"""📤 <b>Отправляем код подтверждения...</b>
Номер: <code>{phone}</code>
Пожалуйста, подождите. Отправляем SMS на ваш номер...
"""
message = await update.message.reply_text(text, parse_mode='HTML')
try:
# Создаем Telethon клиент
api_id = os.getenv('TELETHON_API_ID')
api_hash = os.getenv('TELETHON_API_HASH')
if not api_id or not api_hash:
await message.edit_text(
"❌ Ошибка: API ID или API Hash не установлены",
parse_mode='HTML'
)
return AUTH_PHONE
session_file = f"app/sessions/userbot_auth_{user_id}"
client = TelegramClient(session_file, int(api_id), api_hash)
# Подключаемся и запрашиваем код
await client.connect()
try:
result = await client.send_code_request(phone)
auth_sessions[user_id]['client'] = client
auth_sessions[user_id]['phone_code_hash'] = result.phone_code_hash
await message.edit_text(
f"""✅ <b>Код отправлен!</b>
SMS с кодом подтверждения отправлен на номер:
<code>{phone}</code>
Введите полученный код:
""",
parse_mode='HTML'
)
return AUTH_CODE
except PhoneNumberInvalidError:
await message.edit_text(
f"""❌ <b>Неверный номер телефона</b>
Номер <code>{phone}</code> не является корректным номером Telegram.
Пожалуйста, попробуйте еще раз с корректным номером.
""",
parse_mode='HTML'
)
await client.disconnect()
return AUTH_PHONE
except Exception as e:
logger.error(f"Auth error: {e}")
await message.edit_text(
f"""❌ <b>Ошибка при отправке кода</b>
{str(e)}
Пожалуйста, попробуйте еще раз.
""",
parse_mode='HTML'
)
return AUTH_PHONE
async def handle_code(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Обработка SMS-кода"""
user_id = update.effective_user.id
code = update.message.text.strip()
if not code.isdigit() or len(code) != 5:
await update.message.reply_text(
"❌ Код должен состоять из 5 цифр. Пожалуйста, попробуйте еще раз.",
parse_mode='HTML'
)
return AUTH_CODE
message = await update.message.reply_text(
"🔄 Проверяем код...",
parse_mode='HTML'
)
try:
if user_id not in auth_sessions or 'client' not in auth_sessions[user_id]:
await message.edit_text(
"❌ Сессия потеряна. Пожалуйста, начните авторизацию заново.",
parse_mode='HTML'
)
return AUTH_PHONE
client = auth_sessions[user_id]['client']
phone_code_hash = auth_sessions[user_id]['phone_code_hash']
try:
# Пытаемся войти с кодом
await client.sign_in(
phone=auth_sessions[user_id]['phone'],
code=code,
phone_code_hash=phone_code_hash
)
# Успешная авторизация
await message.edit_text(
"""✅ <b>Авторизация успешна!</b>
Ваш UserBot авторизован и готов к работе.
Сессия сохранена безопасно на сервере.
""",
parse_mode='HTML'
)
# Сохраняем правильное имя сессии
correct_session = f"app/sessions/userbot_{user_id}"
if os.path.exists(f"app/sessions/userbot_auth_{user_id}.session"):
os.rename(
f"app/sessions/userbot_auth_{user_id}.session",
f"{correct_session}.session"
)
await client.disconnect()
# Очищаем временные данные
if user_id in auth_sessions:
del auth_sessions[user_id]
# Возвращаемся в меню
keyboard = [
[InlineKeyboardButton("✅ Готово", callback_data="userbot_menu")],
]
await message.reply_text(
"Нажмите кнопку, чтобы вернуться в меню UserBot.",
reply_markup=InlineKeyboardMarkup(keyboard)
)
return ConversationHandler.END
except SessionPasswordNeededError:
# Нужна двухфакторная аутентификация (облачный пароль)
await message.edit_text(
"""🔐 <b>Требуется облачный пароль (Cloud Password)</b>
Ваш аккаунт Telegram защищен паролем 2FA.
<b>Что вводить:</b>
Введите <b>облачный пароль</b>, который вы установили в Telegram.
<b>⚠️ Это НЕ:</b>
НЕ SMS-код (он был выше)
НЕ пароль от электронной почты
НЕ пароль от другого аккаунта
<b>📍 Где его найти:</b>
На <b>мобильном телефоне</b> откройте Telegram:
Настройки → Приватность и безопасность → Двухфакторная аутентификация
Там будет переключатель и пароль, который вы установили.
<b>💡 Подсказки:</b>
• Пароль чувствителен к регистру (A ≠ a)
• Пароль НЕ содержит SMS-код
• Это пароль, который только вы знаете
• Если забыли - используйте восстановительный код
<i>Введите облачный пароль:</i>
""",
parse_mode='HTML'
)
return AUTH_PASSWORD
except Exception as e:
error_str = str(e).lower()
logger.error(f"Code verification error: {e}")
# Обработка различных типов ошибок
if "expired" in error_str or "code has expired" in error_str:
await message.edit_text(
"""❌ <b>Код истёк</b>
SMS-коды действуют примерно 5 минут. К сожалению, этот код уже недействителен.
<b>Что делать:</b>
<u>Вариант 1 (рекомендуется):</u>
1. Нажмите кнопку ниже "🔄 Получить новый код"
2. Введите полученный код <b>сразу же</b>
<u>Вариант 2 (если ошибка повторяется):</u>
1. Подождите 1-2 часа
2. Попробуйте авторизацию снова
3. (Telegram может блокировать при частых попытках)
<u>Вариант 3 (если стоит 2FA):</u>
1. Временно отключите 2FA в Telegram
2. Попробуйте авторизацию здесь
3. Снова включите 2FA
""",
reply_markup=InlineKeyboardMarkup([
[InlineKeyboardButton("🔄 Получить новый код", callback_data="auth_start_phone")],
[InlineKeyboardButton("⬅️ Назад", callback_data="auth_menu")],
]),
parse_mode='HTML'
)
return AUTH_CODE
elif "unregistered" in error_str or "phone" in error_str:
await message.edit_text(
f"""❌ <b>Ошибка номера телефона</b>
Номер не зарегистрирован в Telegram или некорректен.
<b>Проверьте:</b>
• Правильность номера
• Наличие кода страны (например +82 для Кореи)
• Что это ваш номер (на котором зарегистрирован Telegram)
Ошибка: <code>{str(e)}</code>
""",
reply_markup=InlineKeyboardMarkup([
[InlineKeyboardButton("🔄 Попробовать снова", callback_data="auth_start_phone")],
[InlineKeyboardButton("⬅️ Назад", callback_data="auth_menu")],
]),
parse_mode='HTML'
)
return AUTH_CODE
else:
await message.edit_text(
f"""❌ <b>Ошибка при проверке кода</b>
{str(e)}
<b>Что попробовать:</b>
1. 🔄 Получить новый код (кнопка ниже)
2. ⏱️ Ввести код быстрее (коды действуют ~5 минут)
3. ⏲️ Подождать час перед следующей попыткой
Если ошибка повторяется:
• Проверьте интернет соединение
• Попробуйте авторизацию через мобильное приложение Telegram
• Обратитесь в поддержку Telegram
""",
reply_markup=InlineKeyboardMarkup([
[InlineKeyboardButton("🔄 Получить новый код", callback_data="auth_start_phone")],
[InlineKeyboardButton("⬅️ Назад", callback_data="auth_menu")],
]),
parse_mode='HTML'
)
return AUTH_CODE
async def handle_password(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Обработка пароля 2FA"""
user_id = update.effective_user.id
password = update.message.text
message = await update.message.reply_text(
"🔄 Проверяем пароль...",
parse_mode='HTML'
)
try:
if user_id not in auth_sessions or 'client' not in auth_sessions[user_id]:
await message.edit_text(
"❌ Сессия потеряна. Пожалуйста, начните авторизацию заново.",
parse_mode='HTML'
)
return AUTH_PASSWORD
client = auth_sessions[user_id]['client']
try:
# Пытаемся войти с пароль
await client.sign_in(password=password)
# Успешная авторизация
await message.edit_text(
"""✅ <b>Двухфакторная аутентификация успешна!</b>
Ваш UserBot авторизован и готов к работе.
Сессия сохранена на сервере. Переавторизация не требуется.
""",
parse_mode='HTML'
)
# Сохраняем правильное имя сессии
correct_session = f"app/sessions/userbot_{user_id}"
if os.path.exists(f"app/sessions/userbot_auth_{user_id}.session"):
os.rename(
f"app/sessions/userbot_auth_{user_id}.session",
f"{correct_session}.session"
)
await client.disconnect()
# Очищаем временные данные
if user_id in auth_sessions:
del auth_sessions[user_id]
keyboard = [
[InlineKeyboardButton("✅ Готово", callback_data="userbot_menu")],
]
await message.reply_text(
"Нажмите кнопку, чтобы вернуться в меню UserBot.",
reply_markup=InlineKeyboardMarkup(keyboard)
)
return ConversationHandler.END
except Exception as password_error:
error_msg = str(password_error).lower()
# Проверяем тип ошибки
if "password" in error_msg or "invalid" in error_msg:
await message.edit_text(
"""❌ <b>Неверный облачный пароль</b>
Пароль, который вы ввели, неправильный.
<b>Проверьте:</b>
• Копируете ли вы пароль правильно (без пробелов)
• Совпадает ли пароль с тем, что вы установили в Telegram
• Учитывается регистр букв (A ≠ a)
Не содержит ли пароль SMS-код
<b>Если забыли пароль:</b>
1. На мобильном телефоне откройте Telegram
2. Настройки → Приватность и безопасность → Двухфакторная аутентификация
3. Нажмите "Восстановительный код"
4. Используйте этот код вместо пароля
<i>Введите пароль еще раз:</i>
""",
parse_mode='HTML'
)
else:
await message.edit_text(
f"""❌ <b>Ошибка при проверке облачного пароля</b>
{error_msg[:100]}
Пожалуйста, попробуйте еще раз или начните авторизацию заново.
""",
parse_mode='HTML'
)
return AUTH_PASSWORD
except Exception as e:
logger.error(f"Password verification error: {e}")
await message.edit_text(
f"""❌ <b>Критическая ошибка</b>
{str(e)[:150]}
Пожалуйста, начните авторизацию заново.
""",
parse_mode='HTML'
)
return AUTH_PASSWORD
async def cancel_auth(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Отмена авторизации"""
user_id = update.effective_user.id
# Отключаем клиент если он есть
if user_id in auth_sessions and 'client' in auth_sessions[user_id]:
try:
await auth_sessions[user_id]['client'].disconnect()
except:
pass
del auth_sessions[user_id]
query = update.callback_query
if query:
await query.answer()
await query.delete_message()
await update.effective_message.reply_text(
"Авторизация отменена.",
parse_mode='HTML'
)
return ConversationHandler.END
def get_auth_conversation_handler():
"""Возвращает ConversationHandler для авторизации"""
return ConversationHandler(
entry_points=[
# Когда пользователь нажимает кнопку авторизации
],
states={
AUTH_START: [
# Информация об авторизации
],
AUTH_PHONE: [
# Обработка номера телефона
],
AUTH_CODE: [
# Обработка SMS кода
],
AUTH_PASSWORD: [
# Обработка пароля 2FA
],
},
fallbacks=[],
name="userbot_auth",
persistent=True
)