Prechádzať zdrojové kódy

refactor(transport): replace Result-based logic with ResponsesPorts

- rename MessageRouter to ResponsesRouter
- replace Result dispatch in ResponsesRouter with ResponsesPorts
- remove obsolete Results module
- implement translate support directly in Content objects
- add RequestContext and replace aiogram Message with it in ResponsesRouter
Librellium 1 týždeň pred
rodič
commit
285d22e8ee

+ 2 - 2
anonflow/services/transport/__init__.py

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

+ 22 - 14
anonflow/services/transport/content.py

@@ -1,30 +1,38 @@
-from dataclasses import dataclass, field
+from abc import ABC, abstractmethod
+from dataclasses import dataclass
 from enum import Enum
-from typing import List, Optional
+from typing import Callable, Iterable, Optional
 
 
 class MediaType(str, Enum):
     PHOTO = "photo"
     VIDEO = "video"
 
-@dataclass(frozen=True)
-class ContentItem:
-    pass
+@dataclass
+class ContentItem(ABC):
+    @abstractmethod
+    def translate(self, translator: Callable): ...
 
-@dataclass(frozen=True)
-class ContentGroup:
-    pass
-
-@dataclass(frozen=True)
+@dataclass
 class ContentTextItem(ContentItem):
     text: str
 
-@dataclass(frozen=True)
+    def translate(self, translator: Callable):
+        self.text = translator(self.text)
+
+@dataclass
 class ContentMediaItem(ContentItem):
     type: MediaType
     file_id: str
     caption: Optional[str] = None
 
-@dataclass(frozen=True)
-class ContentMediaGroup(ContentGroup):
-    items: List[ContentMediaItem] = field(default_factory=list)
+    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 [])
+
+    def translate(self, translator: Callable):
+        for item in self:
+            item.translate(translator)

+ 52 - 16
anonflow/services/transport/delivery.py

@@ -1,4 +1,4 @@
-from typing import Optional, Union
+from typing import Optional, List, Union
 
 from aiogram import Bot
 from aiogram.client.bot import Default
@@ -6,10 +6,16 @@ from aiogram.types import (
     ChatIdUnion,
     InputMediaPhoto,
     InputMediaVideo,
+    MediaUnion,
     ReplyMarkupUnion
 )
 
-from .content import ContentMediaGroup, ContentMediaItem, MediaType
+from .content import (
+    ContentGroup,
+    ContentItem,
+    ContentTextItem,
+    MediaType
+)
 
 
 class DeliveryService:
@@ -17,24 +23,57 @@ class DeliveryService:
         self._bot = bot
 
     @staticmethod
-    def _wrap_media(item: ContentMediaItem):
+    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)
+            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)
+            return InputMediaVideo(media=item.file_id, caption=item.caption, parse_mode=parse_mode)
         else:
             raise ValueError("Media item type is invalid.")
 
+    async def delete(self, chat_id: ChatIdUnion, message_id: int):
+        return await self._bot.delete_message(chat_id, message_id)
+
+    async def send_content(
+        self,
+        chat_id: ChatIdUnion,
+        content: Union[ContentItem, ContentGroup],
+        parse_mode: Optional[Union[str, Default]] = Default("parse_mode"),
+        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
+            )
+        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
+                    ]
+                )
+            elif len(content) == 1:
+                await self.send_media(
+                    chat_id,
+                    self._wrap_content_item(content[0]),
+                    parse_mode=parse_mode,
+                    reply_markup=reply_markup
+                )
+
     async def send_media(
         self,
         chat_id: ChatIdUnion,
-        media_item: ContentMediaItem,
+        media: MediaUnion,
         parse_mode: Optional[Union[str, Default]] = Default("parse_mode"),
         reply_markup: Optional[ReplyMarkupUnion] = None
     ):
-        media = self._wrap_media(media_item)
         if isinstance(media, InputMediaPhoto):
