Selaa lähdekoodia

style(anonflow): format codebase

Librellium 1 viikko sitten
vanhempi
sitoutus
d4598a9f1e
40 muutettua tiedostoa jossa 292 lisäystä ja 388 poistoa
  1. 1 0
      anonflow/__main__.py
  2. 48 36
      anonflow/app/app.py
  3. 7 18
      anonflow/app/builders/middlewares.py
  4. 2 5
      anonflow/app/builders/routers.py
  5. 1 1
      anonflow/bot/middlewares/user/__init__.py
  6. 6 2
      anonflow/bot/middlewares/user/banned.py
  7. 1 2
      anonflow/bot/middlewares/user/language.py
  8. 3 1
      anonflow/bot/middlewares/user/not_registered.py
  9. 6 2
      anonflow/bot/middlewares/user/subscription.py
  10. 10 10
      anonflow/bot/middlewares/user/throttling.py
  11. 1 5
      anonflow/bot/routers/__init__.py
  12. 8 16
      anonflow/bot/routers/media.py
  13. 3 1
      anonflow/bot/routers/start.py
  14. 3 8
      anonflow/bot/routers/text.py
  15. 1 4
      anonflow/bot/transport/__init__.py
  16. 4 0
      anonflow/bot/transport/content.py
  17. 21 32
      anonflow/bot/transport/delivery.py
  18. 18 33
      anonflow/bot/transport/router.py
  19. 3 11
      anonflow/config/config.py
  20. 1 1
      anonflow/config/models.py
  21. 1 1
      anonflow/database/__init__.py
  22. 5 3
      anonflow/database/database.py
  23. 2 4
      anonflow/database/migrations/env.py
  24. 8 14
      anonflow/database/orm.py
  25. 1 5
      anonflow/database/repositories/__init__.py
  26. 4 19
      anonflow/database/repositories/ban.py
  27. 11 21
      anonflow/database/repositories/base.py
  28. 6 31
      anonflow/database/repositories/moderator.py
  29. 5 21
      anonflow/database/repositories/user.py
  30. 4 7
      anonflow/interfaces/post.py
  31. 1 1
      anonflow/moderation/__init__.py
  32. 2 0
      anonflow/moderation/events.py
  33. 2 0
      anonflow/moderation/exceptions.py
  34. 8 3
      anonflow/moderation/executor.py
  35. 32 36
      anonflow/moderation/planner.py
  36. 10 3
      anonflow/moderation/service.py
  37. 1 0
      anonflow/services/moderator/exceptions.py
  38. 1 0
      anonflow/services/moderator/permissions.py
  39. 21 14
      anonflow/services/moderator/service.py
  40. 19 17
      anonflow/translator/translator.py

+ 1 - 0
anonflow/__main__.py

@@ -7,5 +7,6 @@ async def main():
     app = Application()
     await app.run()
 
+
 if __name__ == "__main__":
     asyncio.run(main())

+ 48 - 36
anonflow/app/app.py

@@ -6,33 +6,24 @@ from aiogram.client.bot import DefaultBotProperties
 from aiogram.fsm.storage.memory import MemoryStorage
 
 from anonflow import __version_str__, paths
-from anonflow.bot.transport import (
-    DeliveryService,
-    ResponsesRouter
-)
+from anonflow.bot.transport import DeliveryService, ResponsesRouter
 from anonflow.config import Config
 from anonflow.database import (
     BanRepository,
     Database,
     ModeratorRepository,
-    UserRepository
+    UserRepository,
 )
 from anonflow.moderation import (
     ModerationExecutor,
     ModerationPlanner,
     ModerationService,
-    RuleManager
-)
-from anonflow.services import (
-    ModeratorService,
-    UserService
+    RuleManager,
 )
+from anonflow.services import ModeratorService, UserService
 from anonflow.translator import Translator
 
-from .builders import (
-    build_middlewares,
-    build_routers
-)
+from .builders import build_middlewares, build_routers
 from .helpers import require
 
 
@@ -58,7 +49,9 @@ class Application:
 
         if not config_filepath.exists():
             Config().save(config_filepath)
-            raise RuntimeError("Config file was just created. Please fill it out and restart the application.")
+            raise RuntimeError(
+                "Config file was just created. Please fill it out and restart the application."
+            )
 
         self._config = Config.load(config_filepath)
 
@@ -76,15 +69,10 @@ class Application:
             await self._database.init()
 
             self._moderator_service = ModeratorService(
-                self._database,
-                BanRepository(),
-                ModeratorRepository()
+                self._database, BanRepository(), ModeratorRepository()
             )
             await self._moderator_service.init()
-            self._user_service = UserService(
-                self._database,
-                UserRepository()
-            )
+            self._user_service = UserService(self._database, UserRepository())
 
     def _init_bot(self):
         with require(self, "_config") as config:
@@ -94,7 +82,7 @@ class Application:
 
             self._bot = Bot(
                 token=bot_token.get_secret_value(),
-                default=DefaultBotProperties(parse_mode="HTML")
+                default=DefaultBotProperties(parse_mode="HTML"),
             )
             self._dispatcher = Dispatcher(storage=MemoryStorage())
 
@@ -102,18 +90,23 @@ class Application:
         self._translator = Translator(translations_dir=paths.TRANSLATIONS_DIR)
 
     def _init_transport(self):
-        with require(
-            self, "_bot", "_config", "_translator"
-        ) as (bot, config, translator):
+        with require(self, "_bot", "_config", "_translator") as (
+            bot,
+            config,
+            translator,
+        ):
             self._responses_router = ResponsesRouter(
                 moderation_chat_ids=config.forwarding.moderation_chat_ids,
                 publication_channel_ids=config.forwarding.publication_channel_ids,
                 delivery_service=DeliveryService(bot),
-                translator=translator
+                translator=translator,
             )
 
     def _init_moderation(self):
-        with require(self, "_config", "_responses_router") as (config, responses_router):
+        with require(self, "_config", "_responses_router") as (
+            config,
+            responses_router,
+        ):
             self._rule_manager = RuleManager(rules_dir=paths.RULES_DIR)
             self._rule_manager.reload()
 
@@ -132,20 +125,32 @@ class Application:
                 base_url=str(base_url) if base_url else None,
                 proxy=str(proxy) if proxy else None,
                 timeout=config.openai.timeout,
