226 lines
6.7 KiB
Python
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()
|