소스 검색

style(anonflow): format codebase

Librellium 1 주 전
부모
커밋
d4598a9f1e
40개의 변경된 파일292개의 추가작업 그리고 388개의 파일을 삭제
  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()
     app = Application()
     await app.run()
     await app.run()
 
 
+
 if __name__ == "__main__":
 if __name__ == "__main__":
     asyncio.run(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 aiogram.fsm.storage.memory import MemoryStorage
 
 
 from anonflow import __version_str__, paths
 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.config import Config
 from anonflow.database import (
 from anonflow.database import (
     BanRepository,
     BanRepository,
     Database,
     Database,
     ModeratorRepository,
     ModeratorRepository,
-    UserRepository
+    UserRepository,
 )
 )
 from anonflow.moderation import (
 from anonflow.moderation import (
     ModerationExecutor,
     ModerationExecutor,
     ModerationPlanner,
     ModerationPlanner,
     ModerationService,
     ModerationService,
-    RuleManager
-)
-from anonflow.services import (
-    ModeratorService,
-    UserService
+    RuleManager,
 )
 )
+from anonflow.services import ModeratorService, UserService
 from anonflow.translator import Translator
 from anonflow.translator import Translator
 
 
-from .builders import (
-    build_middlewares,
-    build_routers
-)
+from .builders import build_middlewares, build_routers
 from .helpers import require
 from .helpers import require
 
 
 
 
@@ -58,7 +49,9 @@ class Application:
 
 
         if not config_filepath.exists():
         if not config_filepath.exists():
             Config().save(config_filepath)
             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)
         self._config = Config.load(config_filepath)
 
 
@@ -76,15 +69,10 @@ class Application:
             await self._database.init()
             await self._database.init()
 
 
             self._moderator_service = ModeratorService(
             self._moderator_service = ModeratorService(
-                self._database,
-                BanRepository(),
-                ModeratorRepository()
+                self._database, BanRepository(), ModeratorRepository()
             )
             )
             await self._moderator_service.init()
             await self._moderator_service.init()
-            self._user_service = UserService(
-                self._database,
-                UserRepository()
-            )
+            self._user_service = UserService(self._database, UserRepository())
 
 
     def _init_bot(self):
     def _init_bot(self):
         with require(self, "_config") as config:
         with require(self, "_config") as config:
@@ -94,7 +82,7 @@ class Application:
 
 
             self._bot = Bot(
             self._bot = Bot(
                 token=bot_token.get_secret_value(),
                 token=bot_token.get_secret_value(),
-                default=DefaultBotProperties(parse_mode="HTML")
+                default=DefaultBotProperties(parse_mode="HTML"),
             )
             )
             self._dispatcher = Dispatcher(storage=MemoryStorage())
             self._dispatcher = Dispatcher(storage=MemoryStorage())
 
 
@@ -102,18 +90,23 @@ class Application:
         self._translator = Translator(translations_dir=paths.TRANSLATIONS_DIR)
         self._translator = Translator(translations_dir=paths.TRANSLATIONS_DIR)
 
 
     def _init_transport(self):
     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(
             self._responses_router = ResponsesRouter(
                 moderation_chat_ids=config.forwarding.moderation_chat_ids,
                 moderation_chat_ids=config.forwarding.moderation_chat_ids,
                 publication_channel_ids=config.forwarding.publication_channel_ids,
                 publication_channel_ids=config.forwarding.publication_channel_ids,
                 delivery_service=DeliveryService(bot),
                 delivery_service=DeliveryService(bot),
-                translator=translator
+                translator=translator,
             )
             )
 
 
     def _init_moderation(self):
     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 = RuleManager(rules_dir=paths.RULES_DIR)
             self._rule_manager.reload()
             self._rule_manager.reload()
 
 
