229 lines
8.4 KiB
Python
229 lines
8.4 KiB
Python
"""Telegram bot — registers employees by chat_id and lets them trigger checks.
|
||
|
||
Run as a separate process: `python -m app.bot`.
|
||
|
||
Bot commands (set via @BotFather → /setcommands):
|
||
start - Подключить себя как сотрудника
|
||
list - Список своих проектов
|
||
check - Проверить все мои проекты сейчас
|
||
whoami - Показать свой chat_id
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import logging
|
||
|
||
from sqlalchemy.orm import joinedload
|
||
from telegram import Update
|
||
from telegram.ext import (
|
||
Application,
|
||
CommandHandler,
|
||
ContextTypes,
|
||
)
|
||
|
||
from app.config import settings
|
||
from app.db import SessionLocal, init_db
|
||
from app.models import Employee, Project
|
||
from app.services.monitor import run_check_for_project
|
||
|
||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def _portal_user_code(context: ContextTypes.DEFAULT_TYPE) -> str | None:
|
||
if not context.args:
|
||
return None
|
||
code = context.args[0].strip()
|
||
return code or None
|
||
|
||
|
||
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||
if not update.effective_user or not update.effective_chat:
|
||
return
|
||
user = update.effective_user
|
||
chat_id = str(update.effective_chat.id)
|
||
username = user.username
|
||
portal_user_id = _portal_user_code(context)
|
||
|
||
db = SessionLocal()
|
||
try:
|
||
existing = (
|
||
db.query(Employee).filter(Employee.tg_chat_id == chat_id).first()
|
||
)
|
||
if existing:
|
||
if portal_user_id and existing.portal_user_id and existing.portal_user_id != portal_user_id:
|
||
await update.message.reply_text(
|
||
"Этот Telegram уже подключен к другому пользователю Portal.",
|
||
)
|
||
return
|
||
if portal_user_id and not existing.portal_user_id:
|
||
clash = db.query(Employee).filter(Employee.portal_user_id == portal_user_id).first()
|
||
if clash and clash.id != existing.id:
|
||
await update.message.reply_text(
|
||
"Этот пользователь Portal уже подключен к другому Telegram.",
|
||
)
|
||
return
|
||
existing.portal_user_id = portal_user_id
|
||
existing.tg_username = username
|
||
db.commit()
|
||
await update.message.reply_text(
|
||
f"✅ Вы уже подключены как <b>{existing.name}</b>.\n"
|
||
f"chat_id: <code>{chat_id}</code>",
|
||
parse_mode="HTML",
|
||
)
|
||
return
|
||
|
||
if portal_user_id:
|
||
employee = db.query(Employee).filter(Employee.portal_user_id == portal_user_id).first()
|
||
name = (user.full_name or username or f"user_{chat_id}").strip()
|
||
if employee:
|
||
if employee.tg_chat_id and employee.tg_chat_id != chat_id:
|
||
await update.message.reply_text(
|
||
"Этот пользователь Portal уже подключен к другому Telegram.",
|
||
)
|
||
return
|
||
employee.name = employee.name or name
|
||
employee.tg_chat_id = chat_id
|
||
employee.tg_username = username
|
||
else:
|
||
employee = Employee(
|
||
name=name,
|
||
portal_user_id=portal_user_id,
|
||
tg_chat_id=chat_id,
|
||
tg_username=username,
|
||
)
|
||
db.add(employee)
|
||
db.commit()
|
||
await update.message.reply_text(
|
||
f"✅ Привет, <b>{name}</b>! Telegram подключен к вашему аккаунту Portal.\n"
|
||
f"Теперь можно добавлять объекты мониторинга в Portal.",
|
||
parse_mode="HTML",
|
||
)
|
||
return
|
||
|
||
# Try to find by username (admin pre-created employee w/o chat_id)
|
||
if username:
|
||
placeholder = (
|
||
db.query(Employee)
|
||
.filter(Employee.tg_username == username, Employee.tg_chat_id.is_(None))
|
||
.first()
|
||
)
|
||
if placeholder:
|
||
placeholder.tg_chat_id = chat_id
|
||
db.commit()
|
||
await update.message.reply_text(
|
||
f"✅ Привет, <b>{placeholder.name}</b>! Вы успешно подключены.\n"
|
||
f"Уведомления будут приходить сюда.",
|
||
parse_mode="HTML",
|
||
)
|
||
return
|
||
|
||
await update.message.reply_text(
|
||
"Откройте Portal → Мониторинг PF и нажмите подключение Telegram.\n"
|
||
"Бот должен получить команду вида:\n"
|
||
"<code>/start ваш_код_из_Portal</code>",
|
||
parse_mode="HTML",
|
||
)
|
||
finally:
|
||
db.close()
|
||
|
||
|
||
async def cmd_whoami(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None:
|
||
if not update.effective_chat:
|
||
return
|
||
chat_id = str(update.effective_chat.id)
|
||
db = SessionLocal()
|
||
try:
|
||
e = db.query(Employee).filter(Employee.tg_chat_id == chat_id).first()
|
||
if e:
|
||
await update.message.reply_text(
|
||
f"Вы: <b>{e.name}</b>\nchat_id: <code>{chat_id}</code>",
|
||
parse_mode="HTML",
|
||
)
|
||
else:
|
||
await update.message.reply_text(
|
||
f"Вы пока не подключены. Отправьте /start.\nchat_id: <code>{chat_id}</code>",
|
||
parse_mode="HTML",
|
||
)
|
||
finally:
|
||
db.close()
|
||
|
||
|
||
async def cmd_list(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None:
|
||
if not update.effective_chat:
|
||
return
|
||
chat_id = str(update.effective_chat.id)
|
||
db = SessionLocal()
|
||
try:
|
||
e = (
|
||
db.query(Employee)
|
||
.options(joinedload(Employee.projects))
|
||
.filter(Employee.tg_chat_id == chat_id)
|
||
.first()
|
||
)
|
||
if not e:
|
||
await update.message.reply_text("Сначала /start.")
|
||
return
|
||
if not e.projects:
|
||
await update.message.reply_text("У вас пока нет проектов.")
|
||
return
|
||
lines = [f"<b>Ваши проекты ({len(e.projects)}):</b>"]
|
||
for p in e.projects:
|
||
lines.append(
|
||
f"• #{p.id} {p.title} — <code>{p.dld_permit}</code> "
|
||
f"({p.deal_type.value})"
|
||
)
|
||
await update.message.reply_text("\n".join(lines), parse_mode="HTML")
|
||
finally:
|
||
db.close()
|
||
|
||
|
||
async def cmd_check(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None:
|
||
if not update.effective_chat:
|
||
return
|
||
chat_id = str(update.effective_chat.id)
|
||
db = SessionLocal()
|
||
try:
|
||
e = (
|
||
db.query(Employee)
|
||
.options(joinedload(Employee.projects))
|
||
.filter(Employee.tg_chat_id == chat_id)
|
||
.first()
|
||
)
|
||
if not e:
|
||
await update.message.reply_text("Сначала /start.")
|
||
return
|
||
if not e.projects:
|
||
await update.message.reply_text("У вас нет проектов.")
|
||
return
|
||
ids = [p.id for p in e.projects]
|
||
finally:
|
||
db.close()
|
||
|
||
await update.message.reply_text(f"⏳ Запускаю проверку {len(ids)} проектов…")
|
||
total_changes = 0
|
||
for pid in ids:
|
||
try:
|
||
total_changes += await asyncio.to_thread(run_check_for_project, pid)
|
||
except Exception as ex:
|
||
logger.exception("check failed for %s: %s", pid, ex)
|
||
await update.message.reply_text(f"✅ Готово. Изменений: {total_changes}")
|
||
|
||
|
||
def main() -> None:
|
||
if not settings.tg_bot_token:
|
||
raise SystemExit("TG_BOT_TOKEN не задан в k8s/secrets.yaml")
|
||
init_db()
|
||
app = Application.builder().token(settings.tg_bot_token).build()
|
||
app.add_handler(CommandHandler("start", cmd_start))
|
||
app.add_handler(CommandHandler("whoami", cmd_whoami))
|
||
app.add_handler(CommandHandler("list", cmd_list))
|
||
app.add_handler(CommandHandler("check", cmd_check))
|
||
logger.info("Bot polling…")
|
||
app.run_polling(allowed_updates=Update.ALL_TYPES)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|