import logging import os import sqlite3 from pathlib import Path from telegram import Update from telegram.constants import ParseMode from telegram.ext import ( Application, CommandHandler, ContextTypes, MessageHandler, filters, ) ENV_FILE = "/etc/support-bot.env" DB_FILE = "/var/lib/support-bot/messages.db" START_TEXT = ( "Добро пожаловать в StreamHelp.\n\n" "Для того чтобы мы могли проанализировать вашу ситуацию и оказать помощь, " "пожалуйста, отправьте следующее сообщение, содержащее:\n\n" "• краткое описание проблемы;\n" "• ссылку на ваш канал;\n" "• платформу, на которой осуществляется трансляция.\n\n" "После получения данной информации оператор ответит вам в данном чате." ) def load_env(path: str) -> None: """Загружает KEY=VALUE из env-файла без внешних библиотек.""" env_path = Path(path) if not env_path.exists(): return for raw_line in env_path.read_text(encoding="utf-8").splitlines(): line = raw_line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) key = key.strip() value = value.strip().strip('"').strip("'") if key and key not in os.environ: os.environ[key] = value load_env(ENV_FILE) BOT_TOKEN = os.getenv("BOT_TOKEN") SUPPORT_CHAT_ID_RAW = os.getenv("SUPPORT_CHAT_ID", "0") if not BOT_TOKEN: raise RuntimeError(f"Не найден BOT_TOKEN в {ENV_FILE}") try: SUPPORT_CHAT_ID = int(SUPPORT_CHAT_ID_RAW) except ValueError as exc: raise RuntimeError("SUPPORT_CHAT_ID должен быть числом") from exc if SUPPORT_CHAT_ID == 0: raise RuntimeError(f"Укажи реальный SUPPORT_CHAT_ID в {ENV_FILE}") logging.basicConfig( format="%(asctime)s | %(levelname)s | %(name)s | %(message)s", level=logging.INFO, ) logger = logging.getLogger(__name__) def init_db() -> None: db_path = Path(DB_FILE) db_path.parent.mkdir(parents=True, exist_ok=True) with sqlite3.connect(DB_FILE) as conn: conn.execute( """ CREATE TABLE IF NOT EXISTS message_map ( support_message_id INTEGER PRIMARY KEY, user_chat_id INTEGER NOT NULL ) """ ) conn.commit() def save_mapping(support_message_id: int, user_chat_id: int) -> None: with sqlite3.connect(DB_FILE) as conn: conn.execute( """ INSERT OR REPLACE INTO message_map (support_message_id, user_chat_id) VALUES (?, ?) """, (support_message_id, user_chat_id), ) conn.commit() def get_user_chat_id(support_message_id: int) -> int | None: with sqlite3.connect(DB_FILE) as conn: row = conn.execute( """ SELECT user_chat_id FROM message_map WHERE support_message_id = ? """, (support_message_id,), ).fetchone() return row[0] if row else None async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if not update.effective_message: return await update.effective_message.reply_text(START_TEXT) async def user_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Получает сообщение клиента и отправляет его в чат поддержки.""" message = update.effective_message user = update.effective_user chat = update.effective_chat if not message or not user or not chat: return if chat.id == SUPPORT_CHAT_ID: return username = f"@{user.username}" if user.username else "нет" full_name = user.full_name or "Без имени" header = await context.bot.send_message( chat_id=SUPPORT_CHAT_ID, text=( "Новое сообщение в StreamHelp\n\n" f"Клиент: {full_name}\n" f"Username: {username}\n" f"User ID: {user.id}\n\n" "Ответьте реплаем на сообщение клиента ниже." ), parse_mode=ParseMode.HTML, ) copied = await context.bot.copy_message( chat_id=SUPPORT_CHAT_ID, from_chat_id=chat.id, message_id=message.message_id, reply_to_message_id=header.message_id, ) save_mapping(copied.message_id, chat.id) async def operator_reply(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Отправляет ответ оператора из SUPPORT_CHAT_ID обратно клиенту.""" message = update.effective_message chat = update.effective_chat if not message or not chat or chat.id != SUPPORT_CHAT_ID: return if not message.reply_to_message: return replied_message_id = message.reply_to_message.message_id user_chat_id = get_user_chat_id(replied_message_id) if not user_chat_id: return try: await context.bot.copy_message( chat_id=user_chat_id, from_chat_id=SUPPORT_CHAT_ID, message_id=message.message_id, ) except Exception: logger.exception( "Не удалось отправить ответ пользователю %s", user_chat_id, ) await message.reply_text( "Не удалось отправить ответ клиенту. " "Возможно, пользователь заблокировал бота." ) def main() -> None: init_db() application = Application.builder().token(BOT_TOKEN).build() application.add_handler(CommandHandler("start", start)) # Сначала обрабатываем ответы операторов в чате поддержки. application.add_handler( MessageHandler( filters.Chat(chat_id=SUPPORT_CHAT_ID) & filters.REPLY & ~filters.COMMAND, operator_reply, ) ) # Все сообщения пользователей, кроме сообщений из чата поддержки. application.add_handler( MessageHandler( ~filters.Chat(chat_id=SUPPORT_CHAT_ID) & ~filters.COMMAND, user_message, ) ) logger.info("StreamHelp support bot started") application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main()