@@ -132,20 +125,32 @@ class Application:
                 base_url=str(base_url) if base_url else None,
                 base_url=str(base_url) if base_url else None,
                 proxy=str(proxy) if proxy else None,
                 proxy=str(proxy) if proxy else None,
                 timeout=config.openai.timeout,
                 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_planner.set_enabled(config.moderation.enabled)
             self._moderation_executor = ModerationExecutor(self._moderation_planner)
             self._moderation_executor = ModerationExecutor(self._moderation_planner)
 
 
             self._moderation_service = ModerationService(
             self._moderation_service = ModerationService(
-                responses_router,
-                self._moderation_executor
+                responses_router, self._moderation_executor
             )
             )
 
 
     def _init_routers(self):
     def _init_routers(self):
         with require(
         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(
             dispatcher.include_router(
                 build_routers(
                 build_routers(
                     config=config,
                     config=config,
@@ -158,13 +163,18 @@ class Application:
 
 
     def _init_middleware(self):
     def _init_middleware(self):
         with require(
         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):
         ) as (dispatcher, config, responses_router, user_service, moderator_service):
             middlewares = build_middlewares(
             middlewares = build_middlewares(
                 config=config,
                 config=config,
                 responses_router=responses_router,
                 responses_router=responses_router,
                 user_service=user_service,
                 user_service=user_service,
-                moderator_service=moderator_service
+                moderator_service=moderator_service,
             )
             )
 
 
             for middleware in middlewares:
             for middleware in middlewares:
@@ -193,7 +203,9 @@ class Application:
                 await self._moderation_planner.close()
                 await self._moderation_planner.close()
             raise
             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(
         with require(
             self, "_bot", "_dispatcher", "_database", "_moderation_planner"
             self, "_bot", "_dispatcher", "_database", "_moderation_planner"

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

@@ -4,7 +4,7 @@ from anonflow.bot.middlewares.user import (
     UserLanguageMiddleware,
     UserLanguageMiddleware,
     UserNotRegisteredMiddleware,
     UserNotRegisteredMiddleware,
     UserSubscriptionMiddleware,
     UserSubscriptionMiddleware,
-    UserThrottlingMiddleware
+    UserThrottlingMiddleware,
 )
 )
 from anonflow.bot.transport import ResponsesRouter
 from anonflow.bot.transport import ResponsesRouter
 from anonflow.config import Config
 from anonflow.config import Config
@@ -19,20 +19,13 @@ def build_middlewares(
 ):
 ):
     middlewares = []
     middlewares = []
 
 
-    middlewares.append(
-        UserContextMiddleware(
-            user_service=user_service
-        )
-    )
+    middlewares.append(UserContextMiddleware(user_service=user_service))
 
 
-    middlewares.append(
-        UserLanguageMiddleware()
-    )
+    middlewares.append(UserLanguageMiddleware())
 
 
     middlewares.append(
     middlewares.append(
         UserBannedMiddleware(
         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(
         middlewares.append(
             UserSubscriptionMiddleware(
             UserSubscriptionMiddleware(
                 responses_port=responses_router,
                 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:
     if config.behavior.throttling.enabled:
         middlewares.append(
         middlewares.append(
             UserThrottlingMiddleware(
             UserThrottlingMiddleware(
                 responses_port=responses_router,
                 responses_port=responses_router,
                 delay=config.behavior.throttling.delay,
                 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()
     main_router = Router()
 
 
     routers = [
     routers = [
-        StartRouter(
-            responses_port=responses_router,
-            user_service=user_service
-        ),
+        StartRouter(responses_port=responses_router, user_service=user_service),
         TextRouter(
         TextRouter(
             responses_port=responses_router,
             responses_port=responses_router,
             moderation_service=moderation_service,
             moderation_service=moderation_service,
-            forwarding_types=config.forwarding.types
+            forwarding_types=config.forwarding.types,
         ),
         ),
         MediaRouter(
         MediaRouter(
             responses_port=responses_router,
             responses_port=responses_router,

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

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

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

@@ -7,7 +7,9 @@ from anonflow.services import ModeratorService
 
 
 
 
 class UserBannedMiddleware(BaseMiddleware):
 class UserBannedMiddleware(BaseMiddleware):
-    def __init__(self, responses_port: UserResponsesPort, moderator_service: ModeratorService):
+    def __init__(
+        self, responses_port: UserResponsesPort, moderator_service: ModeratorService
+    ):
         super().__init__()
         super().__init__()
 
 
         self._responses_port = responses_port
         self._responses_port = responses_port
@@ -17,7 +19,9 @@ class UserBannedMiddleware(BaseMiddleware):
         message = getattr(event, "message", None)
         message = getattr(event, "message", None)
         if isinstance(message, Message):
         if isinstance(message, Message):
             if await self._moderator_service.is_banned(message.chat.id):
             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
 
 
         return await handler(event, data)
         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:
         if isinstance(message, Message) and message.from_user:
             user = data.get("user")
             user = data.get("user")
             data["user_language"] = (
             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)
         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
             is_user_exists = data.get("user") is not None
             if not is_user_exists and not text.startswith("/start"):
             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
 
 
         return await handler(event, data)
         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):
 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__()
         super().__init__()
 
 
         self._responses_port = responses_port
         self._responses_port = responses_port
@@ -27,7 +29,9 @@ class UserSubscriptionMiddleware(BaseMiddleware):
             for channel_id in self._channel_ids:
             for channel_id in self._channel_ids:
                 member = await message.bot.get_chat_member(channel_id, user_id)
                 member = await message.bot.get_chat_member(channel_id, user_id)
                 if member.status in (ChatMemberStatus.KICKED, ChatMemberStatus.LEFT):
                 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
 
 
         return await handler(event, data)
         return await handler(event, data)

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

@@ -14,7 +14,7 @@ class UserThrottlingMiddleware(BaseMiddleware):
         self,
         self,
         responses_port: UserResponsesPort,
         responses_port: UserResponsesPort,
         delay: float,
         delay: float,
-        allowed_chat_ids: Optional[Iterable[ChatIdUnion]] = None
+        allowed_chat_ids: Optional[Iterable[ChatIdUnion]] = None,
     ):
     ):
         super().__init__()
         super().__init__()
 
 
@@ -29,17 +29,16 @@ class UserThrottlingMiddleware(BaseMiddleware):
 
 
     async def __call__(self, handler, event, data):
     async def __call__(self, handler, event, data):
         message = getattr(event, "message", None)
         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 ""
             text = message.text or message.caption or ""
             if not text.startswith("/"):
             if not text.startswith("/"):
                 async with self._lock:
                 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():
                 if user_lock.locked():
                     start_time = self._user_times.get(message.chat.id) or 0
                     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"]),
                         RequestContext(message.chat.id, data["user_language"]),
                         remaining_time=(
                         remaining_time=(
                             round(self._delay - (current_time - start_time))
                             round(self._delay - (current_time - start_time))
-                            if start_time else 0
-                        )
+                            if start_time
+                            else 0
+                        ),
                     )
                     )
                     return
                     return
 
 

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

@@ -2,8 +2,4 @@ from .media import MediaRouter
 from .start import StartRouter
 from .start import StartRouter
 from .text import TextRouter
 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.config.models import ForwardingType
 from anonflow.interfaces import PostResponsesPort
 from anonflow.interfaces import PostResponsesPort
 from anonflow.moderation import ModerationService
 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
 from anonflow.bot.transport.types import RequestContext
 
 
 
 
@@ -25,7 +21,7 @@ class MediaRouter(Router):
         self,
         self,
         responses_port: PostResponsesPort,
         responses_port: PostResponsesPort,
         moderation_service: ModerationService,
         moderation_service: ModerationService,
-        forwarding_types: FrozenSet[ForwardingType]
+        forwarding_types: FrozenSet[ForwardingType],
     ):
     ):
         super().__init__()
         super().__init__()
 
 
@@ -50,8 +46,8 @@ class MediaRouter(Router):
 
 
     def _can_send_media(self, msgs: List[Message]):
     def _can_send_media(self, msgs: List[Message]):
         return any(
         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
             for msg in msgs
         )
         )
 
 
@@ -76,18 +72,14 @@ class MediaRouter(Router):
 
 
         for message in messages:
         for message in messages:
             is_approved = await self._moderation_service.process(
             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)
             media = self._get_media(message)
             if media:
             if media:
                 content_group.append(ContentMediaItem(**media, caption=caption))
                 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):
     async def _on_media(self, message: Message, user_language: str):
         if message.chat.type != ChatType.PRIVATE:
         if message.chat.type != ChatType.PRIVATE:
@@ -99,8 +91,8 @@ class MediaRouter(Router):
             with suppress(CancelledError):
             with suppress(CancelledError):
                 await asyncio.sleep(2)
                 await asyncio.sleep(2)
                 async with self._media_groups_lock:
                 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)
                 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):
     async def _on_start(self, message: Message, user_language: str):
         if message.from_user:
         if message.from_user:
             await self._user_service.add(message.from_user.id)
             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):
     def setup(self):
         self.message.register(self._on_start, CommandStart())
         self.message.register(self._on_start, CommandStart())

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

@@ -16,7 +16,7 @@ class TextRouter(Router):
         self,
         self,
         responses_port: PostResponsesPort,
         responses_port: PostResponsesPort,
         moderation_service: ModerationService,
         moderation_service: ModerationService,
-        forwarding_types: FrozenSet[ForwardingType]
+        forwarding_types: FrozenSet[ForwardingType],
     ):
     ):
         super().__init__()
         super().__init__()
 
 
@@ -25,15 +25,10 @@ class TextRouter(Router):
         self._forwarding_types = forwarding_types
         self._forwarding_types = forwarding_types
 
 
     async def _on_text(self, message: Message, user_language: str):
     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)
             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(
             await self._responses_port.post_prepared(
                 context, ContentTextItem(message.text or ""), is_approved
                 context, ContentTextItem(message.text or ""), is_approved

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

@@ -1,7 +1,4 @@
 from .delivery import DeliveryService
 from .delivery import DeliveryService
 from .router import ResponsesRouter
 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"
     PHOTO = "photo"
     VIDEO = "video"
     VIDEO = "video"
 
 
+
 @dataclass
 @dataclass
 class ContentItem(ABC):
 class ContentItem(ABC):
     @abstractmethod
     @abstractmethod
     def translate(self, translator: Callable): ...
     def translate(self, translator: Callable): ...
 
 
+
 @dataclass
 @dataclass
 class ContentTextItem(ContentItem):
 class ContentTextItem(ContentItem):
     text: str
     text: str
@@ -20,6 +22,7 @@ class ContentTextItem(ContentItem):
     def translate(self, translator: Callable):
     def translate(self, translator: Callable):
         self.text = translator(self.text)
         self.text = translator(self.text)
 
 
+
 @dataclass
 @dataclass
 class ContentMediaItem(ContentItem):
 class ContentMediaItem(ContentItem):
     type: MediaType
     type: MediaType
@@ -29,6 +32,7 @@ class ContentMediaItem(ContentItem):
     def translate(self, translator: Callable):
     def translate(self, translator: Callable):
         self.caption = translator(self.caption)
         self.caption = translator(self.caption)
 
 
+
 class ContentGroup(list):
 class ContentGroup(list):
     def __init__(self, items: Optional[Iterable[ContentItem]] = None):
     def __init__(self, items: Optional[Iterable[ContentItem]] = None):
         return super().__init__(items or [])
         return super().__init__(items or [])

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

@@ -7,15 +7,10 @@ from aiogram.types import (
     InputMediaPhoto,
     InputMediaPhoto,
     InputMediaVideo,
     InputMediaVideo,
     MediaUnion,
     MediaUnion,
-    ReplyMarkupUnion
+    ReplyMarkupUnion,
 )
 )
 
 
-from .content import (
-    ContentGroup,
-    ContentItem,
-    ContentTextItem,
-    MediaType
-)
+from .content import ContentGroup, ContentItem, ContentTextItem, MediaType
 
 
 
 
 class DeliveryService:
 class DeliveryService:
@@ -23,11 +18,17 @@ class DeliveryService:
         self._bot = bot
         self._bot = bot
 
 
     @staticmethod
     @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:
         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:
         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:
         else:
             raise ValueError("Media item type is invalid.")
             raise ValueError("Media item type is invalid.")
 
 
@@ -39,30 +40,24 @@ class DeliveryService:
         chat_id: ChatIdUnion,
         chat_id: ChatIdUnion,
         content: Union[ContentItem, ContentGroup],
         content: Union[ContentItem, ContentGroup],
         parse_mode: Optional[Union[str, Default]] = Default("parse_mode"),
         parse_mode: Optional[Union[str, Default]] = Default("parse_mode"),
-        reply_markup: Optional[ReplyMarkupUnion] = None
+        reply_markup: Optional[ReplyMarkupUnion] = None,
     ):
     ):
         if isinstance(content, ContentTextItem):
         if isinstance(content, ContentTextItem):
             await self.send_text(
             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):
         elif isinstance(content, ContentGroup):
             if len(content) > 1:
             if len(content) > 1:
                 await self.send_media_group(
                 await self.send_media_group(
                     chat_id,
                     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:
             elif len(content) == 1:
                 await self.send_media(
                 await self.send_media(
                     chat_id,
                     chat_id,
                     self._wrap_content_item(content[0]),
                     self._wrap_content_item(content[0]),
                     parse_mode=parse_mode,
                     parse_mode=parse_mode,
-                    reply_markup=reply_markup
+                    reply_markup=reply_markup,
                 )
                 )
 
 
     async def send_media(
     async def send_media(
@@ -70,7 +65,7 @@ class DeliveryService:
         chat_id: ChatIdUnion,
         chat_id: ChatIdUnion,
         media: MediaUnion,
         media: MediaUnion,
         parse_mode: Optional[Union[str, Default]] = Default("parse_mode"),
         parse_mode: Optional[Union[str, Default]] = Default("parse_mode"),
-        reply_markup: Optional[ReplyMarkupUnion] = None
+        reply_markup: Optional[ReplyMarkupUnion] = None,
     ):
     ):
         if isinstance(media, InputMediaPhoto):
         if isinstance(media, InputMediaPhoto):
             return await self._bot.send_photo(
             return await self._bot.send_photo(
@@ -78,7 +73,7 @@ class DeliveryService:
                 media.media,
                 media.media,
                 caption=media.caption,
                 caption=media.caption,
                 parse_mode=parse_mode,
                 parse_mode=parse_mode,
-                reply_markup=reply_markup
+                reply_markup=reply_markup,
             )
             )
         elif isinstance(media, InputMediaVideo):
         elif isinstance(media, InputMediaVideo):
             return await self._bot.send_video(
             return await self._bot.send_video(
@@ -86,7 +81,7 @@ class DeliveryService:
                 media.media,
                 media.media,
                 caption=media.caption,
                 caption=media.caption,
                 parse_mode=parse_mode,
                 parse_mode=parse_mode,
-                reply_markup=reply_markup
+                reply_markup=reply_markup,
             )
             )
 
 
     async def send_media_group(
     async def send_media_group(
@@ -94,21 +89,15 @@ class DeliveryService:
         chat_id: ChatIdUnion,
         chat_id: ChatIdUnion,
         media_group: List[MediaUnion],
         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(
     async def send_text(
         self,
         self,
         chat_id: ChatIdUnion,
         chat_id: ChatIdUnion,
         text: str,
         text: str,
         parse_mode: Optional[Union[str, Default]] = Default("parse_mode"),
         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(
         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],
         moderation_chat_ids: Tuple[ChatIdUnion],
         publication_channel_ids: Tuple[ChatIdUnion],
         publication_channel_ids: Tuple[ChatIdUnion],
         delivery_service: DeliveryService,
         delivery_service: DeliveryService,
-        translator: Translator
+        translator: Translator,
     ):
     ):
         self._moderation_chat_ids = moderation_chat_ids
         self._moderation_chat_ids = moderation_chat_ids
         self._publication_channel_ids = publication_channel_ids
         self._publication_channel_ids = publication_channel_ids
@@ -25,10 +25,7 @@ class ResponsesRouter(PostResponsesPort, UserResponsesPort):
         self._translator = translator
         self._translator = translator
 
 
     async def post_moderation_decision(
     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)
         _ = await self._translator.get(context.user_language)
         for chat_id in self._moderation_chat_ids:
         for chat_id in self._moderation_chat_ids:
@@ -38,7 +35,7 @@ class ResponsesRouter(PostResponsesPort, UserResponsesPort):
                     _(
                     _(
                         "messages.staff.moderation_approved",
                         "messages.staff.moderation_approved",
                         reason=reason,
                         reason=reason,
-                    )
+                    ),
                 )
                 )
             else:
             else:
                 await self._delivery_service.send_text(
                 await self._delivery_service.send_text(
@@ -46,78 +43,66 @@ class ResponsesRouter(PostResponsesPort, UserResponsesPort):
                     _(
                     _(
                         "messages.staff.moderation_rejected",
                         "messages.staff.moderation_rejected",
                         reason=reason,
                         reason=reason,
-                    )
+                    ),
                 )
                 )
 
 
         if not is_approved:
         if not is_approved:
             await self._delivery_service.send_text(
             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):
     async def post_moderation_started(self, context: RequestContext):
         _ = await self._translator.get(context.user_language)
         _ = await self._translator.get(context.user_language)
         await self._delivery_service.send_text(
         await self._delivery_service.send_text(
-            context.chat_id,
-            _("messages.user.moderation_started")
+            context.chat_id, _("messages.user.moderation_started")
         )
         )
 
 
     async def post_prepared(
     async def post_prepared(
         self,
         self,
         context: RequestContext,
         context: RequestContext,
         content: Union[ContentItem, ContentGroup],
         content: Union[ContentItem, ContentGroup],
-        is_approved: bool
+        is_approved: bool,
     ):
     ):
         _ = await self._translator.get(context.user_language)
         _ = await self._translator.get(context.user_language)
 
 
         chat_ids = (
         chat_ids = (
             chain(self._moderation_chat_ids, self._publication_channel_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:
         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:
         if is_approved:
             await self._delivery_service.send_text(
             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):
     async def user_banned(self, context: RequestContext):
         _ = await self._translator.get(context.user_language)
         _ = await self._translator.get(context.user_language)
         await self._delivery_service.send_text(
         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):
     async def user_not_registered(self, context: RequestContext):
         _ = await self._translator.get(context.user_language)
         _ = await self._translator.get(context.user_language)
         await self._delivery_service.send_text(
         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):
     async def user_start(self, context: RequestContext):
         _ = await self._translator.get(context.user_language)
         _ = await self._translator.get(context.user_language)
         await self._delivery_service.send_text(
         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):
     async def user_subscription_required(self, context: RequestContext):
         _ = await self._translator.get(context.user_language)
         _ = await self._translator.get(context.user_language)
         await self._delivery_service.send_text(
         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):
     async def user_throttled(self, context: RequestContext, remaining_time: int):
@@ -127,6 +112,6 @@ class ResponsesRouter(PostResponsesPort, UserResponsesPort):
             _(
             _(
                 "messages.user.throttled",
                 "messages.user.throttled",
                 n=remaining_time,
                 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 pydantic import BaseModel, SecretStr
 from sqlalchemy.engine import URL
 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):
 class Config(BaseModel):
@@ -73,7 +65,7 @@ class Config(BaseModel):
                 template = Template(f.read())
                 template = Template(f.read())
                 rendered = template.safe_substitute(dotenv_values())
                 rendered = template.safe_substitute(dotenv_values())
                 data = yaml.safe_load(rendered) or {}
                 data = yaml.safe_load(rendered) or {}
-            return cls(**data) # type: ignore
+            return cls(**data)  # type: ignore
 
 
         return cls()
         return cls()
 
 
@@ -97,5 +89,5 @@ class Config(BaseModel):
                 config_file,
                 config_file,
                 width=float("inf"),
                 width=float("inf"),
                 sort_keys=False,
                 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):
 class Behavior(BaseModel):
     throttling: BehaviorThrottling = BehaviorThrottling()
     throttling: BehaviorThrottling = BehaviorThrottling()
-    subscription_requirement: BehaviorSubscriptionRequirement = BehaviorSubscriptionRequirement()
+    subscription_requirement: BehaviorSubscriptionRequirement = BehaviorSubscriptionRequirement()  # fmt: skip
     model_config = {"frozen": True}
     model_config = {"frozen": True}
 
 
 
 

+ 1 - 1
anonflow/database/__init__.py

@@ -9,5 +9,5 @@ __all__ = [
     "User",
     "User",
     "BanRepository",
     "BanRepository",
     "ModeratorRepository",
     "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._engine = create_async_engine(self.url, echo=echo)
         self._session_maker = sessionmaker(
         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
     @asynccontextmanager
     async def begin_session(self) -> AsyncGenerator[AsyncSession, None]:
     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():
             async with session.begin():
                 yield session
                 yield session
 
 
@@ -27,7 +29,7 @@ class Database:
         await self._engine.dispose()
         await self._engine.dispose()
 
 
     def get_session(self) -> AsyncSession:
     def get_session(self) -> AsyncSession:
-        return self._session_maker() # type: ignore
+        return self._session_maker()  # type: ignore
 
 
     async def init(self):
     async def init(self):
         async with self._engine.begin() as conn:
         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)
 app_config = Config.load(paths.CONFIG_FILEPATH)
 config.set_main_option(
 config.set_main_option(
     "sqlalchemy.url",
     "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:
     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():
         with context.begin_transaction():
             context.run_migrations()
             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 sqlalchemy.orm import relationship
 
 
 from .base import Base
 from .base import Base
@@ -20,18 +12,18 @@ class Ban(Base):
         Integer,
         Integer,
         ForeignKey("users.user_id", ondelete="CASCADE"),
         ForeignKey("users.user_id", ondelete="CASCADE"),
         index=True,
         index=True,
-        nullable=False
+        nullable=False,
     )
     )
     is_active = Column(Boolean, nullable=False, default=True)
     is_active = Column(Boolean, nullable=False, default=True)
 
 
     banned_at = Column(
     banned_at = Column(
         DateTime(timezone=True),
         DateTime(timezone=True),
         server_default=func.now(),
         server_default=func.now(),
-        nullable=False
+        nullable=False,
     )
     )
     unbanned_at = Column(
     unbanned_at = Column(
         DateTime(timezone=True),
         DateTime(timezone=True),
-        nullable=True
+        nullable=True,
     )
     )
 
 
     banned_by = Column(Integer, ForeignKey("moderators.user_id"))
     banned_by = Column(Integer, ForeignKey("moderators.user_id"))
@@ -39,6 +31,7 @@ class Ban(Base):
 
 
     user = relationship("User", back_populates="bans")
     user = relationship("User", back_populates="bans")
 
 
+
 class Moderator(Base):
 class Moderator(Base):
     __tablename__ = "moderators"
     __tablename__ = "moderators"
 
 
@@ -52,6 +45,7 @@ class Moderator(Base):
 
 
     user = relationship("User", back_populates="moderator")
     user = relationship("User", back_populates="moderator")
 
 
+
 class User(Base):
 class User(Base):
     __tablename__ = "users"
     __tablename__ = "users"
 
 
@@ -62,11 +56,11 @@ class User(Base):
         "Moderator",
         "Moderator",
         uselist=False,
         uselist=False,
         back_populates="user",
         back_populates="user",
-        cascade="all, delete-orphan"
+        cascade="all, delete-orphan",
     )
     )
     bans = relationship(
     bans = relationship(
         "Ban",
         "Ban",
         back_populates="user",
         back_populates="user",
         cascade="all, delete-orphan",
         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 .moderator import ModeratorRepository
 from .user import UserRepository
 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:
 class BanRepository:
     async def ban(self, session: AsyncSession, actor_user_id: int, user_id: int):
     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)
         session.add(ban)
 
 
     async def is_banned(self, session: AsyncSession, user_id: int):
     async def is_banned(self, session: AsyncSession, user_id: int):
         result = await session.execute(
         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())
         return bool(result.scalar_one_or_none())
 
 
     async def unban(self, session: AsyncSession, actor_user_id: int, user_id: int):
     async def unban(self, session: AsyncSession, actor_user_id: int, user_id: int):
         await session.execute(
         await session.execute(
             update(Ban)
             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]
     model: Type[Any]
 
 
     def __init__(self):
     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]):
     async def _add(self, session: AsyncSession, model_args: Dict[str, Any]):
         obj = self.model(**model_args)
         obj = self.model(**model_args)
         session.add(obj)
         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(
         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()
         return result.scalar_one_or_none()
 
 
     async def _has(self, session: AsyncSession, filters: Dict[str, Any]):
     async def _has(self, session: AsyncSession, filters: Dict[str, Any]):
         result = await session.execute(
         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())
         return bool(result.scalar_one_or_none())
 
 
     async def _remove(self, session: AsyncSession, filters: Dict[str, Any]):
     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:
         if not fields:
             return
             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):
 class ModeratorRepository(BaseRepository):
     model = Moderator
     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]:
     async def get(self, session: AsyncSession, user_id: int) -> Optional[Moderator]:
         return await super()._get(
         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):
     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):
     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):
     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
     model = User
 
 
     async def add(self, session: AsyncSession, user_id: int):
     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]:
     async def get(self, session: AsyncSession, user_id: int) -> Optional[User]:
         return await super()._get(
         return await super()._get(
             session,
             session,
             filters={"user_id": user_id},
             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):
     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):
     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):
     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 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
 from anonflow.bot.transport.types import RequestContext
 
 
 
 
 class PostResponsesPort(Protocol):
 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",
     "ModerationExecutor",
     "ModerationPlanner",
     "ModerationPlanner",
     "RuleManager",
     "RuleManager",
-    "ModerationService"
+    "ModerationService",
 ]
 ]

