From f9f49a893b93083551f6303ebde3922896950420 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 4 Apr 2018 20:58:58 +0200 Subject: [PATCH 01/11] Return a single message from client.forward on non-list inputs --- telethon/events/__init__.py | 2 +- telethon/telegram_client.py | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index d440ef7c..cd5de10d 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -387,7 +387,7 @@ class NewMessage(_EventBuilder): Forwards the message. This is a shorthand for ``client.forward_messages(entity, event.message, event.chat)``. """ - kwargs['messages'] = [self.message.id] + kwargs['messages'] = self.message.id kwargs['from_peer'] = self.input_chat return self._client.forward_messages(*args, **kwargs) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 18bfda43..22ad6074 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -819,9 +819,11 @@ class TelegramClient(TelegramBareClient): order for the forward to work. Returns: - The list of forwarded :tl:`Message`. + The list of forwarded :tl:`Message`, or a single one if a list + wasn't provided as input. """ - if not utils.is_list_like(messages): + single = not utils.is_list_like(messages) + if single: messages = (messages,) if not from_peer: @@ -852,7 +854,8 @@ class TelegramClient(TelegramBareClient): elif isinstance(update, (UpdateNewMessage, UpdateNewChannelMessage)): id_to_message[update.message.id] = update.message - return [id_to_message[random_to_id[rnd]] for rnd in req.random_id] + result = [id_to_message[random_to_id[rnd]] for rnd in req.random_id] + return result[0] if single else result def edit_message(self, entity, message_id, message=None, parse_mode='md', link_preview=True): @@ -1398,7 +1401,8 @@ class TelegramClient(TelegramBareClient): it will be used to determine metadata from audio and video files. Returns: - The :tl:`Message` (or messages) containing the sent file. + The :tl:`Message` (or messages) containing the sent file, + or messages if a list of them was passed. """ # First check if the user passed an iterable, in which case # we may want to send as an album if all are photo files. @@ -2317,13 +2321,11 @@ class TelegramClient(TelegramBareClient): error will be raised. Returns: - :tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the input - entity. + :tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the + input entity. A list will be returned if more than one was given. """ - if utils.is_list_like(entity): - single = False - else: - single = True + single = not utils.is_list_like(entity) + if single: entity = (entity,) # Group input entities by string (resolve username), From b7c3f80679c8184329292122e80112200503894b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 5 Apr 2018 20:14:22 +0200 Subject: [PATCH 02/11] Split events into separate files --- telethon/events/__init__.py | 1274 +---------------------------- telethon/events/chataction.py | 310 +++++++ telethon/events/common.py | 224 +++++ telethon/events/messagedeleted.py | 33 + telethon/events/messageedited.py | 22 + telethon/events/messageread.py | 137 ++++ telethon/events/newmessage.py | 411 ++++++++++ telethon/events/userupdate.py | 153 ++++ 8 files changed, 1299 insertions(+), 1265 deletions(-) create mode 100644 telethon/events/chataction.py create mode 100644 telethon/events/common.py create mode 100644 telethon/events/messagedeleted.py create mode 100644 telethon/events/messageedited.py create mode 100644 telethon/events/messageread.py create mode 100644 telethon/events/newmessage.py create mode 100644 telethon/events/userupdate.py diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index cd5de10d..bc429724 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -1,1268 +1,9 @@ -import abc -import datetime -import itertools -import re -import warnings - -from .. import utils -from ..errors import RPCError -from ..extensions import markdown -from ..tl import TLObject, types, functions - - -def _into_id_set(client, chats): - """Helper util to turn the input chat or chats into a set of IDs.""" - if chats is None: - return None - - if not utils.is_list_like(chats): - chats = (chats,) - - result = set() - for chat in chats: - if isinstance(chat, int): - if chat < 0: - result.add(chat) # Explicitly marked IDs are negative - else: - result.update({ # Support all valid types of peers - utils.get_peer_id(types.PeerUser(chat)), - utils.get_peer_id(types.PeerChat(chat)), - utils.get_peer_id(types.PeerChannel(chat)), - }) - elif isinstance(chat, TLObject) and chat.SUBCLASS_OF_ID == 0x2d45687: - # 0x2d45687 == crc32(b'Peer') - result.add(utils.get_peer_id(chat)) - else: - chat = client.get_input_entity(chat) - if isinstance(chat, types.InputPeerSelf): - chat = client.get_me(input_peer=True) - result.add(utils.get_peer_id(chat)) - - return result - - -class _EventBuilder(abc.ABC): - """ - The common event builder, with builtin support to filter per chat. - - Args: - chats (`entity`, optional): - May be one or more entities (username/peer/etc.). By default, - only matching chats will be handled. - - blacklist_chats (`bool`, optional): - Whether to treat the chats as a blacklist instead of - as a whitelist (default). This means that every chat - will be handled *except* those specified in ``chats`` - which will be ignored if ``blacklist_chats=True``. - """ - def __init__(self, chats=None, blacklist_chats=False): - self.chats = chats - self.blacklist_chats = blacklist_chats - self._self_id = None - - @abc.abstractmethod - def build(self, update): - """Builds an event for the given update if possible, or returns None""" - - def resolve(self, client): - """Helper method to allow event builders to be resolved before usage""" - self.chats = _into_id_set(client, self.chats) - self._self_id = client.get_me(input_peer=True).user_id - - def _filter_event(self, event): - """ - If the ID of ``event._chat_peer`` isn't in the chats set (or it is - but the set is a blacklist) returns ``None``, otherwise the event. - """ - if self.chats is not None: - inside = utils.get_peer_id(event._chat_peer) in self.chats - if inside == self.blacklist_chats: - # If this chat matches but it's a blacklist ignore. - # If it doesn't match but it's a whitelist ignore. - return None - return event - - -class _EventCommon(abc.ABC): - """Intermediate class with common things to all events""" - _event_name = 'Event' - - def __init__(self, chat_peer=None, msg_id=None, broadcast=False): - self._entities = {} - self._client = None - self._chat_peer = chat_peer - self._message_id = msg_id - self._input_chat = None - self._chat = None - - self.pattern_match = None - - self.is_private = isinstance(chat_peer, types.PeerUser) - self.is_group = ( - isinstance(chat_peer, (types.PeerChat, types.PeerChannel)) - and not broadcast - ) - self.is_channel = isinstance(chat_peer, types.PeerChannel) - - def _get_entity(self, msg_id, entity_id, chat=None): - """ - Helper function to call :tl:`GetMessages` on the give msg_id and - return the input entity whose ID is the given entity ID. - - If ``chat`` is present it must be an :tl:`InputPeer`. - - Returns a tuple of ``(entity, input_peer)`` if it was found, or - a tuple of ``(None, None)`` if it couldn't be. - """ - try: - if isinstance(chat, types.InputPeerChannel): - result = self._client( - functions.channels.GetMessagesRequest(chat, [ - types.InputMessageID(msg_id) - ]) - ) - else: - result = self._client( - functions.messages.GetMessagesRequest([ - types.InputMessageID(msg_id) - ]) - ) - except RPCError: - return None, None - - entity = { - utils.get_peer_id(x): x for x in itertools.chain( - getattr(result, 'chats', []), - getattr(result, 'users', [])) - }.get(entity_id) - if entity: - return entity, utils.get_input_peer(entity) - else: - return None, None - - @property - def input_chat(self): - """ - The (:tl:`InputPeer`) (group, megagroup or channel) on which - the event occurred. This doesn't have the title or anything, - but is useful if you don't need those to avoid further - requests. - - Note that this might be ``None`` if the library can't find it. - """ - - if self._input_chat is None and self._chat_peer is not None: - try: - self._input_chat = self._client.get_input_entity( - self._chat_peer - ) - except (ValueError, TypeError): - # The library hasn't seen this chat, get the message - if not isinstance(self._chat_peer, types.PeerChannel): - # TODO For channels, getDifference? Maybe looking - # in the dialogs (which is already done) is enough. - if self._message_id is not None: - self._chat, self._input_chat = self._get_entity( - self._message_id, - utils.get_peer_id(self._chat_peer) - ) - return self._input_chat - - @property - def client(self): - return self._client - - @property - def chat(self): - """ - The (:tl:`User` | :tl:`Chat` | :tl:`Channel`, optional) on which - the event occurred. This property may make an API call the first time - to get the most up to date version of the chat (mostly when the event - doesn't belong to a channel), so keep that in mind. - """ - if not self.input_chat: - return None - - if self._chat is None: - self._chat = self._entities.get(utils.get_peer_id(self._input_chat)) - - if self._chat is None: - self._chat = self._client.get_entity(self._input_chat) - - return self._chat - - def __str__(self): - return TLObject.pretty_format(self.to_dict()) - - def stringify(self): - return TLObject.pretty_format(self.to_dict(), indent=0) - - def to_dict(self): - d = {k: v for k, v in self.__dict__.items() if k[0] != '_'} - d['_'] = self._event_name - return d - - -class Raw(_EventBuilder): - """ - Represents a raw event. The event is the update itself. - """ - def resolve(self, client): - pass - - def build(self, update): - return update - - -def _name_inner_event(cls): - """Decorator to rename cls.Event 'Event' as 'cls.Event'""" - if hasattr(cls, 'Event'): - cls.Event._event_name = '{}.Event'.format(cls.__name__) - else: - warnings.warn('Class {} does not have a inner Event'.format(cls)) - return cls - - -# Classes defined here are actually Event builders -# for their inner Event classes. Inner ._client is -# set later by the creator TelegramClient. -@_name_inner_event -class NewMessage(_EventBuilder): - """ - Represents a new message event builder. - - Args: - incoming (`bool`, optional): - If set to ``True``, only **incoming** messages will be handled. - Mutually exclusive with ``outgoing`` (can only set one of either). - - outgoing (`bool`, optional): - If set to ``True``, only **outgoing** messages will be handled. - Mutually exclusive with ``incoming`` (can only set one of either). - - pattern (`str`, `callable`, `Pattern`, optional): - If set, only messages matching this pattern will be handled. - You can specify a regex-like string which will be matched - against the message, a callable function that returns ``True`` - if a message is acceptable, or a compiled regex pattern. - """ - def __init__(self, incoming=None, outgoing=None, - chats=None, blacklist_chats=False, pattern=None): - if incoming and outgoing: - raise ValueError('Can only set either incoming or outgoing') - - super().__init__(chats=chats, blacklist_chats=blacklist_chats) - self.incoming = incoming - self.outgoing = outgoing - if isinstance(pattern, str): - self.pattern = re.compile(pattern).match - elif not pattern or callable(pattern): - self.pattern = pattern - elif hasattr(pattern, 'match') and callable(pattern.match): - self.pattern = pattern.match - else: - raise TypeError('Invalid pattern type given') - - def build(self, update): - if isinstance(update, - (types.UpdateNewMessage, types.UpdateNewChannelMessage)): - if not isinstance(update.message, types.Message): - return # We don't care about MessageService's here - event = NewMessage.Event(update.message) - elif isinstance(update, types.UpdateShortMessage): - event = NewMessage.Event(types.Message( - out=update.out, - mentioned=update.mentioned, - media_unread=update.media_unread, - silent=update.silent, - id=update.id, - to_id=types.PeerUser(update.user_id), - from_id=self._self_id if update.out else update.user_id, - message=update.message, - date=update.date, - fwd_from=update.fwd_from, - via_bot_id=update.via_bot_id, - reply_to_msg_id=update.reply_to_msg_id, - entities=update.entities - )) - elif isinstance(update, types.UpdateShortChatMessage): - event = NewMessage.Event(types.Message( - out=update.out, - mentioned=update.mentioned, - media_unread=update.media_unread, - silent=update.silent, - id=update.id, - from_id=update.from_id, - to_id=types.PeerChat(update.chat_id), - message=update.message, - date=update.date, - fwd_from=update.fwd_from, - via_bot_id=update.via_bot_id, - reply_to_msg_id=update.reply_to_msg_id, - entities=update.entities - )) - else: - return - - event._entities = update._entities - return self._message_filter_event(event) - - def _message_filter_event(self, event): - # Short-circuit if we let pass all events - if all(x is None for x in (self.incoming, self.outgoing, self.chats, - self.pattern)): - return event - - if self.incoming and event.message.out: - return - if self.outgoing and not event.message.out: - return - - if self.pattern: - match = self.pattern(event.message.message or '') - if not match: - return - event.pattern_match = match - - return self._filter_event(event) - - class Event(_EventCommon): - """ - Represents the event of a new message. - - Members: - message (:tl:`Message`): - This is the original :tl:`Message` object. - - is_private (`bool`): - True if the message was sent as a private message. - - is_group (`bool`): - True if the message was sent on a group or megagroup. - - is_channel (`bool`): - True if the message was sent on a megagroup or channel. - - is_reply (`str`): - Whether the message is a reply to some other or not. - """ - def __init__(self, message): - if not message.out and isinstance(message.to_id, types.PeerUser): - # Incoming message (e.g. from a bot) has to_id=us, and - # from_id=bot (the actual "chat" from an user's perspective). - chat_peer = types.PeerUser(message.from_id) - else: - chat_peer = message.to_id - - super().__init__(chat_peer=chat_peer, - msg_id=message.id, broadcast=bool(message.post)) - - self.message = message - self._text = None - - self._input_sender = None - self._sender = None - - self.is_reply = bool(message.reply_to_msg_id) - self._reply_message = None - - def respond(self, *args, **kwargs): - """ - Responds to the message (not as a reply). This is a shorthand for - ``client.send_message(event.chat, ...)``. - """ - return self._client.send_message(self.input_chat, *args, **kwargs) - - def reply(self, *args, **kwargs): - """ - Replies to the message (as a reply). This is a shorthand for - ``client.send_message(event.chat, ..., reply_to=event.message.id)``. - """ - kwargs['reply_to'] = self.message.id - return self._client.send_message(self.input_chat, *args, **kwargs) - - def forward_to(self, *args, **kwargs): - """ - Forwards the message. This is a shorthand for - ``client.forward_messages(entity, event.message, event.chat)``. - """ - kwargs['messages'] = self.message.id - kwargs['from_peer'] = self.input_chat - return self._client.forward_messages(*args, **kwargs) - - def edit(self, *args, **kwargs): - """ - Edits the message iff it's outgoing. This is a shorthand for - ``client.edit_message(event.chat, event.message, ...)``. - - Returns ``None`` if the message was incoming, - or the edited message otherwise. - """ - if self.message.fwd_from: - return None - if not self.message.out: - if not isinstance(self.message.to_id, types.PeerUser): - return None - me = self._client.get_me(input_peer=True) - if self.message.to_id.user_id != me.user_id: - return None - - return self._client.edit_message(self.input_chat, - self.message, - *args, **kwargs) - - def delete(self, *args, **kwargs): - """ - Deletes the message. You're responsible for checking whether you - have the permission to do so, or to except the error otherwise. - This is a shorthand for - ``client.delete_messages(event.chat, event.message, ...)``. - """ - return self._client.delete_messages(self.input_chat, - [self.message], - *args, **kwargs) - - @property - def input_sender(self): - """ - This (:tl:`InputPeer`) is the input version of the user who - sent the message. Similarly to ``input_chat``, this doesn't have - things like username or similar, but still useful in some cases. - - Note that this might not be available if the library can't - find the input chat, or if the message a broadcast on a channel. - """ - if self._input_sender is None: - if self.is_channel and not self.is_group: - return None - - try: - self._input_sender = self._client.get_input_entity( - self.message.from_id - ) - except (ValueError, TypeError): - # We can rely on self.input_chat for this - self._sender, self._input_sender = self._get_entity( - self.message.id, - self.message.from_id, - chat=self.input_chat - ) - - return self._input_sender - - @property - def sender(self): - """ - This (:tl:`User`) may make an API call the first time to get - the most up to date version of the sender (mostly when the event - doesn't belong to a channel), so keep that in mind. - - ``input_sender`` needs to be available (often the case). - """ - if not self.input_sender: - return None - - if self._sender is None: - self._sender = \ - self._entities.get(utils.get_peer_id(self._input_sender)) - - if self._sender is None: - self._sender = self._client.get_entity(self._input_sender) - - return self._sender - - @property - def text(self): - """ - The message text, markdown-formatted. - """ - if self._text is None: - if not self.message.entities: - return self.message.message - self._text = markdown.unparse(self.message.message, - self.message.entities or []) - return self._text - - @property - def raw_text(self): - """ - The raw message text, ignoring any formatting. - """ - return self.message.message - - @property - def reply_message(self): - """ - This optional :tl:`Message` will make an API call the first - time to get the full :tl:`Message` object that one was replying to, - so use with care as there is no caching besides local caching yet. - """ - if not self.message.reply_to_msg_id: - return None - - if self._reply_message is None: - if isinstance(self.input_chat, types.InputPeerChannel): - r = self._client(functions.channels.GetMessagesRequest( - self.input_chat, [ - types.InputMessageID(self.message.reply_to_msg_id) - ] - )) - else: - r = self._client(functions.messages.GetMessagesRequest( - [types.InputMessageID(self.message.reply_to_msg_id)] - )) - if not isinstance(r, types.messages.MessagesNotModified): - self._reply_message = r.messages[0] - - return self._reply_message - - @property - def forward(self): - """ - The unmodified :tl:`MessageFwdHeader`, if present.. - """ - return self.message.fwd_from - - @property - def media(self): - """ - The unmodified :tl:`MessageMedia`, if present. - """ - return self.message.media - - @property - def photo(self): - """ - If the message media is a photo, - this returns the :tl:`Photo` object. - """ - if isinstance(self.message.media, types.MessageMediaPhoto): - photo = self.message.media.photo - if isinstance(photo, types.Photo): - return photo - - @property - def document(self): - """ - If the message media is a document, - this returns the :tl:`Document` object. - """ - if isinstance(self.message.media, types.MessageMediaDocument): - doc = self.message.media.document - if isinstance(doc, types.Document): - return doc - - def _document_by_attribute(self, kind, condition=None): - """ - Helper method to return the document only if it has an attribute - that's an instance of the given kind, and passes the condition. - """ - doc = self.document - if doc: - for attr in doc.attributes: - if isinstance(attr, kind): - if not condition or condition(doc): - return doc - - @property - def audio(self): - """ - If the message media is a document with an Audio attribute, - this returns the :tl:`Document` object. - """ - return self._document_by_attribute(types.DocumentAttributeAudio, - lambda attr: not attr.voice) - - @property - def voice(self): - """ - If the message media is a document with a Voice attribute, - this returns the :tl:`Document` object. - """ - return self._document_by_attribute(types.DocumentAttributeAudio, - lambda attr: attr.voice) - - @property - def video(self): - """ - If the message media is a document with a Video attribute, - this returns the :tl:`Document` object. - """ - return self._document_by_attribute(types.DocumentAttributeVideo) - - @property - def video_note(self): - """ - If the message media is a document with a Video attribute, - this returns the :tl:`Document` object. - """ - return self._document_by_attribute(types.DocumentAttributeVideo, - lambda attr: attr.round_message) - - @property - def gif(self): - """ - If the message media is a document with an Animated attribute, - this returns the :tl:`Document` object. - """ - return self._document_by_attribute(types.DocumentAttributeAnimated) - - @property - def sticker(self): - """ - If the message media is a document with a Sticker attribute, - this returns the :tl:`Document` object. - """ - return self._document_by_attribute(types.DocumentAttributeSticker) - - @property - def out(self): - """ - Whether the message is outgoing (i.e. you sent it from - another session) or incoming (i.e. someone else sent it). - """ - return self.message.out - - -@_name_inner_event -class ChatAction(_EventBuilder): - """ - Represents an action in a chat (such as user joined, left, or new pin). - """ - def build(self, update): - if isinstance(update, types.UpdateChannelPinnedMessage) and update.id == 0: - # Telegram does not always send - # UpdateChannelPinnedMessage for new pins - # but always for unpin, with update.id = 0 - event = ChatAction.Event(types.PeerChannel(update.channel_id), - unpin=True) - - elif isinstance(update, types.UpdateChatParticipantAdd): - event = ChatAction.Event(types.PeerChat(update.chat_id), - added_by=update.inviter_id or True, - users=update.user_id) - - elif isinstance(update, types.UpdateChatParticipantDelete): - event = ChatAction.Event(types.PeerChat(update.chat_id), - kicked_by=True, - users=update.user_id) - - elif (isinstance(update, ( - types.UpdateNewMessage, types.UpdateNewChannelMessage)) - and isinstance(update.message, types.MessageService)): - msg = update.message - action = update.message.action - if isinstance(action, types.MessageActionChatJoinedByLink): - event = ChatAction.Event(msg, - added_by=True, - users=msg.from_id) - elif isinstance(action, types.MessageActionChatAddUser): - event = ChatAction.Event(msg, - added_by=msg.from_id or True, - users=action.users) - elif isinstance(action, types.MessageActionChatDeleteUser): - event = ChatAction.Event(msg, - kicked_by=msg.from_id or True, - users=action.user_id) - elif isinstance(action, types.MessageActionChatCreate): - event = ChatAction.Event(msg, - users=action.users, - created=True, - new_title=action.title) - elif isinstance(action, types.MessageActionChannelCreate): - event = ChatAction.Event(msg, - created=True, - users=msg.from_id, - new_title=action.title) - elif isinstance(action, types.MessageActionChatEditTitle): - event = ChatAction.Event(msg, - users=msg.from_id, - new_title=action.title) - elif isinstance(action, types.MessageActionChatEditPhoto): - event = ChatAction.Event(msg, - users=msg.from_id, - new_photo=action.photo) - elif isinstance(action, types.MessageActionChatDeletePhoto): - event = ChatAction.Event(msg, - users=msg.from_id, - new_photo=True) - elif isinstance(action, types.MessageActionPinMessage): - # Telegram always sends this service message for new pins - event = ChatAction.Event(msg, - users=msg.from_id, - new_pin=msg.reply_to_msg_id) - else: - return - else: - return - - event._entities = update._entities - return self._filter_event(event) - - class Event(_EventCommon): - """ - Represents the event of a new chat action. - - Members: - new_pin (`bool`): - ``True`` if there is a new pin. - - new_photo (`bool`): - ``True`` if there's a new chat photo (or it was removed). - - photo (:tl:`Photo`, optional): - The new photo (or ``None`` if it was removed). - - - user_added (`bool`): - ``True`` if the user was added by some other. - - user_joined (`bool`): - ``True`` if the user joined on their own. - - user_left (`bool`): - ``True`` if the user left on their own. - - user_kicked (`bool`): - ``True`` if the user was kicked by some other. - - created (`bool`, optional): - ``True`` if this chat was just created. - - new_title (`bool`, optional): - The new title string for the chat, if applicable. - - unpin (`bool`): - ``True`` if the existing pin gets unpinned. - """ - def __init__(self, where, new_pin=None, new_photo=None, - added_by=None, kicked_by=None, created=None, - users=None, new_title=None, unpin=None): - if isinstance(where, types.MessageService): - self.action_message = where - where = where.to_id - else: - self.action_message = None - - super().__init__(chat_peer=where, msg_id=new_pin) - - self.new_pin = isinstance(new_pin, int) - self._pinned_message = new_pin - - self.new_photo = new_photo is not None - self.photo = \ - new_photo if isinstance(new_photo, types.Photo) else None - - self._added_by = None - self._kicked_by = None - self.user_added, self.user_joined, self.user_left,\ - self.user_kicked, self.unpin = (False, False, False, False, False) - - if added_by is True: - self.user_joined = True - elif added_by: - self.user_added = True - self._added_by = added_by - - if kicked_by is True: - self.user_left = True - elif kicked_by: - self.user_kicked = True - self._kicked_by = kicked_by - - self.created = bool(created) - self._user_peers = users if isinstance(users, list) else [users] - self._users = None - self._input_users = None - self.new_title = new_title - self.unpin = unpin - - def respond(self, *args, **kwargs): - """ - Responds to the chat action message (not as a reply). - Shorthand for ``client.send_message(event.chat, ...)``. - """ - return self._client.send_message(self.input_chat, *args, **kwargs) - - def reply(self, *args, **kwargs): - """ - Replies to the chat action message (as a reply). Shorthand for - ``client.send_message(event.chat, ..., reply_to=event.message.id)``. - - Has the same effect as ``.respond()`` if there is no message. - """ - if not self.action_message: - return self.respond(*args, **kwargs) - - kwargs['reply_to'] = self.action_message.id - return self._client.send_message(self.input_chat, *args, **kwargs) - - def delete(self, *args, **kwargs): - """ - Deletes the chat action message. You're responsible for checking - whether you have the permission to do so, or to except the error - otherwise. This is a shorthand for - ``client.delete_messages(event.chat, event.message, ...)``. - - Does nothing if no message action triggered this event. - """ - if self.action_message: - return self._client.delete_messages(self.input_chat, - [self.action_message], - *args, **kwargs) - - @property - def pinned_message(self): - """ - If ``new_pin`` is ``True``, this returns the (:tl:`Message`) - object that was pinned. - """ - if self._pinned_message == 0: - return None - - if isinstance(self._pinned_message, int) and self.input_chat: - r = self._client(functions.channels.GetMessagesRequest( - self._input_chat, [ - types.InputMessageID(self._pinned_message) - ] - )) - try: - self._pinned_message = next( - x for x in r.messages - if isinstance(x, types.Message) - and x.id == self._pinned_message - ) - except StopIteration: - pass - - if isinstance(self._pinned_message, types.Message): - return self._pinned_message - - @property - def added_by(self): - """ - The user who added ``users``, if applicable (``None`` otherwise). - """ - if self._added_by and not isinstance(self._added_by, types.User): - self._added_by =\ - self._entities.get(utils.get_peer_id(self._added_by)) - - if not self._added_by: - self._added_by = self._client.get_entity(self._added_by) - - return self._added_by - - @property - def kicked_by(self): - """ - The user who kicked ``users``, if applicable (``None`` otherwise). - """ - if self._kicked_by and not isinstance(self._kicked_by, types.User): - self._kicked_by =\ - self._entities.get(utils.get_peer_id(self._kicked_by)) - - if not self._kicked_by: - self._kicked_by = self._client.get_entity(self._kicked_by) - - return self._kicked_by - - @property - def user(self): - """ - The single user that takes part in this action (e.g. joined). - - Might be ``None`` if the information can't be retrieved or - there is no user taking part. - """ - if self.users: - return self._users[0] - - @property - def input_user(self): - """ - Input version of the ``self.user`` property. - """ - if self.input_users: - return self._input_users[0] - - @property - def users(self): - """ - A list of users that take part in this action (e.g. joined). - - Might be empty if the information can't be retrieved or there - are no users taking part. - """ - if not self._user_peers: - return [] - - if self._users is None: - have, missing = [], [] - for peer in self._user_peers: - user = self._entities.get(utils.get_peer_id(peer)) - if user: - have.append(user) - else: - missing.append(peer) - - try: - missing = self._client.get_entity(missing) - except (TypeError, ValueError): - missing = [] - - self._users = have + missing - - return self._users - - @property - def input_users(self): - """ - Input version of the ``self.users`` property. - """ - if self._input_users is None and self._user_peers: - self._input_users = [] - for peer in self._user_peers: - try: - self._input_users.append(self._client.get_input_entity( - peer - )) - except (TypeError, ValueError): - pass - return self._input_users - - -@_name_inner_event -class UserUpdate(_EventBuilder): - """ - Represents an user update (gone online, offline, joined Telegram). - """ - def build(self, update): - if isinstance(update, types.UpdateUserStatus): - event = UserUpdate.Event(update.user_id, - status=update.status) - else: - return - - event._entities = update._entities - return self._filter_event(event) - - class Event(_EventCommon): - """ - Represents the event of an user status update (last seen, joined). - - Members: - online (`bool`, optional): - ``True`` if the user is currently online, ``False`` otherwise. - Might be ``None`` if this information is not present. - - last_seen (`datetime`, optional): - Exact date when the user was last seen if known. - - until (`datetime`, optional): - Until when will the user remain online. - - within_months (`bool`): - ``True`` if the user was seen within 30 days. - - within_weeks (`bool`): - ``True`` if the user was seen within 7 days. - - recently (`bool`): - ``True`` if the user was seen within a day. - - action (:tl:`SendMessageAction`, optional): - The "typing" action if any the user is performing if any. - - cancel (`bool`): - ``True`` if the action was cancelling other actions. - - typing (`bool`): - ``True`` if the action is typing a message. - - recording (`bool`): - ``True`` if the action is recording something. - - uploading (`bool`): - ``True`` if the action is uploading something. - - playing (`bool`): - ``True`` if the action is playing a game. - - audio (`bool`): - ``True`` if what's being recorded/uploaded is an audio. - - round (`bool`): - ``True`` if what's being recorded/uploaded is a round video. - - video (`bool`): - ``True`` if what's being recorded/uploaded is an video. - - document (`bool`): - ``True`` if what's being uploaded is document. - - geo (`bool`): - ``True`` if what's being uploaded is a geo. - - photo (`bool`): - ``True`` if what's being uploaded is a photo. - - contact (`bool`): - ``True`` if what's being uploaded (selected) is a contact. - """ - def __init__(self, user_id, status=None, typing=None): - super().__init__(types.PeerUser(user_id)) - - self.online = None if status is None else \ - isinstance(status, types.UserStatusOnline) - - self.last_seen = status.was_online if \ - isinstance(status, types.UserStatusOffline) else None - - self.until = status.expires if \ - isinstance(status, types.UserStatusOnline) else None - - if self.last_seen: - diff = datetime.datetime.now() - self.last_seen - if diff < datetime.timedelta(days=30): - self.within_months = True - if diff < datetime.timedelta(days=7): - self.within_weeks = True - if diff < datetime.timedelta(days=1): - self.recently = True - else: - self.within_months = self.within_weeks = self.recently = False - if isinstance(status, (types.UserStatusOnline, - types.UserStatusRecently)): - self.within_months = self.within_weeks = True - self.recently = True - elif isinstance(status, types.UserStatusLastWeek): - self.within_months = self.within_weeks = True - elif isinstance(status, types.UserStatusLastMonth): - self.within_months = True - - self.action = typing - if typing: - self.cancel = self.typing = self.recording = self.uploading = \ - self.playing = False - self.audio = self.round = self.video = self.document = \ - self.geo = self.photo = self.contact = False - - if isinstance(typing, types.SendMessageCancelAction): - self.cancel = True - elif isinstance(typing, types.SendMessageTypingAction): - self.typing = True - elif isinstance(typing, types.SendMessageGamePlayAction): - self.playing = True - elif isinstance(typing, types.SendMessageGeoLocationAction): - self.geo = True - elif isinstance(typing, types.SendMessageRecordAudioAction): - self.recording = self.audio = True - elif isinstance(typing, types.SendMessageRecordRoundAction): - self.recording = self.round = True - elif isinstance(typing, types.SendMessageRecordVideoAction): - self.recording = self.video = True - elif isinstance(typing, types.SendMessageChooseContactAction): - self.uploading = self.contact = True - elif isinstance(typing, types.SendMessageUploadAudioAction): - self.uploading = self.audio = True - elif isinstance(typing, types.SendMessageUploadDocumentAction): - self.uploading = self.document = True - elif isinstance(typing, types.SendMessageUploadPhotoAction): - self.uploading = self.photo = True - elif isinstance(typing, types.SendMessageUploadRoundAction): - self.uploading = self.round = True - elif isinstance(typing, types.SendMessageUploadVideoAction): - self.uploading = self.video = True - - @property - def user(self): - """Alias around the chat (conversation).""" - return self.chat - - -@_name_inner_event -class MessageEdited(NewMessage): - """ - Event fired when a message has been edited. - """ - def build(self, update): - if isinstance(update, (types.UpdateEditMessage, - types.UpdateEditChannelMessage)): - event = MessageEdited.Event(update.message) - else: - return - - event._entities = update._entities - return self._message_filter_event(event) - - class Event(NewMessage.Event): - pass # Required if we want a different name for it - - -@_name_inner_event -class MessageDeleted(_EventBuilder): - """ - Event fired when one or more messages are deleted. - """ - def build(self, update): - if isinstance(update, types.UpdateDeleteMessages): - event = MessageDeleted.Event( - deleted_ids=update.messages, - peer=None - ) - elif isinstance(update, types.UpdateDeleteChannelMessages): - event = MessageDeleted.Event( - deleted_ids=update.messages, - peer=types.PeerChannel(update.channel_id) - ) - else: - return - - event._entities = update._entities - return self._filter_event(event) - - class Event(_EventCommon): - def __init__(self, deleted_ids, peer): - super().__init__( - chat_peer=peer, msg_id=(deleted_ids or [0])[0] - ) - self.deleted_id = None if not deleted_ids else deleted_ids[0] - self.deleted_ids = deleted_ids - - -@_name_inner_event -class MessageRead(_EventBuilder): - """ - Event fired when one or more messages have been read. - - Args: - inbox (`bool`, optional): - If this argument is ``True``, then when you read someone else's - messages the event will be fired. By default (``False``) only - when messages you sent are read by someone else will fire it. - """ - def __init__(self, inbox=False, chats=None, blacklist_chats=None): - super().__init__(chats, blacklist_chats) - self.inbox = inbox - - def build(self, update): - if isinstance(update, types.UpdateReadHistoryInbox): - event = MessageRead.Event(update.peer, update.max_id, False) - elif isinstance(update, types.UpdateReadHistoryOutbox): - event = MessageRead.Event(update.peer, update.max_id, True) - elif isinstance(update, types.UpdateReadChannelInbox): - event = MessageRead.Event(types.PeerChannel(update.channel_id), - update.max_id, False) - elif isinstance(update, types.UpdateReadChannelOutbox): - event = MessageRead.Event(types.PeerChannel(update.channel_id), - update.max_id, True) - elif isinstance(update, types.UpdateReadMessagesContents): - event = MessageRead.Event(message_ids=update.messages, - contents=True) - elif isinstance(update, types.UpdateChannelReadMessagesContents): - event = MessageRead.Event(types.PeerChannel(update.channel_id), - message_ids=update.messages, - contents=True) - else: - return - - if self.inbox == event.outbox: - return - - event._entities = update._entities - return self._filter_event(event) - - class Event(_EventCommon): - """ - Represents the event of one or more messages being read. - - Members: - max_id (`int`): - Up to which message ID has been read. Every message - with an ID equal or lower to it have been read. - - outbox (`bool`): - ``True`` if someone else has read your messages. - - contents (`bool`): - ``True`` if what was read were the contents of a message. - This will be the case when e.g. you play a voice note. - It may only be set on ``inbox`` events. - """ - def __init__(self, peer=None, max_id=None, out=False, contents=False, - message_ids=None): - self.outbox = out - self.contents = contents - self._message_ids = message_ids or [] - self._messages = None - self.max_id = max_id or max(message_ids or [], default=None) - super().__init__(peer, self.max_id) - - @property - def inbox(self): - """ - ``True`` if you have read someone else's messages. - """ - return not self.outbox - - @property - def message_ids(self): - """ - The IDs of the messages **which contents'** were read. - - Use :meth:`is_read` if you need to check whether a message - was read instead checking if it's in here. - """ - return self._message_ids - - @property - def messages(self): - """ - The list of :tl:`Message` **which contents'** were read. - - Use :meth:`is_read` if you need to check whether a message - was read instead checking if it's in here. - """ - if self._messages is None: - chat = self.input_chat - if not chat: - self._messages = [] - elif isinstance(chat, types.InputPeerChannel): - ids = [types.InputMessageID(x) for x in self._message_ids] - self._messages =\ - self._client(functions.channels.GetMessagesRequest( - chat, ids - )).messages - else: - ids = [types.InputMessageID(x) for x in self._message_ids] - self._messages =\ - self._client(functions.messages.GetMessagesRequest( - ids - )).messages - - return self._messages - - def is_read(self, message): - """ - Returns ``True`` if the given message (or its ID) has been read. - - If a list-like argument is provided, this method will return a - list of booleans indicating which messages have been read. - """ - if utils.is_list_like(message): - return [(m if isinstance(m, int) else m.id) <= self.max_id - for m in message] - else: - return (message if isinstance(message, int) - else message.id) <= self.max_id - - def __contains__(self, message): - """``True`` if the message(s) are read message.""" - if utils.is_list_like(message): - return all(self.is_read(message)) - else: - return self.is_read(message) +from .chataction import ChatAction +from .messagedeleted import MessageDeleted +from .messageedited import MessageEdited +from .messageread import MessageRead +from .newmessage import NewMessage +from .userupdate import UserUpdate class StopPropagation(Exception): @@ -1272,6 +13,9 @@ class StopPropagation(Exception): It can be seen as the ``StopIteration`` in a for loop but for events. Example usage: + >>> from telethon import TelegramClient, events + >>> client = TelegramClient(...) + >>> >>> @client.on(events.NewMessage) ... def delete(event): ... event.delete() diff --git a/telethon/events/chataction.py b/telethon/events/chataction.py new file mode 100644 index 00000000..1a67394b --- /dev/null +++ b/telethon/events/chataction.py @@ -0,0 +1,310 @@ +from .common import EventBuilder, EventCommon, name_inner_event +from .. import utils +from ..tl import types, functions + + +@name_inner_event +class ChatAction(EventBuilder): + """ + Represents an action in a chat (such as user joined, left, or new pin). + """ + def build(self, update): + if isinstance(update, types.UpdateChannelPinnedMessage) and update.id == 0: + # Telegram does not always send + # UpdateChannelPinnedMessage for new pins + # but always for unpin, with update.id = 0 + event = ChatAction.Event(types.PeerChannel(update.channel_id), + unpin=True) + + elif isinstance(update, types.UpdateChatParticipantAdd): + event = ChatAction.Event(types.PeerChat(update.chat_id), + added_by=update.inviter_id or True, + users=update.user_id) + + elif isinstance(update, types.UpdateChatParticipantDelete): + event = ChatAction.Event(types.PeerChat(update.chat_id), + kicked_by=True, + users=update.user_id) + + elif (isinstance(update, ( + types.UpdateNewMessage, types.UpdateNewChannelMessage)) + and isinstance(update.message, types.MessageService)): + msg = update.message + action = update.message.action + if isinstance(action, types.MessageActionChatJoinedByLink): + event = ChatAction.Event(msg, + added_by=True, + users=msg.from_id) + elif isinstance(action, types.MessageActionChatAddUser): + event = ChatAction.Event(msg, + added_by=msg.from_id or True, + users=action.users) + elif isinstance(action, types.MessageActionChatDeleteUser): + event = ChatAction.Event(msg, + kicked_by=msg.from_id or True, + users=action.user_id) + elif isinstance(action, types.MessageActionChatCreate): + event = ChatAction.Event(msg, + users=action.users, + created=True, + new_title=action.title) + elif isinstance(action, types.MessageActionChannelCreate): + event = ChatAction.Event(msg, + created=True, + users=msg.from_id, + new_title=action.title) + elif isinstance(action, types.MessageActionChatEditTitle): + event = ChatAction.Event(msg, + users=msg.from_id, + new_title=action.title) + elif isinstance(action, types.MessageActionChatEditPhoto): + event = ChatAction.Event(msg, + users=msg.from_id, + new_photo=action.photo) + elif isinstance(action, types.MessageActionChatDeletePhoto): + event = ChatAction.Event(msg, + users=msg.from_id, + new_photo=True) + elif isinstance(action, types.MessageActionPinMessage): + # Telegram always sends this service message for new pins + event = ChatAction.Event(msg, + users=msg.from_id, + new_pin=msg.reply_to_msg_id) + else: + return + else: + return + + event._entities = update._entities + return self._filter_event(event) + + class Event(EventCommon): + """ + Represents the event of a new chat action. + + Members: + new_pin (`bool`): + ``True`` if there is a new pin. + + new_photo (`bool`): + ``True`` if there's a new chat photo (or it was removed). + + photo (:tl:`Photo`, optional): + The new photo (or ``None`` if it was removed). + + + user_added (`bool`): + ``True`` if the user was added by some other. + + user_joined (`bool`): + ``True`` if the user joined on their own. + + user_left (`bool`): + ``True`` if the user left on their own. + + user_kicked (`bool`): + ``True`` if the user was kicked by some other. + + created (`bool`, optional): + ``True`` if this chat was just created. + + new_title (`bool`, optional): + The new title string for the chat, if applicable. + + unpin (`bool`): + ``True`` if the existing pin gets unpinned. + """ + def __init__(self, where, new_pin=None, new_photo=None, + added_by=None, kicked_by=None, created=None, + users=None, new_title=None, unpin=None): + if isinstance(where, types.MessageService): + self.action_message = where + where = where.to_id + else: + self.action_message = None + + super().__init__(chat_peer=where, msg_id=new_pin) + + self.new_pin = isinstance(new_pin, int) + self._pinned_message = new_pin + + self.new_photo = new_photo is not None + self.photo = \ + new_photo if isinstance(new_photo, types.Photo) else None + + self._added_by = None + self._kicked_by = None + self.user_added, self.user_joined, self.user_left,\ + self.user_kicked, self.unpin = (False, False, False, False, False) + + if added_by is True: + self.user_joined = True + elif added_by: + self.user_added = True + self._added_by = added_by + + if kicked_by is True: + self.user_left = True + elif kicked_by: + self.user_kicked = True + self._kicked_by = kicked_by + + self.created = bool(created) + self._user_peers = users if isinstance(users, list) else [users] + self._users = None + self._input_users = None + self.new_title = new_title + self.unpin = unpin + + def respond(self, *args, **kwargs): + """ + Responds to the chat action message (not as a reply). + Shorthand for ``client.send_message(event.chat, ...)``. + """ + return self._client.send_message(self.input_chat, *args, **kwargs) + + def reply(self, *args, **kwargs): + """ + Replies to the chat action message (as a reply). Shorthand for + ``client.send_message(event.chat, ..., reply_to=event.message.id)``. + + Has the same effect as ``.respond()`` if there is no message. + """ + if not self.action_message: + return self.respond(*args, **kwargs) + + kwargs['reply_to'] = self.action_message.id + return self._client.send_message(self.input_chat, *args, **kwargs) + + def delete(self, *args, **kwargs): + """ + Deletes the chat action message. You're responsible for checking + whether you have the permission to do so, or to except the error + otherwise. This is a shorthand for + ``client.delete_messages(event.chat, event.message, ...)``. + + Does nothing if no message action triggered this event. + """ + if self.action_message: + return self._client.delete_messages(self.input_chat, + [self.action_message], + *args, **kwargs) + + @property + def pinned_message(self): + """ + If ``new_pin`` is ``True``, this returns the (:tl:`Message`) + object that was pinned. + """ + if self._pinned_message == 0: + return None + + if isinstance(self._pinned_message, int) and self.input_chat: + r = self._client(functions.channels.GetMessagesRequest( + self._input_chat, [ + types.InputMessageID(self._pinned_message) + ] + )) + try: + self._pinned_message = next( + x for x in r.messages + if isinstance(x, types.Message) + and x.id == self._pinned_message + ) + except StopIteration: + pass + + if isinstance(self._pinned_message, types.Message): + return self._pinned_message + + @property + def added_by(self): + """ + The user who added ``users``, if applicable (``None`` otherwise). + """ + if self._added_by and not isinstance(self._added_by, types.User): + self._added_by =\ + self._entities.get(utils.get_peer_id(self._added_by)) + + if not self._added_by: + self._added_by = self._client.get_entity(self._added_by) + + return self._added_by + + @property + def kicked_by(self): + """ + The user who kicked ``users``, if applicable (``None`` otherwise). + """ + if self._kicked_by and not isinstance(self._kicked_by, types.User): + self._kicked_by =\ + self._entities.get(utils.get_peer_id(self._kicked_by)) + + if not self._kicked_by: + self._kicked_by = self._client.get_entity(self._kicked_by) + + return self._kicked_by + + @property + def user(self): + """ + The single user that takes part in this action (e.g. joined). + + Might be ``None`` if the information can't be retrieved or + there is no user taking part. + """ + if self.users: + return self._users[0] + + @property + def input_user(self): + """ + Input version of the ``self.user`` property. + """ + if self.input_users: + return self._input_users[0] + + @property + def users(self): + """ + A list of users that take part in this action (e.g. joined). + + Might be empty if the information can't be retrieved or there + are no users taking part. + """ + if not self._user_peers: + return [] + + if self._users is None: + have, missing = [], [] + for peer in self._user_peers: + user = self._entities.get(utils.get_peer_id(peer)) + if user: + have.append(user) + else: + missing.append(peer) + + try: + missing = self._client.get_entity(missing) + except (TypeError, ValueError): + missing = [] + + self._users = have + missing + + return self._users + + @property + def input_users(self): + """ + Input version of the ``self.users`` property. + """ + if self._input_users is None and self._user_peers: + self._input_users = [] + for peer in self._user_peers: + try: + self._input_users.append(self._client.get_input_entity( + peer + )) + except (TypeError, ValueError): + pass + return self._input_users diff --git a/telethon/events/common.py b/telethon/events/common.py new file mode 100644 index 00000000..8c475973 --- /dev/null +++ b/telethon/events/common.py @@ -0,0 +1,224 @@ +import abc +import datetime +import itertools +import re +import warnings + +from .. import utils +from ..errors import RPCError +from ..extensions import markdown +from ..tl import TLObject, types, functions + + +def _into_id_set(client, chats): + """Helper util to turn the input chat or chats into a set of IDs.""" + if chats is None: + return None + + if not utils.is_list_like(chats): + chats = (chats,) + + result = set() + for chat in chats: + if isinstance(chat, int): + if chat < 0: + result.add(chat) # Explicitly marked IDs are negative + else: + result.update({ # Support all valid types of peers + utils.get_peer_id(types.PeerUser(chat)), + utils.get_peer_id(types.PeerChat(chat)), + utils.get_peer_id(types.PeerChannel(chat)), + }) + elif isinstance(chat, TLObject) and chat.SUBCLASS_OF_ID == 0x2d45687: + # 0x2d45687 == crc32(b'Peer') + result.add(utils.get_peer_id(chat)) + else: + chat = client.get_input_entity(chat) + if isinstance(chat, types.InputPeerSelf): + chat = client.get_me(input_peer=True) + result.add(utils.get_peer_id(chat)) + + return result + + +class EventBuilder(abc.ABC): + """ + The common event builder, with builtin support to filter per chat. + + Args: + chats (`entity`, optional): + May be one or more entities (username/peer/etc.). By default, + only matching chats will be handled. + + blacklist_chats (`bool`, optional): + Whether to treat the chats as a blacklist instead of + as a whitelist (default). This means that every chat + will be handled *except* those specified in ``chats`` + which will be ignored if ``blacklist_chats=True``. + """ + def __init__(self, chats=None, blacklist_chats=False): + self.chats = chats + self.blacklist_chats = blacklist_chats + self._self_id = None + + @abc.abstractmethod + def build(self, update): + """Builds an event for the given update if possible, or returns None""" + + def resolve(self, client): + """Helper method to allow event builders to be resolved before usage""" + self.chats = _into_id_set(client, self.chats) + self._self_id = client.get_me(input_peer=True).user_id + + def _filter_event(self, event): + """ + If the ID of ``event._chat_peer`` isn't in the chats set (or it is + but the set is a blacklist) returns ``None``, otherwise the event. + """ + if self.chats is not None: + inside = utils.get_peer_id(event._chat_peer) in self.chats + if inside == self.blacklist_chats: + # If this chat matches but it's a blacklist ignore. + # If it doesn't match but it's a whitelist ignore. + return None + return event + + +class EventCommon(abc.ABC): + """Intermediate class with common things to all events""" + _event_name = 'Event' + + def __init__(self, chat_peer=None, msg_id=None, broadcast=False): + self._entities = {} + self._client = None + self._chat_peer = chat_peer + self._message_id = msg_id + self._input_chat = None + self._chat = None + + self.pattern_match = None + + self.is_private = isinstance(chat_peer, types.PeerUser) + self.is_group = ( + isinstance(chat_peer, (types.PeerChat, types.PeerChannel)) + and not broadcast + ) + self.is_channel = isinstance(chat_peer, types.PeerChannel) + + def _get_entity(self, msg_id, entity_id, chat=None): + """ + Helper function to call :tl:`GetMessages` on the give msg_id and + return the input entity whose ID is the given entity ID. + + If ``chat`` is present it must be an :tl:`InputPeer`. + + Returns a tuple of ``(entity, input_peer)`` if it was found, or + a tuple of ``(None, None)`` if it couldn't be. + """ + try: + if isinstance(chat, types.InputPeerChannel): + result = self._client( + functions.channels.GetMessagesRequest(chat, [ + types.InputMessageID(msg_id) + ]) + ) + else: + result = self._client( + functions.messages.GetMessagesRequest([ + types.InputMessageID(msg_id) + ]) + ) + except RPCError: + return None, None + + entity = { + utils.get_peer_id(x): x for x in itertools.chain( + getattr(result, 'chats', []), + getattr(result, 'users', [])) + }.get(entity_id) + if entity: + return entity, utils.get_input_peer(entity) + else: + return None, None + + @property + def input_chat(self): + """ + The (:tl:`InputPeer`) (group, megagroup or channel) on which + the event occurred. This doesn't have the title or anything, + but is useful if you don't need those to avoid further + requests. + + Note that this might be ``None`` if the library can't find it. + """ + + if self._input_chat is None and self._chat_peer is not None: + try: + self._input_chat = self._client.get_input_entity( + self._chat_peer + ) + except (ValueError, TypeError): + # The library hasn't seen this chat, get the message + if not isinstance(self._chat_peer, types.PeerChannel): + # TODO For channels, getDifference? Maybe looking + # in the dialogs (which is already done) is enough. + if self._message_id is not None: + self._chat, self._input_chat = self._get_entity( + self._message_id, + utils.get_peer_id(self._chat_peer) + ) + return self._input_chat + + @property + def client(self): + return self._client + + @property + def chat(self): + """ + The (:tl:`User` | :tl:`Chat` | :tl:`Channel`, optional) on which + the event occurred. This property may make an API call the first time + to get the most up to date version of the chat (mostly when the event + doesn't belong to a channel), so keep that in mind. + """ + if not self.input_chat: + return None + + if self._chat is None: + self._chat = self._entities.get(utils.get_peer_id(self._input_chat)) + + if self._chat is None: + self._chat = self._client.get_entity(self._input_chat) + + return self._chat + + def __str__(self): + return TLObject.pretty_format(self.to_dict()) + + def stringify(self): + return TLObject.pretty_format(self.to_dict(), indent=0) + + def to_dict(self): + d = {k: v for k, v in self.__dict__.items() if k[0] != '_'} + d['_'] = self._event_name + return d + + +class Raw(EventBuilder): + """ + Represents a raw event. The event is the update itself. + """ + def resolve(self, client): + pass + + def build(self, update): + return update + + +def name_inner_event(cls): + """Decorator to rename cls.Event 'Event' as 'cls.Event'""" + if hasattr(cls, 'Event'): + cls.Event._event_name = '{}.Event'.format(cls.__name__) + else: + warnings.warn('Class {} does not have a inner Event'.format(cls)) + return cls diff --git a/telethon/events/messagedeleted.py b/telethon/events/messagedeleted.py new file mode 100644 index 00000000..4116f852 --- /dev/null +++ b/telethon/events/messagedeleted.py @@ -0,0 +1,33 @@ +from .common import EventBuilder, EventCommon, name_inner_event +from ..tl import types + + +@name_inner_event +class MessageDeleted(EventBuilder): + """ + Event fired when one or more messages are deleted. + """ + def build(self, update): + if isinstance(update, types.UpdateDeleteMessages): + event = MessageDeleted.Event( + deleted_ids=update.messages, + peer=None + ) + elif isinstance(update, types.UpdateDeleteChannelMessages): + event = MessageDeleted.Event( + deleted_ids=update.messages, + peer=types.PeerChannel(update.channel_id) + ) + else: + return + + event._entities = update._entities + return self._filter_event(event) + + class Event(EventCommon): + def __init__(self, deleted_ids, peer): + super().__init__( + chat_peer=peer, msg_id=(deleted_ids or [0])[0] + ) + self.deleted_id = None if not deleted_ids else deleted_ids[0] + self.deleted_ids = deleted_ids diff --git a/telethon/events/messageedited.py b/telethon/events/messageedited.py new file mode 100644 index 00000000..5ea42a50 --- /dev/null +++ b/telethon/events/messageedited.py @@ -0,0 +1,22 @@ +from .common import name_inner_event +from .newmessage import NewMessage +from ..tl import types + + +@name_inner_event +class MessageEdited(NewMessage): + """ + Event fired when a message has been edited. + """ + def build(self, update): + if isinstance(update, (types.UpdateEditMessage, + types.UpdateEditChannelMessage)): + event = MessageEdited.Event(update.message) + else: + return + + event._entities = update._entities + return self._message_filter_event(event) + + class Event(NewMessage.Event): + pass # Required if we want a different name for it diff --git a/telethon/events/messageread.py b/telethon/events/messageread.py new file mode 100644 index 00000000..da7c39ea --- /dev/null +++ b/telethon/events/messageread.py @@ -0,0 +1,137 @@ +from .common import EventBuilder, EventCommon, name_inner_event +from .. import utils +from ..tl import types, functions + + +@name_inner_event +class MessageRead(EventBuilder): + """ + Event fired when one or more messages have been read. + + Args: + inbox (`bool`, optional): + If this argument is ``True``, then when you read someone else's + messages the event will be fired. By default (``False``) only + when messages you sent are read by someone else will fire it. + """ + def __init__(self, inbox=False, chats=None, blacklist_chats=None): + super().__init__(chats, blacklist_chats) + self.inbox = inbox + + def build(self, update): + if isinstance(update, types.UpdateReadHistoryInbox): + event = MessageRead.Event(update.peer, update.max_id, False) + elif isinstance(update, types.UpdateReadHistoryOutbox): + event = MessageRead.Event(update.peer, update.max_id, True) + elif isinstance(update, types.UpdateReadChannelInbox): + event = MessageRead.Event(types.PeerChannel(update.channel_id), + update.max_id, False) + elif isinstance(update, types.UpdateReadChannelOutbox): + event = MessageRead.Event(types.PeerChannel(update.channel_id), + update.max_id, True) + elif isinstance(update, types.UpdateReadMessagesContents): + event = MessageRead.Event(message_ids=update.messages, + contents=True) + elif isinstance(update, types.UpdateChannelReadMessagesContents): + event = MessageRead.Event(types.PeerChannel(update.channel_id), + message_ids=update.messages, + contents=True) + else: + return + + if self.inbox == event.outbox: + return + + event._entities = update._entities + return self._filter_event(event) + + class Event(EventCommon): + """ + Represents the event of one or more messages being read. + + Members: + max_id (`int`): + Up to which message ID has been read. Every message + with an ID equal or lower to it have been read. + + outbox (`bool`): + ``True`` if someone else has read your messages. + + contents (`bool`): + ``True`` if what was read were the contents of a message. + This will be the case when e.g. you play a voice note. + It may only be set on ``inbox`` events. + """ + def __init__(self, peer=None, max_id=None, out=False, contents=False, + message_ids=None): + self.outbox = out + self.contents = contents + self._message_ids = message_ids or [] + self._messages = None + self.max_id = max_id or max(message_ids or [], default=None) + super().__init__(peer, self.max_id) + + @property + def inbox(self): + """ + ``True`` if you have read someone else's messages. + """ + return not self.outbox + + @property + def message_ids(self): + """ + The IDs of the messages **which contents'** were read. + + Use :meth:`is_read` if you need to check whether a message + was read instead checking if it's in here. + """ + return self._message_ids + + @property + def messages(self): + """ + The list of :tl:`Message` **which contents'** were read. + + Use :meth:`is_read` if you need to check whether a message + was read instead checking if it's in here. + """ + if self._messages is None: + chat = self.input_chat + if not chat: + self._messages = [] + elif isinstance(chat, types.InputPeerChannel): + ids = [types.InputMessageID(x) for x in self._message_ids] + self._messages =\ + self._client(functions.channels.GetMessagesRequest( + chat, ids + )).messages + else: + ids = [types.InputMessageID(x) for x in self._message_ids] + self._messages =\ + self._client(functions.messages.GetMessagesRequest( + ids + )).messages + + return self._messages + + def is_read(self, message): + """ + Returns ``True`` if the given message (or its ID) has been read. + + If a list-like argument is provided, this method will return a + list of booleans indicating which messages have been read. + """ + if utils.is_list_like(message): + return [(m if isinstance(m, int) else m.id) <= self.max_id + for m in message] + else: + return (message if isinstance(message, int) + else message.id) <= self.max_id + + def __contains__(self, message): + """``True`` if the message(s) are read message.""" + if utils.is_list_like(message): + return all(self.is_read(message)) + else: + return self.is_read(message) diff --git a/telethon/events/newmessage.py b/telethon/events/newmessage.py new file mode 100644 index 00000000..5c21bb36 --- /dev/null +++ b/telethon/events/newmessage.py @@ -0,0 +1,411 @@ +import abc +import datetime +import itertools +import re +import warnings + +from .. import utils +from ..errors import RPCError +from ..extensions import markdown +from ..tl import TLObject, types, functions + + +from .common import EventBuilder, EventCommon, name_inner_event + + +@name_inner_event +class NewMessage(EventBuilder): + """ + Represents a new message event builder. + + Args: + incoming (`bool`, optional): + If set to ``True``, only **incoming** messages will be handled. + Mutually exclusive with ``outgoing`` (can only set one of either). + + outgoing (`bool`, optional): + If set to ``True``, only **outgoing** messages will be handled. + Mutually exclusive with ``incoming`` (can only set one of either). + + pattern (`str`, `callable`, `Pattern`, optional): + If set, only messages matching this pattern will be handled. + You can specify a regex-like string which will be matched + against the message, a callable function that returns ``True`` + if a message is acceptable, or a compiled regex pattern. + """ + def __init__(self, incoming=None, outgoing=None, + chats=None, blacklist_chats=False, pattern=None): + if incoming and outgoing: + raise ValueError('Can only set either incoming or outgoing') + + super().__init__(chats=chats, blacklist_chats=blacklist_chats) + self.incoming = incoming + self.outgoing = outgoing + if isinstance(pattern, str): + self.pattern = re.compile(pattern).match + elif not pattern or callable(pattern): + self.pattern = pattern + elif hasattr(pattern, 'match') and callable(pattern.match): + self.pattern = pattern.match + else: + raise TypeError('Invalid pattern type given') + + def build(self, update): + if isinstance(update, + (types.UpdateNewMessage, types.UpdateNewChannelMessage)): + if not isinstance(update.message, types.Message): + return # We don't care about MessageService's here + event = NewMessage.Event(update.message) + elif isinstance(update, types.UpdateShortMessage): + event = NewMessage.Event(types.Message( + out=update.out, + mentioned=update.mentioned, + media_unread=update.media_unread, + silent=update.silent, + id=update.id, + to_id=types.PeerUser(update.user_id), + from_id=self._self_id if update.out else update.user_id, + message=update.message, + date=update.date, + fwd_from=update.fwd_from, + via_bot_id=update.via_bot_id, + reply_to_msg_id=update.reply_to_msg_id, + entities=update.entities + )) + elif isinstance(update, types.UpdateShortChatMessage): + event = NewMessage.Event(types.Message( + out=update.out, + mentioned=update.mentioned, + media_unread=update.media_unread, + silent=update.silent, + id=update.id, + from_id=update.from_id, + to_id=types.PeerChat(update.chat_id), + message=update.message, + date=update.date, + fwd_from=update.fwd_from, + via_bot_id=update.via_bot_id, + reply_to_msg_id=update.reply_to_msg_id, + entities=update.entities + )) + else: + return + + event._entities = update._entities + return self._message_filter_event(event) + + def _message_filter_event(self, event): + # Short-circuit if we let pass all events + if all(x is None for x in (self.incoming, self.outgoing, self.chats, + self.pattern)): + return event + + if self.incoming and event.message.out: + return + if self.outgoing and not event.message.out: + return + + if self.pattern: + match = self.pattern(event.message.message or '') + if not match: + return + event.pattern_match = match + + return self._filter_event(event) + + class Event(EventCommon): + """ + Represents the event of a new message. + + Members: + message (:tl:`Message`): + This is the original :tl:`Message` object. + + is_private (`bool`): + True if the message was sent as a private message. + + is_group (`bool`): + True if the message was sent on a group or megagroup. + + is_channel (`bool`): + True if the message was sent on a megagroup or channel. + + is_reply (`str`): + Whether the message is a reply to some other or not. + """ + def __init__(self, message): + if not message.out and isinstance(message.to_id, types.PeerUser): + # Incoming message (e.g. from a bot) has to_id=us, and + # from_id=bot (the actual "chat" from an user's perspective). + chat_peer = types.PeerUser(message.from_id) + else: + chat_peer = message.to_id + + super().__init__(chat_peer=chat_peer, + msg_id=message.id, broadcast=bool(message.post)) + + self.message = message + self._text = None + + self._input_sender = None + self._sender = None + + self.is_reply = bool(message.reply_to_msg_id) + self._reply_message = None + + def respond(self, *args, **kwargs): + """ + Responds to the message (not as a reply). This is a shorthand for + ``client.send_message(event.chat, ...)``. + """ + return self._client.send_message(self.input_chat, *args, **kwargs) + + def reply(self, *args, **kwargs): + """ + Replies to the message (as a reply). This is a shorthand for + ``client.send_message(event.chat, ..., reply_to=event.message.id)``. + """ + kwargs['reply_to'] = self.message.id + return self._client.send_message(self.input_chat, *args, **kwargs) + + def forward_to(self, *args, **kwargs): + """ + Forwards the message. This is a shorthand for + ``client.forward_messages(entity, event.message, event.chat)``. + """ + kwargs['messages'] = self.message.id + kwargs['from_peer'] = self.input_chat + return self._client.forward_messages(*args, **kwargs) + + def edit(self, *args, **kwargs): + """ + Edits the message iff it's outgoing. This is a shorthand for + ``client.edit_message(event.chat, event.message, ...)``. + + Returns ``None`` if the message was incoming, + or the edited message otherwise. + """ + if self.message.fwd_from: + return None + if not self.message.out: + if not isinstance(self.message.to_id, types.PeerUser): + return None + me = self._client.get_me(input_peer=True) + if self.message.to_id.user_id != me.user_id: + return None + + return self._client.edit_message(self.input_chat, + self.message, + *args, **kwargs) + + def delete(self, *args, **kwargs): + """ + Deletes the message. You're responsible for checking whether you + have the permission to do so, or to except the error otherwise. + This is a shorthand for + ``client.delete_messages(event.chat, event.message, ...)``. + """ + return self._client.delete_messages(self.input_chat, + [self.message], + *args, **kwargs) + + @property + def input_sender(self): + """ + This (:tl:`InputPeer`) is the input version of the user who + sent the message. Similarly to ``input_chat``, this doesn't have + things like username or similar, but still useful in some cases. + + Note that this might not be available if the library can't + find the input chat, or if the message a broadcast on a channel. + """ + if self._input_sender is None: + if self.is_channel and not self.is_group: + return None + + try: + self._input_sender = self._client.get_input_entity( + self.message.from_id + ) + except (ValueError, TypeError): + # We can rely on self.input_chat for this + self._sender, self._input_sender = self._get_entity( + self.message.id, + self.message.from_id, + chat=self.input_chat + ) + + return self._input_sender + + @property + def sender(self): + """ + This (:tl:`User`) may make an API call the first time to get + the most up to date version of the sender (mostly when the event + doesn't belong to a channel), so keep that in mind. + + ``input_sender`` needs to be available (often the case). + """ + if not self.input_sender: + return None + + if self._sender is None: + self._sender = \ + self._entities.get(utils.get_peer_id(self._input_sender)) + + if self._sender is None: + self._sender = self._client.get_entity(self._input_sender) + + return self._sender + + @property + def text(self): + """ + The message text, markdown-formatted. + """ + if self._text is None: + if not self.message.entities: + return self.message.message + self._text = markdown.unparse(self.message.message, + self.message.entities or []) + return self._text + + @property + def raw_text(self): + """ + The raw message text, ignoring any formatting. + """ + return self.message.message + + @property + def reply_message(self): + """ + This optional :tl:`Message` will make an API call the first + time to get the full :tl:`Message` object that one was replying to, + so use with care as there is no caching besides local caching yet. + """ + if not self.message.reply_to_msg_id: + return None + + if self._reply_message is None: + if isinstance(self.input_chat, types.InputPeerChannel): + r = self._client(functions.channels.GetMessagesRequest( + self.input_chat, [ + types.InputMessageID(self.message.reply_to_msg_id) + ] + )) + else: + r = self._client(functions.messages.GetMessagesRequest( + [types.InputMessageID(self.message.reply_to_msg_id)] + )) + if not isinstance(r, types.messages.MessagesNotModified): + self._reply_message = r.messages[0] + + return self._reply_message + + @property + def forward(self): + """ + The unmodified :tl:`MessageFwdHeader`, if present.. + """ + return self.message.fwd_from + + @property + def media(self): + """ + The unmodified :tl:`MessageMedia`, if present. + """ + return self.message.media + + @property + def photo(self): + """ + If the message media is a photo, + this returns the :tl:`Photo` object. + """ + if isinstance(self.message.media, types.MessageMediaPhoto): + photo = self.message.media.photo + if isinstance(photo, types.Photo): + return photo + + @property + def document(self): + """ + If the message media is a document, + this returns the :tl:`Document` object. + """ + if isinstance(self.message.media, types.MessageMediaDocument): + doc = self.message.media.document + if isinstance(doc, types.Document): + return doc + + def _document_by_attribute(self, kind, condition=None): + """ + Helper method to return the document only if it has an attribute + that's an instance of the given kind, and passes the condition. + """ + doc = self.document + if doc: + for attr in doc.attributes: + if isinstance(attr, kind): + if not condition or condition(doc): + return doc + + @property + def audio(self): + """ + If the message media is a document with an Audio attribute, + this returns the :tl:`Document` object. + """ + return self._document_by_attribute(types.DocumentAttributeAudio, + lambda attr: not attr.voice) + + @property + def voice(self): + """ + If the message media is a document with a Voice attribute, + this returns the :tl:`Document` object. + """ + return self._document_by_attribute(types.DocumentAttributeAudio, + lambda attr: attr.voice) + + @property + def video(self): + """ + If the message media is a document with a Video attribute, + this returns the :tl:`Document` object. + """ + return self._document_by_attribute(types.DocumentAttributeVideo) + + @property + def video_note(self): + """ + If the message media is a document with a Video attribute, + this returns the :tl:`Document` object. + """ + return self._document_by_attribute(types.DocumentAttributeVideo, + lambda attr: attr.round_message) + + @property + def gif(self): + """ + If the message media is a document with an Animated attribute, + this returns the :tl:`Document` object. + """ + return self._document_by_attribute(types.DocumentAttributeAnimated) + + @property + def sticker(self): + """ + If the message media is a document with a Sticker attribute, + this returns the :tl:`Document` object. + """ + return self._document_by_attribute(types.DocumentAttributeSticker) + + @property + def out(self): + """ + Whether the message is outgoing (i.e. you sent it from + another session) or incoming (i.e. someone else sent it). + """ + return self.message.out diff --git a/telethon/events/userupdate.py b/telethon/events/userupdate.py new file mode 100644 index 00000000..7f87cde5 --- /dev/null +++ b/telethon/events/userupdate.py @@ -0,0 +1,153 @@ +import datetime + +from .common import EventBuilder, EventCommon, name_inner_event +from ..tl import types + + +@name_inner_event +class UserUpdate(EventBuilder): + """ + Represents an user update (gone online, offline, joined Telegram). + """ + def build(self, update): + if isinstance(update, types.UpdateUserStatus): + event = UserUpdate.Event(update.user_id, + status=update.status) + else: + return + + event._entities = update._entities + return self._filter_event(event) + + class Event(EventCommon): + """ + Represents the event of an user status update (last seen, joined). + + Members: + online (`bool`, optional): + ``True`` if the user is currently online, ``False`` otherwise. + Might be ``None`` if this information is not present. + + last_seen (`datetime`, optional): + Exact date when the user was last seen if known. + + until (`datetime`, optional): + Until when will the user remain online. + + within_months (`bool`): + ``True`` if the user was seen within 30 days. + + within_weeks (`bool`): + ``True`` if the user was seen within 7 days. + + recently (`bool`): + ``True`` if the user was seen within a day. + + action (:tl:`SendMessageAction`, optional): + The "typing" action if any the user is performing if any. + + cancel (`bool`): + ``True`` if the action was cancelling other actions. + + typing (`bool`): + ``True`` if the action is typing a message. + + recording (`bool`): + ``True`` if the action is recording something. + + uploading (`bool`): + ``True`` if the action is uploading something. + + playing (`bool`): + ``True`` if the action is playing a game. + + audio (`bool`): + ``True`` if what's being recorded/uploaded is an audio. + + round (`bool`): + ``True`` if what's being recorded/uploaded is a round video. + + video (`bool`): + ``True`` if what's being recorded/uploaded is an video. + + document (`bool`): + ``True`` if what's being uploaded is document. + + geo (`bool`): + ``True`` if what's being uploaded is a geo. + + photo (`bool`): + ``True`` if what's being uploaded is a photo. + + contact (`bool`): + ``True`` if what's being uploaded (selected) is a contact. + """ + def __init__(self, user_id, status=None, typing=None): + super().__init__(types.PeerUser(user_id)) + + self.online = None if status is None else \ + isinstance(status, types.UserStatusOnline) + + self.last_seen = status.was_online if \ + isinstance(status, types.UserStatusOffline) else None + + self.until = status.expires if \ + isinstance(status, types.UserStatusOnline) else None + + if self.last_seen: + diff = datetime.datetime.now() - self.last_seen + if diff < datetime.timedelta(days=30): + self.within_months = True + if diff < datetime.timedelta(days=7): + self.within_weeks = True + if diff < datetime.timedelta(days=1): + self.recently = True + else: + self.within_months = self.within_weeks = self.recently = False + if isinstance(status, (types.UserStatusOnline, + types.UserStatusRecently)): + self.within_months = self.within_weeks = True + self.recently = True + elif isinstance(status, types.UserStatusLastWeek): + self.within_months = self.within_weeks = True + elif isinstance(status, types.UserStatusLastMonth): + self.within_months = True + + self.action = typing + if typing: + self.cancel = self.typing = self.recording = self.uploading = \ + self.playing = False + self.audio = self.round = self.video = self.document = \ + self.geo = self.photo = self.contact = False + + if isinstance(typing, types.SendMessageCancelAction): + self.cancel = True + elif isinstance(typing, types.SendMessageTypingAction): + self.typing = True + elif isinstance(typing, types.SendMessageGamePlayAction): + self.playing = True + elif isinstance(typing, types.SendMessageGeoLocationAction): + self.geo = True + elif isinstance(typing, types.SendMessageRecordAudioAction): + self.recording = self.audio = True + elif isinstance(typing, types.SendMessageRecordRoundAction): + self.recording = self.round = True + elif isinstance(typing, types.SendMessageRecordVideoAction): + self.recording = self.video = True + elif isinstance(typing, types.SendMessageChooseContactAction): + self.uploading = self.contact = True + elif isinstance(typing, types.SendMessageUploadAudioAction): + self.uploading = self.audio = True + elif isinstance(typing, types.SendMessageUploadDocumentAction): + self.uploading = self.document = True + elif isinstance(typing, types.SendMessageUploadPhotoAction): + self.uploading = self.photo = True + elif isinstance(typing, types.SendMessageUploadRoundAction): + self.uploading = self.round = True + elif isinstance(typing, types.SendMessageUploadVideoAction): + self.uploading = self.video = True + + @property + def user(self): + """Alias around the chat (conversation).""" + return self.chat From 0980d828d8c1454a02e7bd64ec6cdc92a032c29e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 5 Apr 2018 20:19:33 +0200 Subject: [PATCH 03/11] Modify events documentation to list the new files --- readthedocs/telethon.events.rst | 47 +++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/readthedocs/telethon.events.rst b/readthedocs/telethon.events.rst index 37ce9f48..531427b9 100644 --- a/readthedocs/telethon.events.rst +++ b/readthedocs/telethon.events.rst @@ -7,3 +7,50 @@ telethon\.events package :members: :undoc-members: :show-inheritance: + + +Every event (builder) subclasses `telethon.events.common.EventBuilder`, +so all the methods in it can be used from any event builder/event instance. + +.. automodule:: telethon.events.common + :members: + :undoc-members: + :show-inheritance: + + +Below all the event types are listed: + +.. automodule:: telethon.events.newmessage + :members: + :undoc-members: + :show-inheritance: + + +.. automodule:: telethon.events.chataction + :members: + :undoc-members: + :show-inheritance: + + +.. automodule:: telethon.events.userupdate + :members: + :undoc-members: + :show-inheritance: + + +.. automodule:: telethon.events.messageedited + :members: + :undoc-members: + :show-inheritance: + + +.. automodule:: telethon.events.messagedeleted + :members: + :undoc-members: + :show-inheritance: + + +.. automodule:: telethon.events.messageread + :members: + :undoc-members: + :show-inheritance: From baa6976a0b3cbebb6491f2ceeb9199ca029eed6a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 6 Apr 2018 19:00:21 +0200 Subject: [PATCH 04/11] Fix broken links, add more examples and a new section --- readthedocs/extra/basic/entities.rst | 7 ++ readthedocs/extra/basic/telegram-client.rst | 14 ++- .../extra/examples/chats-and-channels.rst | 111 +++++++++++++++--- .../examples/projects-using-telethon.rst | 41 +++++++ readthedocs/index.rst | 1 + telethon/telegram_client.py | 7 ++ 6 files changed, 158 insertions(+), 23 deletions(-) create mode 100644 readthedocs/extra/examples/projects-using-telethon.rst diff --git a/readthedocs/extra/basic/entities.rst b/readthedocs/extra/basic/entities.rst index ab04a165..c0473247 100644 --- a/readthedocs/extra/basic/entities.rst +++ b/readthedocs/extra/basic/entities.rst @@ -20,6 +20,13 @@ in response to certain methods, such as :tl:`GetUsersRequest`. or even entire :tl:`User`, :tl:`Chat` and :tl:`Channel` objects and even phone numbers from people you have in your contacts. + To "encounter" an ID, you would have to "find it" like you would in the + normal app. If the peer is in your dialogs, you would need to + `client.get_dialogs() `. + If the peer is someone in a group, you would similarly + `client.get_participants(group) `. + + Getting entities **************** diff --git a/readthedocs/extra/basic/telegram-client.rst b/readthedocs/extra/basic/telegram-client.rst index decb3765..81edf83a 100644 --- a/readthedocs/extra/basic/telegram-client.rst +++ b/readthedocs/extra/basic/telegram-client.rst @@ -58,6 +58,11 @@ Many other common methods for quick scripts are also available: # Note that you can use 'me' or 'self' to message yourself client.send_message('username', 'Hello World from Telethon!') + # .send_message's parse mode defaults to markdown, so you + # can use **bold**, __italics__, [links](https://example.com), `code`, + # and even [mentions](@username)/[mentions](tg://user?id=123456789) + client.send_message('username', '**Using** __markdown__ `too`!') + client.send_file('username', '/home/myself/Pictures/holidays.jpg') # The utils package has some goodies, like .get_display_name() @@ -83,15 +88,16 @@ a single line. Available methods ***************** -This page lists all the "handy" methods available for you to use in the -``TelegramClient`` class. These are simply wrappers around the "raw" -Telegram API, making it much more manageable and easier to work with. +The :ref:`reference ` lists all the "handy" methods +available for you to use in the ``TelegramClient`` class. These are simply +wrappers around the "raw" Telegram API, making it much more manageable and +easier to work with. Please refer to :ref:`accessing-the-full-api` if these aren't enough, and don't be afraid to read the source code of the InteractiveTelegramClient_ or even the TelegramClient_ itself to learn how it works. -To see the methods available in the client, see :ref:`telethon-package`. +See the mentioned :ref:`telethon-package` to find the available methods. .. _InteractiveTelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon_examples/interactive_telegram_client.py .. _TelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon/telegram_client.py diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst index 9851282f..4465bb70 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -35,6 +35,10 @@ to, you can make use of the `JoinChannelRequest`__ to join such channel: For more on channels, check the `channels namespace`__. +__ https://lonamiwebs.github.io/Telethon/methods/channels/join_channel.html +__ https://lonamiwebs.github.io/Telethon/methods/channels/index.html + + Joining a private chat or channel ********************************* @@ -51,6 +55,9 @@ example, is the ``hash`` of the chat or channel. Now you can use updates = client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg')) +__ https://lonamiwebs.github.io/Telethon/methods/messages/import_chat_invite.html + + Adding someone else to such chat or channel ******************************************* @@ -63,13 +70,15 @@ use is very straightforward, or `InviteToChannelRequest`__ for channels: # For normal chats from telethon.tl.functions.messages import AddChatUserRequest + # Note that ``user_to_add`` is NOT the name of the parameter. + # It's the user you want to add (``user_id=user_to_add``). client(AddChatUserRequest( chat_id, user_to_add, fwd_limit=10 # Allow the user to see the 10 last messages )) - # For channels + # For channels (which includes megagroups) from telethon.tl.functions.channels import InviteToChannelRequest client(InviteToChannelRequest( @@ -78,6 +87,9 @@ use is very straightforward, or `InviteToChannelRequest`__ for channels: )) +__ https://lonamiwebs.github.io/Telethon/methods/messages/add_chat_user.html +__ https://lonamiwebs.github.io/Telethon/methods/channels/invite_to_channel.html + Checking a link without joining ******************************* @@ -86,14 +98,7 @@ If you don't need to join but rather check whether it's a group or a channel, you can use the `CheckChatInviteRequest`__, which takes in the hash of said channel or group. -__ https://lonamiwebs.github.io/Telethon/constructors/chat.html -__ https://lonamiwebs.github.io/Telethon/constructors/channel.html -__ https://lonamiwebs.github.io/Telethon/types/chat.html -__ https://lonamiwebs.github.io/Telethon/methods/channels/join_channel.html -__ https://lonamiwebs.github.io/Telethon/methods/channels/index.html -__ https://lonamiwebs.github.io/Telethon/methods/messages/import_chat_invite.html -__ https://lonamiwebs.github.io/Telethon/methods/messages/add_chat_user.html -__ https://lonamiwebs.github.io/Telethon/methods/channels/invite_to_channel.html + __ https://lonamiwebs.github.io/Telethon/methods/messages/check_chat_invite.html @@ -151,7 +156,7 @@ which may have more information you need (like the role of the participants, total count of members, etc.) __ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html -__ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html +__ https://lonamiwebs.github.io/Telethon/types/input_channel.html __ https://lonamiwebs.github.io/Telethon/types/channel_participants_filter.html __ https://lonamiwebs.github.io/Telethon/constructors/channel_participants_search.html __ https://github.com/LonamiWebs/Telethon/issues/573 @@ -208,20 +213,88 @@ Giving or revoking admin permissions can be done with the `EditAdminRequest`__: # User will now be able to change group info, delete other people's # messages and pin messages. - -| Thanks to `@Kyle2142`__ for `pointing out`__ that you **cannot** set all -| parameters to ``True`` to give a user full permissions, as not all -| permissions are related to both broadcast channels/megagroups. -| -| E.g. trying to set ``post_messages=True`` in a megagroup will raise an -| error. It is recommended to always use keyword arguments, and to set only -| the permissions the user needs. If you don't need to change a permission, -| it can be omitted (full list `here`__). + + +.. note:: + + Thanks to `@Kyle2142`__ for `pointing out`__ that you **cannot** set all + parameters to ``True`` to give a user full permissions, as not all + permissions are related to both broadcast channels/megagroups. + + E.g. trying to set ``post_messages=True`` in a megagroup will raise an + error. It is recommended to always use keyword arguments, and to set only + the permissions the user needs. If you don't need to change a permission, + it can be omitted (full list `here`__). + + +Restricting Users +***************** + +Similar to how you give or revoke admin permissions, you can edit the +banned rights of an user through `EditAdminRequest`__ and its parameter +`ChannelBannedRights`__: + + .. code-block:: python + + from telethon.tl.functions.channels import EditBannedRequest + from telethon.tl.types import ChannelBannedRights + + from datetime import datetime, timedelta + + # Restricting an user for 7 days, only allowing view/send messages. + # + # Note that it's "reversed". You must set to ``True`` the permissions + # you want to REMOVE, and leave as ``None`` those you want to KEEP. + rights = ChannelBannedRights( + until_date=datetime.now() + timedelta(days=7), + view_messages=None, + send_messages=None, + send_media=True, + send_stickers=True, + send_gifs=True, + send_games=True, + send_inline=True, + embed_links=True + ) + + # The above is equivalent to + rights = ChannelBannedRights( + until_date=datetime.now() + timedelta(days=7), + send_media=True, + send_stickers=True, + send_gifs=True, + send_games=True, + send_inline=True, + embed_links=True + ) + + client(EditBannedRequest(channel, user, rights)) + + +Kicking a member +**************** + +Telegram doesn't actually have a request to kick an user from a group. +Instead, you need to restrict them so they can't see messages. Any date +is enough: + + .. code-block:: python + + from telethon.tl.functions.channels import EditBannedRequest + from telethon.tl.types import ChannelBannedRights + + client(EditBannedRequest(channel, user, ChannelBannedRights( + until_date=None, + view_messages=True + ))) + __ https://lonamiwebs.github.io/Telethon/methods/channels/edit_admin.html __ https://github.com/Kyle2142 __ https://github.com/LonamiWebs/Telethon/issues/490 __ https://lonamiwebs.github.io/Telethon/constructors/channel_admin_rights.html +__ https://lonamiwebs.github.io/Telethon/methods/channels/edit_banned.html +__ https://lonamiwebs.github.io/Telethon/constructors/channel_banned_rights.html Increasing View Count in a Channel diff --git a/readthedocs/extra/examples/projects-using-telethon.rst b/readthedocs/extra/examples/projects-using-telethon.rst new file mode 100644 index 00000000..d688360c --- /dev/null +++ b/readthedocs/extra/examples/projects-using-telethon.rst @@ -0,0 +1,41 @@ +======================= +Projects using Telethon +======================= + +This page lists some real world examples showcasing what can be built with +the library. + +.. note:: + + Do you have a project that uses the library or know of any that's not + listed here? Feel free to leave a comment at + `issue 744 `_ + so it can be included in the next revision of the documentation! + + +telegram-export +*************** + +`Link `_ / +`Author's website `_ + +A tool to download Telegram data (users, chats, messages, and media) +into a database (and display the saved data). + + +mautrix-telegram +**************** + +`Link `_ / +`Author's website `_ + +A Matrix-Telegram hybrid puppeting/relaybot bridge. + + +TelegramTUI +*********** + +`Link `_ / +`Author's website `_ + +A Telegram client on your terminal. diff --git a/readthedocs/index.rst b/readthedocs/index.rst index a3982d86..89d2c510 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -62,6 +62,7 @@ heavy job for you, so you can focus on developing an application. extra/examples/working-with-messages extra/examples/chats-and-channels extra/examples/bots + extra/examples/projects-using-telethon .. _Troubleshooting: diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 22ad6074..631adfb8 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -712,6 +712,13 @@ class TelegramClient(TelegramBareClient): """ Sends the given message to the specified entity (user/chat/channel). + The default parse mode is the same as the official applications + (a custom flavour of markdown). ``**bold**, `code` or __italic__`` + are available. In addition you can send ``[links](https://example.com)`` + and ``[mentions](@username)`` (or using IDs like in the Bot API: + ``[mention](tg://user?id=123456789)``) and ``pre`` blocks with three + backticks. + Args: entity (`entity`): To who will it be sent. From 0cd44b245c672776b6870e9f7117d493995f60d3 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 6 Apr 2018 19:11:31 +0200 Subject: [PATCH 05/11] Allow auto-casting custom.Dialog into input_entity --- telethon/telegram_client.py | 5 +++-- telethon/utils.py | 7 +++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 631adfb8..3df26e79 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -2463,9 +2463,10 @@ class TelegramClient(TelegramBareClient): original_peer = peer if not isinstance(peer, int): try: - if peer.SUBCLASS_OF_ID != 0x2d45687: # crc32(b'Peer') + if getattr(peer, 'SUBCLASS_OF_ID', 0) != 0x2d45687: + # 0x2d45687 == crc32(b'Peer') return utils.get_input_peer(peer) - except (AttributeError, TypeError): + except TypeError: peer = None if not peer: diff --git a/telethon/utils.py b/telethon/utils.py index 2427ce13..ef1e901d 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -91,7 +91,11 @@ def get_input_peer(entity, allow_self=True): if entity.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') return entity except AttributeError: - _raise_cast_fail(entity, 'InputPeer') + if hasattr(entity, 'input_entity'): + # e.g. custom.Dialog (can't cyclic import) + return entity.input_entity + else: + _raise_cast_fail(entity, 'InputPeer') if isinstance(entity, User): if entity.is_self and allow_self: @@ -105,7 +109,6 @@ def get_input_peer(entity, allow_self=True): if isinstance(entity, (Channel, ChannelForbidden)): return InputPeerChannel(entity.id, entity.access_hash or 0) - # Less common cases if isinstance(entity, InputUser): return InputPeerUser(entity.user_id, entity.access_hash) From 3b2d065d358c6d7a76b7aa62f5b437e30a241c6f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 6 Apr 2018 19:21:02 +0200 Subject: [PATCH 06/11] Simplify .get_input_entity code flow (since 591e34b) --- telethon/telegram_client.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 3df26e79..ab5260d2 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -14,6 +14,7 @@ from io import BytesIO from mimetypes import guess_type from .crypto import CdnDecrypter +from .tl import TLObject from .tl.custom import InputSizedFile from .tl.functions.upload import ( SaveBigFilePartRequest, SaveFilePartRequest, GetFileRequest @@ -38,8 +39,7 @@ from .errors import ( RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError, SessionPasswordNeededError, FileMigrateError, PhoneNumberUnoccupiedError, - PhoneNumberOccupiedError, EmailUnconfirmedError, PasswordEmptyError, - UsernameNotOccupiedError + PhoneNumberOccupiedError, UsernameNotOccupiedError ) from .network import ConnectionMode from .tl.custom import Draft, Dialog @@ -2460,19 +2460,12 @@ class TelegramClient(TelegramBareClient): if isinstance(peer, str): return utils.get_input_peer(self._get_entity_from_string(peer)) - original_peer = peer - if not isinstance(peer, int): - try: - if getattr(peer, 'SUBCLASS_OF_ID', 0) != 0x2d45687: - # 0x2d45687 == crc32(b'Peer') - return utils.get_input_peer(peer) - except TypeError: - peer = None - - if not peer: - raise TypeError( - 'Cannot turn "{}" into an input entity.'.format(original_peer) - ) + if not isinstance(peer, int) and (not isinstance(peer, TLObject) + or peer.SUBCLASS_OF_ID != 0x2d45687): + # Try casting the object into an input peer. Might TypeError. + # Don't do it if a not-found ID was given (instead ValueError). + # Also ignore Peer (0x2d45687 == crc32(b'Peer'))'s, lacking hash. + return utils.get_input_peer(peer) raise ValueError( 'Could not find the input entity corresponding to "{}". ' From e69c1867828eb54d51e2d484ef772400833b1255 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 7 Apr 2018 12:31:30 +0200 Subject: [PATCH 07/11] Support more filter types for convenience (#745) --- telethon/telegram_client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index ab5260d2..736b9297 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -84,7 +84,8 @@ from .tl.types import ( InputMessageEntityMentionName, DocumentAttributeVideo, UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates, MessageMediaWebPage, ChannelParticipantsSearch, PhotoSize, PhotoCachedSize, - PhotoSizeEmpty, MessageService, ChatParticipants + PhotoSizeEmpty, MessageService, ChatParticipants, + ChannelParticipantsBanned, ChannelParticipantsKicked ) from .tl.types.messages import DialogsSlice from .tl.types.account import PasswordInputSettings, NoPassword @@ -1211,7 +1212,12 @@ class TelegramClient(TelegramBareClient): or :tl:`ChatParticipants` for normal chats. """ if isinstance(filter, type): - filter = filter() + if filter in (ChannelParticipantsBanned, ChannelParticipantsKicked, + ChannelParticipantsSearch): + # These require a `q` parameter (support types for convenience) + filter = filter('') + else: + filter = filter() entity = self.get_input_entity(entity) if search and (filter or not isinstance(entity, InputPeerChannel)): From 5fd615516872d56c55dc15210274055d487013d2 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 8 Apr 2018 11:22:59 +0200 Subject: [PATCH 08/11] Fix remove_event_handler's loop --- telethon/telegram_client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 736b9297..d84ca57d 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -2276,8 +2276,10 @@ class TelegramClient(TelegramBareClient): if event and not isinstance(event, type): event = type(event) - for i, ec in enumerate(self._event_builders): - ev, cb = ec + i = len(self._event_builders) + while i: + i -= 1 + ev, cb = self._event_builders[i] if cb == callback and (not event or isinstance(ev, event)): del self._event_builders[i] found += 1 From 8e01946957201b976ad4a379a7c584866d416fb5 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 8 Apr 2018 11:47:18 +0200 Subject: [PATCH 09/11] Fix .start() failing on some terminals --- telethon/telegram_client.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index d84ca57d..e619ee1f 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -356,7 +356,14 @@ class TelegramClient(TelegramBareClient): me = self.sign_in(phone=phone, password=password) # We won't reach here if any step failed (exit by exception) - print('Signed in successfully as', utils.get_display_name(me)) + signed, name = 'Signed in successfully as', utils.get_display_name(me) + try: + print(signed, name) + except UnicodeEncodeError: + # Some terminals don't support certain characters + print(signed, name.encode('utf-8', errors='ignore') + .decode('ascii', errors='ignore')) + self._check_events_pending_resolve() return self From 259bb6ace129cc3e46c8ca333340c992c785f497 Mon Sep 17 00:00:00 2001 From: Jeff Date: Sun, 8 Apr 2018 20:15:26 +0800 Subject: [PATCH 10/11] Several documentation fixes/additions (#750) --- README.rst | 2 +- readthedocs/extra/examples/bots.rst | 9 ++-- .../extra/examples/chats-and-channels.rst | 51 ++++++------------- .../extra/examples/working-with-messages.rst | 7 +-- telethon/events/chataction.py | 3 ++ 5 files changed, 24 insertions(+), 48 deletions(-) diff --git a/README.rst b/README.rst index a2e0d3de..6dee1cdd 100755 --- a/README.rst +++ b/README.rst @@ -52,7 +52,7 @@ Doing stuff client.send_file('username', '/home/myself/Pictures/holidays.jpg') client.download_profile_photo('me') - messages = client.get_message_history('username') + messages = client.get_messages('username') client.download_media(messages[0]) diff --git a/readthedocs/extra/examples/bots.rst b/readthedocs/extra/examples/bots.rst index fd4d54de..536d239a 100644 --- a/readthedocs/extra/examples/bots.rst +++ b/readthedocs/extra/examples/bots.rst @@ -13,7 +13,7 @@ Talking to Inline Bots You can query an inline bot, such as `@VoteBot`__ (note, *query*, not *interact* with a voting message), by making use of the -`GetInlineBotResultsRequest`__ request: +:tl:`GetInlineBotResultsRequest` request: .. code-block:: python @@ -24,7 +24,7 @@ not *interact* with a voting message), by making use of the )) And you can select any of their results by using -`SendInlineBotResultRequest`__: +:tl:`SendInlineBotResultRequest`: .. code-block:: python @@ -41,7 +41,7 @@ Talking to Bots with special reply markup ***************************************** To interact with a message that has a special reply markup, such as -`@VoteBot`__ polls, you would use `GetBotCallbackAnswerRequest`__: +`@VoteBot`__ polls, you would use :tl:`GetBotCallbackAnswerRequest`: .. code-block:: python @@ -58,7 +58,4 @@ show it visually (button rows, and buttons within each row, each with its own data). __ https://t.me/vote -__ https://lonamiwebs.github.io/Telethon/methods/messages/get_inline_bot_results.html -__ https://lonamiwebs.github.io/Telethon/methods/messages/send_inline_bot_result.html -__ https://lonamiwebs.github.io/Telethon/methods/messages/get_bot_callback_answer.html __ https://t.me/vote diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst index 4465bb70..f59277a7 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -20,7 +20,7 @@ Joining a public channel ************************ Once you have the :ref:`entity ` of the channel you want to join -to, you can make use of the `JoinChannelRequest`__ to join such channel: +to, you can make use of the :tl:`JoinChannelRequest` to join such channel: .. code-block:: python @@ -35,7 +35,6 @@ to, you can make use of the `JoinChannelRequest`__ to join such channel: For more on channels, check the `channels namespace`__. -__ https://lonamiwebs.github.io/Telethon/methods/channels/join_channel.html __ https://lonamiwebs.github.io/Telethon/methods/channels/index.html @@ -47,7 +46,7 @@ If all you have is a link like this one: enough information to join! The part after the ``https://t.me/joinchat/``, this is, ``AAAAAFFszQPyPEZ7wgxLtd`` on this example, is the ``hash`` of the chat or channel. Now you can use -`ImportChatInviteRequest`__ as follows: +:tl:`ImportChatInviteRequest` as follows: .. code-block:: python @@ -55,15 +54,12 @@ example, is the ``hash`` of the chat or channel. Now you can use updates = client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg')) -__ https://lonamiwebs.github.io/Telethon/methods/messages/import_chat_invite.html - - Adding someone else to such chat or channel ******************************************* If you don't want to add yourself, maybe because you're already in, -you can always add someone else with the `AddChatUserRequest`__, which -use is very straightforward, or `InviteToChannelRequest`__ for channels: +you can always add someone else with the :tl:`AddChatUserRequest`, which +use is very straightforward, or :tl:`InviteToChannelRequest` for channels: .. code-block:: python @@ -87,21 +83,14 @@ use is very straightforward, or `InviteToChannelRequest`__ for channels: )) -__ https://lonamiwebs.github.io/Telethon/methods/messages/add_chat_user.html -__ https://lonamiwebs.github.io/Telethon/methods/channels/invite_to_channel.html - - Checking a link without joining ******************************* If you don't need to join but rather check whether it's a group or a -channel, you can use the `CheckChatInviteRequest`__, which takes in +channel, you can use the :tl:`CheckChatInviteRequest`, which takes in the hash of said channel or group. -__ https://lonamiwebs.github.io/Telethon/methods/messages/check_chat_invite.html - - Retrieving all chat members (channels too) ****************************************** @@ -113,11 +102,11 @@ Most of the time you will just need ``client.get_participants(entity)``. This is what said method is doing behind the scenes as an example. In order to get all the members from a mega-group or channel, you need -to use `GetParticipantsRequest`__. As we can see it needs an -`InputChannel`__, (passing the mega-group or channel you're going to -use will work), and a mandatory `ChannelParticipantsFilter`__. The +to use :tl:`GetParticipantsRequest`. As we can see it needs an +:tl:`InputChannel`, (passing the mega-group or channel you're going to +use will work), and a mandatory :tl:`ChannelParticipantsFilter`. The closest thing to "no filter" is to simply use -`ChannelParticipantsSearch`__ with an empty ``'q'`` string. +:tl:`ChannelParticipantsSearch` with an empty ``'q'`` string. If we want to get *all* the members, we need to use a moving offset and a fixed limit: @@ -151,34 +140,28 @@ a fixed limit: Refer to `issue 573`__ for more on this. -Note that ``GetParticipantsRequest`` returns `ChannelParticipants`__, +Note that :tl:`GetParticipantsRequest` returns :tl:`ChannelParticipants`, which may have more information you need (like the role of the participants, total count of members, etc.) -__ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html -__ https://lonamiwebs.github.io/Telethon/types/input_channel.html -__ https://lonamiwebs.github.io/Telethon/types/channel_participants_filter.html -__ https://lonamiwebs.github.io/Telethon/constructors/channel_participants_search.html __ https://github.com/LonamiWebs/Telethon/issues/573 -__ https://lonamiwebs.github.io/Telethon/constructors/channels/channel_participants.html Recent Actions ************** "Recent actions" is simply the name official applications have given to -the "admin log". Simply use `GetAdminLogRequest`__ for that, and +the "admin log". Simply use :tl:`GetAdminLogRequest` for that, and you'll get AdminLogResults.events in return which in turn has the final `.action`__. -__ https://lonamiwebs.github.io/Telethon/methods/channels/get_admin_log.html __ https://lonamiwebs.github.io/Telethon/types/channel_admin_log_event_action.html Admin Permissions ***************** -Giving or revoking admin permissions can be done with the `EditAdminRequest`__: +Giving or revoking admin permissions can be done with the :tl:`EditAdminRequest`: .. code-block:: python @@ -231,8 +214,8 @@ Restricting Users ***************** Similar to how you give or revoke admin permissions, you can edit the -banned rights of an user through `EditAdminRequest`__ and its parameter -`ChannelBannedRights`__: +banned rights of an user through :tl:`EditAdminRequest` and its parameter +:tl:`ChannelBannedRights`: .. code-block:: python @@ -289,12 +272,9 @@ is enough: ))) -__ https://lonamiwebs.github.io/Telethon/methods/channels/edit_admin.html __ https://github.com/Kyle2142 __ https://github.com/LonamiWebs/Telethon/issues/490 __ https://lonamiwebs.github.io/Telethon/constructors/channel_admin_rights.html -__ https://lonamiwebs.github.io/Telethon/methods/channels/edit_banned.html -__ https://lonamiwebs.github.io/Telethon/constructors/channel_banned_rights.html Increasing View Count in a Channel @@ -302,7 +282,7 @@ Increasing View Count in a Channel It has been asked `quite`__ `a few`__ `times`__ (really, `many`__), and while I don't understand why so many people ask this, the solution is to -use `GetMessagesViewsRequest`__, setting ``increment=True``: +use :tl:`GetMessagesViewsRequest`, setting ``increment=True``: .. code-block:: python @@ -326,4 +306,3 @@ __ https://github.com/LonamiWebs/Telethon/issues/233 __ https://github.com/LonamiWebs/Telethon/issues/305 __ https://github.com/LonamiWebs/Telethon/issues/409 __ https://github.com/LonamiWebs/Telethon/issues/447 -__ https://lonamiwebs.github.io/Telethon/methods/messages/get_messages_views.html diff --git a/readthedocs/extra/examples/working-with-messages.rst b/readthedocs/extra/examples/working-with-messages.rst index 3db1aed0..36abdf0d 100644 --- a/readthedocs/extra/examples/working-with-messages.rst +++ b/readthedocs/extra/examples/working-with-messages.rst @@ -51,7 +51,7 @@ too, if that's all you have. Searching Messages ******************* -Messages are searched through the obvious SearchRequest_, but you may run +Messages are searched through the obvious :tl:`SearchRequest`, but you may run into issues_. A valid example would be: .. code-block:: python @@ -75,7 +75,7 @@ into issues_. A valid example would be: )) It's important to note that the optional parameter ``from_id`` could have -been omitted (defaulting to ``None``). Changing it to InputUserEmpty_, as one +been omitted (defaulting to ``None``). Changing it to :tl:`InputUserEmpty`, as one could think to specify "no user", won't work because this parameter is a flag, and it being unspecified has a different meaning. @@ -128,7 +128,4 @@ send yourself the very first sticker you have: )) -.. _ForwardMessagesRequest: https://lonamiwebs.github.io/Telethon/methods/messages/forward_messages.html -.. _SearchRequest: https://lonamiwebs.github.io/Telethon/methods/messages/search.html .. _issues: https://github.com/LonamiWebs/Telethon/issues/215 -.. _InputUserEmpty: https://lonamiwebs.github.io/Telethon/constructors/input_user_empty.html diff --git a/telethon/events/chataction.py b/telethon/events/chataction.py index 1a67394b..9f65a920 100644 --- a/telethon/events/chataction.py +++ b/telethon/events/chataction.py @@ -83,6 +83,9 @@ class ChatAction(EventBuilder): Represents the event of a new chat action. Members: + action_message (`MessageAction `_): + The message invoked by this Chat Action. + new_pin (`bool`): ``True`` if there is a new pin. From 93b5909be593df3a9ce4e7358bff5a6a8b2073e9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 8 Apr 2018 14:22:11 +0200 Subject: [PATCH 11/11] Add chat_id-like convenience properties to the events --- telethon/events/chataction.py | 18 +++++++++++++++++- telethon/events/common.py | 8 ++++++++ telethon/events/newmessage.py | 8 ++++++++ telethon/events/userupdate.py | 10 ++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/telethon/events/chataction.py b/telethon/events/chataction.py index 9f65a920..6649957f 100644 --- a/telethon/events/chataction.py +++ b/telethon/events/chataction.py @@ -251,7 +251,7 @@ class ChatAction(EventBuilder): @property def user(self): """ - The single user that takes part in this action (e.g. joined). + The first user that takes part in this action (e.g. joined). Might be ``None`` if the information can't be retrieved or there is no user taking part. @@ -267,6 +267,14 @@ class ChatAction(EventBuilder): if self.input_users: return self._input_users[0] + @property + def user_id(self): + """ + Returns the marked signed ID of the first user, if any. + """ + if self.input_users: + return utils.get_peer_id(self._input_users[0]) + @property def users(self): """ @@ -311,3 +319,11 @@ class ChatAction(EventBuilder): except (TypeError, ValueError): pass return self._input_users + + @property + def user_ids(self): + """ + Returns the marked signed ID of the users, if any. + """ + if self.input_users: + return [utils.get_peer_id(u) for u in self._input_users] diff --git a/telethon/events/common.py b/telethon/events/common.py index 8c475973..057be9f4 100644 --- a/telethon/events/common.py +++ b/telethon/events/common.py @@ -192,6 +192,14 @@ class EventCommon(abc.ABC): return self._chat + @property + def chat_id(self): + """ + Returns the marked integer ID of the chat, if any. + """ + if self._chat_peer: + return utils.get_peer_id(self._chat_peer) + def __str__(self): return TLObject.pretty_format(self.to_dict()) diff --git a/telethon/events/newmessage.py b/telethon/events/newmessage.py index 5c21bb36..d443909a 100644 --- a/telethon/events/newmessage.py +++ b/telethon/events/newmessage.py @@ -258,6 +258,14 @@ class NewMessage(EventBuilder): return self._sender + @property + def sender_id(self): + """ + Returns the marked sender integer ID, if present. + """ + if self.input_sender: + return utils.get_peer_id(self._input_sender) + @property def text(self): """ diff --git a/telethon/events/userupdate.py b/telethon/events/userupdate.py index 7f87cde5..4bd6b446 100644 --- a/telethon/events/userupdate.py +++ b/telethon/events/userupdate.py @@ -151,3 +151,13 @@ class UserUpdate(EventBuilder): def user(self): """Alias around the chat (conversation).""" return self.chat + + @property + def input_user(self): + """Alias around the input chat.""" + return self.input_chat + + @property + def user_id(self): + """Alias around `chat_id`.""" + return self.chat_id