Add monitoring PF service

This commit is contained in:
Grendgi
2026-06-04 14:55:41 +03:00
commit dd3edd7088
41 changed files with 3194 additions and 0 deletions

182
app/bot.py Normal file
View File

@@ -0,0 +1,182 @@
"""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 не задан в .env")
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()