Files
2026-08-12 14:18:24 +03:00

226 lines
6.7 KiB
Python

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=(
"<b>Новое сообщение в StreamHelp</b>\n\n"
f"<b>Клиент:</b> {full_name}\n"
f"<b>Username:</b> {username}\n"
f"<b>User ID:</b> <code>{user.id}</code>\n\n"
"Ответьте <b>реплаем на сообщение клиента ниже</b>."
),
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()