-            await self._bot.send_photo(
+            return await self._bot.send_photo(
                 chat_id,
                 media.media,
                 caption=media.caption,
@@ -42,7 +81,7 @@ class DeliveryService:
                 reply_markup=reply_markup
             )
         elif isinstance(media, InputMediaVideo):
-            await self._bot.send_video(
+            return await self._bot.send_video(
                 chat_id,
                 media.media,
                 caption=media.caption,
@@ -53,14 +92,11 @@ class DeliveryService:
     async def send_media_group(
         self,
         chat_id: ChatIdUnion,
-        media_group: ContentMediaGroup,
+        media_group: List[MediaUnion],
     ):
-        await self._bot.send_media_group(
+        return await self._bot.send_media_group(
             chat_id=chat_id,
-            media=[
-                self._wrap_media(item)
-                for item in media_group.items
-            ]
+            media=media_group
         )
 
     async def send_text(
@@ -70,7 +106,7 @@ class DeliveryService:
         parse_mode: Optional[Union[str, Default]] = Default("parse_mode"),
         reply_markup: Optional[ReplyMarkupUnion] = None
     ):
-        await self._bot.send_message(
+        return await self._bot.send_message(
             chat_id=chat_id,
             text=text,
             parse_mode=parse_mode,

+ 0 - 59
anonflow/services/transport/results.py

@@ -1,59 +0,0 @@
-from dataclasses import dataclass
-from typing import TypeAlias, Union
-
-from .content import ContentMediaGroup, ContentMediaItem, ContentTextItem
-
-
-@dataclass(frozen=True)
-class Result:
-    pass
-
-@dataclass(frozen=True)
-class CommandInfoResult(Result):
-    pass
-
-@dataclass(frozen=True)
-class CommandStartResult(Result):
-    pass
-
-@dataclass(frozen=True)
-class PostPreparedResult(Result):
-    content: Union[ContentTextItem, ContentMediaItem, ContentMediaGroup]
-    moderation_approved: bool
-
-@dataclass(frozen=True)
-class ModerationDecisionResult(Result):
-    is_approved: bool
-    reason: str
-
-@dataclass(frozen=True)
-class ModerationStartedResult(Result):
-    pass
-
-@dataclass(frozen=True)
-class UserBannedResult(Result):
-    pass
-
-@dataclass(frozen=True)
-class UserSubscriptionRequiredResult(Result):
-    pass
-
-@dataclass(frozen=True)
-class UserThrottledResult(Result):
-    remaining_time: int
-
-@dataclass(frozen=True)
-class UserNotRegisteredResult(Result):
-    pass
-
-Results: TypeAlias = Union[
-    CommandInfoResult,
-    CommandStartResult,
-    PostPreparedResult,
-    ModerationDecisionResult,
-    ModerationStartedResult,
-    UserBannedResult,
-    UserSubscriptionRequiredResult,
-    UserThrottledResult,
-    UserNotRegisteredResult
-]

+ 96 - 99
anonflow/services/transport/router.py

@@ -1,27 +1,17 @@
 from itertools import chain
-from typing import Any, Callable, Dict, Tuple
+from typing import Tuple, Union
 
-from aiogram.types import ChatIdUnion, Message
+from aiogram.types import ChatIdUnion
 
+from anonflow.interfaces import PostResponsesPort, UserResponsesPort
 from anonflow.translator import Translator
 
-from .content import ContentMediaGroup, ContentTextItem
+from .content import ContentGroup, ContentItem
 from .delivery import DeliveryService
-from .results import (
-    Results,
-    CommandInfoResult,
-    CommandStartResult,
-    ModerationDecisionResult,
-    ModerationStartedResult,
-    PostPreparedResult,
-    UserBannedResult,
-    UserNotRegisteredResult,
-    UserSubscriptionRequiredResult,
-    UserThrottledResult
-)
-
-
-class MessageRouter:
+from .types import RequestContext
+
+
+class ResponsesRouter(PostResponsesPort, UserResponsesPort):
     def __init__(
         self,
         moderation_chat_ids: Tuple[ChatIdUnion],
@@ -29,107 +19,114 @@ class MessageRouter:
         delivery_service: DeliveryService,
         translator: Translator
     ):
-        self.moderation_chat_ids = moderation_chat_ids
-        self.publication_channel_ids = publication_channel_ids
-        self.delivery_service = delivery_service
-        self.translator = translator
-
-        self._handlers: Dict[Any, Callable] = {
-            CommandInfoResult: self._handle_command_info,
-            CommandStartResult: self._handle_command_start,
-            PostPreparedResult: self._handle_post_prepared,
-            ModerationStartedResult: self._handle_moderation_started,
-            ModerationDecisionResult: self._handle_moderation_decision,
-            UserBannedResult: self._handle_user_banned,
-            UserNotRegisteredResult: self._handle_user_not_registered,
-            UserSubscriptionRequiredResult: self._handle_user_subscription_required,
-            UserThrottledResult: self._handle_user_throttled
-        }
-
-    async def _handle_command_info(self, result: CommandInfoResult, message: Message, _):
-        await self.delivery_service.send_text(message.chat.id, _("messages.user.command_info", message=message))
-
-    async def _handle_command_start(self, result: CommandStartResult, message: Message, _):
-        await self.delivery_service.send_text(message.chat.id, _("messages.user.command_start", message=message))
-
-    async def _handle_post_prepared(self, result: PostPreparedResult, message: Message, _):
-        chat_ids = (
-            chain(self.moderation_chat_ids, self.publication_channel_ids)
-            if result.moderation_approved
-            else iter(self.moderation_chat_ids)
-        )
+        self._moderation_chat_ids = moderation_chat_ids
+        self._publication_channel_ids = publication_channel_ids
+        self._delivery_service = delivery_service
+        self._translator = translator
 
-        content = result.content
-        for chat_id in chat_ids:
-            if isinstance(content, ContentTextItem):
-                await self.delivery_service.send_text(chat_id, _("messages.channel.text", text=content.text))
-            elif isinstance(content, ContentMediaGroup):
-                items = content.items
-                if len(items) > 1:
-                    await self.delivery_service.send_media_group(chat_id, content)
-                elif len(items) == 1:
-                    await self.delivery_service.send_media(chat_id, items[0])
-
-        if result.moderation_approved:
-            await message.answer(_("messages.user.moderation_approved", message=message))
-
-    async def _handle_moderation_started(self, result: ModerationStartedResult, message: Message, _):
-        await self.delivery_service.send_text(
-            message.chat.id,
-            _("messages.user.moderation_started", message=message)
-        )
-
-    async def _handle_moderation_decision(self, result: ModerationDecisionResult, message: Message, _):
-        for chat_id in self.moderation_chat_ids:
-            if result.is_approved:
-                await self.delivery_service.send_text(
+    async def post_moderation_decision(
+        self,
+        context: RequestContext,
+        is_approved: bool,
+        reason: str
+    ):
+        _ = await self._translator.get(context.user_language)
+        for chat_id in self._moderation_chat_ids:
+            if is_approved:
+                await self._delivery_service.send_text(
                     chat_id,
                     _(
                         "messages.staff.moderation_approved",
-                        message=message,
-                        explanation=result.reason,
+                        reason=reason,
                     )
                 )
             else:
-                await self.delivery_service.send_text(
+                await self._delivery_service.send_text(
                     chat_id,
                     _(
                         "messages.staff.moderation_rejected",
-                        message=message,
-                        explanation=result.reason,
+                        reason=reason,
                     )
                 )
 
-        if not result.is_approved:
-            await self.delivery_service.send_text(
-                message.chat.id,
-                _("messages.user.moderation_rejected", message=message)
+        if not is_approved:
+            await self._delivery_service.send_text(
+                context.chat_id,
+                _("messages.user.moderation_rejected")
             )
 
-    async def _handle_user_banned(self, result: UserBannedResult, message: Message, _):
-        await self.delivery_service.send_text(message.chat.id, _("messages.user.banned", message))
+    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")
+        )
 
-    async def _handle_user_not_registered(self, result: UserNotRegisteredResult, message: Message, _):
-        await self.delivery_service.send_text(message.chat.id, _("messages.user.not_registered", message))
+    async def post_prepared(
+        self,
+        context: RequestContext,
+        content: Union[ContentItem, ContentGroup],
+        is_approved: bool
+    ):
+        _ = await self._translator.get(context.user_language)
 
-    async def _handle_user_subscription_required(self, result: UserSubscriptionRequiredResult, message: Message, _):
-        await self.delivery_service.send_text(message.chat.id, _("messages.user.subscription_required", message))
+        chat_ids = (
+            chain(self._moderation_chat_ids, self._publication_channel_ids)
+            if is_approved else iter(self._moderation_chat_ids)
+        )
 
-    async def _handle_user_throttled(self, result: UserThrottledResult, message: Message, _):
-        await self.delivery_service.send_text(
-            message.chat.id,
-            _(
-                "messages.user.throttled",
-                message,
-                remaining=result.remaining_time
+        translator = lambda t: _(
+            "messages.channel.post",
+            text=t
+        )
+        content.translate(translator)
+
+        for chat_id in chat_ids:
+            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")
+            )
+
+    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")
         )
 
-    async def dispatch(self, result: Results, message: Message):
-        _ = self.translator.get()
+    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")
+        )
 
-        handler = self._handlers.get(type(result))
-        if handler is None:
-            return
+    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")
+        )
+
+    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")
+        )
 
-        await handler(result, message, _)
+    async def user_throttled(self, context: RequestContext, remaining_time: int):
+        _ = await self._translator.get(context.user_language)
+        await self._delivery_service.send_text(
+            context.chat_id,
+            _(
+                "messages.user.throttled",
+                n=remaining_time,
+                remaining_time=remaining_time
+            )
+        )

+ 9 - 0
anonflow/services/transport/types.py

@@ -0,0 +1,9 @@
+from dataclasses import dataclass
+
+from aiogram.types import ChatIdUnion
+
+
+@dataclass(frozen=True)
+class RequestContext:
+    chat_id: ChatIdUnion
+    user_language: str