diff --git a/src/aiogram_dialog/api/entities/new_message.py b/src/aiogram_dialog/api/entities/new_message.py index 4ac6883a..e360563a 100644 --- a/src/aiogram_dialog/api/entities/new_message.py +++ b/src/aiogram_dialog/api/entities/new_message.py @@ -7,6 +7,7 @@ Chat, ForceReply, InlineKeyboardMarkup, + LinkPreviewOptions, ReplyKeyboardMarkup, ReplyKeyboardRemove, ) @@ -45,3 +46,4 @@ class NewMessage: show_mode: ShowMode = ShowMode.AUTO disable_web_page_preview: Optional[bool] = None media: Optional[MediaAttachment] = None + link_preview_options: Optional[LinkPreviewOptions] = None diff --git a/src/aiogram_dialog/api/internal/__init__.py b/src/aiogram_dialog/api/internal/__init__.py index 62dcf102..2dc28901 100644 --- a/src/aiogram_dialog/api/internal/__init__.py +++ b/src/aiogram_dialog/api/internal/__init__.py @@ -4,7 +4,7 @@ "CALLBACK_DATA_KEY", "CONTEXT_KEY", "EVENT_SIMULATED", "STACK_KEY", "STORAGE_KEY", "ButtonVariant", "DataGetter", "InputWidget", "KeyboardWidget", - "MediaWidget", "RawKeyboard", "TextWidget", "Widget", + "LinkPreviewWidget", "MediaWidget", "RawKeyboard", "TextWidget", "Widget", "WindowProtocol", ] @@ -24,6 +24,7 @@ DataGetter, InputWidget, KeyboardWidget, + LinkPreviewWidget, MediaWidget, RawKeyboard, TextWidget, diff --git a/src/aiogram_dialog/api/internal/widgets.py b/src/aiogram_dialog/api/internal/widgets.py index c3976817..8472b216 100644 --- a/src/aiogram_dialog/api/internal/widgets.py +++ b/src/aiogram_dialog/api/internal/widgets.py @@ -12,6 +12,7 @@ CallbackQuery, InlineKeyboardButton, KeyboardButton, + LinkPreviewOptions, Message, ) @@ -41,6 +42,16 @@ async def render_text( raise NotImplementedError +@runtime_checkable +class LinkPreviewWidget(Widget, Protocol): + @abstractmethod + async def render_link_preview( + self, data: dict, manager: DialogManager, + ) -> Optional[LinkPreviewOptions]: + """Create link preview.""" + raise NotImplementedError + + ButtonVariant = Union[InlineKeyboardButton, KeyboardButton] RawKeyboard = list[list[ButtonVariant]] diff --git a/src/aiogram_dialog/manager/message_manager.py b/src/aiogram_dialog/manager/message_manager.py index f5836157..1d729e6d 100644 --- a/src/aiogram_dialog/manager/message_manager.py +++ b/src/aiogram_dialog/manager/message_manager.py @@ -129,10 +129,13 @@ def need_voice(self, new_message: NewMessage) -> bool: def _message_changed( self, new_message: NewMessage, old_message: OldMessage, ) -> bool: - if new_message.text != old_message.text: - return True - # we cannot actually compare reply keyboards - if new_message.reply_markup or old_message.has_reply_keyboard: + if ( + (new_message.text != old_message.text) or + # we cannot actually compare reply keyboards + (new_message.reply_markup or old_message.has_reply_keyboard) or + # we do not know if link preview changed + new_message.link_preview_options + ): return True if self.had_media(old_message) != self.need_media(new_message): @@ -349,6 +352,7 @@ async def edit_text( reply_markup=new_message.reply_markup, parse_mode=new_message.parse_mode, disable_web_page_preview=new_message.disable_web_page_preview, + link_preview_options=new_message.link_preview_options, ) async def edit_media( @@ -395,6 +399,7 @@ async def send_text(self, bot: Bot, new_message: NewMessage) -> Message: disable_web_page_preview=new_message.disable_web_page_preview, reply_markup=new_message.reply_markup, parse_mode=new_message.parse_mode, + link_preview_options=new_message.link_preview_options, ) async def send_media(self, bot: Bot, new_message: NewMessage) -> Message: diff --git a/src/aiogram_dialog/widgets/link_preview/__init__.py b/src/aiogram_dialog/widgets/link_preview/__init__.py new file mode 100644 index 00000000..1d848cca --- /dev/null +++ b/src/aiogram_dialog/widgets/link_preview/__init__.py @@ -0,0 +1,3 @@ +__all__ = ["LinkPreviewBase", "LinkPreview"] + +from .base import LinkPreview, LinkPreviewBase diff --git a/src/aiogram_dialog/widgets/link_preview/base.py b/src/aiogram_dialog/widgets/link_preview/base.py new file mode 100644 index 00000000..ac9f40ef --- /dev/null +++ b/src/aiogram_dialog/widgets/link_preview/base.py @@ -0,0 +1,61 @@ +from typing import Optional + +from aiogram.types import LinkPreviewOptions + +from aiogram_dialog import DialogManager +from aiogram_dialog.api.internal import LinkPreviewWidget, TextWidget +from aiogram_dialog.widgets.common import BaseWidget, Whenable, WhenCondition + + +class LinkPreviewBase(Whenable, BaseWidget, LinkPreviewWidget): + def __init__(self, when: WhenCondition = None): + super().__init__(when=when) + + async def render_link_preview( + self, data: dict, manager: DialogManager, + ) -> Optional[LinkPreviewOptions]: + if not self.is_(data, manager): + return None + return await self._render_link_preview(data, manager) + + async def _render_link_preview( + self, data: dict, manager: DialogManager, + ) -> Optional[LinkPreviewOptions]: + return None + + +class LinkPreview(LinkPreviewBase): + def __init__( + self, + url: TextWidget, + is_disabled: bool = False, + prefer_small_media: bool = False, + prefer_large_media: bool = False, + show_above_text: bool = False, + when: WhenCondition = None, + ): + super().__init__(when=when) + self.url = url + self.is_disabled = is_disabled + self.prefer_small_media = prefer_small_media + self.prefer_large_media = prefer_large_media + self.show_above_text = show_above_text + + async def render_link_preview( + self, data: dict, manager: DialogManager, + ) -> Optional[LinkPreviewOptions]: + if not self.is_(data, manager): + return None + return await self._render_link_preview(data, manager) + + async def _render_link_preview( + self, data: dict, manager: DialogManager, + ) -> Optional[LinkPreviewOptions]: + url = await self.url.render_text(data, manager) + return LinkPreviewOptions( + url=url, + is_disabled=self.is_disabled, + prefer_small_media=self.prefer_small_media, + prefer_large_media=self.prefer_large_media, + show_above_text=self.show_above_text, + ) diff --git a/src/aiogram_dialog/widgets/utils.py b/src/aiogram_dialog/widgets/utils.py index 16aa047d..4fd98b9f 100644 --- a/src/aiogram_dialog/widgets/utils.py +++ b/src/aiogram_dialog/widgets/utils.py @@ -2,16 +2,19 @@ from typing import Union from aiogram_dialog.api.exceptions import InvalidWidgetType -from aiogram_dialog.api.internal import DataGetter +from aiogram_dialog.api.internal import DataGetter, LinkPreviewWidget from .data.data_context import CompositeGetter, StaticGetter from .input import BaseInput, CombinedInput, MessageHandlerFunc, MessageInput from .kbd import Group, Keyboard +from .link_preview import LinkPreviewBase from .media import Media from .text import Format, Multi, Text from .widget_event import WidgetEventProcessor -WidgetSrc = Union[str, Text, Keyboard, MessageHandlerFunc, Media, BaseInput] +WidgetSrc = Union[ + str, Text, Keyboard, MessageHandlerFunc, Media, BaseInput, LinkPreviewBase, +] SingleGetterBase = Union[DataGetter, dict] GetterVariant = Union[ @@ -71,13 +74,26 @@ def ensure_media(widget: Union[Media, Sequence[Media]]) -> Media: return Media() +def ensure_link_preview( + widget: Union[LinkPreviewWidget, Sequence[LinkPreviewWidget]], +) -> LinkPreviewWidget: + if isinstance(widget, LinkPreviewWidget): + return widget + if len(widget) > 1: + raise ValueError("Only one link preview widget is supported") + if len(widget) == 1: + return widget[0] + return LinkPreviewBase() + + def ensure_widgets( widgets: Sequence[WidgetSrc], -) -> tuple[Text, Keyboard, Union[BaseInput, None], Media]: +) -> tuple[Text, Keyboard, Union[BaseInput, None], Media, LinkPreviewWidget]: texts = [] keyboards = [] inputs = [] media = [] + link_preview = [] for w in widgets: if isinstance(w, (str, Text)): @@ -88,6 +104,8 @@ def ensure_widgets( inputs.append(ensure_input(w)) elif isinstance(w, Media): media.append(ensure_media(w)) + elif isinstance(w, LinkPreviewBase): + link_preview.append(ensure_link_preview(w)) else: raise InvalidWidgetType( f"Cannot add widget of type {type(w)}. " @@ -99,6 +117,7 @@ def ensure_widgets( ensure_keyboard(keyboards), ensure_input(inputs), ensure_media(media), + ensure_link_preview(link_preview), ) diff --git a/src/aiogram_dialog/window.py b/src/aiogram_dialog/window.py index 832f30fc..c4eaf58c 100644 --- a/src/aiogram_dialog/window.py +++ b/src/aiogram_dialog/window.py @@ -5,6 +5,7 @@ from aiogram.types import ( UNSET_PARSE_MODE, CallbackQuery, + LinkPreviewOptions, Message, ) from aiogram.types.base import UNSET_DISABLE_WEB_PAGE_PREVIEW @@ -55,6 +56,7 @@ def __init__( self.keyboard, self.on_message, self.media, + self.link_preview, ) = ensure_widgets(widgets) self.getter = PreviewAwareGetter( ensure_data_getter(getter), @@ -87,6 +89,13 @@ async def render_kbd( data, manager, keyboard, ) + async def render_link_preview( + self, data: dict, manager: DialogManager, + ) -> Optional[LinkPreviewOptions]: + if self.link_preview: + return await self.link_preview.render_link_preview(data, manager) + return None + async def load_data( self, dialog: "DialogProtocol", manager: DialogManager, @@ -145,6 +154,9 @@ async def render( parse_mode=self.parse_mode, disable_web_page_preview=self.disable_web_page_preview, media=await self.render_media(current_data, manager), + link_preview_options=await self.render_link_preview( + current_data, manager, + ), ) except Exception: logger.error("Cannot render window for state %s", self.state)