From dab237e758ef0e2c9c7c781fff97d4e83914ecde Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 6 Sep 2019 13:45:31 +0200 Subject: [PATCH] Support sending scheduled messages --- telethon/client/messageparse.py | 44 +++++++++++++++++++++++------ telethon/client/messages.py | 49 +++++++++++++++++++++++++++------ telethon/client/uploads.py | 18 ++++++++---- 3 files changed, 89 insertions(+), 22 deletions(-) diff --git a/telethon/client/messageparse.py b/telethon/client/messageparse.py index 9e5e20a9..5f3c95c9 100644 --- a/telethon/client/messageparse.py +++ b/telethon/client/messageparse.py @@ -123,6 +123,7 @@ class MessageParseMethods: random_to_id = {} id_to_message = {} + sched_to_message = {} # scheduled IDs may collide with normal IDs for update in updates: if isinstance(update, types.UpdateMessageID): random_to_id[update.random_id] = update.id @@ -145,12 +146,30 @@ class MessageParseMethods: update.message._finish_init(self, entities, input_chat) return update.message + elif isinstance(update, types.UpdateNewScheduledMessage): + update.message._finish_init(self, entities, input_chat) + sched_to_message[update.message.id] = update.message + if request is None: return id_to_message + # Use the scheduled mapping if we got a request with a scheduled message + # + # This breaks if the schedule date is too young, however, since the message + # is sent immediately, so have a fallback. + if getattr(request, 'schedule_date', None) is None: + mapping = id_to_message + opposite = {} # if there's no schedule it can never be scheduled + else: + mapping = sched_to_message + opposite = id_to_message # scheduled may be treated as normal, though + random_id = request if isinstance(request, int) else request.random_id if not utils.is_list_like(random_id): - msg = id_to_message.get(random_to_id.get(random_id)) + msg = mapping.get(random_to_id.get(random_id)) + if not msg: + msg = opposite.get(random_to_id.get(random_id)) + if not msg: self._log[__name__].warning( 'Request %s had missing message mapping %s', request, result) @@ -158,15 +177,22 @@ class MessageParseMethods: return msg try: - return [id_to_message[random_to_id[rnd]] for rnd in random_id] + return [mapping[random_to_id[rnd]] for rnd in random_id] except KeyError: - # Sometimes forwards fail (`MESSAGE_ID_INVALID` if a message gets - # deleted or `WORKER_BUSY_TOO_LONG_RETRY` if there are issues at - # Telegram), in which case we get some "missing" message mappings. - # Log them with the hope that we can better work around them. - self._log[__name__].warning( - 'Request %s had missing message mappings %s', request, result) + try: + return [opposite[random_to_id[rnd]] for rnd in random_id] + except KeyError: + # Sometimes forwards fail (`MESSAGE_ID_INVALID` if a message gets + # deleted or `WORKER_BUSY_TOO_LONG_RETRY` if there are issues at + # Telegram), in which case we get some "missing" message mappings. + # Log them with the hope that we can better work around them. + self._log[__name__].warning( + 'Request %s had missing message mappings %s', request, result) - return [id_to_message.get(random_to_id.get(rnd)) for rnd in random_to_id] + return [ + mapping.get(random_to_id.get(rnd)) + or opposite.get(random_to_id.get(rnd)) + for rnd in random_to_id + ] # endregion diff --git a/telethon/client/messages.py b/telethon/client/messages.py index 18e3e192..fa3d8633 100644 --- a/telethon/client/messages.py +++ b/telethon/client/messages.py @@ -536,7 +536,9 @@ class MessageMethods: force_document: bool = False, clear_draft: bool = False, buttons: 'hints.MarkupLike' = None, - silent: bool = None) -> 'types.Message': + silent: bool = None, + schedule: 'hints.DateLike' = None + ) -> 'types.Message': """ Sends a message to the specified user, chat or channel. @@ -609,6 +611,11 @@ class MessageMethods: channel or not. Defaults to `False`, which means it will notify them. Set it to `True` to alter this behaviour. + schedule (`hints.DateLike`, optional): + If set, the message won't send immediately, and instead + it will be scheduled to be automatically sent at a later + time. + Returns The sent `custom.Message `. @@ -663,6 +670,10 @@ class MessageMethods: # Forcing replies or clearing buttons. await client.send_message(chat, 'Reply to me', buttons=Button.force_reply()) await client.send_message(chat, 'Bye Keyboard!', buttons=Button.clear()) + + # Scheduling a message to be sent after 5 minutes + from datetime import timedelta + await client.send_message(chat, 'Hi, future!', schedule=timedelta(minutes=5)) """ if file is not None: return await self.send_file( @@ -690,7 +701,8 @@ class MessageMethods: silent=silent, reply_to=reply_to, buttons=markup, - entities=message.entities + entities=message.entities, + schedule=schedule ) request = functions.messages.SendMessageRequest( @@ -702,7 +714,8 @@ class MessageMethods: entities=message.entities, clear_draft=clear_draft, no_webpage=not isinstance( - message.media, types.MessageMediaWebPage) + message.media, types.MessageMediaWebPage), + schedule_date=schedule ) message = message.message else: @@ -720,7 +733,8 @@ class MessageMethods: reply_to_msg_id=utils.get_message_id(reply_to), clear_draft=clear_draft, silent=silent, - reply_markup=self.build_reply_markup(buttons) + reply_markup=self.build_reply_markup(buttons), + schedule_date=schedule ) result = await self(request) @@ -747,7 +761,9 @@ class MessageMethods: from_peer: 'hints.EntityLike' = None, *, silent: bool = None, - as_album: bool = None) -> 'typing.Sequence[types.Message]': + as_album: bool = None, + schedule: 'hints.DateLike' = None + ) -> 'typing.Sequence[types.Message]': """ Forwards the given messages to the specified entity. @@ -787,6 +803,11 @@ class MessageMethods: `True` will group always (even converting separate images into albums), and `False` will never group. + schedule (`hints.DateLike`, optional): + If set, the message(s) won't forward immediately, and + instead they will be scheduled to be automatically sent + at a later time. + Returns The list of forwarded `Message `, or a single one if a list wasn't provided as input. @@ -873,7 +894,8 @@ class MessageMethods: # Trying to send a single message as grouped will cause # GROUPED_MEDIA_INVALID. If more than one message is forwarded # (even without media...), this error goes away. - grouped=len(chunk) > 1 and grouped + grouped=len(chunk) > 1 and grouped, + schedule_date=schedule ) result = await self(req) sent.extend(self._get_response_message(req, result, entity)) @@ -889,7 +911,9 @@ class MessageMethods: parse_mode: str = (), link_preview: bool = True, file: 'hints.FileLike' = None, - buttons: 'hints.MarkupLike' = None) -> 'types.Message': + buttons: 'hints.MarkupLike' = None, + schedule: 'hints.DateLike' = None + ) -> 'types.Message': """ Edits the given message to change its text or media. @@ -936,6 +960,14 @@ class MessageMethods: you have signed in as a bot. You can also pass your own :tl:`ReplyMarkup` here. + schedule (`hints.DateLike`, optional): + If set, the message won't be edited immediately, and instead + it will be scheduled to be automatically edited at a later + time. + + Note that this parameter will have no effect if you are + trying to edit a message that was sent via inline bots. + Returns The edited `Message `, unless `entity` was a :tl:`InputBotInlineMessageID` in which @@ -993,7 +1025,8 @@ class MessageMethods: no_webpage=not link_preview, entities=msg_entities, media=media, - reply_markup=self.build_reply_markup(buttons) + reply_markup=self.build_reply_markup(buttons), + schedule_date=schedule ) msg = self._get_response_message(request, await self(request), entity) await self._cache_media(msg, file, file_handle, image=image) diff --git a/telethon/client/uploads.py b/telethon/client/uploads.py index bebeec46..68c9e86e 100644 --- a/telethon/client/uploads.py +++ b/telethon/client/uploads.py @@ -104,6 +104,7 @@ class UploadMethods: buttons: 'hints.MarkupLike' = None, silent: bool = None, supports_streaming: bool = False, + schedule: 'hints.DateLike' = None, **kwargs) -> 'types.Message': """ Sends message with the given file to the specified entity. @@ -230,6 +231,11 @@ class UploadMethods: these to MP4 before sending if you want them to be streamable. Unsupported formats will result in ``VideoContentTypeError``. + schedule (`hints.DateLike`, optional): + If set, the file won't send immediately, and instead + it will be scheduled to be automatically sent at a later + time. + Returns The `Message ` (or messages) containing the sent file, or messages if a list of them was passed. @@ -295,7 +301,7 @@ class UploadMethods: result += await self._send_album( entity, images[:10], caption=image_captions[:10], progress_callback=progress_callback, reply_to=reply_to, - parse_mode=parse_mode, silent=silent + parse_mode=parse_mode, silent=silent, schedule=schedule ) images = images[10:] image_captions = image_captions[10:] @@ -307,7 +313,7 @@ class UploadMethods: progress_callback=progress_callback, reply_to=reply_to, attributes=attributes, thumb=thumb, voice_note=voice_note, video_note=video_note, buttons=buttons, silent=silent, - supports_streaming=supports_streaming, + supports_streaming=supports_streaming, schedule=schedule, **kwargs )) @@ -339,7 +345,8 @@ class UploadMethods: markup = self.build_reply_markup(buttons) request = functions.messages.SendMediaRequest( entity, media, reply_to_msg_id=reply_to, message=caption, - entities=msg_entities, reply_markup=markup, silent=silent + entities=msg_entities, reply_markup=markup, silent=silent, + schedule_date=schedule ) msg = self._get_response_message(request, await self(request), entity) await self._cache_media(msg, file, file_handle, image=image) @@ -348,7 +355,7 @@ class UploadMethods: async def _send_album(self: 'TelegramClient', entity, files, caption='', progress_callback=None, reply_to=None, - parse_mode=(), silent=None): + parse_mode=(), silent=None, schedule=None): """Specialized version of .send_file for albums""" # We don't care if the user wants to avoid cache, we will use it # anyway. Why? The cached version will be exactly the same thing @@ -398,7 +405,8 @@ class UploadMethods: # Now we can construct the multi-media request result = await self(functions.messages.SendMultiMediaRequest( - entity, reply_to_msg_id=reply_to, multi_media=media, silent=silent + entity, reply_to_msg_id=reply_to, multi_media=media, + silent=silent, schedule_date=schedule )) # We never sent a `random_id` for the messages that resulted from