- Publisher: пуши по одному (lock), fetch → проверка конфликта с base_sha → чистое дерево origin/main → черновик → коммит от tgit-bot с трейлером Telegram-User → push; отказ push → rebase и повтор (до 3 раз); защита от путей вне клона; лимит 20 пушей/час - «💬 Сообщение коммита» (1–200 символов, первая строка ≤ 72) и «🚀 Push» с подтверждением; исходы: запушено (sha, ссылка, ⏳ CI) · конфликт → «Перезаписать» · нечего коммитить · лимит · сбой - запись в pushes (chat_id, message_id — для обновления статуса CI/CD в #11), черновик очищается - ruff: RUF001–003 выключены — кириллица в текстах это норма Refs #9 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
76 lines
2.7 KiB
Python
76 lines
2.7 KiB
Python
"""Entry point: settings → texts → dispatcher → long polling (ADR-002)."""
|
|
|
|
import asyncio
|
|
import contextlib
|
|
import logging
|
|
|
|
from aiogram import Bot, Dispatcher
|
|
|
|
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)),
|
|
]
|
|
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:
|
|
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))
|