-                max_retries=config.openai.max_retries
+                max_retries=config.openai.max_retries,
             )
             self._moderation_planner.set_enabled(config.moderation.enabled)
             self._moderation_executor = ModerationExecutor(self._moderation_planner)
 
             self._moderation_service = ModerationService(
-                responses_router,
-                self._moderation_executor
+                responses_router, self._moderation_executor
             )
 
     def _init_routers(self):
         with require(
-            self, "_dispatcher", "_config", "_responses_router", "_user_service", "_moderator_service", "_moderation_service"
-        ) as (dispatcher, config, responses_router, user_service, moderator_service, moderation_service):
+            self,
+            "_dispatcher",
+            "_config",
+            "_responses_router",
+            "_user_service",
+            "_moderator_service",
+            "_moderation_service",
+        ) as (
+            dispatcher,
+            config,
+            responses_router,
+            user_service,
+            moderator_service,
+            moderation_service,
+        ):
             dispatcher.include_router(
                 build_routers(
                     config=config,
@@ -158,13 +163,18 @@ class Application:
 
     def _init_middleware(self):
         with require(
-            self, "_dispatcher", "_config", "_responses_router", "_user_service", "_moderator_service"
+            self,
+            "_dispatcher",
+            "_config",
+            "_responses_router",
+            "_user_service",
+            "_moderator_service",
         ) as (dispatcher, config, responses_router, user_service, moderator_service):
             middlewares = build_middlewares(
                 config=config,
                 responses_router=responses_router,
                 user_service=user_service,
-                moderator_service=moderator_service
+                moderator_service=moderator_service,
             )
 
             for middleware in middlewares:
@@ -193,7 +203,9 @@ class Application:
                 await self._moderation_planner.close()
             raise
 
-        self._logger.info(f"Anonflow v{__version_str__} has been successfully initialized.")
+        self._logger.info(
+            f"Anonflow v{__version_str__} has been successfully initialized."
+        )
 
         with require(
             self, "_bot", "_dispatcher", "_database", "_moderation_planner"

+ 7 - 18
anonflow/app/builders/middlewares.py

@@ -4,7 +4,7 @@ from anonflow.bot.middlewares.user import (
     UserLanguageMiddleware,
     UserNotRegisteredMiddleware,
     UserSubscriptionMiddleware,
-    UserThrottlingMiddleware
+    UserThrottlingMiddleware,
 )
 from anonflow.bot.transport import ResponsesRouter
 from anonflow.config import Config
@@ -19,20 +19,13 @@ def build_middlewares(
 ):
     middlewares = []
 
-    middlewares.append(
-        UserContextMiddleware(
-            user_service=user_service
-        )
-    )
+    middlewares.append(UserContextMiddleware(user_service=user_service))
 
-    middlewares.append(
-        UserLanguageMiddleware()
-    )
+    middlewares.append(UserLanguageMiddleware())
 
     middlewares.append(
         UserBannedMiddleware(
-            responses_port=responses_router,
-            moderator_service=moderator_service
+            responses_port=responses_router, moderator_service=moderator_service
         )
     )
 
@@ -40,22 +33,18 @@ def build_middlewares(
         middlewares.append(
             UserSubscriptionMiddleware(
                 responses_port=responses_router,
-                channel_ids=config.behavior.subscription_requirement.channel_ids
+                channel_ids=config.behavior.subscription_requirement.channel_ids,
             )
         )
 
-    middlewares.append(
-        UserNotRegisteredMiddleware(
-            responses_port=responses_router
-        )
-    )
+    middlewares.append(UserNotRegisteredMiddleware(responses_port=responses_router))
 
     if config.behavior.throttling.enabled:
         middlewares.append(
             UserThrottlingMiddleware(
                 responses_port=responses_router,
                 delay=config.behavior.throttling.delay,
-                allowed_chat_ids=config.forwarding.moderation_chat_ids
+                allowed_chat_ids=config.forwarding.moderation_chat_ids,
             )
         )
 

+ 2 - 5
anonflow/app/builders/routers.py

@@ -17,14 +17,11 @@ def build_routers(
     main_router = Router()
 
     routers = [
-        StartRouter(
-            responses_port=responses_router,
-            user_service=user_service
-        ),
+        StartRouter(responses_port=responses_router, user_service=user_service),
         TextRouter(
             responses_port=responses_router,
             moderation_service=moderation_service,
-            forwarding_types=config.forwarding.types
+            forwarding_types=config.forwarding.types,
         ),
         MediaRouter(
             responses_port=responses_router,

+ 1 - 1
anonflow/bot/middlewares/user/__init__.py

@@ -11,5 +11,5 @@ __all__ = [
     "UserLanguageMiddleware",
     "UserNotRegisteredMiddleware",
     "UserSubscriptionMiddleware",
-    "UserThrottlingMiddleware"
+    "UserThrottlingMiddleware",
 ]

+ 6 - 2
anonflow/bot/middlewares/user/banned.py

@@ -7,7 +7,9 @@ from anonflow.services import ModeratorService
 
 
 class UserBannedMiddleware(BaseMiddleware):
-    def __init__(self, responses_port: UserResponsesPort, moderator_service: ModeratorService):
+    def __init__(
+        self, responses_port: UserResponsesPort, moderator_service: ModeratorService
+    ):
         super().__init__()
 
         self._responses_port = responses_port
@@ -17,7 +19,9 @@ class UserBannedMiddleware(BaseMiddleware):
         message = getattr(event, "message", None)
         if isinstance(message, Message):
             if await self._moderator_service.is_banned(message.chat.id):
-                await self._responses_port.user_banned(RequestContext(message.chat.id, data["user_language"]))
+                await self._responses_port.user_banned(
+                    RequestContext(message.chat.id, data["user_language"])
+                )
                 return
 
         return await handler(event, data)

+ 1 - 2
anonflow/bot/middlewares/user/language.py

@@ -13,8 +13,7 @@ class UserLanguageMiddleware(BaseMiddleware):
         if isinstance(message, Message) and message.from_user:
             user = data.get("user")
             data["user_language"] = (
-                user.language
-                if user else message.from_user.language_code
+                user.language if user else message.from_user.language_code
             )
 
         return await handler(event, data)

+ 3 - 1
anonflow/bot/middlewares/user/not_registered.py

@@ -19,7 +19,9 @@ class UserNotRegisteredMiddleware(BaseMiddleware):
 
             is_user_exists = data.get("user") is not None
             if not is_user_exists and not text.startswith("/start"):
-                await self._responses_port.user_not_registered(RequestContext(message.chat.id, data["user_language"]))
+                await self._responses_port.user_not_registered(
+                    RequestContext(message.chat.id, data["user_language"])
+                )
                 return
 
         return await handler(event, data)

+ 6 - 2
anonflow/bot/middlewares/user/subscription.py

@@ -9,7 +9,9 @@ from anonflow.interfaces import UserResponsesPort
 
 
 class UserSubscriptionMiddleware(BaseMiddleware):
-    def __init__(self, responses_port: UserResponsesPort, channel_ids: Iterable[ChatIdUnion]):
+    def __init__(
+        self, responses_port: UserResponsesPort, channel_ids: Iterable[ChatIdUnion]
+    ):
         super().__init__()
 
         self._responses_port = responses_port
@@ -27,7 +29,9 @@ class UserSubscriptionMiddleware(BaseMiddleware):
             for channel_id in self._channel_ids:
                 member = await message.bot.get_chat_member(channel_id, user_id)
                 if member.status in (ChatMemberStatus.KICKED, ChatMemberStatus.LEFT):
-                    await self._responses_port.user_subscription_required(RequestContext(message.chat.id, data["user_language"]))
+                    await self._responses_port.user_subscription_required(
+                        RequestContext(message.chat.id, data["user_language"])
+                    )
                     return
 
         return await handler(event, data)

+ 10 - 10
anonflow/bot/middlewares/user/throttling.py

@@ -14,7 +14,7 @@ class UserThrottlingMiddleware(BaseMiddleware):
         self,
         responses_port: UserResponsesPort,
         delay: float,
-        allowed_chat_ids: Optional[Iterable[ChatIdUnion]] = None
+        allowed_chat_ids: Optional[Iterable[ChatIdUnion]] = None,
     ):
         super().__init__()
 
@@ -29,17 +29,16 @@ class UserThrottlingMiddleware(BaseMiddleware):
 
     async def __call__(self, handler, event, data):
         message = getattr(event, "message", None)
-        if (
-            isinstance(message, Message)
-            and (
-                self._allowed_chat_ids is not None
-                and message.chat.id not in self._allowed_chat_ids
-            )
+        if isinstance(message, Message) and (
+            self._allowed_chat_ids is not None
+            and message.chat.id not in self._allowed_chat_ids
         ):
             text = message.text or message.caption or ""
             if not text.startswith("/"):
                 async with self._lock:
-                    user_lock = self._user_locks.setdefault(message.chat.id, asyncio.Lock())
+                    user_lock = self._user_locks.setdefault(
+                        message.chat.id, asyncio.Lock()
+                    )
 
                 if user_lock.locked():
                     start_time = self._user_times.get(message.chat.id) or 0
@@ -49,8 +48,9 @@ class UserThrottlingMiddleware(BaseMiddleware):
                         RequestContext(message.chat.id, data["user_language"]),
                         remaining_time=(
                             round(self._delay - (current_time - start_time))
-                            if start_time else 0
-                        )
+                            if start_time
+                            else 0
+                        ),
                     )
                     return
 

+ 1 - 5
anonflow/bot/routers/__init__.py

@@ -2,8 +2,4 @@ from .media import MediaRouter
 from .start import StartRouter
 from .text import TextRouter
 
-__all__ = [
-    "MediaRouter",
-    "StartRouter",
-    "TextRouter"
-]
+__all__ = ["MediaRouter", "StartRouter", "TextRouter"]

+ 8 - 16
anonflow/bot/routers/media.py

@@ -12,11 +12,7 @@ from aiogram.types import Message
 from anonflow.config.models import ForwardingType
 from anonflow.interfaces import PostResponsesPort
 from anonflow.moderation import ModerationService
-from anonflow.bot.transport.content import (
-    ContentGroup,
-    ContentMediaItem,
-    MediaType
-)
+from anonflow.bot.transport.content import ContentGroup, ContentMediaItem, MediaType
 from anonflow.bot.transport.types import RequestContext
 
 
@@ -25,7 +21,7 @@ class MediaRouter(Router):
         self,
         responses_port: PostResponsesPort,
         moderation_service: ModerationService,
-        forwarding_types: FrozenSet[ForwardingType]
+        forwarding_types: FrozenSet[ForwardingType],
     ):
         super().__init__()
 
@@ -50,8 +46,8 @@ class MediaRouter(Router):
 
     def _can_send_media(self, msgs: List[Message]):
         return any(
-            (msg.photo and "photo" in self._forwarding_types) or
-            (msg.video and "video" in self._forwarding_types)
+            (msg.photo and "photo" in self._forwarding_types)
+            or (msg.video and "video" in self._forwarding_types)
             for msg in msgs
         )
 
@@ -76,18 +72,14 @@ class MediaRouter(Router):
 
         for message in messages:
             is_approved = await self._moderation_service.process(
-                context,
-                message.caption,
-                await self.get_b64image(message)
+                context, message.caption, await self.get_b64image(message)
             )
 
             media = self._get_media(message)
             if media:
                 content_group.append(ContentMediaItem(**media, caption=caption))
 
-        await self._responses_port.post_prepared(
-            context, content_group, is_approved
-        )
+        await self._responses_port.post_prepared(context, content_group, is_approved)
 
     async def _on_media(self, message: Message, user_language: str):
         if message.chat.type != ChatType.PRIVATE:
@@ -99,8 +91,8 @@ class MediaRouter(Router):
             with suppress(CancelledError):
                 await asyncio.sleep(2)
                 async with self._media_groups_lock:
-                    messages = self.media_groups.pop(media_group_id, []) # type: ignore
-                    self.media_groups_tasks.pop(media_group_id, None) # type: ignore
+                    messages = self.media_groups.pop(media_group_id, [])  # type: ignore
+                    self.media_groups_tasks.pop(media_group_id, None)  # type: ignore
 
                 await self._process_messages(messages, user_language)
 

+ 3 - 1
anonflow/bot/routers/start.py

@@ -17,7 +17,9 @@ class StartRouter(Router):
     async def _on_start(self, message: Message, user_language: str):
         if message.from_user:
             await self._user_service.add(message.from_user.id)
-        await self._responses_port.user_start(RequestContext(message.chat.id, user_language))
+        await self._responses_port.user_start(
+            RequestContext(message.chat.id, user_language)
+        )
 
     def setup(self):
         self.message.register(self._on_start, CommandStart())

+ 3 - 8
anonflow/bot/routers/text.py

@@ -16,7 +16,7 @@ class TextRouter(Router):
         self,
         responses_port: PostResponsesPort,
         moderation_service: ModerationService,
-        forwarding_types: FrozenSet[ForwardingType]
+        forwarding_types: FrozenSet[ForwardingType],
     ):
         super().__init__()
 
@@ -25,15 +25,10 @@ class TextRouter(Router):
         self._forwarding_types = forwarding_types
 
     async def _on_text(self, message: Message, user_language: str):
-        if (
-            message.chat.type == ChatType.PRIVATE
-            and "text" in self._forwarding_types
-        ):
+        if message.chat.type == ChatType.PRIVATE and "text" in self._forwarding_types:
             context = RequestContext(message.chat.id, user_language)
 
-            is_approved = await self._moderation_service.process(
-                context, message.text
-            )
+            is_approved = await self._moderation_service.process(context, message.text)
 
             await self._responses_port.post_prepared(
                 context, ContentTextItem(message.text or ""), is_approved

+ 1 - 4
anonflow/bot/transport/__init__.py

@@ -1,7 +1,4 @@
 from .delivery import DeliveryService
 from .router import ResponsesRouter
 
-__all__ = [
-    "DeliveryService",
-    "ResponsesRouter"
-]
+__all__ = ["DeliveryService", "ResponsesRouter"]

+ 4 - 0
anonflow/bot/transport/content.py

@@ -8,11 +8,13 @@ class MediaType(str, Enum):
     PHOTO = "photo"
     VIDEO = "video"
 
+
 @dataclass
 class ContentItem(ABC):
     @abstractmethod
     def translate(self, translator: Callable): ...
 
+
 @dataclass
 class ContentTextItem(ContentItem):
     text: str
@@ -20,6 +22,7 @@ class ContentTextItem(ContentItem):
     def translate(self, translator: Callable):
         self.text = translator(self.text)
 
+
 @dataclass
 class ContentMediaItem(ContentItem):
     type: MediaType
@@ -29,6 +32,7 @@ class ContentMediaItem(ContentItem):
     def translate(self, translator: Callable):
         self.caption = translator(self.caption)
 
+
 class ContentGroup(list):
     def __init__(self, items: Optional[Iterable[ContentItem]] = None):
         return super().__init__(items or [])

+ 21 - 32
anonflow/bot/transport/delivery.py

@@ -7,15 +7,10 @@ from aiogram.types import (
     InputMediaPhoto,
     InputMediaVideo,
     MediaUnion,
-    ReplyMarkupUnion
+    ReplyMarkupUnion,
 )
 
-from .content import (
-    ContentGroup,
-    ContentItem,
-    ContentTextItem,
-    MediaType
-)
+from .content import ContentGroup, ContentItem, ContentTextItem, MediaType
 
 
 class DeliveryService:
@@ -23,11 +18,17 @@ class DeliveryService:
         self._bot = bot
 
     @staticmethod
-    def _wrap_content_item(item, parse_mode: Optional[Union[str, Default]] = Default("parse_mode")):
+    def _wrap_content_item(
+        item, parse_mode: Optional[Union[str, Default]] = Default("parse_mode")
+    ):
         if item.type == MediaType.PHOTO:
-            return InputMediaPhoto(media=item.file_id, caption=item.caption, parse_mode=parse_mode)
+            return InputMediaPhoto(
+                media=item.file_id, caption=item.caption, parse_mode=parse_mode
+            )
         elif item.type == MediaType.VIDEO:
-            return InputMediaVideo(media=item.file_id, caption=item.caption, parse_mode=parse_mode)
+            return InputMediaVideo(
+                media=item.file_id, caption=item.caption, parse_mode=parse_mode
+            )
         else:
             raise ValueError("Media item type is invalid.")
 
@@ -39,30 +40,24 @@ class DeliveryService:
         chat_id: ChatIdUnion,
         content: Union[ContentItem, ContentGroup],
         parse_mode: Optional[Union[str, Default]] = Default("parse_mode"),
-        reply_markup: Optional[ReplyMarkupUnion] = None
+        reply_markup: Optional[ReplyMarkupUnion] = None,
     ):
         if isinstance(content, ContentTextItem):
             await self.send_text(
-                chat_id,
-                content.text,
-                parse_mode=parse_mode,
-                reply_markup=reply_markup
+                chat_id, content.text, parse_mode=parse_mode, reply_markup=reply_markup
             )
         elif isinstance(content, ContentGroup):
             if len(content) > 1:
                 await self.send_media_group(
                     chat_id,
-                    [
-                        self._wrap_content_item(item, parse_mode)
-                        for item in content
-                    ]
+                    [self._wrap_content_item(item, parse_mode) for item in content],
                 )
             elif len(content) == 1:
                 await self.send_media(
                     chat_id,
                     self._wrap_content_item(content[0]),
                     parse_mode=parse_mode,
-                    reply_markup=reply_markup
+                    reply_markup=reply_markup,
                 )
 
     async def send_media(
@@ -70,7 +65,7 @@ class DeliveryService:
         chat_id: ChatIdUnion,
         media: MediaUnion,
         parse_mode: Optional[Union[str, Default]] = Default("parse_mode"),
-        reply_markup: Optional[ReplyMarkupUnion] = None
+        reply_markup: Optional[ReplyMarkupUnion] = None,
     ):
         if isinstance(media, InputMediaPhoto):
             return await self._bot.send_photo(
@@ -78,7 +73,7 @@ class DeliveryService:
                 media.media,
                 caption=media.caption,
                 parse_mode=parse_mode,
-                reply_markup=reply_markup
+                reply_markup=reply_markup,
             )
         elif isinstance(media, InputMediaVideo):
             return await self._bot.send_video(
@@ -86,7 +81,7 @@ class DeliveryService:
                 media.media,
                 caption=media.caption,
                 parse_mode=parse_mode,
-                reply_markup=reply_markup
+                reply_markup=reply_markup,
             )
 
     async def send_media_group(
@@ -94,21 +89,15 @@ class DeliveryService:
         chat_id: ChatIdUnion,
         media_group: List[MediaUnion],
     ):
-        return await self._bot.send_media_group(
-            chat_id=chat_id,
-            media=media_group
-        )
+        return await self._bot.send_media_group(chat_id=chat_id, media=media_group)
 
     async def send_text(
         self,
         chat_id: ChatIdUnion,
         text: str,
         parse_mode: Optional[Union[str, Default]] = Default("parse_mode"),
-        reply_markup: Optional[ReplyMarkupUnion] = None
+        reply_markup: Optional[ReplyMarkupUnion] = None,
     ):
         return await self._bot.send_message(
-            chat_id=chat_id,
-            text=text,
-            parse_mode=parse_mode,
-            reply_markup=reply_markup
+            chat_id=chat_id, text=text, parse_mode=parse_mode, reply_markup=reply_markup
         )

+ 18 - 33
anonflow/bot/transport/router.py

@@ -17,7 +17,7 @@ class ResponsesRouter(PostResponsesPort, UserResponsesPort):
         moderation_chat_ids: Tuple[ChatIdUnion],
         publication_channel_ids: Tuple[ChatIdUnion],
         delivery_service: DeliveryService,
-        translator: Translator
+        translator: Translator,
     ):
         self._moderation_chat_ids = moderation_chat_ids
         self._publication_channel_ids = publication_channel_ids
@@ -25,10 +25,7 @@ class ResponsesRouter(PostResponsesPort, UserResponsesPort):
         self._translator = translator
 
     async def post_moderation_decision(
-        self,
-        context: RequestContext,
-        is_approved: bool,
-        reason: str
+        self, context: RequestContext, is_approved: bool, reason: str
     ):
         _ = await self._translator.get(context.user_language)
         for chat_id in self._moderation_chat_ids:
@@ -38,7 +35,7 @@ class ResponsesRouter(PostResponsesPort, UserResponsesPort):
                     _(
                         "messages.staff.moderation_approved",
                         reason=reason,
-                    )
+                    ),
                 )
             else:
                 await self._delivery_service.send_text(
@@ -46,78 +43,66 @@ class ResponsesRouter(PostResponsesPort, UserResponsesPort):
                     _(
                         "messages.staff.moderation_rejected",
                         reason=reason,
-                    )
+                    ),
                 )
 
         if not is_approved:
             await self._delivery_service.send_text(
-                context.chat_id,
-                _("messages.user.moderation_rejected")
+                context.chat_id, _("messages.user.moderation_rejected")
             )
 
     async def post_moderation_started(self, context: RequestContext):
         _ = await self._translator.get(context.user_language)
         await self._delivery_service.send_text(
-            context.chat_id,
-            _("messages.user.moderation_started")
+            context.chat_id, _("messages.user.moderation_started")
         )
 
     async def post_prepared(
         self,
         context: RequestContext,
         content: Union[ContentItem, ContentGroup],
-        is_approved: bool
+        is_approved: bool,
     ):
         _ = await self._translator.get(context.user_language)
 
         chat_ids = (
             chain(self._moderation_chat_ids, self._publication_channel_ids)
-            if is_approved else iter(self._moderation_chat_ids)
+            if is_approved
+            else iter(self._moderation_chat_ids)
         )
 
