From f52c59376d8ec8427ccf300faa423ccce8a098c9 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 15 Jul 2018 21:00:35 +0300 Subject: [PATCH 1/7] Fixed markup-entities parsing. --- aiogram/types/message.py | 63 +++++++++++++++++------ aiogram/types/message_entity.py | 91 ++++++++++++++++++--------------- 2 files changed, 98 insertions(+), 56 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 459091ee..9f57f483 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1,5 +1,6 @@ import datetime import functools +import sys import typing from . import base @@ -21,6 +22,7 @@ from .video import Video from .video_note import VideoNote from .voice import Voice from ..utils import helper +from ..utils import markdown as md class Message(base.TelegramObject): @@ -157,6 +159,49 @@ class Message(base.TelegramObject): if command: return command[1].strip() + def parse_entities(self, as_html=True): + """ + Text or caption formatted as HTML. + + :return: str + """ + + text = self.text or self.caption + if text is None: + raise TypeError("This message doesn't have any text.") + + quote_fn = md.quote_html if as_html else md.escape_md + + if not self.entities: + return quote_fn(text) + + if not sys.maxunicode == 0xffff: + text = text.encode('utf-16-le') + + result = '' + offset = 0 + + for entity in sorted(self.entities, key=lambda item: item.offset): + entity_text = entity.parse(text, as_html=as_html) + + if sys.maxunicode == 0xffff: + part = text[offset:entity.offset] + result += quote_fn(part) + entity_text + else: + part = text[offset * 2:entity.offset * 2] + result += quote_fn(part.decode('utf-16-le')) + entity_text + + offset = entity.offset + entity.length + + if sys.maxunicode == 0xffff: + part = text[offset:] + result += quote_fn(part) + else: + part = text[offset * 2:] + result += quote_fn(part.decode('utf-16-le')) + + return result + @property def md_text(self) -> str: """ @@ -164,28 +209,16 @@ class Message(base.TelegramObject): :return: str """ - text = self.caption if self.caption else self.text - - if self.text and self.entities: - for entity in reversed(self.entities): - text = entity.apply_md(text) - - return text + return self.parse_entities(False) @property def html_text(self) -> str: """ - Text or caption formatted as HTML. + Text or caption formatted as HTML :return: str """ - text = self.caption if self.caption else self.text - - if self.text and self.entities: - for entity in reversed(self.entities): - text = entity.apply_html(text) - - return text + return self.parse_entities(True) async def reply(self, text, parse_mode=None, disable_web_page_preview=None, disable_notification=None, reply_markup=None, reply=True) -> 'Message': diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index 24e6da5f..6ca81518 100644 --- a/aiogram/types/message_entity.py +++ b/aiogram/types/message_entity.py @@ -1,3 +1,5 @@ +import sys + from . import base from . import fields from .user import User @@ -16,56 +18,63 @@ class MessageEntity(base.TelegramObject): url: base.String = fields.Field() user: User = fields.Field(base=User) - def _apply(self, text, func): - return text[:self.offset] + \ - func(text[self.offset:self.offset + self.length]) + \ - text[self.offset + self.length:] - - def apply_md(self, text): + def get_text(self, text): """ - Apply entity for text as Markdown + Get value of entity - :param text: - :return: + :param text: full text + :return: part of text """ + if sys.maxunicode == 0xffff: + return text[self.offset:self.offset + self.length] + + if not isinstance(text, bytes): + entity_text = text.encode('utf-16-le') + else: + entity_text = text + + entity_text = entity_text[self.offset * 2:(self.offset + self.length) * 2] + return entity_text.decode('utf-16-le') + + def parse(self, text, as_html=True): + """ + Get entity value with markup + + :param text: original text + :param as_html: as html? + :return: entity text with markup + """ + if not text: + return text + entity_text = self.get_text(text) + if self.type == MessageEntityType.BOLD: - return self._apply(text, markdown.bold) + if as_html: + return markdown.hbold(entity_text) + return markdown.bold(entity_text) elif self.type == MessageEntityType.ITALIC: - return self._apply(text, markdown.italic) + if as_html: + return markdown.hitalic(entity_text) + return markdown.italic(entity_text) elif self.type == MessageEntityType.PRE: - return self._apply(text, markdown.pre) + if as_html: + return markdown.hpre(entity_text) + return markdown.pre(entity_text) elif self.type == MessageEntityType.CODE: - return self._apply(text, markdown.code) + if as_html: + return markdown.hcode(entity_text) + return markdown.code(entity_text) elif self.type == MessageEntityType.URL: - return self._apply(text, lambda url: markdown.link(url, url)) + if as_html: + return markdown.hlink(entity_text, entity_text) + return markdown.link(entity_text, entity_text) elif self.type == MessageEntityType.TEXT_LINK: - return self._apply(text, lambda url: markdown.link(url, self.url)) - if self.type == MessageEntityType.TEXT_MENTION and self.user: - return self._apply(text, lambda name: self.user.get_mention(name, as_html=False)) - return text - - def apply_html(self, text): - """ - Apply entity for text as HTML - - :param text: - :return: - """ - if self.type == MessageEntityType.BOLD: - return self._apply(text, markdown.hbold) - elif self.type == MessageEntityType.ITALIC: - return self._apply(text, markdown.hitalic) - elif self.type == MessageEntityType.PRE: - return self._apply(text, markdown.hpre) - elif self.type == MessageEntityType.CODE: - return self._apply(text, markdown.hcode) - elif self.type == MessageEntityType.URL: - return self._apply(text, lambda url: markdown.hlink(url, url)) - elif self.type == MessageEntityType.TEXT_LINK: - return self._apply(text, lambda url: markdown.hlink(url, self.url)) - if self.type == MessageEntityType.TEXT_MENTION and self.user: - return self._apply(text, lambda name: self.user.get_mention(name, as_html=True)) - return text + if as_html: + return markdown.hlink(entity_text, self.url) + return markdown.link(entity_text, self.url) + elif self.type == MessageEntityType.TEXT_MENTION and self.user: + return self.user.get_mention(entity_text) + return entity_text class MessageEntityType(helper.Helper): From 59bde14db336756ca9b17c668076a22e2cae4fb5 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 15 Jul 2018 21:07:37 +0300 Subject: [PATCH 2/7] Fixed pydoc for `Message.parse_entities`. --- aiogram/types/message.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 9f57f483..16b3e264 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -161,7 +161,7 @@ class Message(base.TelegramObject): def parse_entities(self, as_html=True): """ - Text or caption formatted as HTML. + Text or caption formatted as HTML or Markdown. :return: str """ @@ -218,7 +218,7 @@ class Message(base.TelegramObject): :return: str """ - return self.parse_entities(True) + return self.parse_entities() async def reply(self, text, parse_mode=None, disable_web_page_preview=None, disable_notification=None, reply_markup=None, reply=True) -> 'Message': From ebc0a94201ea38c8cf50a6b9e528636819814a53 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 15 Jul 2018 21:49:53 +0300 Subject: [PATCH 3/7] Moooore error types. --- aiogram/utils/exceptions.py | 79 ++++++++++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index 63bc036e..43692247 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -9,8 +9,13 @@ TelegramAPIError MessageToDeleteNotFound MessageIdentifierNotSpecified MessageTextIsEmpty + MessageCantBeEdited + MessageToEditNotFound ToMuchMessages + ObjectExpectedAsReplyMarkup + InlineKeyboardExpected ChatNotFound + ChatDescriptionIsNotModified InvalidQueryID InvalidPeerID InvalidHTTPUrlContent @@ -29,11 +34,19 @@ TelegramAPIError MethodNotKnown PhotoAsInputFileRequired InvalidStickersSet + NoStickerInRequest ChatAdminRequired + NotEnoughRightsToPinMessage + CantDemoteChatCreator CantRestrictSelf PhotoDimensions UnavailableMembers TypeOfFileMismatch + WrongRemoteFileIdSpecified + PaymentProviderInvalid + CurrencyTotalAmountInvalid + CantParseUrl + CantParseEntities ConflictError TerminatedByOtherGetUpdates CantGetUpdates @@ -42,6 +55,7 @@ TelegramAPIError BotBlocked UserDeactivated CantInitiateConversation + CantTalkWithBots NetworkError RetryAfter MigrateToChat @@ -91,15 +105,6 @@ class _MatchErrorMixin: """ return cls.match.lower() in message - @classmethod - def throw(cls): - """ - Throw error - - :raise: this - """ - raise cls(cls.text or cls.match) - @classmethod def detect(cls, description): description = description.lower() @@ -107,7 +112,7 @@ class _MatchErrorMixin: if err is cls: continue if err.check(description): - err.throw() + raise err(cls.text or description) raise cls(description) @@ -164,6 +169,14 @@ class MessageTextIsEmpty(MessageError): match = 'Message text is empty' +class MessageCantBeEdited(MessageError): + match = 'message can\'t be edited' + + +class MessageToEditNotFound(MessageError): + match = 'message to edit not found' + + class ToMuchMessages(MessageError): """ Will be raised when you try to send media group with more than 10 items. @@ -171,10 +184,22 @@ class ToMuchMessages(MessageError): match = 'Too much messages to send as an album' +class ObjectExpectedAsReplyMarkup(BadRequest): + match = 'object expected as reply markup' + + +class InlineKeyboardExpected(BadRequest): + match = 'inline keyboard expected' + + class ChatNotFound(BadRequest): match = 'chat not found' +class ChatDescriptionIsNotModified(BadRequest): + match = 'chat description is not modified' + + class InvalidQueryID(BadRequest): match = 'QUERY_ID_INVALID' text = 'Invalid query ID' @@ -228,11 +253,23 @@ class InvalidStickersSet(BadRequest): text = 'Stickers set is invalid' +class NoStickerInRequest(BadRequest): + match = 'there is no sticker in the request' + + class ChatAdminRequired(BadRequest): match = 'CHAT_ADMIN_REQUIRED' text = 'Admin permissions is required!' +class NotEnoughRightsToPinMessage(BadRequest): + match = 'not enough rights to pin a message' + + +class CantDemoteChatCreator(BadRequest): + match = 'can\'t demote chat creator' + + class CantRestrictSelf(BadRequest): match = "can't restrict self" text = "Admin can't restrict self." @@ -251,6 +288,20 @@ class TypeOfFileMismatch(BadRequest): match = 'type of file mismatch' +class WrongRemoteFileIdSpecified(BadRequest): + match = 'wrong remote file id specified' + + +class PaymentProviderInvalid(BadRequest): + match = 'PAYMENT_PROVIDER_INVALID' + text = 'payment provider invalid' + + +class CurrencyTotalAmountInvalid(BadRequest): + match = 'currency_total_amount_invalid' + text = 'currency total amount invalid' + + class BadWebhook(BadRequest): __group = True @@ -274,6 +325,10 @@ class CantParseUrl(BadRequest): match = 'can\'t parse URL' +class CantParseEntities(BadRequest): + match = 'can\'t parse entities' + + class NotFound(TelegramAPIError, _MatchErrorMixin): __group = True @@ -316,6 +371,10 @@ class CantInitiateConversation(Unauthorized): match = 'bot can\'t initiate conversation with a user' +class CantTalkWithBots(Unauthorized): + match = 'bot can\'t send messages to bots' + + class NetworkError(TelegramAPIError): pass From 50ea2f37ae8abdc1f406feaa68f7aaa1b23a9313 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 15 Jul 2018 21:55:23 +0300 Subject: [PATCH 4/7] In channels `CallbackQuery.message.from_user` can be empty. --- aiogram/contrib/middlewares/logging.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/aiogram/contrib/middlewares/logging.py b/aiogram/contrib/middlewares/logging.py index ee9ac65a..234c1053 100644 --- a/aiogram/contrib/middlewares/logging.py +++ b/aiogram/contrib/middlewares/logging.py @@ -88,9 +88,13 @@ class LoggingMiddleware(BaseMiddleware): async def on_pre_process_callback_query(self, callback_query: types.CallbackQuery): if callback_query.message: - self.logger.info(f"Received callback query [ID:{callback_query.id}] " - f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}] " - f"from user [ID:{callback_query.message.from_user.id}]") + if callback_query.message.from_user: + self.logger.info(f"Received callback query [ID:{callback_query.id}] " + f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}] " + f"from user [ID:{callback_query.message.from_user.id}]") + else: + self.logger.info(f"Received callback query [ID:{callback_query.id}] " + f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") else: self.logger.info(f"Received callback query [ID:{callback_query.id}] " f"from inline message [ID:{callback_query.inline_message_id}] " @@ -98,10 +102,15 @@ class LoggingMiddleware(BaseMiddleware): async def on_post_process_callback_query(self, callback_query, results): if callback_query.message: - self.logger.debug(f"{HANDLED_STR[bool(len(results))]} " - f"callback query [ID:{callback_query.id}] " - f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}] " - f"from user [ID:{callback_query.message.from_user.id}]") + if callback_query.message.from_user: + self.logger.debug(f"{HANDLED_STR[bool(len(results))]} " + f"callback query [ID:{callback_query.id}] " + f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}] " + f"from user [ID:{callback_query.message.from_user.id}]") + else: + self.logger.debug(f"{HANDLED_STR[bool(len(results))]} " + f"callback query [ID:{callback_query.id}] " + f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") else: self.logger.debug(f"{HANDLED_STR[bool(len(results))]} " f"callback query [ID:{callback_query.id}] " From a67831892732e9954621dac332d51cf92d8ef04d Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 15 Jul 2018 22:16:34 +0300 Subject: [PATCH 5/7] More content types. --- aiogram/types/message.py | 51 ++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 16b3e264..75b86291 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -79,42 +79,52 @@ class Message(base.TelegramObject): def content_type(self): if self.text: return ContentType.TEXT[0] - if self.audio: + elif self.audio: return ContentType.AUDIO[0] - if self.document: + elif self.document: return ContentType.DOCUMENT[0] - if self.game: + elif self.game: return ContentType.GAME[0] - if self.photo: + elif self.photo: return ContentType.PHOTO[0] - if self.sticker: + elif self.sticker: return ContentType.STICKER[0] - if self.video: + elif self.video: return ContentType.VIDEO[0] - if self.video_note: + elif self.video_note: return ContentType.VIDEO_NOTE[0] - if self.voice: + elif self.voice: return ContentType.VOICE[0] - if self.contact: + elif self.contact: return ContentType.CONTACT[0] - if self.venue: + elif self.venue: return ContentType.VENUE[0] - if self.location: + elif self.location: return ContentType.LOCATION[0] - if self.new_chat_members: + elif self.new_chat_members: return ContentType.NEW_CHAT_MEMBERS[0] - if self.left_chat_member: + elif self.left_chat_member: return ContentType.LEFT_CHAT_MEMBER[0] - if self.invoice: + elif self.invoice: return ContentType.INVOICE[0] - if self.successful_payment: + elif self.successful_payment: return ContentType.SUCCESSFUL_PAYMENT[0] - if self.connected_website: + elif self.connected_website: return ContentType.CONNECTED_WEBSITE[0] - if self.migrate_from_chat_id: + elif self.migrate_from_chat_id: return ContentType.MIGRATE_FROM_CHAT_ID[0] - if self.migrate_to_chat_id: + elif self.migrate_to_chat_id: return ContentType.MIGRATE_TO_CHAT_ID[0] + elif self.pinned_message: + return ContentType.PINNED_MESSAGE[0] + elif self.new_chat_title: + return ContentType.NEW_CHAT_TITLE[0] + elif self.new_chat_photo: + return ContentType.NEW_CHAT_PHOTO[0] + elif self.delete_chat_photo: + return ContentType.DELETE_CHAT_PHOTO[0] + elif self.group_chat_created: + return ContentType.GROUP_CHAT_CREATED[0] else: return ContentType.UNKNOWN[0] @@ -754,6 +764,11 @@ class ContentType(helper.Helper): CONNECTED_WEBSITE = helper.ListItem() # connected_website MIGRATE_TO_CHAT_ID = helper.ListItem() # migrate_to_chat_id MIGRATE_FROM_CHAT_ID = helper.ListItem() # migrate_from_chat_id + PINNED_MESSAGE = helper.ListItem() # pinned_message + NEW_CHAT_TITLE = helper.ListItem() # new_chat_title + NEW_CHAT_PHOTO = helper.ListItem() # new_chat_photo + DELETE_CHAT_PHOTO = helper.ListItem() # delete_chat_photo + GROUP_CHAT_CREATED = helper.ListItem() # group_chat_created UNKNOWN = helper.ListItem() # unknown ANY = helper.ListItem() # any From 220f47b00fd942e755f4bb217234e9e24c23de2f Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 15 Jul 2018 22:20:03 +0300 Subject: [PATCH 6/7] InlineQueryResultLocation has live_period --- aiogram/types/inline_query_result.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aiogram/types/inline_query_result.py b/aiogram/types/inline_query_result.py index b5d0df64..62936961 100644 --- a/aiogram/types/inline_query_result.py +++ b/aiogram/types/inline_query_result.py @@ -358,6 +358,7 @@ class InlineQueryResultLocation(InlineQueryResult): latitude: base.Float = fields.Field() longitude: base.Float = fields.Field() title: base.String = fields.Field() + live_period: base.Integer = fields.Field() input_message_content: InputMessageContent = fields.Field(base=InputMessageContent) thumb_url: base.String = fields.Field() thumb_width: base.Integer = fields.Field() @@ -368,13 +369,15 @@ class InlineQueryResultLocation(InlineQueryResult): latitude: base.Float, longitude: base.Float, title: base.String, + live_period: typing.Optional[base.Integer] = None, reply_markup: typing.Optional[InlineKeyboardMarkup] = None, input_message_content: typing.Optional[InputMessageContent] = None, thumb_url: typing.Optional[base.String] = None, thumb_width: typing.Optional[base.Integer] = None, thumb_height: typing.Optional[base.Integer] = None): super(InlineQueryResultLocation, self).__init__(id=id, latitude=latitude, longitude=longitude, - title=title, reply_markup=reply_markup, + title=title, live_period=live_period, + reply_markup=reply_markup, input_message_content=input_message_content, thumb_url=thumb_url, thumb_width=thumb_width, thumb_height=thumb_height) From 3f8034ad902b291b80293cc70d84f8636955c8a2 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 15 Jul 2018 23:01:32 +0300 Subject: [PATCH 7/7] Change version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index ad97a44a..4775972b 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -10,5 +10,5 @@ except ImportError: else: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -__version__ = '1.3.3.dev1' +__version__ = '1.3.3' __api_version__ = '3.6'