183 lines
6.3 KiB
Python
183 lines
6.3 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__)
|
||
|
||
|
||
async def cmd_start(update: Update, _: 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
|
||
|
||
db = SessionLocal()
|
||
try:
|
||
existing = (
|
||
db.query(Employee).filter(Employee.tg_chat_id == chat_id).first()
|
||
)
|
||
if existing:
|
||
await update.message.reply_text(
|
||
f"✅ Вы уже подключены как <b>{existing.name}</b>.\n"
|
||
f"chat_id: <code>{chat_id}</code>",
|
||
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
|
||
|
||
# Create a new employee record from this user
|
||
name = (user.full_name or username or f"user_{chat_id}").strip()
|
||
e = Employee(name=name, tg_chat_id=chat_id, tg_username=username)
|
||
db.add(e)
|
||
db.commit()
|
||
await update.message.reply_text(
|
||
f"👋 Привет, <b>{name}</b>! Вы зарегистрированы как сотрудник.\n"
|
||
f"Откройте веб-интерфейс и создайте проекты, чтобы получать уведомления.\n"
|
||
f"chat_id: <code>{chat_id}</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()
|