From 067f9f31c79d321da5413a3324cf324c08eefe06 Mon Sep 17 00:00:00 2001 From: MpakobecEx Date: Sun, 18 Oct 2026 19:10:32 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9E=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=8E=D0=B7=D0=B5=D1=80=D0=BD=D0=B5?= =?UTF-8?q?=D0=B9=D0=BC=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + bot.py | 1 + checklist.md | 1 + db.py | 47 ++++++++++++++++++++++++++++++++++---- queue_manager.py | 2 ++ tests/test_db.py | 48 +++++++++++++++++++++++++++++++++++++++ tests/test_web.py | 14 ++++++++---- web/templates/detail.html | 2 +- web/templates/index.html | 4 ++-- 9 files changed, 109 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e3cc8ae..d962568 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ http://192.168.31.158:8099 Возможности: - фильтр по Telegram `user_id` (с подсказкой известных пользователей); +- в списке показывается `@username`, если он известен, иначе Telegram ID; - фильтр по статусу и режиму (`t2i`/`i2i`); - пагинация; - превью результата в списке (кэшируются в `storage/.thumbnails`); diff --git a/bot.py b/bot.py index d16a41f..1105d4b 100644 --- a/bot.py +++ b/bot.py @@ -290,6 +290,7 @@ class MpakoBot: try: request, position = await self._queue.enqueue( user_id=user.id, + username=user.username, mode=mode, prompt=prompt, input_file_path=input_path, diff --git a/checklist.md b/checklist.md index 5d20d1d..fda4503 100644 --- a/checklist.md +++ b/checklist.md @@ -297,6 +297,7 @@ ON requests(user_id) WHERE status IN ('queued', 'running'); - [x] `web.py`: FastAPI + Jinja2, отдельный контейнер `web`. - [x] Список запросов с фильтрами `user_id`, `status`, `mode` и пагинацией. +- [x] Отображение `@username` с fallback на Telegram ID; миграция старых БД. - [x] Превью результата в списке, генерация и кэш в `storage/.thumbnails`. - [x] Карточка запроса: промпт, seed, ошибка, `comfy_prompt_id`, вход/результат. - [x] Отдача изображений только из `STORAGE_DIR` (защита от path traversal). diff --git a/db.py b/db.py index 4fcb9b7..4402d16 100644 --- a/db.py +++ b/db.py @@ -29,6 +29,7 @@ class UserAlreadyQueued(RuntimeError): class Request: id: int user_id: int + username: str | None timestamp: str prompt_text: str input_file_path: str | None @@ -40,11 +41,16 @@ class Request: error_text: str | None finished_at: str | None + @property + def display_user(self) -> str: + return f"@{self.username}" if self.username else str(self.user_id) + @classmethod def from_row(cls, row: aiosqlite.Row) -> Request: return cls( id=row["id"], user_id=row["user_id"], + username=row["username"], timestamp=row["timestamp"], prompt_text=row["prompt_text"], input_file_path=row["input_file_path"], @@ -61,14 +67,20 @@ class Request: @dataclass(frozen=True) class UserStat: user_id: int + username: str | None total: int last_seen: str + @property + def display_user(self) -> str: + return f"@{self.username}" if self.username else str(self.user_id) + SCHEMA = """ CREATE TABLE IF NOT EXISTS requests ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, + username TEXT, timestamp TEXT NOT NULL, prompt_text TEXT NOT NULL, input_file_path TEXT, @@ -111,8 +123,16 @@ class Database: await self._conn.execute("PRAGMA busy_timeout=5000") await self._conn.execute("PRAGMA foreign_keys=ON") await self._conn.executescript(SCHEMA) + await self._migrate() await self._conn.commit() + async def _migrate(self) -> None: + conn = self._connection() + async with conn.execute("PRAGMA table_info(requests)") as cursor: + columns = {row["name"] async for row in cursor} + if "username" not in columns: + await conn.execute("ALTER TABLE requests ADD COLUMN username TEXT") + async def close(self) -> None: if self._conn is not None: await self._conn.close() @@ -127,6 +147,7 @@ class Database: self, *, user_id: int, + username: str | None = None, mode: str, prompt_text: str, input_file_path: str | None, @@ -138,10 +159,19 @@ class Database: cursor = await conn.execute( """ INSERT INTO requests - (user_id, timestamp, prompt_text, input_file_path, status, mode, seed) - VALUES (?, ?, ?, ?, 'queued', ?, ?) + (user_id, username, timestamp, prompt_text, + input_file_path, status, mode, seed) + VALUES (?, ?, ?, ?, ?, 'queued', ?, ?) """, - (user_id, utc_now(), prompt_text, input_file_path, mode, seed), + ( + user_id, + username, + utc_now(), + prompt_text, + input_file_path, + mode, + seed, + ), ) request_id = cursor.lastrowid await conn.commit() @@ -255,7 +285,15 @@ class Database: conn = self._connection() async with conn.execute( """ - SELECT user_id, COUNT(*) AS total, MAX(timestamp) AS last_seen + SELECT + user_id, + COUNT(*) AS total, + MAX(timestamp) AS last_seen, + ( + SELECT username FROM requests AS r2 + WHERE r2.user_id = requests.user_id AND r2.username IS NOT NULL + ORDER BY r2.id DESC LIMIT 1 + ) AS username FROM requests GROUP BY user_id ORDER BY last_seen DESC @@ -267,6 +305,7 @@ class Database: return [ UserStat( user_id=int(row["user_id"]), + username=row["username"], total=int(row["total"]), last_seen=str(row["last_seen"]), ) diff --git a/queue_manager.py b/queue_manager.py index a09daf1..52f9d2e 100644 --- a/queue_manager.py +++ b/queue_manager.py @@ -100,6 +100,7 @@ class QueueManager: mode: str, prompt: str, input_file_path: str | None = None, + username: str | None = None, ) -> tuple[Request, int]: async with self._lock: if self._stopping: @@ -113,6 +114,7 @@ class QueueManager: seed = secrets.randbelow(2**32) request = await self._db.create_request( user_id=user_id, + username=username, mode=mode, prompt_text=prompt, input_file_path=input_file_path, diff --git a/tests/test_db.py b/tests/test_db.py index f1f33f1..fd15241 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -1,5 +1,7 @@ from __future__ import annotations +import sqlite3 + import pytest import pytest_asyncio @@ -113,6 +115,7 @@ async def test_recover_marks_running_failed_and_returns_queued(db: Database) -> async def test_history_filters_and_stats(db: Database) -> None: first = await db.create_request( user_id=1, + username="alice", mode="t2i", prompt_text="apple", input_file_path=None, @@ -121,6 +124,7 @@ async def test_history_filters_and_stats(db: Database) -> None: await db.set_done(first.id, "/tmp/1.png") second = await db.create_request( user_id=1, + username="alice", mode="i2i", prompt_text="banana", input_file_path="/tmp/in.png", @@ -156,3 +160,47 @@ async def test_history_filters_and_stats(db: Database) -> None: users = await db.list_users() assert {user.user_id for user in users} == {1, 2} + alice = next(user for user in users if user.user_id == 1) + assert alice.username == "alice" + assert alice.display_user == "@alice" + + +async def test_migrates_legacy_schema_without_username(tmp_path) -> None: + path = tmp_path / "legacy.db" + connection = sqlite3.connect(path) + connection.execute( + """ + CREATE TABLE requests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + timestamp TEXT NOT NULL, + prompt_text TEXT NOT NULL, + input_file_path TEXT, + output_file_path TEXT, + status TEXT NOT NULL DEFAULT 'queued' + CHECK (status IN ('queued', 'running', 'done', 'failed')), + comfy_prompt_id TEXT, + mode TEXT NOT NULL CHECK (mode IN ('t2i', 'i2i')), + seed INTEGER, + error_text TEXT, + finished_at TEXT + ) + """ + ) + connection.commit() + connection.close() + + database = Database(path) + await database.connect() + try: + request = await database.create_request( + user_id=1, + username="alice", + mode="t2i", + prompt_text="apple", + input_file_path=None, + seed=1, + ) + assert request.username == "alice" + finally: + await database.close() diff --git a/tests/test_web.py b/tests/test_web.py index 19dd8a0..ed5df56 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -28,16 +28,18 @@ def insert_request( status: str = "done", mode: str = "t2i", output: str | None = None, + username: str | None = None, ) -> int: connection = sqlite3.connect(path) try: cursor = connection.execute( """ INSERT INTO requests - (user_id, timestamp, prompt_text, status, mode, seed, output_file_path) - VALUES (?, '2026-10-18T12:00:00+00:00', ?, ?, ?, 1, ?) + (user_id, username, timestamp, prompt_text, status, mode, seed, + output_file_path) + VALUES (?, ?, '2026-10-18T12:00:00+00:00', ?, ?, ?, 1, ?) """, - (user_id, prompt, status, mode, output), + (user_id, username, prompt, status, mode, output), ) connection.commit() return int(cursor.lastrowid) @@ -49,13 +51,17 @@ def test_history_page_and_user_filter(tmp_path: Path) -> None: settings = make_settings(tmp_path) app = create_app(settings) with TestClient(app) as client: - insert_request(settings.db_path, user_id=1, prompt="apple prompt") + insert_request( + settings.db_path, user_id=1, username="alice", prompt="apple prompt" + ) insert_request(settings.db_path, user_id=2, prompt="banana prompt") response = client.get("/") assert response.status_code == 200 assert "apple prompt" in response.text assert "banana prompt" in response.text + assert "@alice" in response.text + assert 'title="ID: 2">2' in response.text response = client.get("/?user_id=1") assert response.status_code == 200 diff --git a/web/templates/detail.html b/web/templates/detail.html index 212d557..1605fed 100644 --- a/web/templates/detail.html +++ b/web/templates/detail.html @@ -6,7 +6,7 @@
Пользователь
-
{{ item.user_id }}
+
{{ item.display_user }}
Статус
{{ item.status }}
Режим
diff --git a/web/templates/index.html b/web/templates/index.html index 8d7acba..7508631 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -15,7 +15,7 @@ {% for user in users %} - + {% endfor %} @@ -76,7 +76,7 @@ {% endif %} {{ item.timestamp|fmt_dt }} - {{ item.user_id }} + {{ item.display_user }} {{ item.mode }} {{ item.status }} {{ item.prompt_text|truncate(90, True) }}