From f6223bd01a4dd9089405c3dff290db183b933433 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 21 Oct 2017 16:21:58 +0200 Subject: [PATCH 01/22] Document the InteractiveTelegramClient more nicely --- .../interactive_telegram_client.py | 86 +++++++++++++++---- 1 file changed, 71 insertions(+), 15 deletions(-) diff --git a/telethon_examples/interactive_telegram_client.py b/telethon_examples/interactive_telegram_client.py index 031a78cd..ee179a42 100644 --- a/telethon_examples/interactive_telegram_client.py +++ b/telethon_examples/interactive_telegram_client.py @@ -20,11 +20,11 @@ def sprint(string, *args, **kwargs): def print_title(title): - # Clear previous window - print('\n') - print('=={}=='.format('=' * len(title))) + """Helper function to print titles to the console more nicely""" + sprint('\n') + sprint('=={}=='.format('=' * len(title))) sprint('= {} ='.format(title)) - print('=={}=='.format('=' * len(title))) + sprint('=={}=='.format('=' * len(title))) def bytes_to_string(byte_count): @@ -34,8 +34,9 @@ def bytes_to_string(byte_count): byte_count /= 1024 suffix_index += 1 - return '{:.2f}{}'.format(byte_count, - [' bytes', 'KB', 'MB', 'GB', 'TB'][suffix_index]) + return '{:.2f}{}'.format( + byte_count, [' bytes', 'KB', 'MB', 'GB', 'TB'][suffix_index] + ) class InteractiveTelegramClient(TelegramClient): @@ -48,13 +49,38 @@ class InteractiveTelegramClient(TelegramClient): """ def __init__(self, session_user_id, user_phone, api_id, api_hash, proxy=None): + """ + Initializes the InteractiveTelegramClient. + :param session_user_id: Name of the *.session file. + :param user_phone: The phone of the user that will login. + :param api_id: Telegram's api_id acquired through my.telegram.org. + :param api_hash: Telegram's api_hash. + :param proxy: Optional proxy tuple/dictionary. + """ print_title('Initialization') print('Initializing interactive example...') + + # The first step is to initialize the TelegramClient, as we are + # subclassing it, we need to call super().__init__(). On a more + # normal case you would want 'client = TelegramClient(...)' super().__init__( + # These parameters should be passed always, session name and API session_user_id, api_id, api_hash, + + # You can optionally change the connection mode by using this enum. + # This changes how much data will be sent over the network with + # every request, and how it will be formatted. Default is + # ConnectionMode.TCP_FULL, and smallest is TCP_TCP_ABRIDGED. connection_mode=ConnectionMode.TCP_ABRIDGED, + + # If you're using a proxy, set it here. proxy=proxy, + + # If you want to receive updates, you need to start one or more + # "update workers" which are background threads that will allow + # you to run things when your update handlers (callbacks) are + # called with an Update object. update_workers=1 ) @@ -62,6 +88,8 @@ class InteractiveTelegramClient(TelegramClient): # so it can be downloaded if the user wants self.found_media = set() + # Calling .connect() may return False, so you need to assert it's + # True before continuing. Otherwise you may want to retry as done here. print('Connecting to Telegram servers...') if not self.connect(): print('Initial connection failed. Retrying...') @@ -69,18 +97,24 @@ class InteractiveTelegramClient(TelegramClient): print('Could not connect to Telegram servers.') return - # Then, ensure we're authorized and have access + # If the user hasn't called .sign_in() or .sign_up() yet, they won't + # be authorized. The first thing you must do is authorize. Calling + # .sign_in() should only be done once as the information is saved on + # the *.session file so you don't need to enter the code every time. if not self.is_user_authorized(): print('First run. Sending code request...') - self.send_code_request(user_phone) + self.sign_in(user_phone) self_user = None while self_user is None: code = input('Enter the code you just received: ') try: - self_user = self.sign_in(user_phone, code) + self_user = self.sign_in(code=code) - # Two-step verification may be enabled + # Two-step verification may be enabled, and .sign_in will + # raise this error. If that's the case ask for the password. + # Note that getpass() may not work on PyCharm due to a bug, + # if that's the case simply change it for input(). except SessionPasswordNeededError: pw = getpass('Two step verification is enabled. ' 'Please enter your password: ') @@ -88,16 +122,22 @@ class InteractiveTelegramClient(TelegramClient): self_user = self.sign_in(password=pw) def run(self): - # Listen for updates + """Main loop of the TelegramClient, will wait for user action""" + + # Once everything is ready, we can add an update handler. Every + # update object will be passed to the self.update_handler method, + # where we can process it as we need. self.add_update_handler(self.update_handler) # Enter a while loop to chat as long as the user wants while True: - # Retrieve the top dialogs + # Retrieve the top dialogs. You can set the limit to None to + # retrieve all of them if you wish, but beware that may take + # a long time if you have hundreds of them. dialog_count = 15 # Entities represent the user, chat or channel - # corresponding to the dialog on the same index + # corresponding to the dialog on the same index. dialogs, entities = self.get_dialogs(limit=dialog_count) i = None @@ -119,6 +159,12 @@ class InteractiveTelegramClient(TelegramClient): if i == '!q': return if i == '!l': + # Logging out will cause the user to need to reenter the + # code next time they want to use the library, and will + # also delete the *.session file off the filesystem. + # + # This is not the same as simply calling .disconnect(), + # which simply shuts down everything gracefully. self.log_out() return @@ -158,8 +204,8 @@ class InteractiveTelegramClient(TelegramClient): # History elif msg == '!h': # First retrieve the messages and some information - total_count, messages, senders = self.get_message_history( - entity, limit=10) + total_count, messages, senders = \ + self.get_message_history(entity, limit=10) # Iterate over all (in reverse order so the latest appear # the last in the console) and print them with format: @@ -237,6 +283,7 @@ class InteractiveTelegramClient(TelegramClient): entity, msg, link_preview=False) def send_photo(self, path, entity): + """Sends the file located at path to the desired entity as a photo""" self.send_file( entity, path, progress_callback=self.upload_progress_callback @@ -244,6 +291,7 @@ class InteractiveTelegramClient(TelegramClient): print('Photo sent!') def send_document(self, path, entity): + """Sends the file located at path to the desired entity as a document""" self.send_file( entity, path, force_document=True, @@ -252,6 +300,9 @@ class InteractiveTelegramClient(TelegramClient): print('Document sent!') def download_media_by_id(self, media_id): + """Given a message ID, finds the media this message contained and + downloads it. + """ try: # The user may have entered a non-integer string! msg_media_id = int(media_id) @@ -291,6 +342,11 @@ class InteractiveTelegramClient(TelegramClient): ) def update_handler(self, update): + """Callback method for received Updates""" + + # We have full control over what we want to do with the updates. + # In our case we only want to react to chat messages, so we use + # isinstance() to behave accordingly on these cases. if isinstance(update, UpdateShortMessage): who = self.get_entity(update.user_id) if update.out: From d7f917ebfc54e26c1d3c61159dce910cef8468dd Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 21 Oct 2017 16:59:20 +0200 Subject: [PATCH 02/22] Update docstrings --- telethon/telegram_client.py | 279 ++++++++++++++++++++---------------- 1 file changed, 155 insertions(+), 124 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 3a1ba20e..4a6e25e4 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -126,7 +126,11 @@ class TelegramClient(TelegramBareClient): # region Authorization requests def send_code_request(self, phone): - """Sends a code request to the specified phone number""" + """Sends a code request to the specified phone number. + + :param str | int phone: The phone to which the code will be sent. + :return auth.SentCode: Information about the result of the request. + """ phone = EntityDatabase.parse_phone(phone) or self._phone result = self(SendCodeRequest(phone, self.api_id, self.api_hash)) self._phone = phone @@ -135,26 +139,27 @@ class TelegramClient(TelegramBareClient): def sign_in(self, phone=None, code=None, password=None, bot_token=None, phone_code_hash=None): - """Completes the sign in process with the phone number + code pair. + """ + Starts or completes the sign in process with the given phone number + or code that Telegram sent. - If no phone or code is provided, then the sole password will be used. - The password should be used after a normal authorization attempt - has happened and an SessionPasswordNeededError was raised. + :param str | int phone: + The phone to send the code to if no code was provided, or to + override the phone that was previously used with these requests. + :param str | int code: + The code that Telegram sent. + :param str password: + 2FA password, should be used if a previous call raised + SessionPasswordNeededError. + :param str bot_token: + Used to sign in as a bot. Not all requests will be available. + This should be the hash the @BotFather gave you. + :param str phone_code_hash: + The hash returned by .send_code_request. This can be set to None + to use the last hash known. - If you're calling .sign_in() on two completely different clients - (for example, through an API that creates a new client per phone), - you must first call .sign_in(phone) to receive the code, and then - with the result such method results, call - .sign_in(phone, code, phone_code_hash=result.phone_code_hash). - - If this is done on the same client, the client will fill said values - for you. - - To login as a bot, only `bot_token` should be provided. - This should equal to the bot access hash provided by - https://t.me/BotFather during your bot creation. - - If the login succeeds, the logged in user is returned. + :return auth.SentCode | User: + The signed in user, or the information about .send_code_request(). """ if phone and not code: @@ -198,7 +203,15 @@ class TelegramClient(TelegramBareClient): return result.user def sign_up(self, code, first_name, last_name=''): - """Signs up to Telegram. Make sure you sent a code request first!""" + """ + Signs up to Telegram if you don't have an account yet. + You must call .send_code_request(phone) first. + + :param str | int code: The code sent by Telegram + :param str first_name: The first name to be used by the new account. + :param str last_name: Optional last name. + :return User: The new created user. + """ result = self(SignUpRequest( phone_number=self._phone, phone_code_hash=self._phone_code_hash, @@ -211,8 +224,10 @@ class TelegramClient(TelegramBareClient): return result.user def log_out(self): - """Logs out and deletes the current session. - Returns True if everything went okay.""" + """Logs out Telegram and deletes the current *.session file. + + :return bool: True if the operation was successful. + """ try: self(LogOutRequest()) except RPCError: @@ -224,8 +239,12 @@ class TelegramClient(TelegramBareClient): return True def get_me(self): - """Gets "me" (the self user) which is currently authenticated, - or None if the request fails (hence, not authenticated).""" + """ + Gets "me" (the self user) which is currently authenticated, + or None if the request fails (hence, not authenticated). + + :return User: Your own user. + """ try: return self(GetUsersRequest([InputUserSelf()]))[0] except UnauthorizedError: @@ -240,15 +259,21 @@ class TelegramClient(TelegramBareClient): offset_date=None, offset_id=0, offset_peer=InputPeerEmpty()): - """Returns a tuple of lists ([dialogs], [entities]) - with at least 'limit' items each unless all dialogs were consumed. + """ + Gets N "dialogs" (open "chats" or conversations with other people). - If `limit` is None, all dialogs will be retrieved (from the given - offset) will be retrieved. - - The `entities` represent the user, chat or channel - corresponding to that dialog. If it's an integer, not - all dialogs may be retrieved at once. + :param limit: + How many dialogs to be retrieved as maximum. Can be set to None + to retrieve all dialogs. Note that this may take whole minutes + if you have hundreds of dialogs, as Telegram will tell the library + to slow down through a FloodWaitError. + :param offset_date: + The offset date to be used. + :param offset_id: + The message ID to be used as an offset. + :param offset_peer: + The peer to be used as an offset. + :return: A tuple of lists ([dialogs], [entities]). """ if limit is None: limit = float('inf') @@ -307,8 +332,9 @@ class TelegramClient(TelegramBareClient): """ Gets all open draft messages. - Returns a list of custom `Draft` objects that are easy to work with: You can call - `draft.set_message('text')` to change the message, or delete it through `draft.delete()`. + Returns a list of custom `Draft` objects that are easy to work with: + You can call `draft.set_message('text')` to change the message, + or delete it through `draft.delete()`. :return List[telethon.tl.custom.Draft]: A list of open drafts """ @@ -323,11 +349,14 @@ class TelegramClient(TelegramBareClient): message, reply_to=None, link_preview=True): - """Sends a message to the given entity (or input peer) - and returns the sent message as a Telegram object. + """ + Sends the given message to the specified entity (user/chat/channel). - If 'reply_to' is set to either a message or a message ID, - the sent message will be replying to such message. + :param str | int | User | Chat | Channel entity: To who will it be sent. + :param str message: The message to be sent. + :param int | Message reply_to: Whether to reply to a message or not. + :param link_preview: Should the link preview be shown? + :return Message: the sent message """ entity = self.get_input_entity(entity) request = SendMessageRequest( @@ -370,11 +399,11 @@ class TelegramClient(TelegramBareClient): Deletes a message from a chat, optionally "for everyone" with argument `revoke` set to `True`. - The `revoke` argument has no effect for Channels and Supergroups, + The `revoke` argument has no effect for Channels and Megagroups, where it inherently behaves as being `True`. Note: The `entity` argument can be `None` for normal chats, but it's - mandatory to delete messages from Channels and Supergroups. It is also + mandatory to delete messages from Channels and Megagroups. It is also possible to supply a chat_id which will be automatically resolved to the right type of InputPeer. @@ -419,9 +448,6 @@ class TelegramClient(TelegramBareClient): :return: A tuple containing total message count and two more lists ([messages], [senders]). Note that the sender can be null if it was not found! - - The entity may be a phone or an username at the expense of - some performance loss. """ result = self(GetHistoryRequest( peer=self.get_input_entity(entity), @@ -451,16 +477,15 @@ class TelegramClient(TelegramBareClient): return total_messages, result.messages, entities def send_read_acknowledge(self, entity, messages=None, max_id=None): - """Sends a "read acknowledge" (i.e., notifying the given peer that we've - read their messages, also known as the "double check"). + """ + Sends a "read acknowledge" (i.e., notifying the given peer that we've + read their messages, also known as the "double check"). - Either a list of messages (or a single message) can be given, - or the maximum message ID (until which message we want to send the read acknowledge). - - Returns an AffectedMessages TLObject - - The entity may be a phone or an username at the expense of - some performance loss. + :param entity: The chat where these messages are located. + :param messages: Either a list of messages or a single message. + :param max_id: Overrides messages, until which message should the + acknowledge should be sent. + :return: """ if max_id is None: if not messages: @@ -502,36 +527,36 @@ class TelegramClient(TelegramBareClient): reply_to=None, attributes=None, **kwargs): - """Sends a file to the specified entity. - The file may either be a path, a byte array, or a stream. - Note that if a byte array or a stream is given, a filename - or its type won't be inferred, and it will be sent as an - "unnamed application/octet-stream". + """ + Sends a file to the specified entity. - An optional caption can also be specified for said file. - - If "force_document" is False, the file will be sent as a photo - if it's recognised to have a common image format (e.g. .png, .jpg). - - Otherwise, the file will always be sent as an uncompressed document. - - Subsequent calls with the very same file will result in - immediate uploads, unless .clear_file_cache() is called. - - If "progress_callback" is not None, it should be a function that - takes two parameters, (bytes_uploaded, total_bytes). - - The "reply_to" parameter works exactly as the one on .send_message. - - If "attributes" is set to be a list of DocumentAttribute's, these - will override the automatically inferred ones (so that you can - modify the file name of the file sent for instance). + :param entity: + Who will receive the file. + :param file: + The path of the file, byte array, or stream that will be sent. + Note that if a byte array or a stream is given, a filename + or its type won't be inferred, and it will be sent as an + "unnamed application/octet-stream". + Subsequent calls with the very same file will result in + immediate uploads, unless .clear_file_cache() is called. + :param caption: + Optional caption for the sent media message. + :param force_document: + If left to False and the file is a path that ends with .png, .jpg + and such, the file will be sent as a photo. Otherwise always as + a document. + :param progress_callback: + A callback function accepting two parameters: (sent bytes, total) + :param reply_to: + Same as reply_to from .send_message(). + :param attributes: + Optional attributes that override the inferred ones, like + DocumentAttributeFilename and so on. + :param kwargs: If "is_voice_note" in kwargs, despite its value, and the file is sent as a document, it will be sent as a voice note. - - The entity may be a phone or an username at the expense of - some performance loss. + :return: """ as_photo = False if isinstance(file, str): @@ -622,21 +647,19 @@ class TelegramClient(TelegramBareClient): # region Downloading media requests def download_profile_photo(self, entity, file=None, download_big=True): - """Downloads the profile photo for an user or a chat (channels too). - Returns None if no photo was provided, or if it was Empty. + """ + Downloads the profile photo of the given entity (user/chat/channel). - If an entity itself (an user, chat or channel) is given, the photo - to be downloaded will be downloaded automatically. - - On success, the file path is returned since it may differ from - the one provided. - - The specified output file can either be a file path, a directory, - or a stream-like object. If the path exists and is a file, it will - be overwritten. - - The entity may be a phone or an username at the expense of - some performance loss. + :param entity: + From who the photo will be downloaded. + :param file: + The output file path, directory, or stream-like object. + If the path exists and is a file, it will be overwritten. + :param download_big: + Whether to use the big version of the available photos. + :return: + None if no photo was provided, or if it was Empty. On success + the file path is returned since it may differ from the one given. """ possible_names = [] if not isinstance(entity, TLObject) or type(entity).SUBCLASS_OF_ID in ( @@ -691,21 +714,16 @@ class TelegramClient(TelegramBareClient): return file def download_media(self, message, file=None, progress_callback=None): - """Downloads the media from a specified Message (it can also be - the message.media) into the desired file (a stream or str), - optionally finding its extension automatically. - - The specified output file can either be a file path, a directory, - or a stream-like object. If the path exists and is a file, it will - be overwritten. - - If the operation succeeds, the path will be returned (since - the extension may have been added automatically). Otherwise, - None is returned. - - The progress_callback should be a callback function which takes - two parameters, uploaded size and total file size (both in bytes). - This will be called every time a part is downloaded + """ + Downloads the given media, or the media from a specified Message. + :param message: + The media or message containing the media that will be downloaded. + :param file: + The output file path, directory, or stream-like object. + If the path exists and is a file, it will be overwritten. + :param progress_callback: + A callback function accepting two parameters: (recv bytes, total) + :return: """ # TODO This won't work for messageService if isinstance(message, Message): @@ -884,20 +902,24 @@ class TelegramClient(TelegramBareClient): # region Small utilities to make users' life easier def get_entity(self, entity): - """Turns an entity into a valid Telegram user or chat. - If "entity" is a string which can be converted to an integer, - or if it starts with '+' it will be resolved as if it - were a phone number. + """ + Turns the given entity into a valid Telegram user or chat. - If "entity" is a string and doesn't start with '+', or - it starts with '@', it will be resolved from the username. - If no exact match is returned, an error will be raised. + :param entity: + The entity to be transformed. + If it's a string which can be converted to an integer or starts + with '+' it will be resolved as if it were a phone number. - If "entity" is an integer or a "Peer", its information will - be returned through a call to self.get_input_peer(entity). + If it doesn't start with '+' or starts with a '@' it will be + be resolved from the username. If no exact match is returned, + an error will be raised. - If the entity is neither, and it's not a TLObject, an - error will be raised. + If the entity is an integer or a Peer, its information will be + returned through a call to self.get_input_peer(entity). + + If the entity is neither, and it's not a TLObject, an + error will be raised. + :return: """ try: return self.session.entities[entity] @@ -950,14 +972,23 @@ class TelegramClient(TelegramBareClient): ) def get_input_entity(self, peer): - """Gets the input entity given its PeerUser, PeerChat, PeerChannel. - If no Peer class is used, peer is assumed to be the integer ID - of an User. + """ + Turns the given peer into its input entity version. Most requests + use this kind of InputUser, InputChat and so on, so this is the + most suitable call to make for those cases. - If this Peer hasn't been seen before by the library, all dialogs - will loaded, and their entities saved to the session file. + :param peer: + The integer ID of an user or otherwise either of a + PeerUser, PeerChat or PeerChannel, for which to get its + Input* version. - If even after it's not found, a ValueError is raised. + If this Peer hasn't been seen before by the library, the top + dialogs will be loaded and their entities saved to the session + file (unless this feature was disabled explicitly). + + If in the end the access hash required for the peer was not found, + a ValueError will be raised. + :return: """ try: # First try to get the entity from cache, otherwise figure it out From 6759beac218d6dfd6ddb3354be04526cd5a3327f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 21 Oct 2017 20:23:53 +0200 Subject: [PATCH 03/22] Add __str__ methods to TLMessage and MessageContainer --- telethon/tl/message_container.py | 4 ++++ telethon/tl/tl_message.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/telethon/tl/message_container.py b/telethon/tl/message_container.py index 1f0127a1..12d617cc 100644 --- a/telethon/tl/message_container.py +++ b/telethon/tl/message_container.py @@ -16,6 +16,10 @@ class MessageContainer(TLObject): ' Date: Sun, 22 Oct 2017 11:12:42 +0200 Subject: [PATCH 04/22] Attempt at fixing redundant import from ee01724 (#357) --- telethon_generator/tl_generator.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index 8fc6bb2d..f0a14fbc 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -129,9 +129,11 @@ class TLGenerator: builder.writeln( 'from {}.tl.tlobject import TLObject'.format('.' * depth) ) - builder.writeln( - 'from {}.tl import types'.format('.' * depth) - ) + if ns: + # Only import the parent types if we're not in such file + builder.writeln( + 'from {}.tl import types'.format('.' * depth) + ) # Add the relative imports to the namespaces, # unless we already are in a namespace. From 5de8350d857ae3bfc10a9b882430e3af34ba28cf Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 22 Oct 2017 11:23:15 +0200 Subject: [PATCH 05/22] Reorder another import for #357 --- telethon/tl/entity_database.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/telethon/tl/entity_database.py b/telethon/tl/entity_database.py index 554e2a5a..b0fc70fb 100644 --- a/telethon/tl/entity_database.py +++ b/telethon/tl/entity_database.py @@ -1,13 +1,12 @@ +import re from threading import Lock -import re - -from .. import utils from ..tl import TLObject from ..tl.types import ( User, Chat, Channel, PeerUser, PeerChat, PeerChannel, InputPeerUser, InputPeerChat, InputPeerChannel ) +from .. import utils # Keep this line the last to maybe fix #357 class EntityDatabase: From 8057cea294b3603f3f6341d17c83ab6de5aeb91d Mon Sep 17 00:00:00 2001 From: Andrey Egorov Date: Sun, 22 Oct 2017 14:13:49 +0300 Subject: [PATCH 06/22] Fix resending requests on bad salt/msg notification (#369) These responses from the server could indicate container IDs, which weren't being saved. This fix also accounts for that case. --- telethon/network/mtproto_sender.py | 38 ++++++++++++++++++++++++++---- telethon/tl/tl_message.py | 1 + 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 50281e9b..b4483903 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -82,6 +82,11 @@ class MtProtoSender: message = messages[0] else: message = TLMessage(self.session, MessageContainer(messages)) + # On bad_msg_salt errors, Telegram will reply with the ID of + # the container and not the requests it contains, so in case + # this happens we need to know to which container they belong. + for m in messages: + m.container_msg_id = message.msg_id self._send_message(message) @@ -259,11 +264,34 @@ class MtProtoSender: if message and isinstance(message.request, t): return self._pending_receive.pop(msg_id).request + def _pop_requests_of_container(self, container_msg_id): + """Pops the pending requests (plural) from self._pending_receive if + they were sent on a container that matches container_msg_id. + """ + msgs = [msg for msg in self._pending_receive.values() + if msg.container_msg_id == container_msg_id] + + requests = [msg.request for msg in msgs] + for msg in msgs: + self._pending_receive.pop(msg.msg_id, None) + return requests + def _clear_all_pending(self): for r in self._pending_receive.values(): r.request.confirm_received.set() self._pending_receive.clear() + def _resend_request(self, msg_id): + """Re-sends the request that belongs to a certain msg_id. This may + also be the msg_id of a container if they were sent in one. + """ + request = self._pop_request(msg_id) + if request: + return self.send(request) + requests = self._pop_requests_of_container(msg_id) + if requests: + return self.send(*requests) + def _handle_pong(self, msg_id, sequence, reader): self._logger.debug('Handling pong') pong = reader.tgread_object() @@ -305,10 +333,9 @@ class MtProtoSender: )[0] self.session.save() - request = self._pop_request(bad_salt.bad_msg_id) - if request: - self.send(request) - + # "the bad_server_salt response is received with the + # correct salt, and the message is to be re-sent with it" + self._resend_request(bad_salt.bad_msg_id) return True def _handle_bad_msg_notification(self, msg_id, sequence, reader): @@ -323,15 +350,18 @@ class MtProtoSender: self.session.update_time_offset(correct_msg_id=msg_id) self._logger.debug('Read Bad Message error: ' + str(error)) self._logger.debug('Attempting to use the correct time offset.') + self._resend_request(bad_msg.bad_msg_id) return True elif bad_msg.error_code == 32: # msg_seqno too low, so just pump it up by some "large" amount # TODO A better fix would be to start with a new fresh session ID self.session._sequence += 64 + self._resend_request(bad_msg.bad_msg_id) return True elif bad_msg.error_code == 33: # msg_seqno too high never seems to happen but just in case self.session._sequence -= 16 + self._resend_request(bad_msg.bad_msg_id) return True else: raise error diff --git a/telethon/tl/tl_message.py b/telethon/tl/tl_message.py index 16e9ace6..b524b75b 100644 --- a/telethon/tl/tl_message.py +++ b/telethon/tl/tl_message.py @@ -11,6 +11,7 @@ class TLMessage(TLObject): self.msg_id = session.get_new_msg_id() self.seq_no = session.generate_sequence(request.content_related) self.request = request + self.container_msg_id = None def __bytes__(self): body = GzipPacked.gzip_if_smaller(self.request) From b04eed82ebf713770fe96e3098bd3f351ff53033 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 22 Oct 2017 13:15:52 +0200 Subject: [PATCH 07/22] Add new .idle() method to listen for updates from MainThread --- telethon/telegram_bare_client.py | 53 +++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 6e7029c8..3a6a44f2 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -5,6 +5,7 @@ import warnings from datetime import timedelta, datetime from hashlib import md5 from io import BytesIO +from signal import signal, SIGINT, SIGTERM, SIGABRT from threading import Lock from time import sleep @@ -785,14 +786,31 @@ class TelegramBareClient: ) self._recv_thread.start() - # By using this approach, another thread will be - # created and started upon connection to constantly read - # from the other end. Otherwise, manual calls to .receive() - # must be performed. The MtProtoSender cannot be connected, - # or an error will be thrown. - # - # This way, sending and receiving will be completely independent. - def _recv_thread_impl(self): + def _signal_handler(self, signum, frame): + if self._user_connected: + self.disconnect() + else: + self._logger.debug('Forcing exit...') + os._exit(1) + + def idle(self, stop_signals=(SIGINT, SIGTERM, SIGABRT)): + """ + Idles the program by looping forever and listening for updates + until one of the signals are received, which breaks the loop. + + :param stop_signals: + Iterable containing signals from the signal module that will + be subscribed to TelegramClient.disconnect() (effectively + stopping the idle loop), which will be called on receiving one + of those signals. + :return: + """ + if self._spawn_read_thread and not self._on_read_thread(): + raise ValueError('Can only idle if spawn_read_thread=False') + + for sig in stop_signals: + signal(sig, self._signal_handler) + while self._user_connected: try: if datetime.now() > self._last_ping + self._ping_delay: @@ -810,10 +828,24 @@ class TelegramBareClient: while self._user_connected and not self._reconnect(): sleep(0.1) # Retry forever, this is instant messaging + # By using this approach, another thread will be + # created and started upon connection to constantly read + # from the other end. Otherwise, manual calls to .receive() + # must be performed. The MtProtoSender cannot be connected, + # or an error will be thrown. + # + # This way, sending and receiving will be completely independent. + def _recv_thread_impl(self): + # This thread is "idle" (only listening for updates), but also + # excepts everything unlike the manual idle because it should + # not crash. + while self._user_connected: + try: + self.idle(stop_signals=tuple()) except Exception as error: # Unknown exception, pass it to the main thread - self._logger.debug( - '[ERROR] Unknown error on the read thread, please report', + self._logger.error( + 'Unknown error on the read thread, please report', error ) @@ -835,7 +867,6 @@ class TelegramBareClient: # infinite loop where all we do is raise an exception, so # add a little sleep to avoid the CPU usage going mad. sleep(0.1) - break self._recv_thread = None From 1f1e040af972e6948538d47fab5563303c2243eb Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 22 Oct 2017 13:57:02 +0200 Subject: [PATCH 08/22] Fix setup.py if/elif/else chain --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 13be0144..2058924f 100755 --- a/setup.py +++ b/setup.py @@ -89,7 +89,7 @@ def main(): for x in ('build', 'dist', 'Telethon.egg-info'): rmtree(x, ignore_errors=True) - if len(argv) >= 2 and argv[1] == 'fetch_errors': + elif len(argv) >= 2 and argv[1] == 'fetch_errors': from telethon_generator.error_generator import fetch_errors fetch_errors(ERRORS_JSON) From d58c729af066c3b3e3d1ac8c1482162e76aff2b5 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 24 Oct 2017 09:42:51 +0200 Subject: [PATCH 09/22] Add missing InputPeerSelf case to .get_input_user --- telethon/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/telethon/utils.py b/telethon/utils.py index d8bfb89f..afb24b16 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -142,7 +142,10 @@ def get_input_user(entity): else: return InputUser(entity.id, entity.access_hash) - if isinstance(entity, UserEmpty): + if isinstance(entity, InputPeerSelf): + return InputUserSelf() + + if isinstance(entity, (UserEmpty, InputPeerEmpty)): return InputUserEmpty() if isinstance(entity, UserFull): From b3ca68b7d9cc7a100a9ba4550c0de442146865f4 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 24 Oct 2017 10:07:31 +0200 Subject: [PATCH 10/22] Avoid cyclic imports caused by #348 (fix #357) --- telethon_generator/error_generator.py | 3 +-- telethon_generator/parser/tl_object.py | 3 +-- telethon_generator/tl_generator.py | 26 +++++++++++++++++++------- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/telethon_generator/error_generator.py b/telethon_generator/error_generator.py index feb32b5a..30163dfc 100644 --- a/telethon_generator/error_generator.py +++ b/telethon_generator/error_generator.py @@ -154,11 +154,10 @@ def generate_code(output, json_file, errors_desc): patterns.append((pattern, name)) capture = capture_names.get(name, 'x') if has_captures else None # TODO Some errors have the same name but different code, - # split this accross different files? + # split this across different files? write_error(f, error_code, name, description, capture) f.write('\n\nrpc_errors_all = {\n') for pattern, name in patterns: f.write(' {}: {},\n'.format(repr(pattern), name)) f.write('}\n') - diff --git a/telethon_generator/parser/tl_object.py b/telethon_generator/parser/tl_object.py index 79b4385d..278a66eb 100644 --- a/telethon_generator/parser/tl_object.py +++ b/telethon_generator/parser/tl_object.py @@ -104,8 +104,7 @@ class TLObject: def class_name_for(typename, is_function=False): """Gets the class name following the Python style guidelines""" # Courtesy of http://stackoverflow.com/a/31531797/4759433 - result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), - typename) + result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), typename) result = result[:1].upper() + result[1:].replace('_', '') # If it's a function, let it end with "Request" to identify them if is_function: diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index f0a14fbc..b5d43656 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -129,11 +129,6 @@ class TLGenerator: builder.writeln( 'from {}.tl.tlobject import TLObject'.format('.' * depth) ) - if ns: - # Only import the parent types if we're not in such file - builder.writeln( - 'from {}.tl import types'.format('.' * depth) - ) # Add the relative imports to the namespaces, # unless we already are in a namespace. @@ -646,8 +641,25 @@ class TLGenerator: if not arg.skip_constructor_id: builder.writeln('{} = reader.tgread_object()'.format(name)) else: - builder.writeln('{} = types.{}.from_reader(reader)'.format( - name, TLObject.class_name_for(arg.type))) + # Import the correct type inline to avoid cyclic imports. + # There may be better solutions so that we can just access + # all the types before the files have been parsed, but I + # don't know of any. + sep_index = arg.type.find('.') + if sep_index == -1: + ns, t = '.', arg.type + else: + ns, t = '.' + arg.type[:sep_index], arg.type[sep_index+1:] + class_name = TLObject.class_name_for(t) + + # There would be no need to import the type if we're in the + # file with the same namespace, but since it does no harm + # and we don't have information about such thing in the + # method we just ignore that case. + builder.writeln('from {} import {}'.format(ns, class_name)) + builder.writeln('{} = {}.from_reader(reader)'.format( + name, class_name + )) # End vector and flag blocks if required (if we opened them before) if arg.is_vector: From ceb37cd4c55d67f5b1669860f18a447a668588df Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 24 Oct 2017 15:40:51 +0200 Subject: [PATCH 11/22] Move auth_key generation and InitConnection logic to .invoke() The reasoning behind this is that .connect() should not call any request at all, it should only connect to the servers although it currently still calls GetStateRequest. There were some issues (#291, #360) where the auth_key was None (possibly due to .connect() returning False), so this may fix some of the cases where it returned False. This way we also ensure that we always have an auth_key, or even if it "breaks" (it's not the right key for the server anymore). A few additional changes have been introduced to accommodate this, such as moving InitConnection logic too or importing auths. --- telethon/telegram_bare_client.py | 134 +++++++++++++++---------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 3a6a44f2..a589fa9e 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -33,6 +33,7 @@ from .tl.functions.upload import ( GetFileRequest, SaveBigFilePartRequest, SaveFilePartRequest ) from .tl.types import InputFile, InputFileBig +from .tl.types.auth import ExportedAuthorization from .tl.types.upload import FileCdnRedirect from .update_state import UpdateState from .utils import get_appropriated_part_size @@ -62,7 +63,7 @@ class TelegramBareClient: __version__ = '0.15.3' # TODO Make this thread-safe, all connections share the same DC - _dc_options = None + _config = None # Server configuration (with .dc_options) # region Initialization @@ -161,7 +162,7 @@ class TelegramBareClient: # region Connecting - def connect(self, _exported_auth=None, _sync_updates=True, _cdn=False): + def connect(self, _sync_updates=True): """Connects to the Telegram servers, executing authentication if required. Note that authenticating to the Telegram servers is not the same as authenticating the desired user itself, which @@ -169,62 +170,24 @@ class TelegramBareClient: Note that the optional parameters are meant for internal use. - If '_exported_auth' is not None, it will be used instead to - determine the authorization key for the current session. - If '_sync_updates', sync_updates() will be called and a second thread will be started if necessary. Note that this will FAIL if the client is not connected to the user's native data center, raising a "UserMigrateError", and calling .disconnect() in the process. - - If '_cdn' is False, methods that are not allowed on such data - centers won't be invoked. """ self._main_thread_ident = threading.get_ident() self._background_error = None # Clear previous errors try: self._sender.connect() - if not self.session.auth_key: - # New key, we need to tell the server we're going to use - # the latest layer - try: - self.session.auth_key, self.session.time_offset = \ - authenticator.do_authentication(self._sender.connection) - except BrokenAuthKeyError: - return False - - self.session.layer = LAYER - self.session.save() - init_connection = True - else: - init_connection = self.session.layer != LAYER - - if init_connection: - if _exported_auth is not None: - self._init_connection(ImportAuthorizationRequest( - _exported_auth.id, _exported_auth.bytes - )) - elif not _cdn: - TelegramBareClient._dc_options = \ - self._init_connection(GetConfigRequest()).dc_options - - elif _exported_auth is not None: - self(ImportAuthorizationRequest( - _exported_auth.id, _exported_auth.bytes - )) - - if TelegramBareClient._dc_options is None and not _cdn: - TelegramBareClient._dc_options = \ - self(GetConfigRequest()).dc_options # Connection was successful! Try syncing the update state # UNLESS '_sync_updates' is False (we probably are in # another data center and this would raise UserMigrateError) # to also assert whether the user is logged in or not. self._user_connected = True - if self._authorized is None and _sync_updates and not _cdn: + if self._authorized is None and _sync_updates: try: self.sync_updates() self._set_connected_and_authorized() @@ -239,11 +202,7 @@ class TelegramBareClient: # This is fine, probably layer migration self._logger.debug('Found invalid item, probably migrating', e) self.disconnect() - return self.connect( - _exported_auth=_exported_auth, - _sync_updates=_sync_updates, - _cdn=_cdn - ) + return self.connect(_sync_updates=_sync_updates) except (RPCError, ConnectionError) as error: # Probably errors from the previous session, ignore them @@ -256,8 +215,9 @@ class TelegramBareClient: def is_connected(self): return self._sender.is_connected() - def _init_connection(self, query=None): - result = self(InvokeWithLayerRequest(LAYER, InitConnectionRequest( + def _wrap_init_connection(self, query): + """Wraps query around InvokeWithLayerRequest(InitConnectionRequest())""" + return InvokeWithLayerRequest(LAYER, InitConnectionRequest( api_id=self.api_id, device_model=self.session.device_model, system_version=self.session.system_version, @@ -266,10 +226,7 @@ class TelegramBareClient: system_lang_code=self.session.system_lang_code, lang_pack='', # "langPacks are for official apps only" query=query - ))) - self.session.layer = LAYER - self.session.save() - return result + )) def disconnect(self): """Disconnects from the Telegram server @@ -308,13 +265,18 @@ class TelegramBareClient: except ConnectionResetError: return False else: - self.disconnect() - self.session.auth_key = None # Force creating new auth_key + # Since we're reconnecting possibly due to a UserMigrateError, + # we need to first know the Data Centers we can connect to. Do + # that before disconnecting. dc = self._get_dc(new_dc) - ip = dc.ip_address - self.session.server_address = ip + + self.session.server_address = dc.ip_address self.session.port = dc.port + # auth_key's are associated with a server, which has now changed + # so it's not valid anymore. Set to None to force recreating it. + self.session.auth_key = None self.session.save() + self.disconnect() return self.connect() # endregion @@ -327,10 +289,8 @@ class TelegramBareClient: def _get_dc(self, dc_id, ipv6=False, cdn=False): """Gets the Data Center (DC) associated to 'dc_id'""" - if TelegramBareClient._dc_options is None: - raise ConnectionError( - 'Cannot determine the required data center IP address. ' - 'Stabilise a successful initial connection first.') + if not TelegramBareClient._config: + TelegramBareClient._config = self(GetConfigRequest()) try: if cdn: @@ -339,15 +299,15 @@ class TelegramBareClient: rsa.add_key(pk.public_key) return next( - dc for dc in TelegramBareClient._dc_options if dc.id == dc_id - and bool(dc.ipv6) == ipv6 and bool(dc.cdn) == cdn + dc for dc in TelegramBareClient._config.dc_options + if dc.id == dc_id and bool(dc.ipv6) == ipv6 and bool(dc.cdn) == cdn ) except StopIteration: if not cdn: raise # New configuration, perhaps a new CDN was added? - TelegramBareClient._dc_options = self(GetConfigRequest()).dc_options + TelegramBareClient._config = self(GetConfigRequest()) return self._get_dc(dc_id, ipv6=ipv6, cdn=cdn) def _get_exported_client(self, dc_id): @@ -387,7 +347,14 @@ class TelegramBareClient: proxy=self._sender.connection.conn.proxy, timeout=self._sender.connection.get_timeout() ) - client.connect(_exported_auth=export_auth, _sync_updates=False) + client.connect(_sync_updates=False) + if isinstance(export_auth, ExportedAuthorization): + client(ImportAuthorizationRequest( + id=export_auth.id, bytes=export_auth.bytes + )) + elif export_auth is not None: + self._logger.warning('Unknown return export_auth type', export_auth) + client._authorized = True # We exported the auth, so we got auth return client @@ -409,9 +376,10 @@ class TelegramBareClient: # This will make use of the new RSA keys for this specific CDN. # - # This relies on the fact that TelegramBareClient._dc_options is - # static and it won't be called from this DC (it would fail). - client.connect(_cdn=True) # Avoid invoking non-CDN specific methods + # We won't be calling GetConfigRequest because it's only called + # when needed by ._get_dc, and also it's static so it's likely + # set already. Avoid invoking non-CDN methods by not syncing updates. + client.connect(_sync_updates=False) client._authorized = self._authorized return client @@ -472,12 +440,34 @@ class TelegramBareClient: invoke = __call__ def _invoke(self, sender, call_receive, update_state, *requests): + # We need to specify the new layer (by initializing a new + # connection) if it has changed from the latest known one. + init_connection = self.session.layer != LAYER + try: # Ensure that we start with no previous errors (i.e. resending) for x in requests: x.confirm_received.clear() x.rpc_error = None + if not self.session.auth_key: + # New key, we need to tell the server we're going to use + # the latest layer and initialize the connection doing so. + self.session.auth_key, self.session.time_offset = \ + authenticator.do_authentication(self._sender.connection) + init_connection = True + + if init_connection: + if len(requests) == 1: + requests = [self._wrap_init_connection(requests[0])] + else: + # We need a SINGLE request (like GetConfig) to init conn. + # Once that's done, the N original requests will be + # invoked. + TelegramBareClient._config = self( + self._wrap_init_connection(GetConfigRequest()) + ) + sender.send(*requests) if not call_receive: @@ -493,6 +483,10 @@ class TelegramBareClient: while not all(x.confirm_received.is_set() for x in requests): sender.receive(update_state=update_state) + except BrokenAuthKeyError: + self._logger.error('Broken auth key, a new one will be generated') + self.session.auth_key = None + except TimeoutError: pass # We will just retry @@ -513,6 +507,12 @@ class TelegramBareClient: sleep(0.1) # Retry forever until we can send the request return None + if init_connection: + # We initialized the connection successfully, even if + # a request had an RPC error we have invoked it fine. + self.session.layer = LAYER + self.session.save() + try: raise next(x.rpc_error for x in requests if x.rpc_error) except StopIteration: From d707fd1593b47d228d33bb283bb8634075df12a7 Mon Sep 17 00:00:00 2001 From: Tanuj Date: Tue, 24 Oct 2017 20:32:31 +0100 Subject: [PATCH 12/22] Add example script to print out all updates --- telethon_examples/print_updates.py | 45 ++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100755 telethon_examples/print_updates.py diff --git a/telethon_examples/print_updates.py b/telethon_examples/print_updates.py new file mode 100755 index 00000000..e315ff98 --- /dev/null +++ b/telethon_examples/print_updates.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# A simple script to print all updates received + +from telethon import TelegramClient +from getpass import getpass +from os import environ +# environ is used to get API information from environment variables +# You could also use a config file, pass them as arguments, +# or even hardcode them (not recommended) + +def main(): + session_name = environ.get('TG_SESSION','session') + user_phone = environ['TG_PHONE'], + client = TelegramClient(session_name, + int(environ['TG_API_ID']), + environ['TG_API_HASH'], + proxy=None, + update_workers=4) + + print('INFO: Connecting to Telegram Servers...', end='', flush=True) + client.connect() + print('Done!') + + if not client.is_user_authorized(): + print('INFO: Unauthorized user') + client.send_code_request(user_phone) + code_ok = False + while not code_ok: + code = input('Enter the auth code: ') + try: + code_ok = client.sign_in(user_phone, code) + except SessionPasswordNeededError: + pw = getpass('Two step verification enabled. Please enter your password: ') + code_ok = client.sign_in(password=pw) + print('INFO: Client initialized succesfully!') + + client.add_update_handler(update_handler) + input('Press Enter to stop this!\n') + +def update_handler(update): + print(update) + print('Press Enter to stop this!') + +if __name__ == '__main__': + main() From 9f9da6adda2bf3fb596d6f9b9c6603be1f0f7a80 Mon Sep 17 00:00:00 2001 From: Tanuj Date: Tue, 24 Oct 2017 21:08:44 +0100 Subject: [PATCH 13/22] Remove comma (#376) --- telethon_examples/print_updates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon_examples/print_updates.py b/telethon_examples/print_updates.py index e315ff98..c90302e4 100755 --- a/telethon_examples/print_updates.py +++ b/telethon_examples/print_updates.py @@ -10,7 +10,7 @@ from os import environ def main(): session_name = environ.get('TG_SESSION','session') - user_phone = environ['TG_PHONE'], + user_phone = environ['TG_PHONE'] client = TelegramClient(session_name, int(environ['TG_API_ID']), environ['TG_API_HASH'], From e427559d4c8dd3a97f2772f90295dab450eca011 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 25 Oct 2017 12:22:03 +0200 Subject: [PATCH 14/22] Fix username invalid error having wrong username regex --- telethon_generator/error_descriptions | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon_generator/error_descriptions b/telethon_generator/error_descriptions index f0a14e68..500504d7 100644 --- a/telethon_generator/error_descriptions +++ b/telethon_generator/error_descriptions @@ -45,7 +45,7 @@ PHONE_NUMBER_OCCUPIED=The phone number is already in use PHONE_NUMBER_UNOCCUPIED=The phone number is not yet being used PHOTO_INVALID_DIMENSIONS=The photo dimensions are invalid TYPE_CONSTRUCTOR_INVALID=The type constructor is invalid -USERNAME_INVALID=Unacceptable username. Must match r"[a-zA-Z][\w\d]{4,31}" +USERNAME_INVALID=Unacceptable username. Must match r"[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]" USERNAME_NOT_MODIFIED=The username is not different from the current username USERNAME_NOT_OCCUPIED=The username is not in use by anyone else yet USERNAME_OCCUPIED=The username is already taken From 3db13ccdd299349601afde6da22507a97ac7895a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 25 Oct 2017 12:43:57 +0200 Subject: [PATCH 15/22] Add a more descriptive error when serializing bytes --- telethon/tl/tlobject.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index 67a86300..1bd57878 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -93,8 +93,11 @@ class TLObject: @staticmethod def serialize_bytes(data): """Write bytes by using Telegram guidelines""" - if isinstance(data, str): - data = data.encode('utf-8') + if not isinstance(data, bytes): + if isinstance(data, str): + data = data.encode('utf-8') + else: + raise ValueError('bytes or str expected, not', type(data)) r = [] if len(data) < 254: From c6d30ffceb679e71c9de208f42f86e66fba3c924 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 25 Oct 2017 13:04:12 +0200 Subject: [PATCH 16/22] Fix exception when logging exceptions --- telethon/update_state.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/telethon/update_state.py b/telethon/update_state.py index 8dd2ffad..9410125e 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -119,10 +119,10 @@ class UpdateState: handler(update) except StopIteration: break - except Exception as e: + except: # We don't want to crash a worker thread due to any reason - self._logger.debug( - '[ERROR] Unhandled exception on worker {}'.format(wid), e + self._logger.exception( + '[ERROR] Unhandled exception on worker {}'.format(wid) ) def process(self, update): From e6ac61c1b9db73e18b643f8f424727d2fd5aba1f Mon Sep 17 00:00:00 2001 From: Andrei Fokau Date: Wed, 25 Oct 2017 19:48:46 +0200 Subject: [PATCH 17/22] Add missing __init__.py to telethon_generator package (#382) --- telethon_generator/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 telethon_generator/__init__.py diff --git a/telethon_generator/__init__.py b/telethon_generator/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/telethon_generator/__init__.py @@ -0,0 +1 @@ + From 403c7bd00a52fd716a35b71f7389684245e76ed8 Mon Sep 17 00:00:00 2001 From: Tanuj Date: Thu, 26 Oct 2017 17:03:24 +0100 Subject: [PATCH 18/22] Make pylint happier on print_updates example (#387) --- telethon_examples/print_updates.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/telethon_examples/print_updates.py b/telethon_examples/print_updates.py index c90302e4..ab7ba1d4 100755 --- a/telethon_examples/print_updates.py +++ b/telethon_examples/print_updates.py @@ -1,21 +1,22 @@ #!/usr/bin/env python3 # A simple script to print all updates received -from telethon import TelegramClient from getpass import getpass from os import environ # environ is used to get API information from environment variables -# You could also use a config file, pass them as arguments, +# You could also use a config file, pass them as arguments, # or even hardcode them (not recommended) +from telethon import TelegramClient +from telethon.errors import SessionPasswordNeededError def main(): - session_name = environ.get('TG_SESSION','session') + session_name = environ.get('TG_SESSION', 'session') user_phone = environ['TG_PHONE'] client = TelegramClient(session_name, - int(environ['TG_API_ID']), - environ['TG_API_HASH'], - proxy=None, - update_workers=4) + int(environ['TG_API_ID']), + environ['TG_API_HASH'], + proxy=None, + update_workers=4) print('INFO: Connecting to Telegram Servers...', end='', flush=True) client.connect() @@ -30,15 +31,15 @@ def main(): try: code_ok = client.sign_in(user_phone, code) except SessionPasswordNeededError: - pw = getpass('Two step verification enabled. Please enter your password: ') - code_ok = client.sign_in(password=pw) + password = getpass('Two step verification enabled. Please enter your password: ') + code_ok = client.sign_in(password=password) print('INFO: Client initialized succesfully!') client.add_update_handler(update_handler) input('Press Enter to stop this!\n') def update_handler(update): - print(update) + print(update) print('Press Enter to stop this!') if __name__ == '__main__': From 39a1d5e90d1cbf88c6744791903bce634e8161db Mon Sep 17 00:00:00 2001 From: Tanuj Date: Sat, 28 Oct 2017 10:06:34 +0100 Subject: [PATCH 19/22] Replace broken auto_replier.py with new code (#389) --- telethon_examples/anytime.png | Bin 0 -> 2291 bytes telethon_examples/auto_reply.py | 113 -------------------------- telethon_examples/replier.py | 137 ++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 113 deletions(-) create mode 100644 telethon_examples/anytime.png delete mode 100755 telethon_examples/auto_reply.py create mode 100755 telethon_examples/replier.py diff --git a/telethon_examples/anytime.png b/telethon_examples/anytime.png new file mode 100644 index 0000000000000000000000000000000000000000..c8663cfa21f253019a139d9c41f279b3134f0ecb GIT binary patch literal 2291 zcmbu8c{J4R9>9OIG4>{dFtW_3i7^?*R(1`dE;9_Fx@BvSZ4hR-F+~ioQYgjP#+Id~ z*D@&*VJtV*-|?;m&hKIePRbI$Yme$V%Fp6A(qz5Nc@+2}B}Mb-#(3v7RYjpQyLf1SBRVCMhAgM@njso;pfh@4t@iRsaD90)UGU zkOlxofFKCa_A@{g06;L|v^xX;GeBSn6ec1H7ZaBd0`zN9rYJ7QxZhyr*)MfiD<+Z#qj%TwY<1qLSvm{n!IqL<2)3W0ONR zM@Y7I_D3CDT&ZsECp>t5MO~e{XVAJFPMVui12~S6@EjyDJr0_#P&|uEovbjtTAR)Ud^D;P>(^ zkKY(<=*F+B%jm%#vm+$m_F(6idMPJM%bWxV&tnseue8y+k;+q#bBr>JY~zd2Y9eTK zTPTDPSsYWygk8zin^panHx2`LQ?7TV*zCrOWaW8%{oC@5NJT z)~r*SQ+}MoNp(i*99rx7y)26-tV4zu#RWek+XSbkRIzzJ{l_=MH}oH^RY}y&5^T{P zH)v_yQ%KE{PQ&ZA_FXC=Z17T##&=R>|k}?EVb2~-_KypQ6E1}>Ci9h44dDLIlj=3bCv44 z(cSOj$Yi1#4Bf)CQ))(X(-I`?U%!c2XiMB$WPZ5JxfC2oE@o?`?0hj|0V&r%`Lz?| zv_sDmf6Dk!nBYx#9dosd5yy}m>J=ZK@cc8X<&3PhuBG77EmVctqzbR!lNm%afEySu zIzF10%C``F04-YNOBsHCM_-KqcKsujy&xQ|5 z=j1^;Nb0uV1Y zmMABK_JuaE@V*MVIQ80)xL1}K_qoK(mB~FSfu-5gqcFkB#aXbST8B6jf`ZU^~G;K=ft^ru#zEKRQ&n@n+s zO>OWcw1J+DVmqq4YS2E}^oUQB<7VNU;k3J@e<`mZUYeh&)T=uB$lOw|Bxh#TC$t_g zKPZ*M!Nl7RO+E6mgJWPUxIfWX3L#(AX1cJ+si^X<9bmA|%0znMn))KCk->_?`_j!f$w zWmxhHLS|+cvY}rq#GE-co@^2QYDi@pC^Jd>8 xfxJ89PP4%sY^`P_EY0doaJx|t{qfp&*Q!{0T78H=^?6vWoq;tQFK@9u`Zo{N?wSAq literal 0 HcmV?d00001 diff --git a/telethon_examples/auto_reply.py b/telethon_examples/auto_reply.py deleted file mode 100755 index 2af7d8ca..00000000 --- a/telethon_examples/auto_reply.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python3 -# disclaimer: you should not actually use this. it can be quite spammy. -from telethon import TelegramClient -from telethon.errors import SessionPasswordNeededError -from getpass import getpass -from telethon.tl.types import InputPeerUser,InputPeerChannel -from telethon.tl.types import Updates -from telethon.tl.types import UpdateNewChannelMessage,UpdateNewMessage -from telethon.tl.functions.messages import SendMessageRequest,EditMessageRequest -from telethon.tl.types import MessageService -from nltk.tokenize import word_tokenize -from os import environ -from time import sleep - -CHANNELS = {} -CHANNELNAMES = {} -USERS = {} -EMACS_BLACKLIST = [1058260578, # si @linux_group - 123456789] -REACTS = {'emacs':'Needs more vim.', - 'chrome':'Needs more firefox.', -} - -class NeedsMore(TelegramClient): - def __init__(self): - settings = {'api_id':int(environ['TG_API_ID']), - 'api_hash':environ['TG_API_HASH'], - 'user_phone':environ['TG_PHONE'], - 'session_name':'needsmore'} - super().__init__( - settings.get('session_name','session1'), - settings['api_id'], - settings['api_hash'], - proxy=None, - process_updates=True) - - user_phone = settings['user_phone'] - - print('INFO: Connecting to Telegram Servers...', end='', flush=True) - self.connect() - print('Done!') - - if not self.is_user_authorized(): - print('INFO: Unauthorized user') - self.send_code_request(user_phone) - code_ok = False - while not code_ok: - code = input('Enter the auth code: ') - try: - code_ok = self.sign_in(user_phone, code) - except SessionPasswordNeededError: - pw = getpass('Two step verification enabled. Please enter your password: ') - self.sign_in(password=pw) - print('INFO: Client initialized succesfully!') - - def run(self): - # Listen for updates - while True: - update = self.updates.poll() # This will block until an update is available - triggers = [] - if isinstance(update, Updates): - for x in update.updates: - if not isinstance(x,UpdateNewChannelMessage): continue - if isinstance(x.message,MessageService): continue - # We're only interested in messages to supergroups - words = word_tokenize(x.message.message.lower()) - # Avoid matching 'emacs' in 'spacemacs' and similar - if 'emacs' in words and x.message.to_id.channel_id not in EMACS_BLACKLIST: - triggers.append(('emacs',x.message)) - if 'chrome' in words: - triggers.append(('chrome',x.message)) - if 'x files theme' == x.message.message.lower() and x.message.out: - # Automatically reply to yourself saying 'x files theme' with the audio - msg = x.message - chan = InputPeerChannel(msg.to_id.channel_id,CHANNELS[msg.to_id.channel_id]) - self.send_voice_note(chan,'xfiles.m4a',reply_to=msg.id) - sleep(1) - if '.shrug' in x.message.message.lower() and x.message.out: - # Automatically replace '.shrug' in any message you - # send to a supergroup with the shrug emoticon - msg = x.message - chan = InputPeerChannel(msg.to_id.channel_id,CHANNELS[msg.to_id.channel_id]) - self(EditMessageRequest(chan,msg.id, - message=msg.message.replace('.shrug','¯\_(ツ)_/¯'))) - sleep(1) - - for trigger in triggers: - msg = trigger[1] - chan = InputPeerChannel(msg.to_id.channel_id,CHANNELS[msg.to_id.channel_id]) - log_chat = InputPeerUser(user_id=123456789,access_hash=987654321234567890) - self.send_message(log_chat,"{} said {} in {}. Sending react {}".format( - msg.from_id,msg.message,CHANNELNAMES[msg.to_id.channel_id],REACTS[trigger[0]][:20])) - react = '>{}\n{}'.format(trigger[0],REACTS[trigger[0]]) - self.invoke(SendMessageRequest(chan,react,reply_to_msg_id=msg.id)) - sleep(1) - -if __name__ == "__main__": - #TODO: this block could be moved to __init__ - # You can create these text files using https://github.com/LonamiWebs/Telethon/wiki/Retrieving-all-dialogs - with open('channels.txt','r') as f: - # Format: channel_id access_hash #Channel Name - lines = f.readlines() - chans = [l.split(' #',1)[0].split(' ') for l in lines] - CHANNELS = {int(c[0]):int(c[1]) for c in chans} # id:hash - CHANNELNAMES = {int(l.split()[0]):l.split('#',1)[1].strip() for l in lines} #id:name - with open('users','r') as f: - # Format: [user_id, access_hash, 'username', 'Firstname Lastname'] - lines = f.readlines() - uss = [l.strip()[1:-1].split(',') for l in lines] - USERS = {int(user[0]):int(user[1]) for user in uss} # id:hash - - needsmore = NeedsMore() - needsmore.run() diff --git a/telethon_examples/replier.py b/telethon_examples/replier.py new file mode 100755 index 00000000..66026363 --- /dev/null +++ b/telethon_examples/replier.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +""" +A example script to automatically send messages based on certain triggers. + +The script makes uses of environment variables to determine the API ID, +hash, phone and such to be used. You may want to add these to your .bashrc +file, including TG_API_ID, TG_API_HASH, TG_PHONE and optionally TG_SESSION. + +This script assumes that you have certain files on the working directory, +such as "xfiles.m4a" or "anytime.png" for some of the automated replies. +""" +from getpass import getpass +from collections import defaultdict +from datetime import datetime, timedelta +from os import environ + +import re + +from telethon import TelegramClient +from telethon.errors import SessionPasswordNeededError +from telethon.tl.types import UpdateNewChannelMessage, UpdateShortMessage, MessageService +from telethon.tl.functions.messages import EditMessageRequest + +"""Uncomment this for debugging +import logging +logging.basicConfig(level=logging.DEBUG) +logging.debug('dbg') +logging.info('info') +""" + +REACTS = {'emacs': 'Needs more vim', + 'chrome': 'Needs more Firefox'} + +# A list of dates of reactions we've sent, so we can keep track of floods +recent_reacts = defaultdict(list) + + +def update_handler(update): + global recent_reacts + try: + msg = update.message + except AttributeError: + # print(update, 'did not have update.message') + return + if isinstance(msg, MessageService): + print(msg, 'was service msg') + return + + # React to messages in supergroups and PMs + if isinstance(update, UpdateNewChannelMessage): + words = re.split('\W+', msg.message) + for trigger, response in REACTS.items(): + if len(recent_reacts[msg.to_id.channel_id]) > 3: + # Silently ignore triggers if we've recently sent 3 reactions + break + + if trigger in words: + # Remove recent replies older than 10 minutes + recent_reacts[msg.to_id.channel_id] = [ + a for a in recent_reacts[msg.to_id.channel_id] if + datetime.now() - a < timedelta(minutes=10) + ] + # Send a reaction + client.send_message(msg.to_id, response, reply_to=msg.id) + # Add this reaction to the list of recent actions + recent_reacts[msg.to_id.channel_id].append(datetime.now()) + + if isinstance(update, UpdateShortMessage): + words = re.split('\W+', msg) + for trigger, response in REACTS.items(): + if len(recent_reacts[update.user_id]) > 3: + # Silently ignore triggers if we've recently sent 3 reactions + break + + if trigger in words: + # Send a reaction + client.send_message(update.user_id, response, reply_to=update.id) + # Add this reaction to the list of recent reactions + recent_reacts[update.user_id].append(datetime.now()) + + # Automatically send relevant media when we say certain things + # When invoking requests, get_input_entity needs to be called manually + if isinstance(update, UpdateNewChannelMessage) and msg.out: + if msg.message.lower() == 'x files theme': + client.send_voice_note(msg.to_id, 'xfiles.m4a', reply_to=msg.id) + if msg.message.lower() == 'anytime': + client.send_file(msg.to_id, 'anytime.png', reply_to=msg.id) + if '.shrug' in msg.message: + client(EditMessageRequest( + client.get_input_entity(msg.to_id), msg.id, + message=msg.message.replace('.shrug', r'¯\_(ツ)_/¯') + )) + + if isinstance(update, UpdateShortMessage) and update.out: + if msg.lower() == 'x files theme': + client.send_voice_note(update.user_id, 'xfiles.m4a', reply_to=update.id) + if msg.lower() == 'anytime': + client.send_file(update.user_id, 'anytime.png', reply_to=update.id) + if '.shrug' in msg: + client(EditMessageRequest( + client.get_input_entity(update.user_id), update.id, + message=msg.replace('.shrug', r'¯\_(ツ)_/¯') + )) + + +if __name__ == '__main__': + session_name = environ.get('TG_SESSION', 'session') + user_phone = environ['TG_PHONE'] + client = TelegramClient( + session_name, int(environ['TG_API_ID']), environ['TG_API_HASH'], + proxy=None, update_workers=4 + ) + try: + print('INFO: Connecting to Telegram Servers...', end='', flush=True) + client.connect() + print('Done!') + + if not client.is_user_authorized(): + print('INFO: Unauthorized user') + client.send_code_request(user_phone) + code_ok = False + while not code_ok: + code = input('Enter the auth code: ') + try: + code_ok = client.sign_in(user_phone, code) + except SessionPasswordNeededError: + password = getpass('Two step verification enabled. ' + 'Please enter your password: ') + code_ok = client.sign_in(password=password) + print('INFO: Client initialized successfully!') + + client.add_update_handler(update_handler) + input('Press Enter to stop this!\n') + except KeyboardInterrupt: + pass + finally: + client.disconnect() From af08d59cb79255b73406918f8bd35c2e4b3ae886 Mon Sep 17 00:00:00 2001 From: Tanuj Date: Sat, 28 Oct 2017 10:09:46 +0100 Subject: [PATCH 20/22] Fix bug with semicolons when downloading contacts (#319) --- telethon/telegram_client.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 4a6e25e4..3cfd11f0 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -51,7 +51,6 @@ from .tl.types import ( PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel) from .tl.types.messages import DialogsSlice - class TelegramClient(TelegramBareClient): """Full featured TelegramClient meant to extend the basic functionality - @@ -821,12 +820,18 @@ class TelegramClient(TelegramBareClient): f = file try: + # Remove these pesky characters + first_name = first_name.replace(';','') + if last_name is None: + last_name = '' + else: + last_name = last_name.replace(';','') f.write('BEGIN:VCARD\n') f.write('VERSION:4.0\n') f.write('N:{};{};;;\n'.format( - first_name, last_name if last_name else '') + first_name, last_name) ) - f.write('FN:{}\n'.format(' '.join((first_name, last_name)))) + f.write('FN:{} {}\n'.format(first_name, last_name) f.write('TEL;TYPE=cell;VALUE=uri:tel:+{}\n'.format( phone_number)) f.write('END:VCARD\n') From 2f28050cace33ca983554d08919e1879f9f1cb25 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 26 Oct 2017 11:54:40 +0200 Subject: [PATCH 21/22] Fix generated __bytes__ failing with flag indicator but no flags Likely since the code was ported to get rid of the BinaryWriter, since the flag calculation was inlined. Some types (only channelMessages as of layer 71) had a flag indicator but no flag arguments, so the calculation of which were not None failed. This special case is now handled correctly. --- telethon_generator/tl_generator.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index b5d43656..60f07bd6 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -491,11 +491,15 @@ class TLGenerator: elif arg.flag_indicator: # Calculate the flags with those items which are not None - builder.write("struct.pack(' Date: Sat, 28 Oct 2017 11:11:51 +0200 Subject: [PATCH 22/22] Fix-up af08d59 (missing parenthesis) --- telethon/telegram_client.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 3cfd11f0..c64051bf 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -821,19 +821,17 @@ class TelegramClient(TelegramBareClient): try: # Remove these pesky characters - first_name = first_name.replace(';','') - if last_name is None: - last_name = '' - else: - last_name = last_name.replace(';','') + first_name = first_name.replace(';', '') + last_name = (last_name or '').replace(';', '') f.write('BEGIN:VCARD\n') f.write('VERSION:4.0\n') f.write('N:{};{};;;\n'.format( first_name, last_name) ) - f.write('FN:{} {}\n'.format(first_name, last_name) + f.write('FN:{} {}\n'.format(first_name, last_name)) f.write('TEL;TYPE=cell;VALUE=uri:tel:+{}\n'.format( - phone_number)) + phone_number + )) f.write('END:VCARD\n') finally: # Only close the stream if we opened it