Compare commits

..

No commits in common. "c48998e0855211a770acdbbcc4577aef1feb4371" and "a441253b89446421d00a31e97ccdc5fa8651b4a0" have entirely different histories.

7 changed files with 32 additions and 115 deletions

View file

@ -30,10 +30,6 @@ steps:
tags: tags:
- latest - latest
- "sha-${CI_COMMIT_SHA:0:8}" - "sha-${CI_COMMIT_SHA:0:8}"
cache_from: "git.ihateamerica.ru/dv/intrabot:latest"
build_args:
- "GIT_COMMIT=${CI_COMMIT_SHA:0:8}"
- "BUILDKIT_INLINE_CACHE=1"
when: when:
- event: push - event: push
branch: main branch: main
@ -54,10 +50,6 @@ steps:
- "${CI_COMMIT_TAG}" # v1.2.3 - "${CI_COMMIT_TAG}" # v1.2.3
- latest - latest
- "sha-${CI_COMMIT_SHA:0:8}" # всегда — нужно для отката - "sha-${CI_COMMIT_SHA:0:8}" # всегда — нужно для отката
cache_from: "git.ihateamerica.ru/dv/intrabot:latest"
build_args:
- "GIT_COMMIT=${CI_COMMIT_SHA:0:8}"
- "BUILDKIT_INLINE_CACHE=1"
when: when:
- event: tag - event: tag

View file

@ -7,11 +7,6 @@ RUN mkdir -p /app/data
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
ARG GIT_COMMIT=dev
ARG DOCKER_IMAGE_CREATED=unknown
ENV GIT_COMMIT=$GIT_COMMIT
ENV BUILD_TIME=$DOCKER_IMAGE_CREATED
COPY . . COPY . .
# Продакшен: Gunicorn (gthread, 1 worker) обслуживает Flask. # Продакшен: Gunicorn (gthread, 1 worker) обслуживает Flask.

View file

@ -12,29 +12,9 @@
""" """
from __future__ import annotations from __future__ import annotations
import logging
import time
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
log = logging.getLogger(__name__)
def _timed(label: str):
"""Context manager: логирует старт и завершение операции с временем выполнения."""
import contextlib
@contextlib.contextmanager
def _cm():
log.info("%s ...", label)
t0 = time.monotonic()
try:
yield
finally:
log.info("%s%.2fs", label, time.monotonic() - t0)
return _cm()
# ── Агностичное представление клавиатуры ────────────────────────────────────── # ── Агностичное представление клавиатуры ──────────────────────────────────────
@ -121,18 +101,15 @@ class TelegramChannelNotifier(Notifier):
# ── Реализация интерфейса ────────────────────────────────────────────────── # ── Реализация интерфейса ──────────────────────────────────────────────────
def send(self, text: str, keyboard: Keyboard | None = None) -> int: def send(self, text: str, keyboard: Keyboard | None = None) -> int:
with _timed(f"send_message → channel {self._channel_id}"):
msg = self._bot.send_message( msg = self._bot.send_message(
self._channel_id, text, self._channel_id, text,
parse_mode="Markdown", parse_mode="Markdown",
disable_web_page_preview=True, disable_web_page_preview=True,
reply_markup=self._to_markup(keyboard), reply_markup=self._to_markup(keyboard),
) )
log.info("send_message OK → message_id=%s", msg.message_id)
return msg.message_id return msg.message_id
def edit_text(self, message_id: int, text: str, keyboard: Keyboard | None = None): def edit_text(self, message_id: int, text: str, keyboard: Keyboard | None = None):
with _timed(f"edit_message_text → message_id={message_id}"):
self._bot.edit_message_text( self._bot.edit_message_text(
text, self._channel_id, message_id, text, self._channel_id, message_id,
parse_mode="Markdown", parse_mode="Markdown",
@ -141,26 +118,22 @@ class TelegramChannelNotifier(Notifier):
) )
def edit_keyboard(self, message_id: int, keyboard: Keyboard | None): def edit_keyboard(self, message_id: int, keyboard: Keyboard | None):
with _timed(f"edit_message_reply_markup → message_id={message_id}"):
self._bot.edit_message_reply_markup( self._bot.edit_message_reply_markup(
self._channel_id, message_id, self._channel_id, message_id,
reply_markup=self._to_markup(keyboard), reply_markup=self._to_markup(keyboard),
) )
def delete(self, message_id: int): def delete(self, message_id: int):
with _timed(f"delete_message → message_id={message_id}"):
try: try:
self._bot.delete_message(self._channel_id, message_id) self._bot.delete_message(self._channel_id, message_id)
except Exception as e: except Exception:
log.warning("delete_message failed (message_id=%s): %s", message_id, e) pass
def send_animation(self, url: str, disable_notification: bool = True) -> int: def send_animation(self, url: str, disable_notification: bool = True) -> int:
with _timed(f"send_animation → channel {self._channel_id}"):
msg = self._bot.send_animation( msg = self._bot.send_animation(
self._channel_id, url, self._channel_id, url,
disable_notification=disable_notification, disable_notification=disable_notification,
) )
log.info("send_animation OK → message_id=%s", msg.message_id)
return msg.message_id return msg.message_id
def is_stale_error(self, exc: Exception) -> bool: def is_stale_error(self, exc: Exception) -> bool:

View file

