From f7b7e87cffc9dcb2817b070d7a003ac234c96ec3 Mon Sep 17 00:00:00 2001 From: Tolmachev Igor Date: Mon, 23 Mar 2026 18:07:30 +0300 Subject: Add new_announcement command --- libs/__init__.py | 7 ++++++ libs/fsm.py | 23 +++++++++++++++++ libs/msg.py | 25 +++++++++++++++++++ libs/storage.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+) create mode 100644 libs/__init__.py create mode 100644 libs/fsm.py create mode 100644 libs/msg.py create mode 100644 libs/storage.py (limited to 'libs') diff --git a/libs/__init__.py b/libs/__init__.py new file mode 100644 index 0000000..d8ed122 --- /dev/null +++ b/libs/__init__.py @@ -0,0 +1,7 @@ +from . import fsm, msg, storage + +__all__ = [ + "storage", + "fsm", + "msg", +] diff --git a/libs/fsm.py b/libs/fsm.py new file mode 100644 index 0000000..00ccacb --- /dev/null +++ b/libs/fsm.py @@ -0,0 +1,23 @@ +from contextlib import asynccontextmanager + +from aiogram.fsm.context import FSMContext +from pydantic.main import BaseModel +from typing_extensions import AsyncGenerator + + +async def set_data(state: FSMContext, model: BaseModel) -> None: + await state.set_data(model.model_dump()) + + +async def get_data[T: BaseModel](state: FSMContext, model_type: type[T]) -> T: + return model_type.model_validate(await state.get_data()) + + +@asynccontextmanager +async def edit_data[T: BaseModel]( + state: FSMContext, + model_type: type[T], +) -> AsyncGenerator[T]: + model = await get_data(state, model_type) + yield model + await set_data(state, model) diff --git a/libs/msg.py b/libs/msg.py new file mode 100644 index 0000000..9bcc52a --- /dev/null +++ b/libs/msg.py @@ -0,0 +1,25 @@ +import asyncio +from typing import AsyncGenerator, Iterable + +from aiogram import Bot +from aiogram.exceptions import TelegramAPIError, TelegramRetryAfter + +from models import RichText + + +async def publish( + bot: Bot, + users: Iterable[int], + rich_text: RichText, +) -> AsyncGenerator[int]: + for n, user_id in enumerate(users, start=1): + for _ in range(5): + try: + await rich_text.send(bot, user_id) + break + except TelegramRetryAfter as e: + await asyncio.sleep(e.retry_after + 1) + except TelegramAPIError: + await asyncio.sleep(5) + + yield n diff --git a/libs/storage.py b/libs/storage.py new file mode 100644 index 0000000..6220cfc --- /dev/null +++ b/libs/storage.py @@ -0,0 +1,75 @@ +from pathlib import Path +from typing import Any, Mapping + +from aiofiles import open as open +from aiogram.fsm.state import State +from aiogram.fsm.storage.base import ( + BaseStorage, + DefaultKeyBuilder, + KeyBuilder, + StateType, + StorageKey, +) +from pydantic import TypeAdapter +from pydantic.main import BaseModel + + +class Record(BaseModel): + data: dict[str, Any] = {} + state: str | None = None + + +class JsonStorage(BaseStorage): + file_path: Path + records: dict[str, Record] + records_adapter: TypeAdapter + key_builder: KeyBuilder + + def __init__(self, file_path: Path, key_builder: KeyBuilder | None = None) -> None: + self.file_path = file_path + self.records = {} + self.records_adapter = TypeAdapter(dict[str, Record]) + self.key_builder = DefaultKeyBuilder() if key_builder is None else key_builder + + async def read(self) -> None: + async with open(self.file_path, "rb") as file: + json = await file.read() + self.records = self.records_adapter.validate_json(json) + + async def flush(self) -> None: + async with open(self.file_path, "wb") as file: + json = self.records_adapter.dump_json(self.records) + await file.write(json) + + async def get_record(self, key: StorageKey) -> Record: + await self.read() + record_key = self.key_builder.build(key) + if record_key not in self.records: + self.records[record_key] = Record() + return self.records[record_key] + + async def set_state(self, key: StorageKey, state: StateType = None) -> None: + record = await self.get_record(key) + record.state = state.state if isinstance(state, State) else state + await self.flush() + + async def get_state(self, key: StorageKey) -> str | None: + record = await self.get_record(key) + return record.state + + async def set_data(self, key: StorageKey, data: Mapping[str, Any]) -> None: + if not isinstance(data, dict): + raise TypeError( + f"Data must be a dict or dict-like object, got {type(data).__name__}", + data, + ) + record = await self.get_record(key) + record.data = data.copy() + await self.flush() + + async def get_data(self, key: StorageKey) -> dict[str, Any]: + record = await self.get_record(key) + return record.data + + async def close(self) -> None: + await self.flush() -- cgit v1.3