diff --git a/README.md b/README.md
index 74de8a8d..dfef918f 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
[](https://pypi.python.org/pypi/aiogram)
[](https://pypi.python.org/pypi/aiogram)
[](https://pypi.python.org/pypi/aiogram)
-[](https://core.telegram.org/bots/api)
+[](https://core.telegram.org/bots/api)
[](http://docs.aiogram.dev/en/latest/?badge=latest)
[](https://github.com/aiogram/aiogram/issues)
[](https://opensource.org/licenses/MIT)
diff --git a/README.rst b/README.rst
index 78dc071c..1cf2765d 100644
--- a/README.rst
+++ b/README.rst
@@ -21,7 +21,7 @@ AIOGramBot
:target: https://pypi.python.org/pypi/aiogram
:alt: Supported python versions
-.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.8-blue.svg?style=flat-square&logo=telegram
+.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.9-blue.svg?style=flat-square&logo=telegram
:target: https://core.telegram.org/bots/api
:alt: Telegram Bot API
diff --git a/aiogram/__init__.py b/aiogram/__init__.py
index f06827cd..0dba109a 100644
--- a/aiogram/__init__.py
+++ b/aiogram/__init__.py
@@ -1,3 +1,8 @@
+import sys
+if sys.version_info < (3, 7):
+ raise ImportError('Your Python version {0} is not supported by aiogram, please install '
+ 'Python 3.7+'.format('.'.join(map(str, sys.version_info[:3]))))
+
import asyncio
import os
@@ -38,5 +43,5 @@ __all__ = [
'utils'
]
-__version__ = '2.8'
-__api_version__ = '4.8'
+__version__ = '2.9.1'
+__api_version__ = '4.9'
diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py
index 3b341ec9..1d0c4f7b 100644
--- a/aiogram/bot/api.py
+++ b/aiogram/bot/api.py
@@ -153,7 +153,7 @@ class Methods(Helper):
"""
Helper for Telegram API Methods listed on https://core.telegram.org/bots/api
- List is updated to Bot API 4.8
+ List is updated to Bot API 4.9
"""
mode = HelperMode.lowerCamelCase
diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py
index 6750e8a8..86347e88 100644
--- a/aiogram/bot/base.py
+++ b/aiogram/bot/base.py
@@ -5,7 +5,7 @@ import ssl
import typing
import warnings
from contextvars import ContextVar
-from typing import Dict, List, Optional, Union
+from typing import Dict, List, Optional, Union, Type
import aiohttp
import certifi
@@ -61,6 +61,7 @@ class BaseBot:
api.check_token(token)
self._token = None
self.__token = token
+ self.id = int(token.split(sep=':')[0])
self.proxy = proxy
self.proxy_auth = proxy_auth
@@ -73,6 +74,12 @@ class BaseBot:
# aiohttp main session
ssl_context = ssl.create_default_context(cafile=certifi.where())
+ self._session: Optional[aiohttp.ClientSession] = None
+ self._connector_class: Type[aiohttp.TCPConnector] = aiohttp.TCPConnector
+ self._connector_init = dict(
+ limit=connections_limit, ssl=ssl_context, loop=self.loop
+ )
+
if isinstance(proxy, str) and (proxy.startswith('socks5://') or proxy.startswith('socks4://')):
from aiohttp_socks import SocksConnector
from aiohttp_socks.utils import parse_proxy_url
@@ -84,30 +91,31 @@ class BaseBot:
if not password:
password = proxy_auth.password
- connector = SocksConnector(socks_ver=socks_ver, host=host, port=port,
- username=username, password=password,
- limit=connections_limit, ssl_context=ssl_context,
- rdns=True, loop=self.loop)
-
+ self._connector_class = SocksConnector
+ self._connector_init.update(
+ socks_ver=socks_ver, host=host, port=port,
+ username=username, password=password, rdns=True,
+ )
self.proxy = None
self.proxy_auth = None
- else:
- connector = aiohttp.TCPConnector(limit=connections_limit, ssl=ssl_context, loop=self.loop)
+
self._timeout = None
self.timeout = timeout
- self.session = aiohttp.ClientSession(connector=connector, loop=self.loop, json_serialize=json.dumps)
-
self.parse_mode = parse_mode
- def __del__(self):
- if not hasattr(self, 'loop') or not hasattr(self, 'session'):
- return
- if self.loop.is_running():
- self.loop.create_task(self.close())
- return
- loop = asyncio.new_event_loop()
- loop.run_until_complete(self.close())
+ def get_new_session(self) -> aiohttp.ClientSession:
+ return aiohttp.ClientSession(
+ connector=self._connector_class(**self._connector_init),
+ loop=self.loop,
+ json_serialize=json.dumps
+ )
+
+ @property
+ def session(self) -> Optional[aiohttp.ClientSession]:
+ if self._session is None or self._session.closed:
+ self._session = self.get_new_session()
+ return self._session
@staticmethod
def _prepare_timeout(
diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py
index 950ce60f..164d6aad 100644
--- a/aiogram/dispatcher/dispatcher.py
+++ b/aiogram/dispatcher/dispatcher.py
@@ -10,7 +10,7 @@ from aiohttp.helpers import sentinel
from aiogram.utils.deprecated import renamed_argument
from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, HashTag, Regexp, \
- RegexpCommandsFilter, StateFilter, Text, IDFilter, AdminFilter, IsReplyFilter
+ RegexpCommandsFilter, StateFilter, Text, IDFilter, AdminFilter, IsReplyFilter, ForwardedMessageFilter
from .filters.builtin import IsSenderContact
from .handler import Handler
from .middlewares import MiddlewareManager
@@ -160,6 +160,12 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
self.channel_post_handlers,
self.edited_channel_post_handlers,
])
+ filters_factory.bind(ForwardedMessageFilter, event_handlers=[
+ self.message_handlers,
+ self.edited_channel_post_handlers,
+ self.channel_post_handlers,
+ self.edited_channel_post_handlers
+ ])
def __del__(self):
self.stop_polling()
@@ -236,6 +242,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
if update.poll:
return await self.poll_handlers.notify(update.poll)
if update.poll_answer:
+ types.User.set_current(update.poll_answer.user)
return await self.poll_answer_handlers.notify(update.poll_answer)
except Exception as e:
err = await self.errors_handlers.notify(update, e)
diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py
index 6de3cc7a..edd1959a 100644
--- a/aiogram/dispatcher/filters/__init__.py
+++ b/aiogram/dispatcher/filters/__init__.py
@@ -1,6 +1,6 @@
from .builtin import Command, CommandHelp, CommandPrivacy, CommandSettings, CommandStart, ContentTypeFilter, \
ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, \
- Text, IDFilter, AdminFilter, IsReplyFilter, IsSenderContact
+ Text, IDFilter, AdminFilter, IsReplyFilter, IsSenderContact, ForwardedMessageFilter
from .factory import FiltersFactory
from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \
check_filters, get_filter_spec, get_filters_spec
@@ -32,4 +32,5 @@ __all__ = [
'get_filters_spec',
'execute_filter',
'check_filters',
+ 'ForwardedMessageFilter',
]
diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py
index 0a81998a..c59d9b0d 100644
--- a/aiogram/dispatcher/filters/builtin.py
+++ b/aiogram/dispatcher/filters/builtin.py
@@ -12,6 +12,19 @@ from aiogram.dispatcher.filters.filters import BoundFilter, Filter
from aiogram.types import CallbackQuery, Message, InlineQuery, Poll, ChatType
+ChatIDArgumentType = typing.Union[typing.Iterable[typing.Union[int, str]], str, int]
+
+
+def extract_chat_ids(chat_id: ChatIDArgumentType) -> typing.Set[int]:
+ # since "str" is also an "Iterable", we have to check for it first
+ if isinstance(chat_id, str):
+ return {int(chat_id), }
+ if isinstance(chat_id, Iterable):
+ return {int(item) for (item) in chat_id}
+ # the last possible type is a single "int"
+ return {chat_id, }
+
+
class Command(Filter):
"""
You can handle commands by using this filter.
@@ -545,10 +558,9 @@ class ExceptionsFilter(BoundFilter):
class IDFilter(Filter):
-
def __init__(self,
- user_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None,
- chat_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None,
+ user_id: Optional[ChatIDArgumentType] = None,
+ chat_id: Optional[ChatIDArgumentType] = None,
):
"""
:param user_id:
@@ -557,18 +569,14 @@ class IDFilter(Filter):
if user_id is None and chat_id is None:
raise ValueError("Both user_id and chat_id can't be None")
- self.user_id = None
- self.chat_id = None
+ self.user_id: Optional[typing.Set[int]] = None
+ self.chat_id: Optional[typing.Set[int]] = None
+
if user_id:
- if isinstance(user_id, Iterable):
- self.user_id = list(map(int, user_id))
- else:
- self.user_id = [int(user_id), ]
+ self.user_id = extract_chat_ids(user_id)
+
if chat_id:
- if isinstance(chat_id, Iterable):
- self.chat_id = list(map(int, chat_id))
- else:
- self.chat_id = [int(chat_id), ]
+ self.chat_id = extract_chat_ids(chat_id)
@classmethod
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
@@ -614,22 +622,20 @@ class AdminFilter(Filter):
is_chat_admin is required for InlineQuery.
"""
- def __init__(self, is_chat_admin: Optional[Union[Iterable[Union[int, str]], str, int, bool]] = None):
+ def __init__(self, is_chat_admin: Optional[Union[ChatIDArgumentType, bool]] = None):
self._check_current = False
self._chat_ids = None
if is_chat_admin is False:
raise ValueError("is_chat_admin cannot be False")
- if is_chat_admin:
- if isinstance(is_chat_admin, bool):
- self._check_current = is_chat_admin
- if isinstance(is_chat_admin, Iterable):
- self._chat_ids = list(is_chat_admin)
- else:
- self._chat_ids = [is_chat_admin]
- else:
+ if not is_chat_admin:
self._check_current = True
+ return
+
+ if isinstance(is_chat_admin, bool):
+ self._check_current = is_chat_admin
+ self._chat_ids = extract_chat_ids(is_chat_admin)
@classmethod
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
@@ -675,3 +681,13 @@ class IsReplyFilter(BoundFilter):
return {'reply': msg.reply_to_message}
elif not msg.reply_to_message and not self.is_reply:
return True
+
+
+class ForwardedMessageFilter(BoundFilter):
+ key = 'is_forwarded'
+
+ def __init__(self, is_forwarded: bool):
+ self.is_forwarded = is_forwarded
+
+ async def check(self, message: Message):
+ return bool(getattr(message, "forward_date")) is self.is_forwarded
diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py
index 0d3947f6..4a7287d8 100644
--- a/aiogram/types/chat.py
+++ b/aiogram/types/chat.py
@@ -178,7 +178,7 @@ class Chat(base.TelegramObject):
:return: Returns True on success.
:rtype: :obj:`base.Boolean`
"""
- return await self.bot.delete_chat_description(self.id, description)
+ return await self.bot.set_chat_description(self.id, description)
async def kick(self, user_id: base.Integer,
until_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None) -> base.Boolean:
diff --git a/aiogram/types/dice.py b/aiogram/types/dice.py
index 6dfb190f..7b3f1727 100644
--- a/aiogram/types/dice.py
+++ b/aiogram/types/dice.py
@@ -16,3 +16,4 @@ class Dice(base.TelegramObject):
class DiceEmoji:
DICE = '🎲'
DART = '🎯'
+ BASKETBALL = '🏀'
diff --git a/aiogram/types/inline_query_result.py b/aiogram/types/inline_query_result.py
index a80352d7..fccaa2a1 100644
--- a/aiogram/types/inline_query_result.py
+++ b/aiogram/types/inline_query_result.py
@@ -92,12 +92,13 @@ class InlineQueryResultPhoto(InlineQueryResult):
title: typing.Optional[base.String] = None,
description: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
+ parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None):
super(InlineQueryResultPhoto, self).__init__(id=id, photo_url=photo_url, thumb_url=thumb_url,
photo_width=photo_width, photo_height=photo_height, title=title,
description=description, caption=caption,
- reply_markup=reply_markup,
+ parse_mode=parse_mode, reply_markup=reply_markup,
input_message_content=input_message_content)
@@ -117,6 +118,7 @@ class InlineQueryResultGif(InlineQueryResult):
gif_height: base.Integer = fields.Field()
gif_duration: base.Integer = fields.Field()
thumb_url: base.String = fields.Field()
+ thumb_mime_type: base.String = fields.Field()
title: base.String = fields.Field()
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
@@ -156,6 +158,7 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
mpeg4_height: base.Integer = fields.Field()
mpeg4_duration: base.Integer = fields.Field()
thumb_url: base.String = fields.Field()
+ thumb_mime_type: base.String = fields.Field()
title: base.String = fields.Field()
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
diff --git a/aiogram/types/input_media.py b/aiogram/types/input_media.py
index 952e7a55..d42fac99 100644
--- a/aiogram/types/input_media.py
+++ b/aiogram/types/input_media.py
@@ -137,8 +137,6 @@ class InputMediaAudio(InputMedia):
https://core.telegram.org/bots/api#inputmediaanimation
"""
- width: base.Integer = fields.Field()
- height: base.Integer = fields.Field()
duration: base.Integer = fields.Field()
performer: base.String = fields.Field()
title: base.String = fields.Field()
@@ -146,13 +144,12 @@ class InputMediaAudio(InputMedia):
def __init__(self, media: base.InputFile,
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
- width: base.Integer = None, height: base.Integer = None,
duration: base.Integer = None,
performer: base.String = None,
title: base.String = None,
parse_mode: base.String = None, **kwargs):
- super(InputMediaAudio, self).__init__(type='audio', media=media, thumb=thumb, caption=caption,
- width=width, height=height, duration=duration,
+ super(InputMediaAudio, self).__init__(type='audio', media=media, thumb=thumb,
+ caption=caption, duration=duration,
performer=performer, title=title,
parse_mode=parse_mode, conf=kwargs)
diff --git a/aiogram/types/message.py b/aiogram/types/message.py
index ddbccde6..2ea65bfb 100644
--- a/aiogram/types/message.py
+++ b/aiogram/types/message.py
@@ -51,6 +51,7 @@ class Message(base.TelegramObject):
forward_signature: base.String = fields.Field()
forward_date: datetime.datetime = fields.DateTimeField()
reply_to_message: Message = fields.Field(base='Message')
+ via_bot: User = fields.Field(base=User)
edit_date: datetime.datetime = fields.DateTimeField()
media_group_id: base.String = fields.Field()
author_signature: base.String = fields.Field()
@@ -167,7 +168,8 @@ class Message(base.TelegramObject):
:return: tuple of (command, args)
"""
if self.is_command():
- command, _, args = self.text.partition(' ')
+ command, *args = self.text.split(maxsplit=1)
+ args = args[0] if args else None
return command, args
def get_command(self, pure=False):
@@ -191,7 +193,7 @@ class Message(base.TelegramObject):
"""
command = self.get_full_command()
if command:
- return command[1].strip()
+ return command[1]
def parse_entities(self, as_html=True):
"""
diff --git a/aiogram/types/reply_keyboard.py b/aiogram/types/reply_keyboard.py
index ced20417..ffe07ae1 100644
--- a/aiogram/types/reply_keyboard.py
+++ b/aiogram/types/reply_keyboard.py
@@ -111,11 +111,13 @@ class KeyboardButton(base.TelegramObject):
def __init__(self, text: base.String,
request_contact: base.Boolean = None,
request_location: base.Boolean = None,
- request_poll: KeyboardButtonPollType = None):
+ request_poll: KeyboardButtonPollType = None,
+ **kwargs):
super(KeyboardButton, self).__init__(text=text,
request_contact=request_contact,
request_location=request_location,
- request_poll=request_poll)
+ request_poll=request_poll,
+ **kwargs)
class ReplyKeyboardRemove(base.TelegramObject):
diff --git a/aiogram/utils/markdown.py b/aiogram/utils/markdown.py
index d3c8583b..da27bc39 100644
--- a/aiogram/utils/markdown.py
+++ b/aiogram/utils/markdown.py
@@ -18,7 +18,7 @@ HTML_QUOTES_MAP = {"<": "<", ">": ">", "&": "&", '"': """}
_HQS = HTML_QUOTES_MAP.keys() # HQS for HTML QUOTES SYMBOLS
-def quote_html(*content, sep=" "):
+def quote_html(*content, sep=" ") -> str:
"""
Quote HTML symbols
@@ -33,7 +33,7 @@ def quote_html(*content, sep=" "):
return html_decoration.quote(_join(*content, sep=sep))
-def escape_md(*content, sep=" "):
+def escape_md(*content, sep=" ") -> str:
"""
Escape markdown text
@@ -61,7 +61,7 @@ def text(*content, sep=" "):
return _join(*content, sep=sep)
-def bold(*content, sep=" "):
+def bold(*content, sep=" ") -> str:
"""
Make bold text (Markdown)
@@ -69,12 +69,12 @@ def bold(*content, sep=" "):
:param sep:
:return:
"""
- return markdown_decoration.bold.format(
- value=html_decoration.quote(_join(*content, sep=sep))
+ return markdown_decoration.bold(
+ value=markdown_decoration.quote(_join(*content, sep=sep))
)
-def hbold(*content, sep=" "):
+def hbold(*content, sep=" ") -> str:
"""
Make bold text (HTML)
@@ -82,12 +82,12 @@ def hbold(*content, sep=" "):
:param sep:
:return:
"""
- return html_decoration.bold.format(
+ return html_decoration.bold(
value=html_decoration.quote(_join(*content, sep=sep))
)
-def italic(*content, sep=" "):
+def italic(*content, sep=" ") -> str:
"""
Make italic text (Markdown)
@@ -95,12 +95,12 @@ def italic(*content, sep=" "):
:param sep:
:return:
"""
- return markdown_decoration.italic.format(
- value=html_decoration.quote(_join(*content, sep=sep))
+ return markdown_decoration.italic(
+ value=markdown_decoration.quote(_join(*content, sep=sep))
)
-def hitalic(*content, sep=" "):
+def hitalic(*content, sep=" ") -> str:
"""
Make italic text (HTML)
@@ -108,12 +108,12 @@ def hitalic(*content, sep=" "):
:param sep:
:return:
"""
- return html_decoration.italic.format(
+ return html_decoration.italic(
value=html_decoration.quote(_join(*content, sep=sep))
)
-def code(*content, sep=" "):
+def code(*content, sep=" ") -> str:
"""
Make mono-width text (Markdown)
@@ -121,12 +121,12 @@ def code(*content, sep=" "):
:param sep:
:return:
"""
- return markdown_decoration.code.format(
- value=html_decoration.quote(_join(*content, sep=sep))
+ return markdown_decoration.code(
+ value=markdown_decoration.quote(_join(*content, sep=sep))
)
-def hcode(*content, sep=" "):
+def hcode(*content, sep=" ") -> str:
"""
Make mono-width text (HTML)
@@ -134,12 +134,12 @@ def hcode(*content, sep=" "):
:param sep:
:return:
"""
- return html_decoration.code.format(
+ return html_decoration.code(
value=html_decoration.quote(_join(*content, sep=sep))
)
-def pre(*content, sep="\n"):
+def pre(*content, sep="\n") -> str:
"""
Make mono-width text block (Markdown)
@@ -147,12 +147,12 @@ def pre(*content, sep="\n"):
:param sep:
:return:
"""
- return markdown_decoration.pre.format(
- value=html_decoration.quote(_join(*content, sep=sep))
+ return markdown_decoration.pre(
+ value=markdown_decoration.quote(_join(*content, sep=sep))
)
-def hpre(*content, sep="\n"):
+def hpre(*content, sep="\n") -> str:
"""
Make mono-width text block (HTML)
@@ -160,12 +160,12 @@ def hpre(*content, sep="\n"):
:param sep:
:return:
"""
- return html_decoration.pre.format(
+ return html_decoration.pre(
value=html_decoration.quote(_join(*content, sep=sep))
)
-def underline(*content, sep=" "):
+def underline(*content, sep=" ") -> str:
"""
Make underlined text (Markdown)
@@ -173,12 +173,12 @@ def underline(*content, sep=" "):
:param sep:
:return:
"""
- return markdown_decoration.underline.format(
+ return markdown_decoration.underline(
value=markdown_decoration.quote(_join(*content, sep=sep))
)
-def hunderline(*content, sep=" "):
+def hunderline(*content, sep=" ") -> str:
"""
Make underlined text (HTML)
@@ -186,12 +186,12 @@ def hunderline(*content, sep=" "):
:param sep:
:return:
"""
- return html_decoration.underline.format(
+ return html_decoration.underline(
value=html_decoration.quote(_join(*content, sep=sep))
)
-def strikethrough(*content, sep=" "):
+def strikethrough(*content, sep=" ") -> str:
"""
Make strikethrough text (Markdown)
@@ -199,12 +199,12 @@ def strikethrough(*content, sep=" "):
:param sep:
:return:
"""
- return markdown_decoration.strikethrough.format(
+ return markdown_decoration.strikethrough(
value=markdown_decoration.quote(_join(*content, sep=sep))
)
-def hstrikethrough(*content, sep=" "):
+def hstrikethrough(*content, sep=" ") -> str:
"""
Make strikethrough text (HTML)
@@ -212,7 +212,7 @@ def hstrikethrough(*content, sep=" "):
:param sep:
:return:
"""
- return html_decoration.strikethrough.format(
+ return html_decoration.strikethrough(
value=html_decoration.quote(_join(*content, sep=sep))
)
@@ -225,7 +225,7 @@ def link(title: str, url: str) -> str:
:param url:
:return:
"""
- return markdown_decoration.link.format(value=html_decoration.quote(title), link=url)
+ return markdown_decoration.link(value=markdown_decoration.quote(title), link=url)
def hlink(title: str, url: str) -> str:
@@ -236,7 +236,7 @@ def hlink(title: str, url: str) -> str:
:param url:
:return:
"""
- return html_decoration.link.format(value=html_decoration.quote(title), link=url)
+ return html_decoration.link(value=html_decoration.quote(title), link=url)
def hide_link(url: str) -> str:
diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py
index ad52c9d7..77dc0ff3 100644
--- a/aiogram/utils/text_decorations.py
+++ b/aiogram/utils/text_decorations.py
@@ -1,33 +1,23 @@
from __future__ import annotations
+
import html
import re
-import struct
-from dataclasses import dataclass
-from typing import TYPE_CHECKING, AnyStr, Callable, Generator, Iterable, List, Optional
+from abc import ABC, abstractmethod
+from typing import TYPE_CHECKING, Generator, List, Optional, Pattern, cast
-if TYPE_CHECKING:
+if TYPE_CHECKING: # pragma: no cover
from aiogram.types import MessageEntity
__all__ = (
"TextDecoration",
+ "HtmlDecoration",
+ "MarkdownDecoration",
"html_decoration",
"markdown_decoration",
- "add_surrogate",
- "remove_surrogate",
)
-@dataclass
-class TextDecoration:
- link: str
- bold: str
- italic: str
- code: str
- pre: str
- underline: str
- strikethrough: str
- quote: Callable[[AnyStr], AnyStr]
-
+class TextDecoration(ABC):
def apply_entity(self, entity: MessageEntity, text: str) -> str:
"""
Apply single entity to text
@@ -36,24 +26,28 @@ class TextDecoration:
:param text:
:return:
"""
- if entity.type in (
- "bold",
- "italic",
- "code",
- "pre",
- "underline",
- "strikethrough",
- ):
- return getattr(self, entity.type).format(value=text)
- elif entity.type == "text_mention":
- return self.link.format(value=text, link=f"tg://user?id={entity.user.id}")
- elif entity.type == "text_link":
- return self.link.format(value=text, link=entity.url)
- elif entity.type == "url":
+ if entity.type in {"bot_command", "url", "mention", "phone_number"}:
+ # This entities should not be changed
return text
+ if entity.type in {"bold", "italic", "code", "underline", "strikethrough"}:
+ return cast(str, getattr(self, entity.type)(value=text))
+ if entity.type == "pre":
+ return (
+ self.pre_language(value=text, language=entity.language)
+ if entity.language
+ else self.pre(value=text)
+ )
+ if entity.type == "text_mention":
+ from aiogram.types import User
+
+ user = cast(User, entity.user)
+ return self.link(value=text, link=f"tg://user?id={user.id}")
+ if entity.type == "text_link":
+ return self.link(value=text, link=cast(str, entity.url))
+
return self.quote(text)
- def unparse(self, text, entities: Optional[List[MessageEntity]] = None) -> str:
+ def unparse(self, text: str, entities: Optional[List[MessageEntity]] = None) -> str:
"""
Unparse message entities
@@ -61,22 +55,22 @@ class TextDecoration:
:param entities: Array of MessageEntities
:return:
"""
- text = add_surrogate(text)
result = "".join(
self._unparse_entities(
text, sorted(entities, key=lambda item: item.offset) if entities else []
)
)
- return remove_surrogate(result)
+ return result
def _unparse_entities(
self,
text: str,
- entities: Iterable[MessageEntity],
+ entities: List[MessageEntity],
offset: Optional[int] = None,
length: Optional[int] = None,
) -> Generator[str, None, None]:
- offset = offset or 0
+ if offset is None:
+ offset = 0
length = length or len(text)
for index, entity in enumerate(entities):
@@ -88,7 +82,7 @@ class TextDecoration:
offset = entity.offset + entity.length
sub_entities = list(
- filter(lambda e: e.offset < offset, entities[index + 1 :])
+ filter(lambda e: e.offset < (offset or 0), entities[index + 1 :])
)
yield self.apply_entity(
entity,
@@ -102,42 +96,102 @@ class TextDecoration:
if offset < length:
yield self.quote(text[offset:length])
+ @abstractmethod
+ def link(self, value: str, link: str) -> str: # pragma: no cover
+ pass
-html_decoration = TextDecoration(
- link='{value}',
- bold="{value}",
- italic="{value}",
- code="{value}",
- pre="
{value}",
- underline="{value}",
- strikethrough="{value}"
+
+ def pre(self, value: str) -> str:
+ return f"{value}"
+
+ def pre_language(self, value: str, language: str) -> str:
+ return f'{value}'
+
+ def underline(self, value: str) -> str:
+ return f"{value}"
+
+ def strikethrough(self, value: str) -> str:
+ return f"