From 657c771fa0134ee9662f86bbf6dbcdec7b2ef222 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 12 Mar 2018 09:52:16 +0100 Subject: [PATCH 01/24] Fix incorrect participant count on some channels --- telethon/telegram_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 2c5bbaf2..68f5e810 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1233,7 +1233,7 @@ class TelegramClient(TelegramBareClient): if not participants.users: requests.pop(i) else: - requests[i].offset += len(participants.users) + requests[i].offset += len(participants.participants) for user in participants.users: if user.id not in seen: seen.add(user.id) From 751461f0f559458e0f8c69692c66e76523e197a9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 12 Mar 2018 10:27:49 +0100 Subject: [PATCH 02/24] Modify iter_participants to also include .participant info --- telethon/telegram_client.py | 38 +++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 68f5e810..bd83f938 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1154,7 +1154,7 @@ class TelegramClient(TelegramBareClient): def iter_participants(self, entity, limit=None, search='', aggressive=False, _total_box=None): """ - Gets the list of participants from the specified entity. + Iterator over the participants belonging to the specified chat. Args: entity (:obj:`entity`): @@ -1179,9 +1179,11 @@ class TelegramClient(TelegramBareClient): _total_box (:obj:`_Box`, optional): A _Box instance to pass the total parameter by reference. - Returns: - A list of participants with an additional .total variable on the - list indicating the total amount of members in this group/channel. + Yields: + The ``User`` objects returned by ``GetParticipantsRequest`` + with an additional ``.participant`` attribute which is the + matched ``ChannelParticipant`` type for channels/megagroups + or ``ChatParticipants`` for normal chats. """ entity = self.get_input_entity(entity) limit = float('inf') if limit is None else int(limit) @@ -1234,30 +1236,38 @@ class TelegramClient(TelegramBareClient): requests.pop(i) else: requests[i].offset += len(participants.participants) - for user in participants.users: - if user.id not in seen: - seen.add(user.id) + users = {user.id: user for user in participants.users} + for participant in participants.participants: + if participant.user_id not in seen: + seen.add(participant.user_id) + user = users[participant.user_id] + user.participant = participant yield user if len(seen) >= limit: return elif isinstance(entity, InputPeerChat): - users = self(GetFullChatRequest(entity.chat_id)).users + full = self(GetFullChatRequest(entity.chat_id)) if _total_box: - _total_box.x = len(users) + _total_box.x = len(full.full_chat.participants.participants) have = 0 - for user in users: + users = {user.id: user for user in full.users} + for participant in full.full_chat.participants.participants: have += 1 if have > limit: break else: + user = users[participant.user_id] + user.participant = participant yield user else: if _total_box: _total_box.x = 1 if limit != 0: - yield self.get_entity(entity) + user = self.get_entity(entity) + user.participant = None + yield user def get_participants(self, *args, **kwargs): """ @@ -1266,9 +1276,9 @@ class TelegramClient(TelegramBareClient): """ total_box = _Box(0) kwargs['_total_box'] = total_box - dialogs = UserList(self.iter_participants(*args, **kwargs)) - dialogs.total = total_box.x - return dialogs + participants = UserList(self.iter_participants(*args, **kwargs)) + participants.total = total_box.x + return participants # endregion From 935de0afbb480b366d650f59217507824b5b2cd4 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 12 Mar 2018 11:58:56 +0200 Subject: [PATCH 03/24] Add Python type hints to attributes of TL types (#678) --- requirements.txt | 1 + setup.py | 5 +- telethon_generator/parser/tl_object.py | 23 +++++++- telethon_generator/tl_generator.py | 76 +++++++++++++++++++++++++- 4 files changed, 99 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2b650ec4..45e8c141 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ pyaes rsa +typing diff --git a/setup.py b/setup.py index 143ca0cb..05ca9197 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ Extra supported commands are: # To use a consistent encoding from codecs import open -from sys import argv +from sys import argv, version_info import os import re @@ -153,7 +153,8 @@ def main(): 'telethon_generator/parser/tl_object.py', 'telethon_generator/parser/tl_parser.py', ]), - install_requires=['pyaes', 'rsa'], + install_requires=['pyaes', 'rsa', + 'typing' if version_info < (3, 5) else ""], extras_require={ 'cryptg': ['cryptg'], 'sqlalchemy': ['sqlalchemy'] diff --git a/telethon_generator/parser/tl_object.py b/telethon_generator/parser/tl_object.py index 034cb3c3..0e0045d7 100644 --- a/telethon_generator/parser/tl_object.py +++ b/telethon_generator/parser/tl_object.py @@ -254,7 +254,7 @@ class TLArg: self.generic_definition = generic_definition - def type_hint(self): + def doc_type_hint(self): result = { 'int': 'int', 'long': 'int', @@ -272,6 +272,27 @@ class TLArg: return result + def python_type_hint(self): + type = self.type + if '.' in type: + type = type.split('.')[1] + result = { + 'int': 'int', + 'long': 'int', + 'int128': 'int', + 'int256': 'int', + 'string': 'str', + 'date': 'Optional[datetime]', # None date = 0 timestamp + 'bytes': 'bytes', + 'true': 'bool', + }.get(type, "Type{}".format(type)) + if self.is_vector: + result = 'List[{}]'.format(result) + if self.is_flag and type != 'date': + result = 'Optional[{}]'.format(result) + + return result + def __str__(self): # Find the real type representation by updating it as required real_type = self.type diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index ff12acfe..7c1f6237 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -138,6 +138,7 @@ class TLGenerator: builder.writeln( 'from {}.tl.tlobject import TLObject'.format('.' * depth) ) + builder.writeln('from typing import Optional, List, Union, TYPE_CHECKING') # Add the relative imports to the namespaces, # unless we already are in a namespace. @@ -154,13 +155,81 @@ class TLGenerator: # Import struct for the .__bytes__(self) serialization builder.writeln('import struct') + tlobjects.sort(key=lambda x: x.name) + + type_names = set() + type_defs = [] + + # Find all the types in this file and generate type definitions + # based on the types. The type definitions are written to the + # file at the end. + for t in tlobjects: + if not t.is_function: + type_name = t.result + if '.' in type_name: + type_name = type_name[type_name.rindex('.'):] + if type_name in type_names: + continue + type_names.add(type_name) + constructors = type_constructors[type_name] + if not constructors: + pass + elif len(constructors) == 1: + type_defs.append('Type{} = {}'.format( + type_name, constructors[0].class_name())) + else: + type_defs.append('Type{} = Union[{}]'.format( + type_name, ','.join(c.class_name() + for c in constructors))) + + imports = {} + primitives = ('int', 'long', 'int128', 'int256', 'string', + 'date', 'bytes', 'true') + # Find all the types in other files that are used in this file + # and generate the information required to import those types. + for t in tlobjects: + for arg in t.args: + name = arg.type + if not name or name in primitives: + continue + + import_space = '{}.tl.types'.format('.' * depth) + if '.' in name: + namespace = name.split('.')[0] + name = name.split('.')[1] + import_space += '.{}'.format(namespace) + + if name not in type_names: + type_names.add(name) + if name == 'date': + imports['datetime'] = ['datetime'] + continue + elif not import_space in imports: + imports[import_space] = set() + imports[import_space].add('Type{}'.format(name)) + + # Add imports required for type checking. + builder.writeln('if TYPE_CHECKING:') + for namespace, names in imports.items(): + builder.writeln('from {} import {}'.format( + namespace, ', '.join(names))) + else: + builder.writeln('pass') + builder.end_block() + # Generate the class for every TLObject - for t in sorted(tlobjects, key=lambda x: x.name): + for t in tlobjects: TLGenerator._write_source_code( t, builder, depth, type_constructors ) builder.current_indent = 0 + # Write the type definitions generated earlier. + builder.writeln('') + for line in type_defs: + builder.writeln(line) + + @staticmethod def _write_source_code(tlobject, builder, depth, type_constructors): """Writes the source code corresponding to the given TLObject @@ -218,7 +287,7 @@ class TLGenerator: for arg in args: if not arg.flag_indicator: builder.writeln(':param {} {}:'.format( - arg.type_hint(), arg.name + arg.doc_type_hint(), arg.name )) builder.current_indent -= 1 # It will auto-indent (':') @@ -258,7 +327,8 @@ class TLGenerator: for arg in args: if not arg.can_be_inferred: - builder.writeln('self.{0} = {0}'.format(arg.name)) + builder.writeln('self.{0} = {0} # type: {1}'.format( + arg.name, arg.python_type_hint())) continue # Currently the only argument that can be From a13433653674ffb34f8fc74d01ad202dcb6bed76 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 13 Mar 2018 12:43:59 +0100 Subject: [PATCH 04/24] Fix ResolveUsernameRequest may return ChannelForbidden --- telethon/telegram_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index bd83f938..c022cbbb 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -2312,7 +2312,8 @@ class TelegramClient(TelegramBareClient): return self.get_me() result = self(ResolveUsernameRequest(username)) for entity in itertools.chain(result.users, result.chats): - if entity.username.lower() == username: + if getattr(entity, 'username', None) or ''\ + .lower() == username: return entity try: # Nobody with this username, maybe it's an exact name/title From fd309f0407b7244cfc5390f8678d4f9ec3f9455d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 13 Mar 2018 13:15:02 +0100 Subject: [PATCH 05/24] Add filter parameter to iter_participants and fix search for chats --- telethon/telegram_client.py | 54 ++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index c022cbbb..02bf918e 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1152,7 +1152,7 @@ class TelegramClient(TelegramBareClient): raise TypeError('Invalid message type: {}'.format(type(message))) def iter_participants(self, entity, limit=None, search='', - aggressive=False, _total_box=None): + filter=None, aggressive=False, _total_box=None): """ Iterator over the participants belonging to the specified chat. @@ -1166,6 +1166,12 @@ class TelegramClient(TelegramBareClient): search (:obj:`str`, optional): Look for participants with this string in name/username. + filter (:obj:`ChannelParticipantsFilter`, optional): + The filter to be used, if you want e.g. only admins. See + https://lonamiwebs.github.io/Telethon/types/channel_participants_filter.html. + Note that you might not have permissions for some filter. + This has no effect for normal chats or users. + aggressive (:obj:`bool`, optional): Aggressively looks for all participants in the chat in order to get more than 10,000 members (a hard limit @@ -1174,7 +1180,7 @@ class TelegramClient(TelegramBareClient): participants on groups with 100,000 members. This has no effect for groups or channels with less than - 10,000 members. + 10,000 members, or if a ``filter`` is given. _total_box (:obj:`_Box`, optional): A _Box instance to pass the total parameter by reference. @@ -1185,7 +1191,21 @@ class TelegramClient(TelegramBareClient): matched ``ChannelParticipant`` type for channels/megagroups or ``ChatParticipants`` for normal chats. """ + if isinstance(filter, type): + filter = filter() + entity = self.get_input_entity(entity) + if search and (filter or not isinstance(entity, InputPeerChannel)): + # We need to 'search' ourselves unless we have a PeerChannel + search = search.lower() + + def filter_entity(ent): + return search in utils.get_display_name(ent).lower() or\ + search in (getattr(ent, 'username', '') or None).lower() + else: + def filter_entity(ent): + return True + limit = float('inf') if limit is None else int(limit) if isinstance(entity, InputPeerChannel): total = self(GetFullChannelRequest( @@ -1198,7 +1218,7 @@ class TelegramClient(TelegramBareClient): return seen = set() - if total > 10000 and aggressive: + if total > 10000 and aggressive and not filter: requests = [GetParticipantsRequest( channel=entity, filter=ChannelParticipantsSearch(search + chr(x)), @@ -1209,7 +1229,7 @@ class TelegramClient(TelegramBareClient): else: requests = [GetParticipantsRequest( channel=entity, - filter=ChannelParticipantsSearch(search), + filter=filter or ChannelParticipantsSearch(search), offset=0, limit=200, hash=0 @@ -1238,15 +1258,19 @@ class TelegramClient(TelegramBareClient): requests[i].offset += len(participants.participants) users = {user.id: user for user in participants.users} for participant in participants.participants: - if participant.user_id not in seen: - seen.add(participant.user_id) - user = users[participant.user_id] - user.participant = participant - yield user - if len(seen) >= limit: - return + user = users[participant.user_id] + if not filter_entity(user) or user.id in seen: + continue + + seen.add(participant.user_id) + user = users[participant.user_id] + user.participant = participant + yield user + if len(seen) >= limit: + return elif isinstance(entity, InputPeerChat): + # TODO We *could* apply the `filter` here ourselves full = self(GetFullChatRequest(entity.chat_id)) if _total_box: _total_box.x = len(full.full_chat.participants.participants) @@ -1254,6 +1278,9 @@ class TelegramClient(TelegramBareClient): have = 0 users = {user.id: user for user in full.users} for participant in full.full_chat.participants.participants: + user = users[participant.user_id] + if not filter_entity(user): + continue have += 1 if have > limit: break @@ -1266,8 +1293,9 @@ class TelegramClient(TelegramBareClient): _total_box.x = 1 if limit != 0: user = self.get_entity(entity) - user.participant = None - yield user + if filter_entity(user): + user.participant = None + yield user def get_participants(self, *args, **kwargs): """ From 7e9d19d727663336534d5d1e1ffe71ecf2a66bf9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 14 Mar 2018 10:28:21 +0100 Subject: [PATCH 06/24] Add known entities to all updates and use them in the events This should reduce the amount of API calls made when getting the full sender/chat on events (mostly on channels, where Telegram seems to always send Updates instead only a normal Update). --- telethon/events/__init__.py | 73 +++++++++++++++++++++++++++++-------- telethon/update_state.py | 15 ++++++-- 2 files changed, 70 insertions(+), 18 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index a4ec83b2..8047bb49 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -71,6 +71,7 @@ class _EventCommon(abc.ABC): """Intermediate class with common things to all events""" 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 @@ -104,6 +105,7 @@ class _EventCommon(abc.ABC): ) except RPCError: return + # TODO This could return a tuple to also have the full entity entity = { utils.get_peer_id(x): x for x in itertools.chain( getattr(result, 'chats', []), @@ -148,12 +150,19 @@ class _EventCommon(abc.ABC): def chat(self): """ The (:obj:`User` | :obj:`Chat` | :obj:`Channel`, optional) on which - the event occurred. This property will make an API call the first time - to get the most up to date version of the chat, so use with care as - there is no caching besides local caching yet. + 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 self._chat is None and self.input_chat: + 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 @@ -249,6 +258,7 @@ class NewMessage(_EventBuilder): return # Short-circuit if we let pass all events + event._entities = update.entities if all(x is None for x in (self.incoming, self.outgoing, self.chats, self.pattern)): return event @@ -300,8 +310,6 @@ class NewMessage(_EventBuilder): self.message = message self._text = None - self._input_chat = None - self._chat = None self._input_sender = None self._sender = None @@ -395,14 +403,22 @@ class NewMessage(_EventBuilder): @property def sender(self): """ - This (:obj:`User`) will make an API call the first time to get - the most up to date version of the sender, so use with care as - there is no caching besides local caching yet. + This (:obj:`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 self._sender is None and self.input_sender: + 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 @@ -621,6 +637,7 @@ class ChatAction(_EventBuilder): else: return + event._entities = update.entities return self._filter_event(event) class Event(_EventCommon): @@ -762,7 +779,12 @@ class ChatAction(_EventBuilder): 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._client.get_entity(self._added_by) + 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 @@ -771,7 +793,12 @@ class ChatAction(_EventBuilder): 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._client.get_entity(self._kicked_by) + 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 @@ -801,11 +828,24 @@ class ChatAction(_EventBuilder): Might be empty if the information can't be retrieved or there are no users taking part. """ - if self._users is None and self._user_peers: + 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: - self._users = self._client.get_entity(self._user_peers) + missing = self._client.get_entity(missing) except (TypeError, ValueError): - self._users = [] + missing = [] + + self._user_peers = have + missing return self._users @@ -837,6 +877,7 @@ class UserUpdate(_EventBuilder): else: return + event._entities = update.entities return self._filter_event(event) class Event(_EventCommon): @@ -984,6 +1025,7 @@ class MessageEdited(NewMessage): else: return + event._entities = update.entities return self._filter_event(event) @@ -1005,6 +1047,7 @@ class MessageDeleted(_EventBuilder): else: return + event._entities = update.entities return self._filter_event(event) class Event(_EventCommon): diff --git a/telethon/update_state.py b/telethon/update_state.py index 6a496603..171e9546 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -1,10 +1,10 @@ +import itertools import logging -import pickle -from collections import deque -from queue import Queue, Empty from datetime import datetime +from queue import Queue, Empty from threading import RLock, Thread +from . import utils from .tl import types as tl __log__ = logging.getLogger(__name__) @@ -127,14 +127,23 @@ class UpdateState: # After running the script for over an hour and receiving over # 1000 updates, the only duplicates received were users going # online or offline. We can trust the server until new reports. + # + # TODO Note somewhere that all updates are modified to include + # .entities, which is a dictionary you can access but may be empty. + # This should only be used as read-only. if isinstance(update, tl.UpdateShort): + update.update.entities = {} self._updates.put(update.update) # Expand "Updates" into "Update", and pass these to callbacks. # Since .users and .chats have already been processed, we # don't need to care about those either. elif isinstance(update, (tl.Updates, tl.UpdatesCombined)): + entities = {utils.get_peer_id(x): x for x in + itertools.chain(update.users, update.chats)} for u in update.updates: + u.entities = entities self._updates.put(u) # TODO Handle "tl.UpdatesTooLong" else: + update.entities = {} self._updates.put(update) From 8ae12fbb704015f1ef11e9d73cabba7c800ce06f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 14 Mar 2018 10:32:59 +0100 Subject: [PATCH 07/24] Return the entire entity from the helper events._get_entity too --- telethon/events/__init__.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 8047bb49..36020afb 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -87,12 +87,15 @@ class _EventCommon(abc.ABC): ) self.is_channel = isinstance(chat_peer, types.PeerChannel) - def _get_input_entity(self, msg_id, entity_id, chat=None): + def _get_entity(self, msg_id, entity_id, chat=None): """ Helper function to call 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 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): @@ -104,15 +107,17 @@ class _EventCommon(abc.ABC): functions.messages.GetMessagesRequest([msg_id]) ) except RPCError: - return - # TODO This could return a tuple to also have the full entity + 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 utils.get_input_peer(entity) + return entity, utils.get_input_peer(entity) + else: + return None, None @property def input_chat(self): @@ -136,7 +141,7 @@ class _EventCommon(abc.ABC): # TODO For channels, getDifference? Maybe looking # in the dialogs (which is already done) is enough. if self._message_id is not None: - self._input_chat = self._get_input_entity( + self._chat, self._input_chat = self._get_entity( self._message_id, utils.get_peer_id(self._chat_peer) ) @@ -392,7 +397,7 @@ class NewMessage(_EventBuilder): ) except (ValueError, TypeError): # We can rely on self.input_chat for this - self._input_sender = self._get_input_entity( + self._sender, self._input_sender = self._get_entity( self.message.id, self.message.from_id, chat=self.input_chat From 81944262fb24d46b9604f7966cf3061977fa84a3 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 14 Mar 2018 19:38:36 +0100 Subject: [PATCH 08/24] Clear-up documentation by separating reference from examples --- readthedocs/extra/basic/entities.rst | 8 ++++ readthedocs/extra/basic/telegram-client.rst | 13 +++---- .../extra/basic/working-with-updates.rst | 19 +++++----- readthedocs/index.rst | 3 +- readthedocs/telethon.crypto.rst | 8 ---- readthedocs/telethon.events.rst | 7 +++- readthedocs/telethon.rst | 37 ++++++++++--------- 7 files changed, 51 insertions(+), 44 deletions(-) diff --git a/readthedocs/extra/basic/entities.rst b/readthedocs/extra/basic/entities.rst index 84be3250..ce7e569a 100644 --- a/readthedocs/extra/basic/entities.rst +++ b/readthedocs/extra/basic/entities.rst @@ -10,6 +10,14 @@ The library widely uses the concept of "entities". An entity will refer to any ``User``, ``Chat`` or ``Channel`` object that the API may return in response to certain methods, such as ``GetUsersRequest``. +.. note:: + + When something "entity-like" is required, it means that you need to + provide something that can be turned into an entity. These things include, + but are not limited to, usernames, exact titles, IDs, ``Peer`` objects, + or even entire ``User``, ``Chat`` and ``Channel`` objects and even phone + numbers from people you have in your contacts. + Getting entities **************** diff --git a/readthedocs/extra/basic/telegram-client.rst b/readthedocs/extra/basic/telegram-client.rst index d3375200..decb3765 100644 --- a/readthedocs/extra/basic/telegram-client.rst +++ b/readthedocs/extra/basic/telegram-client.rst @@ -8,6 +8,11 @@ TelegramClient Introduction ************ +.. note:: + + Check the :ref:`telethon-package` if you're looking for the methods + reference instead of this tutorial. + The ``TelegramClient`` is the central class of the library, the one you will be using most of the time. For this reason, it's important to know what it offers. @@ -86,13 +91,7 @@ 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`. .. _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 - - - -.. automodule:: telethon.telegram_client - :members: - :undoc-members: - :show-inheritance: diff --git a/readthedocs/extra/basic/working-with-updates.rst b/readthedocs/extra/basic/working-with-updates.rst index 3c57b792..105e11bd 100644 --- a/readthedocs/extra/basic/working-with-updates.rst +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -7,7 +7,9 @@ Working with Updates The library comes with the :mod:`events` module. *Events* are an abstraction over what Telegram calls `updates`__, and are meant to ease simple and common -usage when dealing with them, since there are many updates. Let's dive in! +usage when dealing with them, since there are many updates. If you're looking +for the method reference, check :ref:`telethon-events-package`, otherwise, +let's dive in! .. note:: @@ -114,12 +116,15 @@ for example: import random - @client.on(events.NewMessage(chats='TelethonOffTopic', incoming=True)) + # Either a single item or a list of them will work for the chats. + # You can also use the IDs, Peers, or even User/Chat/Channel objects. + @client.on(events.NewMessage(chats=('TelethonChat', 'TelethonOffTopic'))) def normal_handler(event): if 'roll' in event.raw_text: event.reply(str(random.randint(1, 6))) + # Similarly, you can use incoming=True for messages that you receive @client.on(events.NewMessage(chats='TelethonOffTopic', outgoing=True)) def admin_handler(event): if event.raw_text.startswith('eval'): @@ -162,14 +167,8 @@ propagation of the update through your handlers to stop: pass -Events module -************* - -.. automodule:: telethon.events - :members: - :undoc-members: - :show-inheritance: - +Remember to check :ref:`telethon-events-package` if you're looking for +the methods reference. __ https://lonamiwebs.github.io/Telethon/types/update.html diff --git a/readthedocs/index.rst b/readthedocs/index.rst index c1d2b6ec..a3982d86 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -15,7 +15,8 @@ or use the menu on the left. Remember to read the :ref:`changelog` when you upgrade! .. important:: - If you're new here, you want to read :ref:`getting-started`. + If you're new here, you want to read :ref:`getting-started`. If you're + looking for the method reference, you should check :ref:`telethon-package`. What is this? diff --git a/readthedocs/telethon.crypto.rst b/readthedocs/telethon.crypto.rst index 3c11416d..8adf55d5 100644 --- a/readthedocs/telethon.crypto.rst +++ b/readthedocs/telethon.crypto.rst @@ -42,14 +42,6 @@ telethon\.crypto\.factorization module :undoc-members: :show-inheritance: -telethon\.crypto\.libssl module -------------------------------- - -.. automodule:: telethon.crypto.libssl - :members: - :undoc-members: - :show-inheritance: - telethon\.crypto\.rsa module ---------------------------- diff --git a/readthedocs/telethon.events.rst b/readthedocs/telethon.events.rst index 071a39bf..37ce9f48 100644 --- a/readthedocs/telethon.events.rst +++ b/readthedocs/telethon.events.rst @@ -1,4 +1,9 @@ +.. _telethon-events-package: + telethon\.events package ======================== - +.. automodule:: telethon.events + :members: + :undoc-members: + :show-inheritance: diff --git a/readthedocs/telethon.rst b/readthedocs/telethon.rst index 96becc9b..0b60b007 100644 --- a/readthedocs/telethon.rst +++ b/readthedocs/telethon.rst @@ -1,11 +1,14 @@ +.. _telethon-package: + + telethon package ================ -telethon\.helpers module ------------------------- +telethon\.telegram\_client module +--------------------------------- -.. automodule:: telethon.helpers +.. automodule:: telethon.telegram_client :members: :undoc-members: :show-inheritance: @@ -18,10 +21,18 @@ telethon\.telegram\_bare\_client module :undoc-members: :show-inheritance: -telethon\.telegram\_client module ---------------------------------- +telethon\.utils module +---------------------- -.. automodule:: telethon.telegram_client +.. automodule:: telethon.utils + :members: + :undoc-members: + :show-inheritance: + +telethon\.helpers module +------------------------ + +.. automodule:: telethon.helpers :members: :undoc-members: :show-inheritance: @@ -42,18 +53,10 @@ telethon\.update\_state module :undoc-members: :show-inheritance: -telethon\.utils module ----------------------- +telethon\.sessions module +------------------------- -.. automodule:: telethon.utils - :members: - :undoc-members: - :show-inheritance: - -telethon\.session module ------------------------- - -.. automodule:: telethon.session +.. automodule:: telethon.sessions :members: :undoc-members: :show-inheritance: From 1ff5826c26c0da5cd9dcccc1874dbd5ed03f56eb Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 14 Mar 2018 21:01:00 +0100 Subject: [PATCH 09/24] Call .get_dialogs only once on entity not found --- telethon/telegram_client.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 02bf918e..c3f8ba72 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -192,6 +192,9 @@ class TelegramClient(TelegramBareClient): # Sometimes we need to know who we are, cache the self peer self._self_input_peer = None + # Don't call .get_dialogs() every time a .get_entity() fails + self._called_get_dialogs = False + # endregion # region Telegram requests functions @@ -2401,16 +2404,18 @@ class TelegramClient(TelegramBareClient): # Add the mark to the peers if the user passed a Peer (not an int), # or said ID is negative. If it's negative it's been marked already. # Look in the dialogs with the hope to find it. - mark = not isinstance(peer, int) or peer < 0 - target_id = utils.get_peer_id(peer) - if mark: - for dialog in self.iter_dialogs(): - if utils.get_peer_id(dialog.entity) == target_id: - return utils.get_input_peer(dialog.entity) - else: - for dialog in self.iter_dialogs(): - if dialog.entity.id == target_id: - return utils.get_input_peer(dialog.entity) + if not self._called_get_dialogs: + self._called_get_dialogs = True + mark = not isinstance(peer, int) or peer < 0 + target_id = utils.get_peer_id(peer) + if mark: + for dialog in self.get_dialogs(100): + if utils.get_peer_id(dialog.entity) == target_id: + return utils.get_input_peer(dialog.entity) + else: + for dialog in self.get_dialogs(100): + if dialog.entity.id == target_id: + return utils.get_input_peer(dialog.entity) raise TypeError( 'Could not find the input entity corresponding to "{}". ' From d5bc3c1a6cfbb03729cf2e7318e17d405fb85902 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 14 Mar 2018 21:05:50 +0100 Subject: [PATCH 10/24] Fix misleading documentation regarding report_errors --- readthedocs/extra/developing/api-status.rst | 8 +++----- telethon/telegram_bare_client.py | 1 - telethon/telegram_client.py | 5 ++--- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/readthedocs/extra/developing/api-status.rst b/readthedocs/extra/developing/api-status.rst index 492340a4..181ed3ae 100644 --- a/readthedocs/extra/developing/api-status.rst +++ b/readthedocs/extra/developing/api-status.rst @@ -10,11 +10,9 @@ anyone can query, made by `Daniil `__. All the information sent is a ``GET`` request with the error code, error message and method used. -If you still would like to opt out, simply set -``client.session.report_errors = False`` to disable this feature, or -pass ``report_errors=False`` as a named parameter when creating a -``TelegramClient`` instance. However Daniil would really thank you if -you helped him (and everyone) by keeping it on! +If you still would like to opt out, you can disable this feature by setting +``client.session.report_errors = False``. However Daniil would really thank +you if you helped him (and everyone) by keeping it on! Querying the API status *********************** diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 7164bb17..2ada33f5 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -73,7 +73,6 @@ class TelegramBareClient: update_workers=None, spawn_read_thread=False, timeout=timedelta(seconds=5), - loop=None, device_model=None, system_version=None, app_version=None, diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index c3f8ba72..642ecbf5 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -147,8 +147,8 @@ class TelegramClient(TelegramBareClient): if you want to run the library without any additional thread. Kwargs: - Extra parameters will be forwarded to the ``Session`` file. - Most relevant parameters are: + Some extra parameters are required when stabilishing the first + connection. These are are (along with their default values): .. code-block:: python @@ -157,7 +157,6 @@ class TelegramClient(TelegramBareClient): app_version = TelegramClient.__version__ lang_code = 'en' system_lang_code = lang_code - report_errors = True """ # region Initialization From 48869f0f4e5f2b4065f916f031ef6c6e8a7dbe73 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 14 Mar 2018 21:09:51 +0100 Subject: [PATCH 11/24] Fix MessageEdited ignoring NewMessage constructor arguments These include outgoing/incoming and pattern which are now handled. --- telethon/events/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 36020afb..4339faef 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -262,8 +262,11 @@ class NewMessage(_EventBuilder): else: return - # Short-circuit if we let pass all events 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 @@ -1031,7 +1034,7 @@ class MessageEdited(NewMessage): return event._entities = update.entities - return self._filter_event(event) + return self._message_filter_event(event) class MessageDeleted(_EventBuilder): From 423f0f366c955b9e20b8b41dd374986ee4647634 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Mar 2018 09:29:54 +0100 Subject: [PATCH 12/24] Update test servers documentation (#700) --- readthedocs/extra/developing/test-servers.rst | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/readthedocs/extra/developing/test-servers.rst b/readthedocs/extra/developing/test-servers.rst index 2ba66897..a3288a25 100644 --- a/readthedocs/extra/developing/test-servers.rst +++ b/readthedocs/extra/developing/test-servers.rst @@ -8,8 +8,7 @@ To run Telethon on a test server, use the following code: .. code-block:: python client = TelegramClient(None, api_id, api_hash) - client.session.server_address = '149.154.167.40' - client.connect() + client.session.set_dc(dc_id, '149.154.167.40', 80) You can check your ``'test ip'`` on https://my.telegram.org. @@ -17,16 +16,20 @@ You should set ``None`` session so to ensure you're generating a new authorization key for it (it would fail if you used a session where you had previously connected to another data center). -Once you're connected, you'll likely need to ``.sign_up()``. Remember -`anyone can access the phone you +Note that port 443 might not work, so you can try with 80 instead. + +Once you're connected, you'll likely be asked to either sign in or sign up. +Remember `anyone can access the phone you choose `__, -so don't store sensitive data here: +so don't store sensitive data here. + +Valid phone numbers are ``99966XYYYY``, where ``X`` is the ``dc_id`` and +``YYYY`` is any number you want, for example, ``1234`` in ``dc_id = 2`` would +be ``9996621234``. The code sent by Telegram will be ``dc_id`` repeated five +times, in this case, ``22222`` so we can hardcode that: .. code-block:: python - from random import randint - - dc_id = '2' # Change this to the DC id of the test server you chose - phone = '99966' + dc_id + str(randint(9999)).zfill(4) - client.send_code_request(phone) - client.sign_up(dc_id * 5, 'Some', 'Name') + client = TelegramClient(None, api_id, api_hash) + client.session.set_dc(2, '149.154.167.40', 80) + client.start(phone='9996621234', code_callback=lambda: '22222') From 45b7318f083450d0574f7a123735f7fd20342b25 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Mar 2018 09:52:45 +0100 Subject: [PATCH 13/24] Fix Telegram only recognises 3 image filetypes --- telethon/utils.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/telethon/utils.py b/telethon/utils.py index daf8c875..ddc920d3 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -4,10 +4,11 @@ to convert between an entity like an User, Chat, etc. into its Input version) """ import math import mimetypes +import os import re import types from collections import UserList -from mimetypes import add_type, guess_extension +from mimetypes import guess_extension from .tl.types import ( Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull, @@ -315,11 +316,13 @@ def get_input_media(media, is_photo=False): def is_image(file): - """Returns True if the file extension looks like an image file""" + """ + Returns True if the file extension looks like an image file to Telegram. + """ if not isinstance(file, str): return False - mime = mimetypes.guess_type(file)[0] or '' - return mime.startswith('image/') and not mime.endswith('/webp') + _, ext = os.path.splitext(file) + return re.match(r'\.(png|jpe?g|gif)', ext, re.IGNORECASE) def is_audio(file): From d7ef0f5e0908417b259b4716435cc397c489d0c7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Mar 2018 10:13:57 +0100 Subject: [PATCH 14/24] Stop sending gifs as images This is often not the case, most gifs are animated and when sent as images inside albums they lose the animation. --- telethon/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/utils.py b/telethon/utils.py index ddc920d3..286853ad 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -322,7 +322,7 @@ def is_image(file): if not isinstance(file, str): return False _, ext = os.path.splitext(file) - return re.match(r'\.(png|jpe?g|gif)', ext, re.IGNORECASE) + return re.match(r'\.(png|jpe?g)', ext, re.IGNORECASE) def is_audio(file): From 3b42bc99912b26348e41d5b26f56c0c0e65e939c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Mar 2018 10:17:45 +0100 Subject: [PATCH 15/24] Slice albums larger than 10 items and allow mixing docs --- telethon/telegram_client.py | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 642ecbf5..93a9793a 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1342,6 +1342,10 @@ class TelegramClient(TelegramBareClient): photo or similar) so that it can be resent without the need to download and re-upload it again. + If a list or similar is provided, the files in it will be + sent as an album in the order in which they appear, sliced + in chunks of 10 if more than 10 are given. + caption (:obj:`str`, optional): Optional caption for the sent media message. @@ -1387,23 +1391,33 @@ class TelegramClient(TelegramBareClient): # First check if the user passed an iterable, in which case # we may want to send as an album if all are photo files. if utils.is_list_like(file): - # Convert to tuple so we can iterate several times - file = tuple(x for x in file) - if all(utils.is_image(x) for x in file): - return self._send_album( - entity, file, caption=caption, + # TODO Fix progress_callback + images = [] + documents = [] + for x in file: + if utils.is_image(x): + images.append(x) + else: + documents.append(x) + + result = [] + while images: + result += self._send_album( + entity, images[:10], caption=caption, progress_callback=progress_callback, reply_to=reply_to, parse_mode=parse_mode ) - # Not all are images, so send all the files one by one - return [ + images = images[10:] + + result.extend( self.send_file( entity, x, allow_cache=False, caption=caption, force_document=force_document, progress_callback=progress_callback, reply_to=reply_to, attributes=attributes, thumb=thumb, **kwargs - ) for x in file - ] + ) for x in documents + ) + return result entity = self.get_input_entity(entity) reply_to = self._get_message_id(reply_to) @@ -1543,6 +1557,10 @@ class TelegramClient(TelegramBareClient): # we need to produce right now to send albums (uploadMedia), and # cache only makes a difference for documents where the user may # want the attributes used on them to change. + # + # In theory documents can be sent inside the albums but they appear + # as different messages (not inside the album), and the logic to set + # the attributes/avoid cache is already written in .send_file(). entity = self.get_input_entity(entity) if not utils.is_list_like(caption): caption = (caption,) From 1e3120b0b66b82003bd0448ea5dc713bdd89c134 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Mar 2018 10:22:21 +0100 Subject: [PATCH 16/24] Bring back report_errors to the constructor --- readthedocs/extra/developing/api-status.rst | 2 ++ telethon/telegram_bare_client.py | 2 ++ telethon/telegram_client.py | 8 +++++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/readthedocs/extra/developing/api-status.rst b/readthedocs/extra/developing/api-status.rst index 181ed3ae..e113c48e 100644 --- a/readthedocs/extra/developing/api-status.rst +++ b/readthedocs/extra/developing/api-status.rst @@ -1,3 +1,5 @@ +.. _api-status: + ========== API Status ========== diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 2ada33f5..8281f869 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -73,6 +73,7 @@ class TelegramBareClient: update_workers=None, spawn_read_thread=False, timeout=timedelta(seconds=5), + report_errors=True, device_model=None, system_version=None, app_version=None, @@ -103,6 +104,7 @@ class TelegramBareClient: DEFAULT_PORT ) + session.report_errors = report_errors self.session = session self.api_id = int(api_id) self.api_hash = api_hash diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 93a9793a..f0e24918 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -146,8 +146,12 @@ class TelegramClient(TelegramBareClient): instantly, as soon as they arrive. Can still be disabled if you want to run the library without any additional thread. + report_errors (:obj:`bool`, optional): + Whether to report RPC errors or not. Defaults to ``True``, + see :ref:`api-status` for more information. + Kwargs: - Some extra parameters are required when stabilishing the first + Some extra parameters are required when establishing the first connection. These are are (along with their default values): .. code-block:: python @@ -168,6 +172,7 @@ class TelegramClient(TelegramBareClient): update_workers=None, timeout=timedelta(seconds=5), spawn_read_thread=True, + report_errors=True, **kwargs): super().__init__( session, api_id, api_hash, @@ -177,6 +182,7 @@ class TelegramClient(TelegramBareClient): update_workers=update_workers, spawn_read_thread=spawn_read_thread, timeout=timeout, + report_errors=report_errors, **kwargs ) From b20aa0ccc91b3d767c26702f3611c44772d87f5a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Mar 2018 10:29:12 +0100 Subject: [PATCH 17/24] Stopping workers should not clear their count (may fix #686) --- telethon/update_state.py | 1 + 1 file changed, 1 insertion(+) diff --git a/telethon/update_state.py b/telethon/update_state.py index 171e9546..9f26e3a4 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -80,6 +80,7 @@ class UpdateState: t.join() self._worker_threads.clear() + self._workers = n def setup_workers(self): if self._worker_threads or not self._workers: From d6c051fd52eccb798a1168c004b3c9ad22525fa9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 16 Mar 2018 09:54:16 +0100 Subject: [PATCH 18/24] Add __str__ and .stringify() to events --- telethon/events/__init__.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 4339faef..b0f21930 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -2,11 +2,12 @@ import abc import datetime import itertools import re +import warnings from .. import utils from ..errors import RPCError from ..extensions import markdown -from ..tl import types, functions +from ..tl import TLObject, types, functions def _into_id_set(client, chats): @@ -170,6 +171,17 @@ class _EventCommon(abc.ABC): 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.__class__.__name__ + return d + class Raw(_EventBuilder): """ @@ -182,9 +194,19 @@ class Raw(_EventBuilder): return update +def _name_inner_event(cls): + """Decorator to rename cls.Event 'Event' as 'cls.Event'""" + if hasattr(cls, 'Event'): + cls.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. @@ -580,6 +602,7 @@ class NewMessage(_EventBuilder): return self.message.out +@_name_inner_event class ChatAction(_EventBuilder): """ Represents an action in a chat (such as user joined, left, or new pin). @@ -874,6 +897,7 @@ class ChatAction(_EventBuilder): return self._input_users +@_name_inner_event class UserUpdate(_EventBuilder): """ Represents an user update (gone online, offline, joined Telegram). @@ -1022,6 +1046,7 @@ class UserUpdate(_EventBuilder): return self.chat +@_name_inner_event class MessageEdited(NewMessage): """ Event fired when a message has been edited. @@ -1037,6 +1062,7 @@ class MessageEdited(NewMessage): return self._message_filter_event(event) +@_name_inner_event class MessageDeleted(_EventBuilder): """ Event fired when one or more messages are deleted. From 36b09a9459798d8ad2712bde6bb350516fe9994a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 16 Mar 2018 09:57:37 +0100 Subject: [PATCH 19/24] .download_file's file is not optional --- telethon/telegram_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index f0e24918..0d2026b6 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -2067,7 +2067,7 @@ class TelegramClient(TelegramBareClient): input_location (:obj:`InputFileLocation`): The file location from which the file will be downloaded. - file (:obj:`str` | :obj:`file`, optional): + file (:obj:`str` | :obj:`file`): The output file path, directory, or stream-like object. If the path exists and is a file, it will be overwritten. From 50256e23e9cbf555a2992fbd538bc4b1ec1bc5fe Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 17 Mar 2018 17:35:41 +0200 Subject: [PATCH 20/24] Add addon-style session dev instructions (#698) --- readthedocs/extra/advanced-usage/sessions.rst | 75 +++---------------- 1 file changed, 12 insertions(+), 63 deletions(-) diff --git a/readthedocs/extra/advanced-usage/sessions.rst b/readthedocs/extra/advanced-usage/sessions.rst index 66fa0560..592d8334 100644 --- a/readthedocs/extra/advanced-usage/sessions.rst +++ b/readthedocs/extra/advanced-usage/sessions.rst @@ -36,77 +36,26 @@ one of the other implementations or implement your own storage. To use a custom session storage, simply pass the custom session instance to ``TelegramClient`` instead of the session name. -Currently, there are three implementations of the abstract ``Session`` class: -* ``MemorySession``. Stores session data in Python variables. -* ``SQLiteSession``, (default). Stores sessions in their own SQLite databases. -* ``AlchemySession``. Stores all sessions in a single database via SQLAlchemy. +Telethon contains two implementations of the abstract ``Session`` class: -Using AlchemySession -~~~~~~~~~~~~~~~~~~~~ -The ``AlchemySession`` implementation can store multiple Sessions in the same -database, but to do this, each session instance needs to have access to the -same models and database session. +* ``MemorySession``: stores session data in Python variables. +* ``SQLiteSession``, (default): stores sessions in their own SQLite databases. -To get started, you need to create an ``AlchemySessionContainer`` which will -contain that shared data. The simplest way to use ``AlchemySessionContainer`` -is to simply pass it the database URL: +There are other community-maintained implementations available: - .. code-block:: python - - container = AlchemySessionContainer('mysql://user:pass@localhost/telethon') - -If you already have SQLAlchemy set up for your own project, you can also pass -the engine separately: - - .. code-block:: python - - my_sqlalchemy_engine = sqlalchemy.create_engine('...') - container = AlchemySessionContainer(engine=my_sqlalchemy_engine) - -By default, the session container will manage table creation/schema updates/etc -automatically. If you want to manage everything yourself, you can pass your -SQLAlchemy Session and ``declarative_base`` instances and set ``manage_tables`` -to ``False``: - - .. code-block:: python - - from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy import orm - import sqlalchemy - - ... - - session_factory = orm.sessionmaker(bind=my_sqlalchemy_engine) - session = session_factory() - my_base = declarative_base() - - ... - - container = AlchemySessionContainer( - session=session, table_base=my_base, manage_tables=False - ) - -You always need to provide either ``engine`` or ``session`` to the container. -If you set ``manage_tables=False`` and provide a ``session``, ``engine`` is not -needed. In any other case, ``engine`` is always required. - -After you have your ``AlchemySessionContainer`` instance created, you can -create new sessions by calling ``new_session``: - - .. code-block:: python - - session = container.new_session('some session id') - client = TelegramClient(session) - -where ``some session id`` is an unique identifier for the session. +* `SQLAlchemy `_: stores all sessions in a single database via SQLAlchemy. +* `Redis `_: stores all sessions in a single Redis data store. Creating your own storage ~~~~~~~~~~~~~~~~~~~~~~~~~ -The easiest way to create your own implementation is to use ``MemorySession`` -as the base and check out how ``SQLiteSession`` or ``AlchemySession`` work. -You can find the relevant Python files under the ``sessions`` directory. +The easiest way to create your own storage implementation is to use ``MemorySession`` +as the base and check out how ``SQLiteSession`` or one of the community-maintained +implementations work. You can find the relevant Python files under the ``sessions`` +directory in Telethon. +After you have made your own implementation, you can add it to the community-maintained +session implementation list above with a pull request. SQLite Sessions and Heroku -------------------------- From dc07d6507533fed8b705bb0ecea4ead49bd25290 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 17 Mar 2018 17:03:22 +0100 Subject: [PATCH 21/24] Add remove_event_handler and list_event_handlers --- telethon/telegram_client.py | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 0d2026b6..e4647a8e 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -2239,22 +2239,46 @@ class TelegramClient(TelegramBareClient): self._event_builders.append((event, callback)) + def remove_event_handler(self, callback, event=None): + """ + Inverse operation of :meth:`add_event_handler`. + + If no event is given, all events for this callback are removed. + Returns how many callbacks were removed. + """ + found = 0 + if event and not isinstance(event, type): + event = type(event) + + for i, ec in enumerate(self._event_builders): + ev, cb = ec + if cb == callback and (not event or isinstance(ev, event)): + del self._event_builders[i] + found += 1 + + return found + + def list_event_handlers(self): + """ + Lists all added event handlers, returning a list of pairs + consisting of (callback, event). + """ + return [(callback, event) for event, callback in self._event_builders] + def add_update_handler(self, handler): - """Adds an update handler (a function which takes a TLObject, - an update, as its parameter) and listens for updates""" warnings.warn( 'add_update_handler is deprecated, use the @client.on syntax ' 'or add_event_handler(callback, events.Raw) instead (see ' 'https://telethon.rtfd.io/en/latest/extra/basic/working-' 'with-updates.html)' ) - self.add_event_handler(handler, events.Raw) + return self.add_event_handler(handler, events.Raw) def remove_update_handler(self, handler): - pass + return self.remove_event_handler(handler) def list_update_handlers(self): - return [] + return [callback for callback, _ in self.list_event_handlers()] # endregion From 6f820cce97997d317292d3a5973555d8ef3921bb Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 17 Mar 2018 17:07:56 +0100 Subject: [PATCH 22/24] Mention add_event_handler in the docs and fix small nit --- readthedocs/extra/basic/working-with-updates.rst | 14 ++++++++++++++ .../developing/telegram-api-in-other-languages.rst | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/readthedocs/extra/basic/working-with-updates.rst b/readthedocs/extra/basic/working-with-updates.rst index 105e11bd..308c3a79 100644 --- a/readthedocs/extra/basic/working-with-updates.rst +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -140,6 +140,20 @@ random number, while if you say ``'eval 4+4'``, you will reply with the solution. Try it! +Events without decorators +************************* + +If for any reason you can't use the ``@client.on`` syntax, don't worry. +You can call ``client.add_event_handler(callback, event)`` to achieve +the same effect. + +Similar to that method, you also have :meth:`client.remove_event_handler` +and :meth:`client.list_event_handlers` which do as they names indicate. + +The ``event`` type is optional in all methods and defaults to ``events.Raw`` +for adding, and ``None`` when removing (so all callbacks would be removed). + + Stopping propagation of Updates ******************************* diff --git a/readthedocs/extra/developing/telegram-api-in-other-languages.rst b/readthedocs/extra/developing/telegram-api-in-other-languages.rst index 7637282e..22bb416a 100644 --- a/readthedocs/extra/developing/telegram-api-in-other-languages.rst +++ b/readthedocs/extra/developing/telegram-api-in-other-languages.rst @@ -38,9 +38,9 @@ Kotlin ****** `Kotlogram `__ is a Telegram -implementation written in Kotlin (the now +implementation written in Kotlin (one of the `official `__ -language for +languages for `Android `__) by `@badoualy `__, currently as a beta– yet working. From 32fd64d655f3f300d01c2ac6d642dab0e00cf22b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 17 Mar 2018 17:38:16 +0100 Subject: [PATCH 23/24] Remove SQLAlchemy session --- optional-requirements.txt | 1 - setup.py | 3 +- telethon/sessions/__init__.py | 1 - telethon/sessions/sqlalchemy.py | 236 -------------------------------- 4 files changed, 1 insertion(+), 240 deletions(-) delete mode 100644 telethon/sessions/sqlalchemy.py diff --git a/optional-requirements.txt b/optional-requirements.txt index fb83c1ab..55bfc014 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -1,4 +1,3 @@ cryptg pysocks hachoir3 -sqlalchemy diff --git a/setup.py b/setup.py index 05ca9197..0d6d757c 100755 --- a/setup.py +++ b/setup.py @@ -156,8 +156,7 @@ def main(): install_requires=['pyaes', 'rsa', 'typing' if version_info < (3, 5) else ""], extras_require={ - 'cryptg': ['cryptg'], - 'sqlalchemy': ['sqlalchemy'] + 'cryptg': ['cryptg'] } ) diff --git a/telethon/sessions/__init__.py b/telethon/sessions/__init__.py index a487a4bd..af3423f3 100644 --- a/telethon/sessions/__init__.py +++ b/telethon/sessions/__init__.py @@ -1,4 +1,3 @@ from .abstract import Session from .memory import MemorySession from .sqlite import SQLiteSession -from .sqlalchemy import AlchemySessionContainer, AlchemySession diff --git a/telethon/sessions/sqlalchemy.py b/telethon/sessions/sqlalchemy.py deleted file mode 100644 index a24df485..00000000 --- a/telethon/sessions/sqlalchemy.py +++ /dev/null @@ -1,236 +0,0 @@ -try: - from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy import Column, String, Integer, LargeBinary, orm - import sqlalchemy as sql -except ImportError: - sql = None - -from .memory import MemorySession, _SentFileType -from .. import utils -from ..crypto import AuthKey -from ..tl.types import ( - InputPhoto, InputDocument, PeerUser, PeerChat, PeerChannel -) - -LATEST_VERSION = 1 - - -class AlchemySessionContainer: - def __init__(self, engine=None, session=None, table_prefix='', - table_base=None, manage_tables=True): - if not sql: - raise ImportError('SQLAlchemy not imported') - if isinstance(engine, str): - engine = sql.create_engine(engine) - - self.db_engine = engine - if not session: - db_factory = orm.sessionmaker(bind=self.db_engine) - self.db = orm.scoping.scoped_session(db_factory) - else: - self.db = session - - table_base = table_base or declarative_base() - (self.Version, self.Session, self.Entity, - self.SentFile) = self.create_table_classes(self.db, table_prefix, - table_base) - - if manage_tables: - table_base.metadata.bind = self.db_engine - if not self.db_engine.dialect.has_table(self.db_engine, - self.Version.__tablename__): - table_base.metadata.create_all() - self.db.add(self.Version(version=LATEST_VERSION)) - self.db.commit() - else: - self.check_and_upgrade_database() - - @staticmethod - def create_table_classes(db, prefix, Base): - class Version(Base): - query = db.query_property() - __tablename__ = '{prefix}version'.format(prefix=prefix) - version = Column(Integer, primary_key=True) - - class Session(Base): - query = db.query_property() - __tablename__ = '{prefix}sessions'.format(prefix=prefix) - - session_id = Column(String, primary_key=True) - dc_id = Column(Integer, primary_key=True) - server_address = Column(String) - port = Column(Integer) - auth_key = Column(LargeBinary) - - class Entity(Base): - query = db.query_property() - __tablename__ = '{prefix}entities'.format(prefix=prefix) - - session_id = Column(String, primary_key=True) - id = Column(Integer, primary_key=True) - hash = Column(Integer, nullable=False) - username = Column(String) - phone = Column(Integer) - name = Column(String) - - class SentFile(Base): - query = db.query_property() - __tablename__ = '{prefix}sent_files'.format(prefix=prefix) - - session_id = Column(String, primary_key=True) - md5_digest = Column(LargeBinary, primary_key=True) - file_size = Column(Integer, primary_key=True) - type = Column(Integer, primary_key=True) - id = Column(Integer) - hash = Column(Integer) - - return Version, Session, Entity, SentFile - - def check_and_upgrade_database(self): - row = self.Version.query.all() - version = row[0].version if row else 1 - if version == LATEST_VERSION: - return - - self.Version.query.delete() - - # Implement table schema updates here and increase version - - self.db.add(self.Version(version=version)) - self.db.commit() - - def new_session(self, session_id): - return AlchemySession(self, session_id) - - def list_sessions(self): - return - - def save(self): - self.db.commit() - - -class AlchemySession(MemorySession): - def __init__(self, container, session_id): - super().__init__() - self.container = container - self.db = container.db - self.Version, self.Session, self.Entity, self.SentFile = ( - container.Version, container.Session, container.Entity, - container.SentFile) - self.session_id = session_id - self._load_session() - - def _load_session(self): - sessions = self._db_query(self.Session).all() - session = sessions[0] if sessions else None - if session: - self._dc_id = session.dc_id - self._server_address = session.server_address - self._port = session.port - self._auth_key = AuthKey(data=session.auth_key) - - def clone(self, to_instance=None): - return super().clone(MemorySession()) - - def set_dc(self, dc_id, server_address, port): - super().set_dc(dc_id, server_address, port) - self._update_session_table() - - sessions = self._db_query(self.Session).all() - session = sessions[0] if sessions else None - if session and session.auth_key: - self._auth_key = AuthKey(data=session.auth_key) - else: - self._auth_key = None - - @MemorySession.auth_key.setter - def auth_key(self, value): - self._auth_key = value - self._update_session_table() - - def _update_session_table(self): - self.Session.query.filter( - self.Session.session_id == self.session_id).delete() - new = self.Session(session_id=self.session_id, dc_id=self._dc_id, - server_address=self._server_address, - port=self._port, - auth_key=(self._auth_key.key - if self._auth_key else b'')) - self.db.merge(new) - - def _db_query(self, dbclass, *args): - return dbclass.query.filter( - dbclass.session_id == self.session_id, *args - ) - - def save(self): - self.container.save() - - def close(self): - # Nothing to do here, connection is managed by AlchemySessionContainer. - pass - - def delete(self): - self._db_query(self.Session).delete() - self._db_query(self.Entity).delete() - self._db_query(self.SentFile).delete() - - def _entity_values_to_row(self, id, hash, username, phone, name): - return self.Entity(session_id=self.session_id, id=id, hash=hash, - username=username, phone=phone, name=name) - - def process_entities(self, tlo): - rows = self._entities_to_rows(tlo) - if not rows: - return - - for row in rows: - self.db.merge(row) - self.save() - - def get_entity_rows_by_phone(self, key): - row = self._db_query(self.Entity, - self.Entity.phone == key).one_or_none() - return (row.id, row.hash) if row else None - - def get_entity_rows_by_username(self, key): - row = self._db_query(self.Entity, - self.Entity.username == key).one_or_none() - return (row.id, row.hash) if row else None - - def get_entity_rows_by_name(self, key): - row = self._db_query(self.Entity, - self.Entity.name == key).one_or_none() - return (row.id, row.hash) if row else None - - def get_entity_rows_by_id(self, key, exact=True): - if exact: - query = self._db_query(self.Entity, self.Entity.id == key) - else: - ids = ( - utils.get_peer_id(PeerUser(key)), - utils.get_peer_id(PeerChat(key)), - utils.get_peer_id(PeerChannel(key)) - ) - query = self._db_query(self.Entity, self.Entity.id in ids) - - row = query.one_or_none() - return (row.id, row.hash) if row else None - - def get_file(self, md5_digest, file_size, cls): - row = self._db_query(self.SentFile, - self.SentFile.md5_digest == md5_digest, - self.SentFile.file_size == file_size, - self.SentFile.type == _SentFileType.from_type( - cls).value).one_or_none() - return (row.id, row.hash) if row else None - - def cache_file(self, md5_digest, file_size, instance): - if not isinstance(instance, (InputDocument, InputPhoto)): - raise TypeError('Cannot cache %s instance' % type(instance)) - - self.db.merge( - self.SentFile(session_id=self.session_id, md5_digest=md5_digest, - type=_SentFileType.from_type(type(instance)).value, - id=instance.id, hash=instance.access_hash)) - self.save() From 7825994393aab11101f85bc37904eee266d2991e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 17 Mar 2018 17:38:46 +0100 Subject: [PATCH 24/24] Update to v0.18.1 --- readthedocs/extra/changelog.rst | 63 +++++++++++++++++++++++++++++++++ telethon/version.py | 2 +- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index e8876d5c..31d58d6b 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -14,6 +14,69 @@ it can take advantage of new goodies! .. contents:: List of All Versions +Iterator methods (v0.18.1) +========================== + +*Published at 2018/03/17* + +All the ``.get_`` methods in the ``TelegramClient`` now have a ``.iter_`` +counterpart, so you can do operations while retrieving items from them. +For instance, you can ``client.iter_dialogs()`` and ``break`` once you +find what you're looking for instead fetching them all at once. + +Another big thing, you can get entities by just their positive ID. This +may cause some collisions (although it's very unlikely), and you can (should) +still be explicit about the type you want. However, it's a lot more convenient +and less confusing. + +Breaking changes +~~~~~~~~~~~~~~~~ + +- The library only offers the default ``SQLiteSession`` again. + See :ref:`sessions` for more on how to use a different storage from now on. + +Additions +~~~~~~~~~ + +- Events now override ``__str__`` and implement ``.stringify()``, just like + every other ``TLObject`` does. +- ``events.ChatAction`` now has :meth:`respond`, :meth:`reply` and + :meth:`delete` for the message that triggered it. +- :meth:`client.iter_participants` (and its :meth:`client.get_participants` + counterpart) now expose the ``filter`` argument, and the returned users + also expose the ``.participant`` they are. +- You can now use :meth:`client.remove_event_handler` and + :meth:`client.list_event_handlers` similar how you could with normal updates. +- New properties on ``events.NewMessage``, like ``.video_note`` and ``.gif`` + to access only specific types of documents. +- The ``Draft`` class now exposes ``.text`` and ``.raw_text``, as well as a + new :meth:`Draft.send` to send it. + +Bug fixes +~~~~~~~~~ + +- ``MessageEdited`` was ignoring ``NewMessage`` constructor arguments. +- Fixes for ``Event.delete_messages`` which wouldn't handle ``MessageService``. +- Bot API style IDs not working on :meth:`client.get_input_entity`. +- :meth:`client.download_media` didn't support ``PhotoSize``. + +Enhancements +~~~~~~~~~~~~ + +- Less RPC are made when accessing the ``.sender`` and ``.chat`` of some + events (mostly those that occur in a channel). +- You can send albums larger than 10 items (they will be sliced for you), + as well as mixing normal files with photos. +- ``TLObject`` now have Python type hints. + +Internal changes +~~~~~~~~~~~~~~~~ + +- Several documentation corrections. +- :meth:`client.get_dialogs` is only called once again when an entity is + not found to avoid flood waits. + + Sessions overhaul (v0.18) ========================= diff --git a/telethon/version.py b/telethon/version.py index 90e8bfe4..90266dbf 100644 --- a/telethon/version.py +++ b/telethon/version.py @@ -1,3 +1,3 @@ # Versions should comply with PEP440. # This line is parsed in setup.py: -__version__ = '0.18' +__version__ = '0.18.1'