-        translator = lambda t: _(
-            "messages.channel.post",
-            text=t
-        )
-        content.translate(translator)
+        content.translate(lambda t: _("messages.channel.post", text=t))
 
         for chat_id in chat_ids:
-            await self._delivery_service.send_content(
-                chat_id, content
-            )
+            await self._delivery_service.send_content(chat_id, content)
 
         if is_approved:
             await self._delivery_service.send_text(
-                context.chat_id,
-                _("messages.user.moderation_approved")
+                context.chat_id, _("messages.user.moderation_approved")
             )
 
     async def user_banned(self, context: RequestContext):
         _ = await self._translator.get(context.user_language)
         await self._delivery_service.send_text(
-            context.chat_id,
-            _("messages.user.banned")
+            context.chat_id, _("messages.user.banned")
         )
 
     async def user_not_registered(self, context: RequestContext):
         _ = await self._translator.get(context.user_language)
         await self._delivery_service.send_text(
-            context.chat_id,
-            _("messages.user.not_registered")
+            context.chat_id, _("messages.user.not_registered")
         )
 
     async def user_start(self, context: RequestContext):
         _ = await self._translator.get(context.user_language)
         await self._delivery_service.send_text(
-            context.chat_id,
-            _("messages.user.command_start")
+            context.chat_id, _("messages.user.command_start")
         )
 
     async def user_subscription_required(self, context: RequestContext):
         _ = await self._translator.get(context.user_language)
         await self._delivery_service.send_text(
-            context.chat_id,
-            _("messages.user.subscription_required")
+            context.chat_id, _("messages.user.subscription_required")
         )
 
     async def user_throttled(self, context: RequestContext, remaining_time: int):