+ 2 - 0
anonflow/moderation/events.py

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

+ 2 - 0
anonflow/moderation/exceptions.py

@@ -1,5 +1,7 @@
 class ModerationError(RuntimeError): ...
 class ModerationError(RuntimeError): ...
 
 
+
 class ModerationOutputParseError(ModerationError): ...
 class ModerationOutputParseError(ModerationError): ...
 
 
+
 class ModerationNoAvailableFunctionsError(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):
     def moderation_decision(self, status: Literal["approve", "reject"], reason: str):
         moderation_map = {"approve": True, "reject": False}
         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.
         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
         This function must be called whenever there is no exact user request or no other available function
@@ -25,7 +28,9 @@ class ModerationExecutor:
         """
         """
     ).strip()
     ).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()
         yield ModerationStartedEvent()
 
 
         functions = await self._moderation_planner.plan(text, image)
         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 (
 from .exceptions import (
     ModerationError,
     ModerationError,
     ModerationNoAvailableFunctionsError,
     ModerationNoAvailableFunctionsError,
-    ModerationOutputParseError
+    ModerationOutputParseError,
 )
 )
 from .rule_manager import RuleManager
 from .rule_manager import RuleManager
 
 
@@ -46,7 +46,7 @@ class ModerationPlanner:
             "base_url": base_url,
             "base_url": base_url,
             "timeout": timeout,
             "timeout": timeout,
             "max_retries": self._max_retries,
             "max_retries": self._max_retries,
