feat(bot): модель данных, миграции Alembic, FSM в БД, TTL-чистка
- app/db/models.py: 13 таблиц (ARCHITECTURE §2.10); перечисления — VARCHAR + CHECK, JSON → JSONB на Postgres, BIGINT/INTEGER-ключи для SQLite - миграция 0001 (автогенерация + ручная чистка: импорт sa.Text, дубли CHECK) - миграции применяются при старте бота программно (app.db.migrate), script_location из пакета - SqlStorage: FSM aiogram поверх fsm_state — состояние переживает рестарт (перенесено из #4) - cleanup_loop: истёкшие черновики, снимки контейнеров и попытки входа старше суток - тесты: upgrade/downgrade, дрейф моделей и миграций, CHECK, CASCADE, FSM, чистка Refs #5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+19
-7
@@ -11,6 +11,9 @@ from app.bot.fsm import make_storage
|
||||
from app.bot.middlewares.access import AccessMiddleware
|
||||
from app.bot.routers.common import build_router
|
||||
from app.config import Settings
|
||||
from app.db.maintenance import cleanup_loop
|
||||
from app.db.migrate import upgrade
|
||||
from app.db.session import SessionMaker, make_engine, make_sessionmaker
|
||||
from app.health import heartbeat
|
||||
from app.logs_setup import setup_logging
|
||||
from app.texts import Texts, load_texts
|
||||
@@ -18,13 +21,13 @@ from app.texts import Texts, load_texts
|
||||
log = logging.getLogger("tgit")
|
||||
|
||||
|
||||
def build_dispatcher(settings: Settings, texts: Texts) -> Dispatcher:
|
||||
def build_dispatcher(settings: Settings, texts: Texts, sessions: SessionMaker) -> Dispatcher:
|
||||
admin_ids = settings.access.admin_ids
|
||||
|
||||
async def is_member(tg_id: int) -> bool:
|
||||
return tg_id in admin_ids
|
||||
|
||||
dp = Dispatcher(storage=make_storage())
|
||||
dp = Dispatcher(storage=make_storage(sessions))
|
||||
access = AccessMiddleware(is_member, texts)
|
||||
# Outer middleware: runs before filters, so nothing — not even a stale button — reaches a handler unchecked.
|
||||
dp.message.outer_middleware(access)
|
||||
@@ -35,18 +38,27 @@ def build_dispatcher(settings: Settings, texts: Texts) -> Dispatcher:
|
||||
|
||||
async def run(settings: Settings) -> None:
|
||||
texts = load_texts(settings.runtime.config_dir)
|
||||
dp = build_dispatcher(settings, texts)
|
||||
engine = make_engine(settings.database.url)
|
||||
await upgrade(engine)
|
||||
sessions = make_sessionmaker(engine)
|
||||
dp = build_dispatcher(settings, texts, sessions)
|
||||
rt = settings.runtime
|
||||
beat = asyncio.create_task(heartbeat(rt.heartbeat_file, rt.heartbeat_interval))
|
||||
background = [
|
||||
asyncio.create_task(heartbeat(rt.heartbeat_file, rt.heartbeat_interval)),
|
||||
asyncio.create_task(cleanup_loop(sessions)),
|
||||
]
|
||||
bot = Bot(settings.telegram.bot_token.get_secret_value())
|
||||
log.info("tgit %s starting, %d bootstrap admin(s)", __version__, len(settings.access.admin_ids))
|
||||
try:
|
||||
await dp.start_polling(bot, handle_signals=True)
|
||||
finally:
|
||||
beat.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await beat
|
||||
for task in background:
|
||||
task.cancel()
|
||||
for task in background:
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await task
|
||||
await bot.session.close()
|
||||
await engine.dispose()
|
||||
log.info("tgit stopped")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user