@@ -127,6 +112,6 @@ class ResponsesRouter(PostResponsesPort, UserResponsesPort):
             _(
                 "messages.user.throttled",
                 n=remaining_time,
-                remaining_time=remaining_time
-            )
+                remaining_time=remaining_time,
+            ),
         )

+ 3 - 11
anonflow/config/config.py

@@ -6,15 +6,7 @@ from dotenv import dotenv_values
 from pydantic import BaseModel, SecretStr
 from sqlalchemy.engine import URL
 
-from .models import (
-    Behavior,
-    Bot,
-    Database,
-    Forwarding,
-    Logging,
-    Moderation,
-    OpenAI
-)
+from .models import Behavior, Bot, Database, Forwarding, Logging, Moderation, OpenAI
 
 
 class Config(BaseModel):
@@ -73,7 +65,7 @@ class Config(BaseModel):
                 template = Template(f.read())
                 rendered = template.safe_substitute(dotenv_values())
                 data = yaml.safe_load(rendered) or {}
-            return cls(**data) # type: ignore
+            return cls(**data)  # type: ignore
 
         return cls()
 
@@ -97,5 +89,5 @@ class Config(BaseModel):
                 config_file,
                 width=float("inf"),
                 sort_keys=False,
-                default_flow_style=False
+                default_flow_style=False,
             )