-            "http_client": self._client
+            "http_client": self._client,
         }
         }
 
 
         self._rule_manager = rule_manager
         self._rule_manager = rule_manager
@@ -56,39 +56,34 @@ class ModerationPlanner:
 
 
     @staticmethod
     @staticmethod
     def _approve(reason: str):
     def _approve(reason: str):
-        return [{
-            "name": "moderation_decision",
-            "args": {
-                "status": "approve",
-                "reason": reason
+        return [
+            {
+                "name": "moderation_decision",
+                "args": {"status": "approve", "reason": reason},
             }
             }
-        }]
+        ]
 
 
     @staticmethod
     @staticmethod
     def _reject(reason: str):
     def _reject(reason: str):
-        return [{
-            "name": "moderation_decision",
-            "args": {
-                "status": "reject",
-                "reason": reason
+        return [
+            {
+                "name": "moderation_decision",
+                "args": {"status": "reject", "reason": reason},
             }
             }
-        }]
+        ]
 
 
     @staticmethod
     @staticmethod
     def _build_content(text: Optional[str] = None, image: Optional[str] = None):
     def _build_content(text: Optional[str] = None, image: Optional[str] = None):
         content = []
         content = []
         if text:
         if text:
-            content.append({
-                "type": "text",
-                "text": text
-            })
+            content.append({"type": "text", "text": text})
         if image:
         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
         return content
 
 
