"""Entry point: settings → texts → dispatcher → long polling (ADR-002).""" import asyncio import contextlib import logging from aiogram import Bot, Dispatcher from aiogram.client.session.aiohttp import AiohttpSession from app import __version__ from app.access.service import AccessService from app.bot.fsm import make_storage from app.bot.middlewares.access import AccessMiddleware from app.bot.routers import commit, common, members 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.repo.drafts import DraftService from app.repo.git import UploadsRepo from app.repo.publisher import Publisher from app.texts import Texts, load_texts log = logging.getLogger("tgit") def build_dispatcher( texts: Texts, sessions: SessionMaker, access: AccessService, repo: UploadsRepo, commit_url_template: str = "" ) -> Dispatcher: dp = Dispatcher(storage=make_storage(sessions)) gate = AccessMiddleware(access, texts) # Outer middleware: runs before filters, so nothing — not even a stale button — reaches a handler unchecked. dp.message.outer_middleware(gate) dp.callback_query.outer_middleware(gate) dp.include_router(members.build_router(texts, access)) dp.include_router( commit.build_router(texts, repo, DraftService(sessions), Publisher(repo, sessions), commit_url_template) ) dp.include_router(common.build_router(texts, access)) return dp async def run(settings: Settings) -> None: texts = load_texts(settings.runtime.config_dir) engine = make_engine(settings.database.url) await upgrade(engine) sessions = make_sessionmaker(engine) access = AccessService(sessions, settings.access) await access.bootstrap_admins() dp = build_dispatcher(texts, sessions, access, UploadsRepo(settings.git), settings.git.commit_url_template) rt = settings.runtime background = [ asyncio.create_task(heartbeat(rt.heartbeat_file, rt.heartbeat_interval)), asyncio.create_task(cleanup_loop(sessions)), ] proxy = settings.telegram.proxy_url session = AiohttpSession(proxy=proxy.get_secret_value()) if proxy else None bot = Bot(settings.telegram.bot_token.get_secret_value(), session=session) log.info( "tgit %s starting, %d bootstrap admin(s), telegram %s", __version__, len(settings.access.admin_ids), "via proxy" if proxy else "direct", ) try: await dp.start_polling(bot, handle_signals=True) finally: 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") def main() -> None: settings = Settings() setup_logging(settings.runtime.log_level) asyncio.run(run(settings))