+ 1 - 1
anonflow/config/models.py

@@ -28,7 +28,7 @@ class BehaviorSubscriptionRequirement(BaseModel):
 
 class Behavior(BaseModel):
     throttling: BehaviorThrottling = BehaviorThrottling()
-    subscription_requirement: BehaviorSubscriptionRequirement = BehaviorSubscriptionRequirement()
+    subscription_requirement: BehaviorSubscriptionRequirement = BehaviorSubscriptionRequirement()  # fmt: skip
     model_config = {"frozen": True}
 
 

+ 1 - 1
anonflow/database/__init__.py

@@ -9,5 +9,5 @@ __all__ = [
     "User",
     "BanRepository",
     "ModeratorRepository",
-    "UserRepository"
+    "UserRepository",
 ]

+ 5 - 3
anonflow/database/database.py

@@ -14,12 +14,14 @@ class Database:
 
         self._engine = create_async_engine(self.url, echo=echo)
         self._session_maker = sessionmaker(
-            self._engine, expire_on_commit=False, class_=AsyncSession # type: ignore
+            self._engine,  # type: ignore
+            expire_on_commit=False,
+            class_=AsyncSession,
         )
 
     @asynccontextmanager
     async def begin_session(self) -> AsyncGenerator[AsyncSession, None]:
-        async with self._session_maker() as session: # type: ignore
+        async with self._session_maker() as session:  # type: ignore
             async with session.begin():
                 yield session
 
@@ -27,7 +29,7 @@ class Database:
         await self._engine.dispose()
 
     def get_session(self) -> AsyncSession:
-        return self._session_maker() # type: ignore
+        return self._session_maker()  # type: ignore
 
     async def init(self):
         async with self._engine.begin() as conn:

+ 2 - 4
anonflow/database/migrations/env.py

@@ -29,7 +29,7 @@ target_metadata = Base.metadata
 app_config = Config.load(paths.CONFIG_FILEPATH)
 config.set_main_option(
     "sqlalchemy.url",
-    app_config.get_migrations_url().render_as_string(hide_password=False)
+    app_config.get_migrations_url().render_as_string(hide_password=False),
 )
 
 
@@ -71,9 +71,7 @@ def run_migrations_online() -> None:
     )
 
     with connectable.connect() as connection:
-        context.configure(
-            connection=connection, target_metadata=target_metadata
-        )
+        context.configure(connection=connection, target_metadata=target_metadata)
 
         with context.begin_transaction():
             context.run_migrations()

+ 8 - 14
anonflow/database/orm.py

@@ -1,12 +1,4 @@
-from sqlalchemy import (
-    Boolean,
-    Column,
-    DateTime,
-    ForeignKey,
-    Integer,
-    String,
-    func
-)
+from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, func
 from sqlalchemy.orm import relationship
 
 from .base import Base
@@ -20,18 +12,18 @@ class Ban(Base):
         Integer,
         ForeignKey("users.user_id", ondelete="CASCADE"),
         index=True,
-        nullable=False
+        nullable=False,
     )
     is_active = Column(Boolean, nullable=False, default=True)
 
     banned_at = Column(
         DateTime(timezone=True),
         server_default=func.now(),
-        nullable=False
+        nullable=False,
     )
     unbanned_at = Column(
         DateTime(timezone=True),
-        nullable=True
+        nullable=True,
     )
 
     banned_by = Column(Integer, ForeignKey("moderators.user_id"))
@@ -39,6 +31,7 @@ class Ban(Base):
 
     user = relationship("User", back_populates="bans")
 
+
 class Moderator(Base):
     __tablename__ = "moderators"
 
@@ -52,6 +45,7 @@ class Moderator(Base):
 
     user = relationship("User", back_populates="moderator")
 
+
 class User(Base):
     __tablename__ = "users"
 
@@ -62,11 +56,11 @@ class User(Base):
         "Moderator",
         uselist=False,
         back_populates="user",
-        cascade="all, delete-orphan"
+        cascade="all, delete-orphan",
     )
     bans = relationship(
         "Ban",
         back_populates="user",
         cascade="all, delete-orphan",
-        order_by="Ban.banned_at.desc()"
+        order_by="Ban.banned_at.desc()",
     )