@@ -98,8 +93,7 @@ class ModerationPlanner:
 
 
         for func in functions:
         for func in functions:
             args = ", ".join(
             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})"
             line = f"- {func['name']}({args})"
@@ -144,7 +138,7 @@ class ModerationPlanner:
                             {
                             {
                                 "role": "system",
                                 "role": "system",
                                 "content": textwrap.dedent(
                                 "content": textwrap.dedent(
-                                    f'''
+                                    f"""
                                     Respond strictly with a JSON array in the following format:
                                     Respond strictly with a JSON array in the following format:
                                     `[{{"name": ..., "args": {{...}}}}, ...]`
                                     `[{{"name": ..., "args": {{...}}}}, ...]`
                                     `name` - the function name, `args` - dict of arguments.
                                     `name` - the function name, `args` - dict of arguments.
@@ -160,14 +154,11 @@ class ModerationPlanner:
 
 
                                     **RULES:**
                                     **RULES:**
                                     {rules_prompt}
                                     {rules_prompt}
-                                    '''
+                                    """
                                 ).strip(),
                                 ).strip(),
                             },
                             },
-                            {
-                                "role": "user",
-                                "content": text
-                            }
-                        ]
+                            {"role": "user", "content": text},
+                        ],
                     )
                     )
                 except OpenAIError as e:
                 except OpenAIError as e:
                     raise ModerationError() from e
                     raise ModerationError() from e
@@ -175,7 +166,9 @@ class ModerationPlanner:
                 try:
                 try:
                     output = json.loads(response.output_text)
                     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()
                         raise ModerationOutputParseError()
 
 
                     break
                     break
@@ -224,7 +217,8 @@ class ModerationPlanner:
             args = {
             args = {
                 name: (
                 name: (
                     getattr(param.annotation, "__name__", str(param.annotation))
                     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()
                 for name, param in sig.parameters.items()
             }
             }
@@ -233,7 +227,7 @@ class ModerationPlanner:
                 {
                 {
                     "name": function.__name__,
                     "name": function.__name__,
                     "args": args,
                     "args": args,
-                    "description": function.description or ""
+                    "description": function.description or "",
                 }
                 }
             )
             )
 
 
@@ -251,7 +245,9 @@ class ModerationPlanner:
     def get_function_names(self) -> List[str]:
     def get_function_names(self) -> List[str]:
         return [f["name"] for f in self._functions if "name" in f]
         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:
         if not self._enabled:
             return self._approve("Moderation is disabled.")
             return self._approve("Moderation is disabled.")
 
 

+ 10 - 3
anonflow/moderation/service.py

@@ -11,17 +11,24 @@ class ModerationService:
     def __init__(
     def __init__(
         self,
         self,
         responses_port: PostResponsesPort,
         responses_port: PostResponsesPort,
-        moderation_executor: ModerationExecutor
+        moderation_executor: ModerationExecutor,
     ):
     ):
         self._responses_port = responses_port
         self._responses_port = responses_port
         self._moderation_executor = moderation_executor
         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
         is_approved = False
         async for event in self._moderation_executor.process(text, image):
         async for event in self._moderation_executor.process(text, image):
             if isinstance(event, ModerationDecisionEvent):
             if isinstance(event, ModerationDecisionEvent):
                 is_approved = event.is_approved
                 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):
             elif isinstance(event, ModerationStartedEvent):
                 await self._responses_port.post_moderation_started(context)
                 await self._responses_port.post_moderation_started(context)
 
 

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

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

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

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

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

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

+ 19 - 17
anonflow/translator/translator.py

@@ -12,34 +12,36 @@ class Translator:
 
 
     @staticmethod
     @staticmethod
     @lru_cache
     @lru_cache
-    def _get_translation(lang: str, translations_dir: Path):
+    def _get_translation(lang: str, domain: str, translations_dir: Path):
         translation = gettext.translation(
         translation = gettext.translation(
-            "messages",
-            translations_dir,
-            languages=[lang],
-            fallback=True
+            domain, translations_dir, languages=[lang], fallback=True
         )
         )
         return translation
         return translation
 
 
     @staticmethod
     @staticmethod
     def _format(s: str, **context):
     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(
             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 _
         return _