From 3dbb105bbc701fac7e6f40add8f6d8cc78d883dd Mon Sep 17 00:00:00 2001 From: YuraBombitel1 Date: Wed, 12 Aug 2026 14:18:24 +0300 Subject: [PATCH] init --- support-bot.env | 2 + support_bot.py | 225 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 support-bot.env create mode 100644 support_bot.py diff --git a/support-bot.env b/support-bot.env new file mode 100644 index 0000000..e49b6c2 --- /dev/null +++ b/support-bot.env @@ -0,0 +1,2 @@ +BOT_TOKEN=ВСТАВЬ_СЮДА_ТОКЕН_БОТА +SUPPORT_CHAT_ID=0 diff --git a/support_bot.py b/support_bot.py new file mode 100644 index 0000000..db0b54e --- /dev/null +++ b/support_bot.py @@ -0,0 +1,225 @@ +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()