+ 1 - 5
anonflow/database/repositories/__init__.py

@@ -2,8 +2,4 @@ from .ban import BanRepository
 from .moderator import ModeratorRepository
 from .user import UserRepository
 
-__all__ = [
-    "BanRepository",
-    "ModeratorRepository",
-    "UserRepository"
-]
+__all__ = ["BanRepository", "ModeratorRepository", "UserRepository"]

+ 4 - 19
anonflow/database/repositories/ban.py

@@ -6,33 +6,18 @@ from anonflow.database.orm import Ban
 
 class BanRepository:
     async def ban(self, session: AsyncSession, actor_user_id: int, user_id: int):
-        ban = Ban(
-            user_id=user_id,
-            banned_by=actor_user_id
-        )
+        ban = Ban(user_id=user_id, banned_by=actor_user_id)
         session.add(ban)
 
     async def is_banned(self, session: AsyncSession, user_id: int):
         result = await session.execute(
-            select(Ban)
-            .where(
-                Ban.user_id == user_id,
-                Ban.is_active.is_(True)
-            )
-            .limit(1)
+            select(Ban).where(Ban.user_id == user_id, Ban.is_active.is_(True)).limit(1)
         )
         return bool(result.scalar_one_or_none())
 
     async def unban(self, session: AsyncSession, actor_user_id: int, user_id: int):
         await session.execute(
             update(Ban)
-            .where(
-                Ban.user_id == user_id,
-                Ban.is_active.is_(True)
-            )
-            .values(
-                is_active=False,
-                unbanned_at=func.now(),
-                unbanned_by=actor_user_id
-            )
+            .where(Ban.user_id == user_id, Ban.is_active.is_(True))
+            .values(is_active=False, unbanned_at=func.now(), unbanned_by=actor_user_id)
         )

+ 11 - 21
anonflow/database/repositories/base.py

@@ -8,43 +8,33 @@ class BaseRepository:
     model: Type[Any]
 
     def __init__(self):
-        self._column_names = frozenset(
-            c.name for c in inspect(self.model).columns
-        )
+        self._column_names = frozenset(c.name for c in inspect(self.model).columns)
 
     async def _add(self, session: AsyncSession, model_args: Dict[str, Any]):
         obj = self.model(**model_args)
         session.add(obj)
 
-    async def _get(self, session: AsyncSession, filters: Dict[str, Any], options: List[Any] = []):
+    async def _get(
+        self, session: AsyncSession, filters: Dict[str, Any], options: List[Any] = []
+    ):
         result = await session.execute(
-            select(self.model)
-            .options(*options)
-            .filter_by(**filters)
+            select(self.model).options(*options).filter_by(**filters)
         )
         return result.scalar_one_or_none()
 
     async def _has(self, session: AsyncSession, filters: Dict[str, Any]):
         result = await session.execute(
-            select(1)
-            .select_from(self.model)
-            .filter_by(**filters)
-            .limit(1)
+            select(1).select_from(self.model).filter_by(**filters).limit(1)
         )
         return bool(result.scalar_one_or_none())
 
     async def _remove(self, session: AsyncSession, filters: Dict[str, Any]):
-        await session.execute(
-            delete(self.model)
-            .filter_by(**filters)
-        )
+        await session.execute(delete(self.model).filter_by(**filters))
 
-    async def _update(self, session: AsyncSession, filters: Dict[str, Any], fields: Dict[str, Any]):
+    async def _update(
+        self, session: AsyncSession, filters: Dict[str, Any], fields: Dict[str, Any]
+    ):
         if not fields:
             return
 
-        await session.execute(
-            update(self.model)
-            .filter_by(**filters)
-            .values(**fields)
-        )
+        await session.execute(update(self.model).filter_by(**filters).values(**fields))

+ 6 - 31
anonflow/database/repositories/moderator.py

@@ -11,44 +11,19 @@ from .base import BaseRepository
 class ModeratorRepository(BaseRepository):
     model = Moderator
 
-    async def add(
-        self,
-        session: AsyncSession,
-        user_id: int,
-        **fields
-    ):
-        await super()._add(
-            session,
-            model_args={
-                "user_id": user_id,
-                **fields
-            }
-        )
+    async def add(self, session: AsyncSession, user_id: int, **fields):
+        await super()._add(session, model_args={"user_id": user_id, **fields})
 
     async def get(self, session: AsyncSession, user_id: int) -> Optional[Moderator]:
         return await super()._get(
-            session,
-            filters={"user_id": user_id},
-            options=[
-                joinedload(Moderator.user)
-            ]
+            session, filters={"user_id": user_id}, options=[joinedload(Moderator.user)]
         )
 
     async def has(self, session: AsyncSession, user_id: int):
-        return await super()._has(
-            session,
-            filters={"user_id": user_id}
-        )
+        return await super()._has(session, filters={"user_id": user_id})
 
     async def remove(self, session: AsyncSession, user_id: int):
-        await super()._remove(
-            session,
-            filters={"user_id": user_id}
-        )
+        await super()._remove(session, filters={"user_id": user_id})
 
     async def update(self, session: AsyncSession, user_id: int, **fields):
-        await super()._update(
-            session,
-            filters={"user_id": user_id},
-            fields=fields
-        )
+        await super()._update(session, filters={"user_id": user_id}, fields=fields)

+ 5 - 21
anonflow/database/repositories/user.py

@@ -12,36 +12,20 @@ class UserRepository(BaseRepository):
     model = User
 
     async def add(self, session: AsyncSession, user_id: int):
-        await super()._add(
-            session,
-            model_args={"user_id": user_id}
-        )
+        await super()._add(session, model_args={"user_id": user_id})
 
     async def get(self, session: AsyncSession, user_id: int) -> Optional[User]:
         return await super()._get(
             session,
             filters={"user_id": user_id},
-            options=[
-                selectinload(User.bans),
-                joinedload(User.moderator)
-            ]
+            options=[selectinload(User.bans), joinedload(User.moderator)],
         )
 
     async def has(self, session: AsyncSession, user_id: int):
-        return await super()._has(
-            session,
-            filters={"user_id": user_id}
-        )
+        return await super()._has(session, filters={"user_id": user_id})
 
     async def remove(self, session: AsyncSession, user_id: int):
-        await super()._remove(
-            session,
-            filters={"user_id": user_id}
-        )
+        await super()._remove(session, filters={"user_id": user_id})
 
     async def update(self, session: AsyncSession, user_id: int, **fields):
-        await super()._update(
-            session,
-            filters={"user_id": user_id},
-            fields=fields
-        )
+        await super()._update(session, filters={"user_id": user_id}, fields=fields)

+ 4 - 7
anonflow/interfaces/post.py

@@ -1,13 +1,10 @@
 from typing import Protocol, Union
 
-from anonflow.bot.transport.content import (
-    ContentGroup,
-    ContentItem
-)
+from anonflow.bot.transport.content import ContentGroup, ContentItem
 from anonflow.bot.transport.types import RequestContext
 
 
 class PostResponsesPort(Protocol):
