- AccessService: одноразовые приглашения (128 бит, в БД только sha256, TTL 24 ч), атомарный захват приглашения (условный UPDATE — из двух сканов одного QR проходит один), антиперебор (> 5 неудач в час — молчание), отзыв (черновик и FSM удаляются), повторное приглашение - ADMIN_IDS заводятся в БД при старте; дальше — только по приглашению - AccessMiddleware: участники из БД (+ last_seen раз в 5 мин), новичкам — только /start <токен> - «👥 Участники» для админов: список, QR (segno) + ссылка, отзыв с подтверждением - один ответ на неверный/истёкший/использованный токен — без подсказок Refs #25 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
68 lines
1.2 KiB
Python
68 lines
1.2 KiB
Python
"""UI texts from `config/texts.yaml`, validated at startup so a missing key fails fast instead of mid-dialog."""
|
|
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
from pydantic import BaseModel, ConfigDict
|
|
|
|
|
|
class _Strict(BaseModel):
|
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
|
|
|
|
class MenuTexts(_Strict):
|
|
commit: str
|
|
status: str
|
|
logs: str
|
|
alerts: str
|
|
members: str
|
|
back: str
|
|
cancel: str
|
|
placeholder: str
|
|
|
|
|
|
class StartTexts(_Strict):
|
|
welcome: str
|
|
no_access: str
|
|
invite_rejected: str
|
|
|
|
|
|
class HelpTexts(_Strict):
|
|
text: str
|
|
|
|
|
|
class SectionTexts(_Strict):
|
|
commit: str
|
|
status: str
|
|
logs: str
|
|
alerts: str
|
|
members: str
|
|
cancelled: str
|
|
|
|
|
|
class MembersTexts(_Strict):
|
|
title: str
|
|
line: str
|
|
never: str
|
|
invite: str
|
|
revoke: str
|
|
confirm: str
|
|
confirm_yes: str
|
|
confirm_no: str
|
|
revoked: str
|
|
not_revoked: str
|
|
invite_caption: str
|
|
|
|
|
|
class Texts(_Strict):
|
|
menu: MenuTexts
|
|
start: StartTexts
|
|
help: HelpTexts
|
|
section: SectionTexts
|
|
members: MembersTexts
|
|
|
|
|
|
def load_texts(config_dir: Path) -> Texts:
|
|
with (config_dir / "texts.yaml").open(encoding="utf-8") as f:
|
|
return Texts.model_validate(yaml.safe_load(f))
|