@ -10,7 +10,6 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import time
import config import config
import database import database
@ -51,10 +50,7 @@ def recreate_post(
Изменения ( ...) дедуплицируются по тексту. Изменения ( ...) дедуплицируются по тексту.
Комментарии (💬 ...) и описания (📝 ...) добавляются всегда. Комментарии (💬 ...) и описания (📝 ...) добавляются всегда.
""" """
t0 = time.monotonic()
log.info("recreate_post START task=%s", task_id)
notifier = get_notifier() notifier = get_notifier()
stored = database.get_stored_events(task_id) stored = database.get_stored_events(task_id)
stored_set = set(stored) stored_set = set(stored)
@ -81,7 +77,8 @@ def recreate_post(
}) })
old = database.get_message_data(task_id) old = database.get_message_data(task_id)
log.info("recreate_post task=%s old_msg=%s", task_id, old["message_id"] if old else None) if old:
notifier.delete(old["message_id"])
try: try:
message_id = notifier.send(full_text, keyboard) message_id = notifier.send(full_text, keyboard)
@ -90,12 +87,6 @@ def recreate_post(
database.save_stored_events(task_id, stored) database.save_stored_events(task_id, stored)
except Exception as e: except Exception as e:
log.error(f"recreate_post failed for task {task_id}: {e}") log.error(f"recreate_post failed for task {task_id}: {e}")
return
if old:
notifier.delete(old["message_id"])
log.info("recreate_post DONE task=%s%.2fs total", task_id, time.monotonic() - t0)
def refresh_responsible(task_id: int, task_number: str): def refresh_responsible(task_id: int, task_number: str):

View file

@ -22,7 +22,7 @@
services: services:
intradbot: intradbot:
image: git.ihateamerica.ru/dv/intrabot:latest # ← замени на свой хост/юзер image: git.example.com/YOUR_USER/intradbot:latest # ← замени на свой хост/юзер
restart: always restart: always
env_file: .env env_file: .env
ports: ports:

View file

@ -38,17 +38,10 @@ access_log_format = '%(h)s "%(r)s" %(s)s %(b)s %(D)sµs'
def on_starting(server): def on_starting(server):
"""Инициализация до форка воркеров (выполняется в мастер-процессе).""" """Инициализация до форка воркеров (выполняется в мастер-процессе)."""
import logging
import config import config
import database import database
from intradesk import reference from intradesk import reference
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
handlers=[logging.StreamHandler()],
)
os.makedirs(os.path.dirname(config.DB_FILE), exist_ok=True) os.makedirs(os.path.dirname(config.DB_FILE), exist_ok=True)
database.init_db() database.init_db()
reference.seed_static() reference.seed_static()

View file

@ -1,7 +1,6 @@
import json import json
import logging import logging
import os import os
import queue
import threading import threading
from flask import Flask, Response, jsonify, render_template, request, stream_with_context, abort from flask import Flask, Response, jsonify, render_template, request, stream_with_context, abort
@ -20,24 +19,6 @@ from intradesk.handler import handle_webhook
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
app = Flask(__name__, template_folder=os.path.join(os.path.dirname(__file__), "templates")) app = Flask(__name__, template_folder=os.path.join(os.path.dirname(__file__), "templates"))
# ── Очередь вебхуков (последовательная обработка) ─────────────────────────────
_webhook_queue: queue.Queue = queue.Queue()
def _webhook_worker():
while True:
data = _webhook_queue.get()
try:
handle_webhook(data)
except Exception:
log.exception("Unhandled error in webhook worker")
finally:
_webhook_queue.task_done()
threading.Thread(target=_webhook_worker, daemon=True, name="webhook-worker").start()
# ── Авторизация ──────────────────────────────────────────────────────────────── # ── Авторизация ────────────────────────────────────────────────────────────────
@ -116,7 +97,7 @@ def webhook():
if not data: if not data:
return jsonify({"error": "Invalid JSON"}), 400 return jsonify({"error": "Invalid JSON"}), 400
_webhook_queue.put(data) threading.Thread(target=handle_webhook, args=(data,), daemon=True).start()
return jsonify({"ok": True}), 200 return jsonify({"ok": True}), 200
@ -125,14 +106,6 @@ def health():
return jsonify({"status": "ok"}), 200 return jsonify({"status": "ok"}), 200
@app.route("/version")
def version():
return jsonify({
"commit": os.environ.get("GIT_COMMIT", "dev"),
"built_at": os.environ.get("BUILD_TIME", "unknown"),
})
# ── Дашборд ──────────────────────────────────────────────────────────────────── # ── Дашборд ────────────────────────────────────────────────────────────────────
@app.route("/") @app.route("/")
@ -172,7 +145,7 @@ def stream():
# ── API заявок ───────────────────────────────────────────────────────────────── # ── API заявок ─────────────────────────────────────────────────────────────────
@app.route("/api/tasks/search") @app.route("/api/tasks/search")
#@require_task_token @require_task_token
def api_tasks_search(): def api_tasks_search():
try: try:
number = int(request.args.get("number", 0)) number = int(request.args.get("number", 0))
@ -185,7 +158,7 @@ def api_tasks_search():
@app.route("/api/tasks/active") @app.route("/api/tasks/active")
#@require_task_token @require_task_token
def api_tasks_active(): def api_tasks_active():
filter_expr = request.args.get( filter_expr = request.args.get(
"filter", config.ACTIVE_TASKS_FILTER "filter", config.ACTIVE_TASKS_FILTER
@ -195,7 +168,7 @@ def api_tasks_active():
@app.route("/api/task/lifetime") @app.route("/api/task/lifetime")
#@require_task_token @require_task_token
def api_task_lifetime(): def api_task_lifetime():
try: try:
task_id = int(request.args.get("task_id", 0)) task_id = int(request.args.get("task_id", 0))