-    async def post_prepared(self, context: RequestContext, content: Union[ContentItem, ContentGroup], is_approved: bool): ...
-    async def post_moderation_decision(self, context: RequestContext, is_approved: bool, reason: str): ...
-    async def post_moderation_started(self, context: RequestContext): ...
+    async def post_prepared(self, context: RequestContext, content: Union[ContentItem, ContentGroup], is_approved: bool): ...  # fmt: skip
+    async def post_moderation_decision(self, context: RequestContext, is_approved: bool, reason: str): ...  # fmt: skip
+    async def post_moderation_started(self, context: RequestContext): ...  # fmt: skip

+ 1 - 1
anonflow/moderation/__init__.py

@@ -7,5 +7,5 @@ __all__ = [
     "ModerationExecutor",
     "ModerationPlanner",
     "RuleManager",
-    "ModerationService"
+    "ModerationService",
 ]

+ 2 - 0
anonflow/moderation/events.py

@@ -5,11 +5,13 @@ from dataclasses import dataclass
 class Event:
     pass
 
+
 @dataclass(frozen=True)
 class ModerationDecisionEvent(Event):
     is_approved: bool
     reason: str
 
+
 @dataclass(frozen=True)
 class ModerationStartedEvent(Event):
     pass

+ 2 - 0
anonflow/moderation/exceptions.py

@@ -1,5 +1,7 @@
 class ModerationError(RuntimeError): ...
 
+
 class ModerationOutputParseError(ModerationError): ...
 
+
 class ModerationNoAvailableFunctionsError(ModerationError): ...

+ 8 - 3
anonflow/moderation/executor.py

@@ -16,8 +16,11 @@ class ModerationExecutor:
 
     def moderation_decision(self, status: Literal["approve", "reject"], reason: str):
         moderation_map = {"approve": True, "reject": False}
-        return ModerationDecisionEvent(is_approved=moderation_map.get(status.lower(), False), reason=reason)
-    moderation_decision.description = textwrap.dedent( # type: ignore
+        return ModerationDecisionEvent(
+            is_approved=moderation_map.get(status.lower(), False), reason=reason
+        )
+
+    moderation_decision.description = textwrap.dedent(  # type: ignore
         """
         Processes a message with a moderation decision by status and reason.
         This function must be called whenever there is no exact user request or no other available function
@@ -25,7 +28,9 @@ class ModerationExecutor:
         """
     ).strip()
 
-    async def process(self, text: Optional[str] = None, image: Optional[str] = None) -> AsyncGenerator[Event, None]:
+    async def process(
+        self, text: Optional[str] = None, image: Optional[str] = None
+    ) -> AsyncGenerator[Event, None]:
         yield ModerationStartedEvent()
 
         functions = await self._moderation_planner.plan(text, image)

+ 32 - 36
anonflow/moderation/planner.py

@@ -14,7 +14,7 @@ from anonflow.config.models import ModerationBackend
 from .exceptions import (
     ModerationError,
     ModerationNoAvailableFunctionsError,
-    ModerationOutputParseError
+    ModerationOutputParseError,
 )
 from .rule_manager import RuleManager
 
@@ -46,7 +46,7 @@ class ModerationPlanner:
             "base_url": base_url,
             "timeout": timeout,
             "max_retries": self._max_retries,
-            "http_client": self._client
+            "http_client": self._client,
         }
 
         self._rule_manager = rule_manager
@@ -56,39 +56,34 @@ class ModerationPlanner:
 
     @staticmethod
     def _approve(reason: str):
-        return [{
-            "name": "moderation_decision",
-            "args": {
-                "status": "approve",
-                "reason": reason
+        return [
+            {
+                "name": "moderation_decision",
+                "args": {"status": "approve", "reason": reason},
             }
-        }]
+        ]
 
     @staticmethod
     def _reject(reason: str):
-        return [{
-            "name": "moderation_decision",
-            "args": {
-                "status": "reject",
-                "reason": reason
+        return [
+            {
+                "name": "moderation_decision",
+                "args": {"status": "reject", "reason": reason},
             }
-        }]
+        ]
 
     @staticmethod
     def _build_content(text: Optional[str] = None, image: Optional[str] = None):
         content = []
         if text:
-            content.append({
-                "type": "text",
-                "text": text
-            })
+            content.append({"type": "text", "text": text})
         if image:
-            content.append({
-                "type": "image_url",
-                "image_url": {
-                    "url": f"data:image/jpeg;base64,{image}"
+            content.append(
+                {
+                    "type": "image_url",
+                    "image_url": {"url": f"data:image/jpeg;base64,{image}"},
                 }
-            })
+            )
 
         return content
 
@@ -98,8 +93,7 @@ class ModerationPlanner:
 
         for func in functions:
             args = ", ".join(
-                f"{arg}: {ann}"
-                for arg, ann in func.get("args", {}).items()
+                f"{arg}: {ann}" for arg, ann in func.get("args", {}).items()
             )
 
             line = f"- {func['name']}({args})"
@@ -144,7 +138,7 @@ class ModerationPlanner:
                             {
                                 "role": "system",
                                 "content": textwrap.dedent(
-                                    f'''
+                                    f"""
                                     Respond strictly with a JSON array in the following format:
                                     `[{{"name": ..., "args": {{...}}}}, ...]`
                                     `name` - the function name, `args` - dict of arguments.
@@ -160,14 +154,11 @@ class ModerationPlanner:
 
                                     **RULES:**
                                     {rules_prompt}
-                                    '''
+                                    """
                                 ).strip(),
                             },
-                            {
-                                "role": "user",
-                                "content": text
-                            }
-                        ]
+                            {"role": "user", "content": text},
+                        ],
                     )
                 except OpenAIError as e:
                     raise ModerationError() from e
@@ -175,7 +166,9 @@ class ModerationPlanner:
                 try:
                     output = json.loads(response.output_text)
 
-                    if not isinstance(output, list) or not all(isinstance(obj, dict) for obj in output):
+                    if not isinstance(output, list) or not all(
+                        isinstance(obj, dict) for obj in output
+                    ):
                         raise ModerationOutputParseError()
 
                     break
@@ -224,7 +217,8 @@ class ModerationPlanner:
             args = {
                 name: (
                     getattr(param.annotation, "__name__", str(param.annotation))
-                    if param.annotation != inspect._empty else "str"
+                    if param.annotation != inspect._empty
+                    else "str"
                 )
                 for name, param in sig.parameters.items()
             }
@@ -233,7 +227,7 @@ class ModerationPlanner:
                 {
                     "name": function.__name__,
                     "args": args,
-                    "description": function.description or ""
+                    "description": function.description or "",
                 }
             )
 
@@ -251,7 +245,9 @@ class ModerationPlanner:
     def get_function_names(self) -> List[str]:
         return [f["name"] for f in self._functions if "name" in f]
 
-    async def plan(self, text: Optional[str] = None, image: Optional[str] = None) -> List[Dict[str, Any]]:
+    async def plan(
+        self, text: Optional[str] = None, image: Optional[str] = None
+    ) -> List[Dict[str, Any]]:
         if not self._enabled:
             return self._approve("Moderation is disabled.")
 

+ 10 - 3
anonflow/moderation/service.py

@@ -11,17 +11,24 @@ class ModerationService:
     def __init__(
         self,
         responses_port: PostResponsesPort,
-        moderation_executor: ModerationExecutor
+        moderation_executor: ModerationExecutor,
     ):
         self._responses_port = responses_port
         self._moderation_executor = moderation_executor
 
-    async def process(self, context: RequestContext, text: Optional[str] = None, image: Optional[str] = None):
+    async def process(
+        self,
+        context: RequestContext,
+        text: Optional[str] = None,
+        image: Optional[str] = None,
+    ):
         is_approved = False
         async for event in self._moderation_executor.process(text, image):
             if isinstance(event, ModerationDecisionEvent):
                 is_approved = event.is_approved
-                await self._responses_port.post_moderation_decision(context, event.is_approved, event.reason)
+                await self._responses_port.post_moderation_decision(
+                    context, event.is_approved, event.reason
+                )
             elif isinstance(event, ModerationStartedEvent):
                 await self._responses_port.post_moderation_started(context)
 

+ 1 - 0
anonflow/services/moderator/exceptions.py

@@ -1,3 +1,4 @@
 class ModeratorPermissionError(PermissionError): ...
 
+
 class SelfActionError(ModeratorPermissionError): ...

+ 1 - 0
anonflow/services/moderator/permissions.py

@@ -11,6 +11,7 @@ class ModeratorPermissions:
     def to_dict(self):
         return asdict(self)
 
+
 class ModeratorPermission(str, Enum):
     APPROVE_POSTS = "can_approve_posts"
     MANAGE_BANS = "can_manage_bans"

+ 21 - 14
anonflow/services/moderator/service.py

@@ -15,7 +15,7 @@ class ModeratorService:
         self,
         database: Database,
         ban_repository: BanRepository,
-        moderator_repository: ModeratorRepository
+        moderator_repository: ModeratorRepository,
     ):
         self._logger = logging.getLogger(__name__)
 
@@ -33,7 +33,9 @@ class ModeratorService:
     async def add(self, actor_user_id: int, user_id: int):
         try:
             async with self._database.begin_session() as session:
-                if await self._can(session, actor_user_id, ModeratorPermission.MANAGE_MODERATORS):
+                if await self._can(
+                    session, actor_user_id, ModeratorPermission.MANAGE_MODERATORS
+                ):
                     self._assert_not_self(actor_user_id, user_id)
                     await self._moderator_repository.add(session, user_id)
                 else:
@@ -53,7 +55,9 @@ class ModeratorService:
                     f"Moderator user_id={actor_user_id} does not have permission to perform 'ban'."
                 )
 
-    async def _can(self, session: AsyncSession, actor_user_id: int, permission: ModeratorPermission) -> bool:
+    async def _can(
+        self, session: AsyncSession, actor_user_id: int, permission: ModeratorPermission
+    ) -> bool:
         moderator = await self._moderator_repository.get(session, actor_user_id)
 
         if not moderator:
@@ -93,7 +97,9 @@ class ModeratorService:
     async def init(self):
         async with self._database.begin_session() as session:
             if not await self._moderator_repository.has(session, SYSTEM_USER_ID):
-                await self._moderator_repository.add(session, SYSTEM_USER_ID, is_root=True)
+                await self._moderator_repository.add(
+                    session, SYSTEM_USER_ID, is_root=True
+                )
 
     async def is_banned(self, user_id: int):
         async with self._database.get_session() as session:
@@ -102,7 +108,9 @@ class ModeratorService:
     async def remove(self, actor_user_id: int, user_id: int):
         try:
             async with self._database.begin_session() as session:
-                if await self._can(session, actor_user_id, ModeratorPermission.MANAGE_MODERATORS):
+                if await self._can(
+                    session, actor_user_id, ModeratorPermission.MANAGE_MODERATORS
+                ):
                     self._assert_not_self(actor_user_id, user_id)
                     await self._moderator_repository.remove(session, user_id)
                 else:
@@ -125,7 +133,9 @@ class ModeratorService:
     async def update(self, actor_user_id: int, user_id: int, **fields):
         try:
             async with self._database.begin_session() as session:
-                if await self._can(session, actor_user_id, ModeratorPermission.MANAGE_MODERATORS):
+                if await self._can(
+                    session, actor_user_id, ModeratorPermission.MANAGE_MODERATORS
+                ):
                     self._assert_not_self(actor_user_id, user_id)
                     await self._moderator_repository.update(session, user_id, **fields)
                 else:
@@ -136,19 +146,16 @@ class ModeratorService:
             self._logger.warning("Failed to update moderator user_id=%s", user_id)
 
     async def update_permissions(
-        self,
-        actor_user_id: int,
-        user_id: int,
-        permissions: ModeratorPermissions
+        self, actor_user_id: int, user_id: int, permissions: ModeratorPermissions
     ):
         try:
             async with self._database.begin_session() as session:
-                if await self._can(session, actor_user_id, ModeratorPermission.MANAGE_MODERATORS):
+                if await self._can(
+                    session, actor_user_id, ModeratorPermission.MANAGE_MODERATORS
+                ):
                     self._assert_not_self(actor_user_id, user_id)
                     await self._moderator_repository.update(
-                        session,
-                        user_id,
-                        **permissions.to_dict()
+                        session, user_id, **permissions.to_dict()
                     )
                 else:
                     raise ModeratorPermissionError(

+ 19 - 17
anonflow/translator/translator.py

@@ -12,34 +12,36 @@ class Translator:
 
     @staticmethod
     @lru_cache
-    def _get_translation(lang: str, translations_dir: Path):
+    def _get_translation(lang: str, domain: str, translations_dir: Path):
         translation = gettext.translation(
-            "messages",
-            translations_dir,
-            languages=[lang],
-            fallback=True
+            domain, translations_dir, languages=[lang], fallback=True
         )
         return translation
 
     @staticmethod
     def _format(s: str, **context):
-        return s.format_map(
-            defaultdict(
-                str,
-                context
-            )
-        )
+        return s.format_map(defaultdict(str, context))
 
-    async def get(self, lang: str = "ru"):
-        translator = await asyncio.to_thread(self._get_translation, lang, self._translations_dir)
+    async def get(self, lang: str = "ru", domain: str = "messages"):
+        translator = await asyncio.to_thread(
+            self._get_translation, lang, domain, self._translations_dir
+        )
 
-        def _(msgid1: str, msgid2: Optional[str] = None, n: Optional[int] = None, **context):
+        def _(
+            msgid1: str,
+            msgid2: Optional[str] = None,
+            n: Optional[int] = None,
+            **context,
+        ):
             return self._format(
                 (
-                    translator.ngettext(msgid1, msgid2 if msgid2 is not None else msgid1, n)
-                    if n is not None else translator.gettext(msgid1)
+                    translator.ngettext(
+                        msgid1, msgid2 if msgid2 is not None else msgid1, n
+                    )
+                    if n is not None
+                    else translator.gettext(msgid1)
                 ),
-                **context
+                **context,
             )
 
         return _