From 39a1d5e90d1cbf88c6744791903bce634e8161db Mon Sep 17 00:00:00 2001 From: Tanuj Date: Sat, 28 Oct 2017 10:06:34 +0100 Subject: [PATCH 01/93] 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 02/93] 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 03/93] 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 04/93] 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 From ef794bf75dd00b73cf12c85ec3a0880088dcfa82 Mon Sep 17 00:00:00 2001 From: Andrei Fokau Date: Sat, 28 Oct 2017 12:21:07 +0200 Subject: [PATCH 05/93] Fix importing dependencies during installing (#384) --- setup.py | 19 ++++--------------- telethon/__init__.py | 5 ++++- telethon/telegram_bare_client.py | 4 ++-- telethon/version.py | 3 +++ 4 files changed, 13 insertions(+), 18 deletions(-) create mode 100644 telethon/version.py diff --git a/setup.py b/setup.py index 2058924f..3f8ee7a6 100755 --- a/setup.py +++ b/setup.py @@ -15,16 +15,11 @@ Extra supported commands are: from codecs import open from sys import argv import os +import re # Always prefer setuptools over distutils from setuptools import find_packages, setup -try: - from telethon import TelegramClient -except Exception as e: - print('Failed to import TelegramClient due to', e) - TelegramClient = None - class TempWorkDir: """Switches the working directory to be the one on which this file lives, @@ -94,21 +89,15 @@ def main(): fetch_errors(ERRORS_JSON) else: - if not TelegramClient: - gen_tl() - from telethon import TelegramClient as TgClient - version = TgClient.__version__ - else: - version = TelegramClient.__version__ - # Get the long description from the README file with open('README.rst', encoding='utf-8') as f: long_description = f.read() + with open('telethon/version.py', encoding='utf-8') as f: + version = re.search(r"^__version__\s+=\s+'(.*)'$", + f.read(), flags=re.MULTILINE).group(1) setup( name='Telethon', - - # Versions should comply with PEP440. version=version, description="Full-featured Telegram client library for Python 3", long_description=long_description, diff --git a/telethon/__init__.py b/telethon/__init__.py index c8593168..1210fa90 100644 --- a/telethon/__init__.py +++ b/telethon/__init__.py @@ -1,4 +1,7 @@ from .telegram_bare_client import TelegramBareClient from .telegram_client import TelegramClient from .network import ConnectionMode -from . import tl +from . import tl, version + + +__version__ = version.__version__ diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index a589fa9e..7ebf5ec1 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -9,7 +9,7 @@ from signal import signal, SIGINT, SIGTERM, SIGABRT from threading import Lock from time import sleep -from . import helpers as utils +from . import helpers as utils, version from .crypto import rsa, CdnDecrypter from .errors import ( RPCError, BrokenAuthKeyError, ServerError, @@ -60,7 +60,7 @@ class TelegramBareClient: """ # Current TelegramClient version - __version__ = '0.15.3' + __version__ = version.__version__ # TODO Make this thread-safe, all connections share the same DC _config = None # Server configuration (with .dc_options) diff --git a/telethon/version.py b/telethon/version.py new file mode 100644 index 00000000..2bde1b96 --- /dev/null +++ b/telethon/version.py @@ -0,0 +1,3 @@ +# Versions should comply with PEP440. +# This line is parsed in setup.py: +__version__ = '0.15.3' From 5adec2e1abe656f694226894209a8d904c1355de Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 28 Oct 2017 19:06:41 +0200 Subject: [PATCH 06/93] Initial attempt at parsing Markdown-like syntax --- telethon/extensions/markdown.py | 107 ++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 telethon/extensions/markdown.py diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py new file mode 100644 index 00000000..d3581cf9 --- /dev/null +++ b/telethon/extensions/markdown.py @@ -0,0 +1,107 @@ +""" +Simple markdown parser which does not support nesting. Intended primarily +for use within the library, which attempts to handle emojies correctly, +since they seem to count as two characters and it's a bit strange. +""" +import re +from enum import Enum + +from ..tl.types import ( + MessageEntityBold, MessageEntityItalic, MessageEntityCode, MessageEntityPre +) + + +class Mode(Enum): + """Different modes supported by Telegram's Markdown""" + NONE = 0 + BOLD = 1 + ITALIC = 2 + CODE = 3 + PRE = 4 + + +EMOJI_PATTERN = re.compile( + '[' + '\U0001F600-\U0001F64F' # emoticons + '\U0001F300-\U0001F5FF' # symbols & pictographs + '\U0001F680-\U0001F6FF' # transport & map symbols + '\U0001F1E0-\U0001F1FF' # flags (iOS) + ']+', flags=re.UNICODE +) + + +def is_emoji(char): + """Returns True if 'char' looks like an emoji""" + return bool(EMOJI_PATTERN.match(char)) + + +def emojiness(char): + """ + Returns the "emojiness" of an emoji, or how many characters it counts as. + 1 if it's not an emoji, 2 usual, 3 "special" (seem to count more). + """ + if not is_emoji(char): + return 1 + if ord(char) < ord('🤐'): + return 2 + else: + return 3 + + +def parse(message, delimiters=None): + """ + Parses the given message and returns the stripped message and a list + of tuples containing (start, end, mode) using the specified delimiters + dictionary (or default if None). + """ + if not delimiters: + if delimiters is not None: + return message, [] + + delimiters = { + '**': Mode.BOLD, + '__': Mode.ITALIC, + '`': Mode.CODE, + '```': Mode.PRE + } + + result = [] + current = Mode.NONE + offset = 0 + i = 0 + while i < len(message): + for d, m in delimiters.items(): + if message[i:i + len(d)] == d and current in (Mode.NONE, m): + if message[i + len(d):i + 2 * len(d)] == d: + continue # ignore two consecutive delimiters + + message = message[:i] + message[i + len(d):] + if current == Mode.NONE: + result.append(offset) + current = m + else: + result[-1] = (result[-1], offset, current) + current = Mode.NONE + break + + offset += emojiness(message[i]) + i += 1 + if result and not isinstance(result[-1], tuple): + result.pop() + return message, result + + +def parse_tg(message, delimiters=None): + """Similar to parse(), but returns a list of MessageEntity's""" + message, tuples = parse(message, delimiters=delimiters) + result = [] + for start, end, mode in tuples: + if mode == Mode.BOLD: + result.append(MessageEntityBold(start, end - start)) + elif mode == Mode.ITALIC: + result.append(MessageEntityItalic(start, end - start)) + elif mode == Mode.CODE: + result.append(MessageEntityCode(start, end - start)) + elif mode == Mode.PRE: + result.append(MessageEntityPre(start, end - start, '')) + return message, result From 9600a9ea0bfd522becd73af38b05aefac65e2f82 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 28 Oct 2017 19:17:18 +0200 Subject: [PATCH 07/93] Fix markdown parsing failing if delimiter was last character --- telethon/extensions/markdown.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index d3581cf9..2e5a899c 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -84,8 +84,10 @@ def parse(message, delimiters=None): current = Mode.NONE break - offset += emojiness(message[i]) - i += 1 + if i < len(message): + offset += emojiness(message[i]) + i += 1 + if result and not isinstance(result[-1], tuple): result.pop() return message, result From 368269cb11fc3b5944bb090f066ef66d4bd7ffaf Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 29 Oct 2017 16:33:10 +0100 Subject: [PATCH 08/93] Add ability to parse inline URLs --- telethon/extensions/markdown.py | 59 +++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index 2e5a899c..90ab9d99 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -7,7 +7,8 @@ import re from enum import Enum from ..tl.types import ( - MessageEntityBold, MessageEntityItalic, MessageEntityCode, MessageEntityPre + MessageEntityBold, MessageEntityItalic, MessageEntityCode, + MessageEntityPre, MessageEntityTextUrl ) @@ -18,6 +19,7 @@ class Mode(Enum): ITALIC = 2 CODE = 3 PRE = 4 + URL = 5 EMOJI_PATTERN = re.compile( @@ -48,12 +50,19 @@ def emojiness(char): return 3 -def parse(message, delimiters=None): +def parse(message, delimiters=None, url_re=r'\[(.+?)\]\((.+?)\)'): """ Parses the given message and returns the stripped message and a list of tuples containing (start, end, mode) using the specified delimiters dictionary (or default if None). + + The url_re(gex) must contain two matching groups: the text to be + clickable and the URL itself. """ + if url_re: + if isinstance(url_re, str): + url_re = re.compile(url_re) + if not delimiters: if delimiters is not None: return message, [] @@ -70,19 +79,35 @@ def parse(message, delimiters=None): offset = 0 i = 0 while i < len(message): - for d, m in delimiters.items(): - if message[i:i + len(d)] == d and current in (Mode.NONE, m): - if message[i + len(d):i + 2 * len(d)] == d: - continue # ignore two consecutive delimiters + if current == Mode.NONE: + url_match = url_re.match(message, pos=i) + if url_match: + message = ''.join(( + message[:url_match.start()], + url_match.group(1), + message[url_match.end():] + )) + emoji_len = sum(emojiness(c) for c in url_match.group(1)) + result.append(( + offset, + i + emoji_len, + (Mode.URL, url_match.group(2)) + )) + i += len(url_match.group(1)) + else: + for d, m in delimiters.items(): + if message[i:i + len(d)] == d and current in (Mode.NONE, m): + if message[i + len(d):i + 2 * len(d)] == d: + continue # ignore two consecutive delimiters - message = message[:i] + message[i + len(d):] - if current == Mode.NONE: - result.append(offset) - current = m - else: - result[-1] = (result[-1], offset, current) - current = Mode.NONE - break + message = message[:i] + message[i + len(d):] + if current == Mode.NONE: + result.append(offset) + current = m + else: + result[-1] = (result[-1], offset, current) + current = Mode.NONE + break if i < len(message): offset += emojiness(message[i]) @@ -98,6 +123,10 @@ def parse_tg(message, delimiters=None): message, tuples = parse(message, delimiters=delimiters) result = [] for start, end, mode in tuples: + extra = None + if isinstance(mode, tuple): + mode, extra = mode + if mode == Mode.BOLD: result.append(MessageEntityBold(start, end - start)) elif mode == Mode.ITALIC: @@ -106,4 +135,6 @@ def parse_tg(message, delimiters=None): result.append(MessageEntityCode(start, end - start)) elif mode == Mode.PRE: result.append(MessageEntityPre(start, end - start, '')) + elif mode == Mode.URL: + result.append(MessageEntityTextUrl(start, end - start, extra)) return message, result From f5fafc6a27bd782a1c6e360c49bdb1183db84e98 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 29 Oct 2017 16:41:30 +0100 Subject: [PATCH 09/93] Enhance emoji detection --- telethon/extensions/markdown.py | 29 ++++++--- telethon_generator/emoji_ranges.py | 101 +++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 9 deletions(-) create mode 100644 telethon_generator/emoji_ranges.py diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index 90ab9d99..fa33aace 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -22,19 +22,30 @@ class Mode(Enum): URL = 5 -EMOJI_PATTERN = re.compile( - '[' - '\U0001F600-\U0001F64F' # emoticons - '\U0001F300-\U0001F5FF' # symbols & pictographs - '\U0001F680-\U0001F6FF' # transport & map symbols - '\U0001F1E0-\U0001F1FF' # flags (iOS) - ']+', flags=re.UNICODE +# using telethon_generator/emoji_ranges.py +EMOJI_RANGES = ( + (8596, 8601), (8617, 8618), (8986, 8987), (9193, 9203), (9208, 9210), + (9642, 9643), (9723, 9726), (9728, 9733), (9735, 9746), (9748, 9751), + (9754, 9884), (9886, 9905), (9907, 9953), (9956, 9983), (9985, 9988), + (9992, 10002), (10035, 10036), (10067, 10069), (10083, 10087), + (10133, 10135), (10548, 10549), (11013, 11015), (11035, 11036), + (126976, 127166), (127169, 127183), (127185, 127231), (127245, 127247), + (127340, 127345), (127358, 127359), (127377, 127386), (127405, 127487), + (127489, 127503), (127538, 127546), (127548, 127551), (127561, 128419), + (128421, 128591), (128640, 128767), (128884, 128895), (128981, 129023), + (129036, 129039), (129096, 129103), (129114, 129119), (129160, 129167), + (129198, 129338), (129340, 129342), (129344, 129349), (129351, 129355), + (129357, 129471), (129473, 131069) ) def is_emoji(char): """Returns True if 'char' looks like an emoji""" - return bool(EMOJI_PATTERN.match(char)) + char = ord(char) + for start, end in EMOJI_RANGES: + if start <= char <= end: + return True + return False def emojiness(char): @@ -44,7 +55,7 @@ def emojiness(char): """ if not is_emoji(char): return 1 - if ord(char) < ord('🤐'): + if ord(char) < 129296: return 2 else: return 3 diff --git a/telethon_generator/emoji_ranges.py b/telethon_generator/emoji_ranges.py new file mode 100644 index 00000000..90597cf6 --- /dev/null +++ b/telethon_generator/emoji_ranges.py @@ -0,0 +1,101 @@ +""" +Simple module to allow fetching unicode.org emoji lists and printing a +Python-like tuple out of them. + +May not be accurate 100%, and is definitely not as efficient as it could be, +but it should only be ran whenever the Unicode consortium decides to add +new emojies to the list. +""" +import os +import sys +import re +import urllib.error +import urllib.request + + +def eprint(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + + +def get(url, enc='utf-8'): + try: + with urllib.request.urlopen(url) as f: + return f.read().decode(enc, errors='replace') + except urllib.error.HTTPError as e: + eprint('Caught', e, 'for', url, '; returning empty') + return '' + + +PREFIX_URL = 'http://unicode.org/Public/emoji/' +SUFFIX_URL = '/emoji-data.txt', '/emoji-sequences.txt' +VERSION_RE = re.compile(r'>(\d+.\d+)/<') +OUTPUT_TXT = 'emojies.txt' +CODEPOINT_RE = re.compile(r'([\da-fA-F]{3,}(?:[\s.]+[\da-fA-F]{3,}))') +EMOJI_START = 0x20e3 # emoji data has many more ranges, falling outside this +EMOJI_END = 200000 # from some tests those outside the range aren't emojies + + +versions = VERSION_RE.findall(get(PREFIX_URL)) +lines = [] +if not os.path.isfile(OUTPUT_TXT): + with open(OUTPUT_TXT, 'w') as f: + for version in versions: + for s in SUFFIX_URL: + url = PREFIX_URL + version + s + for line in get(url).split('\n'): + line = line.strip() + if not line or line.startswith('#'): + continue + m = CODEPOINT_RE.search(line) + if m and m.start() == 0: + f.write(m.group(1) + '\n') + + +points = set() +with open(OUTPUT_TXT) as f: + for line in f: + line = line.strip() + if ' ' in line: + for p in line.split(): + i = int(p, 16) + if i > 255: + points.add(i) + elif '.' in line: + s, e = line.split('..') + for i in range(int(s, 16), int(e, 16) + 1): + if i > 255: + points.add(i) + else: + i = int(line, 16) + if i > 255: + points.add(int(line, 16)) + + +ranges = [] +points = tuple(sorted(points)) +start = points[0] +last = start +for point in points: + if point - last > 1: + if start == last or not (EMOJI_START < start < EMOJI_END): + eprint( + 'Dropping', last - start + 1, + 'character(s) from', hex(start), ':', chr(start) + ) + else: + ranges.append((start, last)) + start = point + + last = point + + +if start == last or not (EMOJI_START < start < EMOJI_END): + eprint( + 'Dropping', last - start + 1, + 'character(s) from', hex(start), ':', chr(start) + ) +else: + ranges.append((start, last)) + + +print('EMOJI_RANGES = ({})'.format(', '.join(repr(r) for r in ranges))) From bcaa8007a3aee7485c31eb4eafb3161b9ac0f748 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 29 Oct 2017 16:43:30 +0100 Subject: [PATCH 10/93] Fix inline URL matching swallowing all parse entities --- telethon/extensions/markdown.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index fa33aace..13246f1e 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -90,6 +90,7 @@ def parse(message, delimiters=None, url_re=r'\[(.+?)\]\((.+?)\)'): offset = 0 i = 0 while i < len(message): + url_match = None if current == Mode.NONE: url_match = url_re.match(message, pos=i) if url_match: @@ -105,7 +106,7 @@ def parse(message, delimiters=None, url_re=r'\[(.+?)\]\((.+?)\)'): (Mode.URL, url_match.group(2)) )) i += len(url_match.group(1)) - else: + if not url_match: for d, m in delimiters.items(): if message[i:i + len(d)] == d and current in (Mode.NONE, m): if message[i + len(d):i + 2 * len(d)] == d: From d47a9f83d038b9462831a8bef641cfd6ae11bb3b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 29 Oct 2017 17:07:37 +0100 Subject: [PATCH 11/93] Fix some special cases which are not treated as emojis (offset 1) --- telethon/extensions/markdown.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index 13246f1e..99c7a25e 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -22,6 +22,37 @@ class Mode(Enum): URL = 5 +# TODO Special cases, these aren't count as emojies. Alternatives? +# These were determined by generating all emojies with EMOJI_RANGES, +# sending the message through an official application, and cherry-picking +# which ones weren't rendered as emojies (from the beginning one). I am +# not responsible for dropping those characters that did not render with +# my font. +NOT_EMOJIES = { + 9733, 9735, 9736, 9737, 9738, 9739, 9740, 9741, 9743, 9744, 9746, 9750, + 9751, 9754, 9755, 9756, 9758, 9759, 9761, 9764, 9765, 9767, 9768, 9769, + 9771, 9772, 9773, 9776, 9777, 9778, 9779, 9780, 9781, 9782, 9783, 9787, + 9788, 9789, 9790, 9791, 9792, 9793, 9794, 9795, 9796, 9797, 9798, 9799, + 9812, 9813, 9814, 9815, 9816, 9817, 9818, 9819, 9820, 9821, 9822, 9823, + 9825, 9826, 9828, 9831, 9833, 9834, 9835, 9836, 9837, 9838, 9839, 9840, + 9841, 9842, 9843, 9844, 9845, 9846, 9847, 9848, 9849, 9850, 9852, 9853, + 9854, 9856, 9857, 9858, 9859, 9860, 9861, 9862, 9863, 9864, 9865, 9866, + 9867, 9868, 9869, 9870, 9871, 9872, 9873, 9877, 9880, 9882, 9886, 9887, + 9890, 9891, 9892, 9893, 9894, 9895, 9896, 9897, 9900, 9901, 9902, 9903, + 9907, 9908, 9909, 9910, 9911, 9912, 9920, 9921, 9922, 9923, 9985, 9987, + 9988, 9998, 10000, 10001, 10085, 10086, 10087, 127027, 127028, 127029, + 127030, 127031, 127032, 127033, 127034, 127035, 127036, 127037, 127038, + 127039, 127040, 127041, 127042, 127043, 127044, 127045, 127046, 127047, + 127048, 127049, 127050, 127051, 127052, 127053, 127054, 127055, 127056, + 127057, 127058, 127059, 127060, 127061, 127062, 127063, 127064, 127065, + 127066, 127067, 127068, 127069, 127070, 127071, 127072, 127073, 127074, + 127075, 127076, 127077, 127078, 127079, 127080, 127081, 127082, 127083, + 127084, 127085, 127086, 127087, 127088, 127089, 127090, 127091, 127092, + 127093, 127094, 127095, 127096, 127097, 127098, 127099, 127100, 127101, + 127102, 127103, 127104, 127105, 127106, 127107, 127108, 127109, 127110, + 127111, 127112, 127113, 127114, 127115, 127116, 127117, 127118, 127119, + 127120, 127121, 127122, 127123 +} # using telethon_generator/emoji_ranges.py EMOJI_RANGES = ( (8596, 8601), (8617, 8618), (8986, 8987), (9193, 9203), (9208, 9210), @@ -44,7 +75,7 @@ def is_emoji(char): char = ord(char) for start, end in EMOJI_RANGES: if start <= char <= end: - return True + return char not in NOT_EMOJIES return False From 2609bd9bd17e4db4e1c6423dbf09d7bcce82bf5d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 29 Oct 2017 18:21:21 +0100 Subject: [PATCH 12/93] Use constants and allow empty URL regex when parsing markdown --- telethon/extensions/markdown.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index 99c7a25e..078736a2 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -70,6 +70,16 @@ EMOJI_RANGES = ( ) +DEFAULT_DELIMITERS = { + '**': Mode.BOLD, + '__': Mode.ITALIC, + '`': Mode.CODE, + '```': Mode.PRE +} + +DEFAULT_URL_RE = re.compile(r'\[(.+?)\]\((.+?)\)') + + def is_emoji(char): """Returns True if 'char' looks like an emoji""" char = ord(char) @@ -92,7 +102,7 @@ def emojiness(char): return 3 -def parse(message, delimiters=None, url_re=r'\[(.+?)\]\((.+?)\)'): +def parse(message, delimiters=None, url_re=None): """ Parses the given message and returns the stripped message and a list of tuples containing (start, end, mode) using the specified delimiters @@ -101,20 +111,16 @@ def parse(message, delimiters=None, url_re=r'\[(.+?)\]\((.+?)\)'): The url_re(gex) must contain two matching groups: the text to be clickable and the URL itself. """ - if url_re: + if url_re is None: + url_re = DEFAULT_URL_RE + elif url_re: if isinstance(url_re, str): url_re = re.compile(url_re) if not delimiters: if delimiters is not None: return message, [] - - delimiters = { - '**': Mode.BOLD, - '__': Mode.ITALIC, - '`': Mode.CODE, - '```': Mode.PRE - } + delimiters = DEFAULT_DELIMITERS result = [] current = Mode.NONE @@ -122,7 +128,7 @@ def parse(message, delimiters=None, url_re=r'\[(.+?)\]\((.+?)\)'): i = 0 while i < len(message): url_match = None - if current == Mode.NONE: + if url_re and current == Mode.NONE: url_match = url_re.match(message, pos=i) if url_match: message = ''.join(( From 6567f4b5674091f8282f9a642b8a1fd2db8738c9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 29 Oct 2017 20:10:29 +0100 Subject: [PATCH 13/93] Clean .download_contact and a wrong indent level --- telethon/telegram_client.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index c64051bf..5a8fb5a6 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -51,6 +51,7 @@ 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 - @@ -825,13 +826,9 @@ class TelegramClient(TelegramBareClient): 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('N:{};{};;;\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 - )) + f.write('TEL;TYPE=cell;VALUE=uri:tel:+{}\n'.format(phone_number)) f.write('END:VCARD\n') finally: # Only close the stream if we opened it @@ -1042,4 +1039,4 @@ class TelegramClient(TelegramBareClient): 'Make sure you have encountered this peer before.'.format(peer) ) - # endregion + # endregion From 05626c827477b21a6c200da613965e20fbacfaa0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 29 Oct 2017 20:13:00 +0100 Subject: [PATCH 14/93] Implement missing .to_dict() and .stringify() on message/container --- telethon/tl/message_container.py | 19 +++++++++++++++---- telethon/tl/tl_message.py | 14 ++++++++++++-- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/telethon/tl/message_container.py b/telethon/tl/message_container.py index 12d617cc..58fb8021 100644 --- a/telethon/tl/message_container.py +++ b/telethon/tl/message_container.py @@ -11,15 +11,20 @@ class MessageContainer(TLObject): self.content_related = False self.messages = messages + def to_dict(self, recursive=True): + return { + 'content_related': self.content_related, + 'messages': + ([] if self.messages is None else [ + None if x is None else x.to_dict() for x in self.messages + ]) if recursive else self.messages, + } + def __bytes__(self): return struct.pack( ' Date: Mon, 30 Oct 2017 10:33:45 +0100 Subject: [PATCH 15/93] Fix InputPeer* with None hash, drop them off database (closes #354) --- telethon/tl/entity_database.py | 20 +++++++++++++++++--- telethon/utils.py | 8 ++++---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/telethon/tl/entity_database.py b/telethon/tl/entity_database.py index b0fc70fb..0c92c75f 100644 --- a/telethon/tl/entity_database.py +++ b/telethon/tl/entity_database.py @@ -24,7 +24,12 @@ class EntityDatabase: self._entities = {} # marked_id: user|chat|channel if input_list: - self._input_entities = {k: v for k, v in input_list} + # TODO For compatibility reasons some sessions were saved with + # 'access_hash': null in the JSON session file. Drop these, as + # it means we don't have access to such InputPeers. Issue #354. + self._input_entities = { + k: v for k, v in input_list if v is not None + } else: self._input_entities = {} # marked_id: hash @@ -69,8 +74,17 @@ class EntityDatabase: try: p = utils.get_input_peer(e, allow_self=False) - new_input[utils.get_peer_id(p, add_mark=True)] = \ - getattr(p, 'access_hash', 0) # chats won't have hash + marked_id = utils.get_peer_id(p, add_mark=True) + + if isinstance(p, InputPeerChat): + # Chats don't have a hash + new_input[marked_id] = 0 + elif p.access_hash: + # Some users and channels seem to be returned without + # an 'access_hash', meaning Telegram doesn't want you + # to access them. This is the reason behind ensuring + # that the 'access_hash' is non-zero. See issue #354. + new_input[marked_id] = p.access_hash if self.enabled_full: if isinstance(e, (User, Chat, Channel)): diff --git a/telethon/utils.py b/telethon/utils.py index afb24b16..a05a4990 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -84,13 +84,13 @@ def get_input_peer(entity, allow_self=True): if entity.is_self and allow_self: return InputPeerSelf() else: - return InputPeerUser(entity.id, entity.access_hash) + return InputPeerUser(entity.id, entity.access_hash or 0) if isinstance(entity, (Chat, ChatEmpty, ChatForbidden)): return InputPeerChat(entity.id) if isinstance(entity, (Channel, ChannelForbidden)): - return InputPeerChannel(entity.id, entity.access_hash) + return InputPeerChannel(entity.id, entity.access_hash or 0) # Less common cases if isinstance(entity, UserEmpty): @@ -120,7 +120,7 @@ def get_input_channel(entity): return entity if isinstance(entity, (Channel, ChannelForbidden)): - return InputChannel(entity.id, entity.access_hash) + return InputChannel(entity.id, entity.access_hash or 0) if isinstance(entity, InputPeerChannel): return InputChannel(entity.channel_id, entity.access_hash) @@ -140,7 +140,7 @@ def get_input_user(entity): if entity.is_self: return InputUserSelf() else: - return InputUser(entity.id, entity.access_hash) + return InputUser(entity.id, entity.access_hash or 0) if isinstance(entity, InputPeerSelf): return InputUserSelf() From 0a14aa1bc6e95b47cb355c6a8c709645be04ab05 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 30 Oct 2017 10:56:39 +0100 Subject: [PATCH 16/93] Remove additional check when calculating emojies length This special check treated some emojies as 3 characters long but this shouldn't have actually been done, likely due to the old regex matching more things as emoji than it should (which would have count as 2 too, making up for 1+3 from the new is_emoji()). --- telethon/extensions/markdown.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index 078736a2..432b1452 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -91,15 +91,10 @@ def is_emoji(char): def emojiness(char): """ - Returns the "emojiness" of an emoji, or how many characters it counts as. - 1 if it's not an emoji, 2 usual, 3 "special" (seem to count more). + Returns 2 if the character is an emoji, or 1 otherwise. + This seems to be the length Telegram uses for offsets and lengths. """ - if not is_emoji(char): - return 1 - if ord(char) < 129296: - return 2 - else: - return 3 + return 2 if is_emoji(char) else 1 def parse(message, delimiters=None, url_re=None): From 82cac4836cc79a5cbb92613b15372ea0047a811b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 30 Oct 2017 11:15:53 +0100 Subject: [PATCH 17/93] Fix markdown URL parsing using character index instead offset --- telethon/extensions/markdown.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index 432b1452..574fe025 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -134,7 +134,7 @@ def parse(message, delimiters=None, url_re=None): emoji_len = sum(emojiness(c) for c in url_match.group(1)) result.append(( offset, - i + emoji_len, + offset + emoji_len, (Mode.URL, url_match.group(2)) )) i += len(url_match.group(1)) From 7e204632e26a109152a58b784e789aaafc70e5fc Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 30 Oct 2017 11:17:22 +0100 Subject: [PATCH 18/93] Add parse_mode parameter to TelegramClient.send_message() --- telethon/telegram_client.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index c64051bf..da248323 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -50,6 +50,8 @@ from .tl.types import ( UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage, PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel) from .tl.types.messages import DialogsSlice +from .extensions import markdown + class TelegramClient(TelegramBareClient): """Full featured TelegramClient meant to extend the basic functionality - @@ -347,21 +349,39 @@ class TelegramClient(TelegramBareClient): entity, message, reply_to=None, + parse_mode=None, link_preview=True): """ Sends the given message to the specified entity (user/chat/channel). - :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? + :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 str parse_mode: + Can be 'md' or 'markdown' for markdown-like parsing, in a similar + fashion how official clients work. + :param link_preview: + Should the link preview be shown? + :return Message: the sent message """ entity = self.get_input_entity(entity) + if parse_mode: + parse_mode = parse_mode.lower() + if parse_mode in {'md', 'markdown'}: + message, msg_entities = markdown.parse_tg(message) + else: + raise ValueError('Unknown parsing mode', parse_mode) + else: + msg_entities = [] + request = SendMessageRequest( peer=entity, message=message, - entities=[], + entities=msg_entities, no_webpage=not link_preview, reply_to_msg_id=self._get_reply_to(reply_to) ) From 3d6c8915e3250d3ec1b26faa73cc08ad1f811081 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 31 Oct 2017 12:48:55 +0100 Subject: [PATCH 19/93] Allow >100 limits when getting message history (implements #290) --- telethon/telegram_client.py | 110 ++++++++++++++++++++++++++---------- 1 file changed, 80 insertions(+), 30 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 9c911ddd..994a59cc 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1,4 +1,5 @@ import os +import time from datetime import datetime, timedelta from mimetypes import guess_type @@ -48,7 +49,8 @@ from .tl.types import ( Message, MessageMediaContact, MessageMediaDocument, MessageMediaPhoto, InputUserSelf, UserProfilePhoto, ChatPhoto, UpdateMessageID, UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage, - PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel) + PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty +) from .tl.types.messages import DialogsSlice from .extensions import markdown @@ -458,43 +460,91 @@ class TelegramClient(TelegramBareClient): """ Gets the message history for the specified entity - :param entity: The entity from whom to retrieve the message history - :param limit: Number of messages to be retrieved - :param offset_date: Offset date (messages *previous* to this date will be retrieved) - :param offset_id: Offset message ID (only messages *previous* to the given ID will be retrieved) - :param max_id: All the messages with a higher (newer) ID or equal to this will be excluded - :param min_id: All the messages with a lower (older) ID or equal to this will be excluded - :param add_offset: Additional message offset (all of the specified offsets + this offset = older messages) + :param entity: + The entity from whom to retrieve the message history. + :param limit: + Number of messages to be retrieved. Due to limitations with the API + retrieving more than 3000 messages will take longer than half a + minute (or even more based on previous calls). The limit may also + be None, which would eventually return the whole history. + :param offset_date: + Offset date (messages *previous* to this date will be retrieved). + :param offset_id: + Offset message ID (only messages *previous* to the given ID will + be retrieved). + :param max_id: + All the messages with a higher (newer) ID or equal to this will + be excluded + :param min_id: + All the messages with a lower (older) ID or equal to this will + be excluded. + :param add_offset: + Additional message offset + (all of the specified offsets + this offset = older messages). :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! """ - result = self(GetHistoryRequest( - peer=self.get_input_entity(entity), - limit=limit, - offset_date=offset_date, - offset_id=offset_id, - max_id=max_id, - min_id=min_id, - add_offset=add_offset - )) + limit = float('inf') if limit is None else int(limit) + total_messages = 0 + messages = [] + entities = {} + while len(messages) < limit: + # Telegram has a hard limit of 100 + real_limit = min(limit - len(messages), 100) + result = self(GetHistoryRequest( + peer=self.get_input_entity(entity), + limit=real_limit, + offset_date=offset_date, + offset_id=offset_id, + max_id=max_id, + min_id=min_id, + add_offset=add_offset + )) + messages.extend( + m for m in result.messages if not isinstance(m, MessageEmpty) + ) + total_messages = getattr(result, 'count', len(result.messages)) - # The result may be a messages slice (not all messages were retrieved) - # or simply a messages TLObject. In the later case, no "count" - # attribute is specified, so the total messages count is simply - # the count of retrieved messages - total_messages = getattr(result, 'count', len(result.messages)) + # TODO We can potentially use self.session.database, but since + # it might be disabled, use a local dictionary. + for u in result.users: + entities[utils.get_peer_id(u, add_mark=True)] = u + for c in result.chats: + entities[utils.get_peer_id(c, add_mark=True)] = c - # Iterate over all the messages and find the sender User - entities = [ - utils.find_user_or_chat(m.from_id, result.users, result.chats) - if m.from_id is not None else - utils.find_user_or_chat(m.to_id, result.users, result.chats) + if len(result.messages) < real_limit: + break - for m in result.messages - ] + offset_id = result.messages[-1].id + offset_date = result.messages[-1].date - return total_messages, result.messages, entities + # Telegram limit seems to be 3000 messages within 30 seconds in + # batches of 100 messages each request (since the FloodWait was + # of 30 seconds). If the limit is greater than that, we will + # sleep 1s between each request. + if limit > 3000: + time.sleep(1) + + # In a new list with the same length as the messages append + # their senders, so people can zip(messages, senders). + senders = [] + for m in messages: + if m.from_id: + who = entities[utils.get_peer_id(m.from_id, add_mark=True)] + elif getattr(m, 'fwd_from', None): + # .from_id is optional, so this is the sanest fallback. + who = entities[utils.get_peer_id( + m.fwd_from.from_id or m.fwd_from.channel_id, + add_mark=True + )] + else: + # If there's not even a FwdHeader, fallback to the sender + # being where the message was sent. + who = entities[utils.get_peer_id(m.to_id, add_mark=True)] + senders.append(who) + + return total_messages, messages, senders def send_read_acknowledge(self, entity, messages=None, max_id=None): """ From 9a12738f0ed60f814c50bbc856032a20876f8d3b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 31 Oct 2017 13:52:43 +0100 Subject: [PATCH 20/93] Fix .get_message_history not working with limit=0 --- telethon/telegram_client.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 994a59cc..0452b72b 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -485,7 +485,16 @@ 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! """ + entity = self.get_input_entity(entity) limit = float('inf') if limit is None else int(limit) + if limit == 0: + # No messages, but we still need to know the total message count + result = self(GetHistoryRequest( + peer=self.get_input_entity(entity), limit=1, + offset_date=None, offset_id=0, max_id=0, min_id=0, add_offset=0 + )) + return getattr(result, 'count', len(result.messages)), [], [] + total_messages = 0 messages = [] entities = {} @@ -493,7 +502,7 @@ class TelegramClient(TelegramBareClient): # Telegram has a hard limit of 100 real_limit = min(limit - len(messages), 100) result = self(GetHistoryRequest( - peer=self.get_input_entity(entity), + peer=entity, limit=real_limit, offset_date=offset_date, offset_id=offset_id, From 0bfd8ff0320a260c2c937f8c5ce5e923bef20d43 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 3 Nov 2017 12:59:17 +0100 Subject: [PATCH 21/93] Add much faster integer factorization (#403 related to #199) --- telethon/crypto/factorization.py | 94 ++++++++++++-------------------- 1 file changed, 34 insertions(+), 60 deletions(-) diff --git a/telethon/crypto/factorization.py b/telethon/crypto/factorization.py index 69c8dcc9..359887d3 100644 --- a/telethon/crypto/factorization.py +++ b/telethon/crypto/factorization.py @@ -1,71 +1,45 @@ from random import randint -try: - import sympy.ntheory -except ImportError: - sympy = None class Factorization: - @staticmethod - def find_small_multiplier_lopatin(what): - """Finds the small multiplier by using Lopatin's method""" - g = 0 - for i in range(3): - q = (randint(0, 127) & 15) + 17 - x = randint(0, 1000000000) + 1 - y = x - lim = 1 << (i + 18) - for j in range(1, lim): - a, b, c = x, x, q - while b != 0: - if (b & 1) != 0: - c += a - if c >= what: - c -= what - a += a - if a >= what: - a -= what - b >>= 1 + @classmethod + def factorize(cls, pq): + if pq % 2 == 0: + return 2, pq // 2 - x = c - z = y - x if x < y else x - y - g = Factorization.gcd(z, what) - if g != 1: + y, c, m = randint(1, pq - 1), randint(1, pq - 1), randint(1, pq - 1) + g = r = q = 1 + x = ys = 0 + + while g == 1: + x = y + for i in range(r): + y = (pow(y, 2, pq) + c) % pq + + k = 0 + while k < r and g == 1: + ys = y + for i in range(min(m, r - k)): + y = (pow(y, 2, pq) + c) % pq + q = q * (abs(x - y)) % pq + + g = cls.gcd(q, pq) + k += m + + r *= 2 + + if g == pq: + while True: + ys = (pow(ys, 2, pq) + c) % pq + g = cls.gcd(abs(x - ys), pq) + if g > 1: break - if (j & (j - 1)) == 0: - y = x - - if g > 1: - break - - p = what // g - return min(p, g) + return g, pq // g @staticmethod def gcd(a, b): - """Calculates the greatest common divisor""" - while a != 0 and b != 0: - while b & 1 == 0: - b >>= 1 + while b: + a, b = b, a % b - while a & 1 == 0: - a >>= 1 - - if a > b: - a -= b - else: - b -= a - - return a if b == 0 else b - - @staticmethod - def factorize(pq): - """Factorizes the given number and returns both - the divisor and the number divided by the divisor - """ - if sympy: - return tuple(sympy.ntheory.factorint(pq).keys()) - else: - divisor = Factorization.find_small_multiplier_lopatin(pq) - return divisor, pq // divisor + return a From 1741608f2867182cd42f0fed9ecf8e89a0ef9602 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 4 Nov 2017 12:34:44 +0100 Subject: [PATCH 22/93] Use larger batches for .get_dialogs(limit=None) --- telethon/telegram_client.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 0452b72b..c638c2b1 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -279,22 +279,21 @@ class TelegramClient(TelegramBareClient): The peer to be used as an offset. :return: A tuple of lists ([dialogs], [entities]). """ - if limit is None: - limit = float('inf') + limit = float('inf') if limit is None else int(limit) + if limit == 0: + return [], [] dialogs = {} # Use peer id as identifier to avoid dupes messages = {} # Used later for sorting TODO also return these? entities = {} while len(dialogs) < limit: - need = limit - len(dialogs) + real_limit = min(limit - len(dialogs), 100) r = self(GetDialogsRequest( offset_date=offset_date, offset_id=offset_id, offset_peer=offset_peer, - limit=need if need < float('inf') else 0 + limit=real_limit )) - if not r.dialogs: - break for d in r.dialogs: dialogs[utils.get_peer_id(d.peer, True)] = d @@ -307,8 +306,9 @@ class TelegramClient(TelegramBareClient): for c in r.chats: entities[c.id] = c - if not isinstance(r, DialogsSlice): - # Don't enter next iteration if we already got all + if len(r.dialogs) < real_limit or not isinstance(r, DialogsSlice): + # Less than we requested means we reached the end, or + # we didn't get a DialogsSlice which means we got all. break offset_date = r.messages[-1].date From 1e35c1cfed96d27486c8c6f65a570f86d3b5bb0e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 4 Nov 2017 13:40:43 +0100 Subject: [PATCH 23/93] Update to layer 72 --- telethon_generator/scheme.tl | 41 +++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/telethon_generator/scheme.tl b/telethon_generator/scheme.tl index 5e949239..ae6544a1 100644 --- a/telethon_generator/scheme.tl +++ b/telethon_generator/scheme.tl @@ -161,12 +161,13 @@ inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia; inputMediaContact#a6e45987 phone_number:string first_name:string last_name:string = InputMedia; inputMediaUploadedDocument#e39621fd flags:# file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector caption:string stickers:flags.0?Vector ttl_seconds:flags.1?int = InputMedia; inputMediaDocument#5acb668e flags:# id:InputDocument caption:string ttl_seconds:flags.0?int = InputMedia; -inputMediaVenue#2827a81a geo_point:InputGeoPoint title:string address:string provider:string venue_id:string = InputMedia; +inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string = InputMedia; inputMediaGifExternal#4843b0fd url:string q:string = InputMedia; inputMediaPhotoExternal#922aec1 flags:# url:string caption:string ttl_seconds:flags.0?int = InputMedia; inputMediaDocumentExternal#b6f74335 flags:# url:string caption:string ttl_seconds:flags.0?int = InputMedia; inputMediaGame#d33f43f3 id:InputGame = InputMedia; inputMediaInvoice#92153685 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string start_param:string = InputMedia; +inputMediaGeoLive#7b1a118f geo_point:InputGeoPoint period:int = InputMedia; inputChatPhotoEmpty#1ca48f57 = InputChatPhoto; inputChatUploadedPhoto#927c55b4 file:InputFile = InputChatPhoto; @@ -222,7 +223,7 @@ channel#cb44b1c flags:# creator:flags.0?true left:flags.2?true editor:flags.3?tr channelForbidden#289da732 flags:# broadcast:flags.5?true megagroup:flags.8?true id:int access_hash:long title:string until_date:flags.16?int = Chat; chatFull#2e02a614 id:int participants:ChatParticipants chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector = ChatFull; -channelFull#17f45fcf flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet = ChatFull; +channelFull#76af5481 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int = ChatFull; chatParticipant#c8d7493e user_id:int inviter_id:int date:int = ChatParticipant; chatParticipantCreator#da13538a user_id:int = ChatParticipant; @@ -245,9 +246,10 @@ messageMediaContact#5e7d2f39 phone_number:string first_name:string last_name:str messageMediaUnsupported#9f84f49e = MessageMedia; messageMediaDocument#7c4414d3 flags:# document:flags.0?Document caption:flags.1?string ttl_seconds:flags.2?int = MessageMedia; messageMediaWebPage#a32dd600 webpage:WebPage = MessageMedia; -messageMediaVenue#7912b71f geo:GeoPoint title:string address:string provider:string venue_id:string = MessageMedia; +messageMediaVenue#2ec0533f geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MessageMedia; messageMediaGame#fdb19008 game:Game = MessageMedia; messageMediaInvoice#84551347 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument receipt_msg_id:flags.2?int currency:string total_amount:long start_param:string = MessageMedia; +messageMediaGeoLive#7c3c2609 geo:GeoPoint period:int = MessageMedia; messageActionEmpty#b6aef7b0 = MessageAction; messageActionChatCreate#a6638b9a title:string users:Vector = MessageAction; @@ -267,6 +269,7 @@ messageActionPaymentSentMe#8f31b327 flags:# currency:string total_amount:long pa messageActionPaymentSent#40699cd0 currency:string total_amount:long = MessageAction; messageActionPhoneCall#80e11a7f flags:# call_id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = MessageAction; messageActionScreenshotTaken#4792929b = MessageAction; +messageActionCustomAction#fae69f56 message:string = MessageAction; dialog#e4def5db flags:# pinned:flags.2?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage = Dialog; @@ -363,6 +366,8 @@ inputMessagesFilterPhoneCalls#80c99768 flags:# missed:flags.0?true = MessagesFil inputMessagesFilterRoundVoice#7a7c17a4 = MessagesFilter; inputMessagesFilterRoundVideo#b549da53 = MessagesFilter; inputMessagesFilterMyMentions#c1f8e69a = MessagesFilter; +inputMessagesFilterContacts#e062db83 = MessagesFilter; +inputMessagesFilterGeo#e7026d0d = MessagesFilter; updateNewMessage#1f2b0afd message:Message pts:int pts_count:int = Update; updateMessageID#4e90bfd6 id:int random_id:long = Update; @@ -429,6 +434,7 @@ updateLangPack#56022f4d difference:LangPackDifference = Update; updateFavedStickers#e511996d = Update; updateChannelReadMessagesContents#89893b45 channel_id:int messages:Vector = Update; updateContactsReset#7084a7be = Update; +updateChannelAvailableMessages#70db6837 channel_id:int available_min_id:int = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -455,7 +461,7 @@ upload.fileCdnRedirect#ea52fe5a dc_id:int file_token:bytes encryption_key:bytes dcOption#5d8c6cc flags:# ipv6:flags.0?true media_only:flags.1?true tcpo_only:flags.2?true cdn:flags.3?true static:flags.4?true id:int ip_address:string port:int = DcOption; -config#8df376a4 flags:# phonecalls_enabled:flags.1?true date:int expires:int test_mode:Bool this_dc:int dc_options:Vector chat_size_max:int megagroup_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int chat_big_size:int push_chat_period_ms:int push_chat_limit:int saved_gifs_limit:int edit_time_limit:int rating_e_decay:int stickers_recent_limit:int stickers_faved_limit:int tmp_sessions:flags.0?int pinned_dialogs_count_max:int call_receive_timeout_ms:int call_ring_timeout_ms:int call_connect_timeout_ms:int call_packet_timeout_ms:int me_url_prefix:string suggested_lang_code:flags.2?string lang_pack_version:flags.2?int disabled_features:Vector = Config; +config#9c840964 flags:# phonecalls_enabled:flags.1?true date:int expires:int test_mode:Bool this_dc:int dc_options:Vector chat_size_max:int megagroup_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int chat_big_size:int push_chat_period_ms:int push_chat_limit:int saved_gifs_limit:int edit_time_limit:int rating_e_decay:int stickers_recent_limit:int stickers_faved_limit:int channels_read_media_period:int tmp_sessions:flags.0?int pinned_dialogs_count_max:int call_receive_timeout_ms:int call_ring_timeout_ms:int call_connect_timeout_ms:int call_packet_timeout_ms:int me_url_prefix:string suggested_lang_code:flags.2?string lang_pack_version:flags.2?int disabled_features:Vector = Config; nearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc; @@ -665,6 +671,7 @@ channelParticipantsBanned#1427a5e1 q:string = ChannelParticipantsFilter; channelParticipantsSearch#656ac4b q:string = ChannelParticipantsFilter; channels.channelParticipants#f56ee2a8 count:int participants:Vector users:Vector = channels.ChannelParticipants; +channels.channelParticipantsNotModified#f0173fe9 = channels.ChannelParticipants; channels.channelParticipant#d0d9b163 participant:ChannelParticipant users:Vector = channels.ChannelParticipant; @@ -680,7 +687,7 @@ messages.savedGifs#2e0709a5 hash:int gifs:Vector = messages.SavedGifs; inputBotInlineMessageMediaAuto#292fed13 flags:# caption:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageText#3dcd7a87 flags:# no_webpage:flags.0?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; -inputBotInlineMessageMediaGeo#f4a59de1 flags:# geo_point:InputGeoPoint reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; +inputBotInlineMessageMediaGeo#c1b15d65 flags:# geo_point:InputGeoPoint period:int reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaVenue#aaafadc8 flags:# geo_point:InputGeoPoint title:string address:string provider:string venue_id:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaContact#2daf01a7 flags:# phone_number:string first_name:string last_name:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageGame#4b425864 flags:# reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; @@ -692,14 +699,14 @@ inputBotInlineResultGame#4fa417f2 id:string short_name:string send_message:Input botInlineMessageMediaAuto#a74b15b flags:# caption:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageText#8c7f65e2 flags:# no_webpage:flags.0?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = BotInlineMessage; -botInlineMessageMediaGeo#3a8fd8b8 flags:# geo:GeoPoint reply_markup:flags.2?ReplyMarkup = BotInlineMessage; +botInlineMessageMediaGeo#b722de65 flags:# geo:GeoPoint period:int reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaVenue#4366232e flags:# geo:GeoPoint title:string address:string provider:string venue_id:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaContact#35edb4d4 flags:# phone_number:string first_name:string last_name:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineResult#9bebaeb9 flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb_url:flags.4?string content_url:flags.5?string content_type:flags.5?string w:flags.6?int h:flags.6?int duration:flags.7?int send_message:BotInlineMessage = BotInlineResult; botInlineMediaResult#17db940b flags:# id:string type:string photo:flags.0?Photo document:flags.1?Document title:flags.2?string description:flags.3?string send_message:BotInlineMessage = BotInlineResult; -messages.botResults#ccd3563d flags:# gallery:flags.0?true query_id:long next_offset:flags.1?string switch_pm:flags.2?InlineBotSwitchPM results:Vector cache_time:int = messages.BotResults; +messages.botResults#947ca848 flags:# gallery:flags.0?true query_id:long next_offset:flags.1?string switch_pm:flags.2?InlineBotSwitchPM results:Vector cache_time:int users:Vector = messages.BotResults; exportedMessageLink#1f486803 link:string = ExportedMessageLink; @@ -903,6 +910,7 @@ channelAdminLogEventActionParticipantInvite#e31c34d8 participant:ChannelParticip channelAdminLogEventActionParticipantToggleBan#e6d83d7e prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction; channelAdminLogEventActionParticipantToggleAdmin#d5676710 prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction; channelAdminLogEventActionChangeStickerSet#b1c3caa7 prev_stickerset:InputStickerSet new_stickerset:InputStickerSet = ChannelAdminLogEventAction; +channelAdminLogEventActionTogglePreHistoryHidden#5f5c95f1 new_value:Bool = ChannelAdminLogEventAction; channelAdminLogEvent#3b5a3e40 id:long date:int user_id:int action:ChannelAdminLogEventAction = ChannelAdminLogEvent; @@ -917,6 +925,14 @@ cdnFileHash#77eec38f offset:int limit:int hash:bytes = CdnFileHash; messages.favedStickersNotModified#9e8fa6d3 = messages.FavedStickers; messages.favedStickers#f37f2f16 hash:int packs:Vector stickers:Vector = messages.FavedStickers; +help.recentMeUrls#e0310d7 urls:Vector chats:Vector users:Vector = help.RecentMeUrls; + +recentMeUrlUser#8dbc3336 url:string user_id:int = RecentMeUrl; +recentMeUrlChat#a01b22f9 url:string chat_id:int = RecentMeUrl; +recentMeUrlStickerSet#bc0a57dc url:string set:StickerSetCovered = RecentMeUrl; +recentMeUrlChatInvite#eb49081d url:string chat_invite:ChatInvite = RecentMeUrl; +recentMeUrlUnknown#46e1d13d url:string = RecentMeUrl; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1048,7 +1064,7 @@ messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_p messages.setInlineBotResults#eb5ea206 flags:# gallery:flags.0?true private:flags.1?true query_id:long results:Vector cache_time:int next_offset:flags.2?string switch_pm:flags.3?InlineBotSwitchPM = Bool; messages.sendInlineBotResult#b16e06fe flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int random_id:long query_id:long id:string = Updates; messages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData; -messages.editMessage#ce91e4ca flags:# no_webpage:flags.1?true peer:InputPeer id:int message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Updates; +messages.editMessage#5d1b8dd flags:# no_webpage:flags.1?true stop_geo_live:flags.12?true peer:InputPeer id:int message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector geo_point:flags.13?InputGeoPoint = Updates; messages.editInlineBotMessage#130c2c85 flags:# no_webpage:flags.1?true id:InputBotInlineMessageID message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Bool; messages.getBotCallbackAnswer#810a9fec flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes = messages.BotCallbackAnswer; messages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long message:flags.0?string url:flags.2?string cache_time:int = Bool; @@ -1080,6 +1096,8 @@ messages.sendScreenshotNotification#c97df020 peer:InputPeer reply_to_msg_id:int messages.getFavedStickers#21ce0b0e hash:int = messages.FavedStickers; messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; messages.getUnreadMentions#46578472 peer:InputPeer offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; +messages.getRecentLocations#249431e2 peer:InputPeer limit:int = messages.Messages; +messages.readMentions#f0189d3 peer:InputPeer = messages.AffectedHistory; updates.getState#edd4882a = updates.State; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; @@ -1108,13 +1126,14 @@ help.getAppChangelog#9010ef6f prev_app_version:string = Updates; help.getTermsOfService#350170f3 = help.TermsOfService; help.setBotUpdatesStatus#ec22cfcd pending_updates_count:int message:string = Bool; help.getCdnConfig#52029342 = CdnConfig; +help.getRecentMeUrls#3dc0f114 referer:string = help.RecentMeUrls; channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool; channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector = messages.AffectedMessages; channels.deleteUserHistory#d10dd71b channel:InputChannel user_id:InputUser = messages.AffectedHistory; channels.reportSpam#fe087810 channel:InputChannel user_id:InputUser id:Vector = Bool; channels.getMessages#93d7b347 channel:InputChannel id:Vector = messages.Messages; -channels.getParticipants#24d98f92 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int = channels.ChannelParticipants; +channels.getParticipants#123e05e9 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int hash:int = channels.ChannelParticipants; channels.getParticipant#546dd7a6 channel:InputChannel user_id:InputUser = channels.ChannelParticipant; channels.getChannels#a7f6bbb id:Vector = messages.Chats; channels.getFullChannel#8736a09 channel:InputChannel = messages.ChatFull; @@ -1139,6 +1158,8 @@ channels.editBanned#bfd915cd channel:InputChannel user_id:InputUser banned_right channels.getAdminLog#33ddf480 flags:# channel:InputChannel q:string events_filter:flags.0?ChannelAdminLogEventsFilter admins:flags.1?Vector max_id:long min_id:long limit:int = channels.AdminLogResults; channels.setStickers#ea8ca4f9 channel:InputChannel stickerset:InputStickerSet = Bool; channels.readMessageContents#eab5dc38 channel:InputChannel id:Vector = Bool; +channels.deleteHistory#af369d42 channel:InputChannel max_id:int = Bool; +channels.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Updates; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; @@ -1169,4 +1190,4 @@ langpack.getStrings#2e1ee318 lang_code:string keys:Vector = Vector; -// LAYER 71 +// LAYER 72 From c8a0953f8e6c22ce17101e2590b1941d8754158c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 4 Nov 2017 13:40:56 +0100 Subject: [PATCH 24/93] Update to v0.15.4 --- telethon/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/version.py b/telethon/version.py index 2bde1b96..bafdcc72 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.15.3' +__version__ = '0.15.4' From f381b267903cad2b7e208a5006a7cd10a02b6eb1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 4 Nov 2017 20:46:02 +0100 Subject: [PATCH 25/93] Add optional force_sms parameter to .send_code_request() --- telethon/telegram_client.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index c638c2b1..59bf4a9a 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -23,7 +23,7 @@ from .tl.functions.account import ( ) from .tl.functions.auth import ( CheckPasswordRequest, LogOutRequest, SendCodeRequest, SignInRequest, - SignUpRequest, ImportBotAuthorizationRequest + SignUpRequest, ResendCodeRequest, ImportBotAuthorizationRequest ) from .tl.functions.contacts import ( GetContactsRequest, ResolveUsernameRequest @@ -129,16 +129,29 @@ class TelegramClient(TelegramBareClient): # region Authorization requests - def send_code_request(self, phone): + def send_code_request(self, phone, force_sms=False): """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. + :param str | int phone: + The phone to which the code will be sent. + :param bool force_sms: + Whether to force sending as SMS. You should call it at least + once before without this set to True first. + :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)) + if force_sms: + if not self._phone_code_hash: + raise ValueError( + 'You must call this method without force_sms at least once.' + ) + result = self(ResendCodeRequest(phone, self._phone_code_hash)) + else: + result = self(SendCodeRequest(phone, self.api_id, self.api_hash)) + self._phone_code_hash = result.phone_code_hash + self._phone = phone - self._phone_code_hash = result.phone_code_hash return result def sign_in(self, phone=None, code=None, From 49eb2812516cd660465c26143a3053ea379f417f Mon Sep 17 00:00:00 2001 From: Viktor Oreshkin Date: Mon, 6 Nov 2017 03:17:22 +0400 Subject: [PATCH 26/93] Proper offset calculation for markdown (#407) Dan suca If Dan shared it with Traitor I'll not have to spend my time on this Not a, sorry for not letting you sleep k thx bye Will this stay in history? --- telethon/extensions/markdown.py | 75 +++------------------------------ 1 file changed, 6 insertions(+), 69 deletions(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index 574fe025..d4f7ce22 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -11,6 +11,8 @@ from ..tl.types import ( MessageEntityPre, MessageEntityTextUrl ) +def tg_string_len(s): + return len(s.encode('utf-16le')) // 2 class Mode(Enum): """Different modes supported by Telegram's Markdown""" @@ -22,54 +24,6 @@ class Mode(Enum): URL = 5 -# TODO Special cases, these aren't count as emojies. Alternatives? -# These were determined by generating all emojies with EMOJI_RANGES, -# sending the message through an official application, and cherry-picking -# which ones weren't rendered as emojies (from the beginning one). I am -# not responsible for dropping those characters that did not render with -# my font. -NOT_EMOJIES = { - 9733, 9735, 9736, 9737, 9738, 9739, 9740, 9741, 9743, 9744, 9746, 9750, - 9751, 9754, 9755, 9756, 9758, 9759, 9761, 9764, 9765, 9767, 9768, 9769, - 9771, 9772, 9773, 9776, 9777, 9778, 9779, 9780, 9781, 9782, 9783, 9787, - 9788, 9789, 9790, 9791, 9792, 9793, 9794, 9795, 9796, 9797, 9798, 9799, - 9812, 9813, 9814, 9815, 9816, 9817, 9818, 9819, 9820, 9821, 9822, 9823, - 9825, 9826, 9828, 9831, 9833, 9834, 9835, 9836, 9837, 9838, 9839, 9840, - 9841, 9842, 9843, 9844, 9845, 9846, 9847, 9848, 9849, 9850, 9852, 9853, - 9854, 9856, 9857, 9858, 9859, 9860, 9861, 9862, 9863, 9864, 9865, 9866, - 9867, 9868, 9869, 9870, 9871, 9872, 9873, 9877, 9880, 9882, 9886, 9887, - 9890, 9891, 9892, 9893, 9894, 9895, 9896, 9897, 9900, 9901, 9902, 9903, - 9907, 9908, 9909, 9910, 9911, 9912, 9920, 9921, 9922, 9923, 9985, 9987, - 9988, 9998, 10000, 10001, 10085, 10086, 10087, 127027, 127028, 127029, - 127030, 127031, 127032, 127033, 127034, 127035, 127036, 127037, 127038, - 127039, 127040, 127041, 127042, 127043, 127044, 127045, 127046, 127047, - 127048, 127049, 127050, 127051, 127052, 127053, 127054, 127055, 127056, - 127057, 127058, 127059, 127060, 127061, 127062, 127063, 127064, 127065, - 127066, 127067, 127068, 127069, 127070, 127071, 127072, 127073, 127074, - 127075, 127076, 127077, 127078, 127079, 127080, 127081, 127082, 127083, - 127084, 127085, 127086, 127087, 127088, 127089, 127090, 127091, 127092, - 127093, 127094, 127095, 127096, 127097, 127098, 127099, 127100, 127101, - 127102, 127103, 127104, 127105, 127106, 127107, 127108, 127109, 127110, - 127111, 127112, 127113, 127114, 127115, 127116, 127117, 127118, 127119, - 127120, 127121, 127122, 127123 -} -# using telethon_generator/emoji_ranges.py -EMOJI_RANGES = ( - (8596, 8601), (8617, 8618), (8986, 8987), (9193, 9203), (9208, 9210), - (9642, 9643), (9723, 9726), (9728, 9733), (9735, 9746), (9748, 9751), - (9754, 9884), (9886, 9905), (9907, 9953), (9956, 9983), (9985, 9988), - (9992, 10002), (10035, 10036), (10067, 10069), (10083, 10087), - (10133, 10135), (10548, 10549), (11013, 11015), (11035, 11036), - (126976, 127166), (127169, 127183), (127185, 127231), (127245, 127247), - (127340, 127345), (127358, 127359), (127377, 127386), (127405, 127487), - (127489, 127503), (127538, 127546), (127548, 127551), (127561, 128419), - (128421, 128591), (128640, 128767), (128884, 128895), (128981, 129023), - (129036, 129039), (129096, 129103), (129114, 129119), (129160, 129167), - (129198, 129338), (129340, 129342), (129344, 129349), (129351, 129355), - (129357, 129471), (129473, 131069) -) - - DEFAULT_DELIMITERS = { '**': Mode.BOLD, '__': Mode.ITALIC, @@ -79,24 +33,6 @@ DEFAULT_DELIMITERS = { DEFAULT_URL_RE = re.compile(r'\[(.+?)\]\((.+?)\)') - -def is_emoji(char): - """Returns True if 'char' looks like an emoji""" - char = ord(char) - for start, end in EMOJI_RANGES: - if start <= char <= end: - return char not in NOT_EMOJIES - return False - - -def emojiness(char): - """ - Returns 2 if the character is an emoji, or 1 otherwise. - This seems to be the length Telegram uses for offsets and lengths. - """ - return 2 if is_emoji(char) else 1 - - def parse(message, delimiters=None, url_re=None): """ Parses the given message and returns the stripped message and a list @@ -131,10 +67,10 @@ def parse(message, delimiters=None, url_re=None): url_match.group(1), message[url_match.end():] )) - emoji_len = sum(emojiness(c) for c in url_match.group(1)) + result.append(( offset, - offset + emoji_len, + offset + tg_string_len(url_match.group(1)), (Mode.URL, url_match.group(2)) )) i += len(url_match.group(1)) @@ -154,11 +90,12 @@ def parse(message, delimiters=None, url_re=None): break if i < len(message): - offset += emojiness(message[i]) + offset += tg_string_len(message[i]) i += 1 if result and not isinstance(result[-1], tuple): result.pop() + return message, result From e8248b4b8be0097f106eb51660e693b734741ba4 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 6 Nov 2017 09:36:54 +0100 Subject: [PATCH 27/93] Remove now unused Emoji ranges generator --- telethon_generator/emoji_ranges.py | 101 ----------------------------- 1 file changed, 101 deletions(-) delete mode 100644 telethon_generator/emoji_ranges.py diff --git a/telethon_generator/emoji_ranges.py b/telethon_generator/emoji_ranges.py deleted file mode 100644 index 90597cf6..00000000 --- a/telethon_generator/emoji_ranges.py +++ /dev/null @@ -1,101 +0,0 @@ -""" -Simple module to allow fetching unicode.org emoji lists and printing a -Python-like tuple out of them. - -May not be accurate 100%, and is definitely not as efficient as it could be, -but it should only be ran whenever the Unicode consortium decides to add -new emojies to the list. -""" -import os -import sys -import re -import urllib.error -import urllib.request - - -def eprint(*args, **kwargs): - print(*args, file=sys.stderr, **kwargs) - - -def get(url, enc='utf-8'): - try: - with urllib.request.urlopen(url) as f: - return f.read().decode(enc, errors='replace') - except urllib.error.HTTPError as e: - eprint('Caught', e, 'for', url, '; returning empty') - return '' - - -PREFIX_URL = 'http://unicode.org/Public/emoji/' -SUFFIX_URL = '/emoji-data.txt', '/emoji-sequences.txt' -VERSION_RE = re.compile(r'>(\d+.\d+)/<') -OUTPUT_TXT = 'emojies.txt' -CODEPOINT_RE = re.compile(r'([\da-fA-F]{3,}(?:[\s.]+[\da-fA-F]{3,}))') -EMOJI_START = 0x20e3 # emoji data has many more ranges, falling outside this -EMOJI_END = 200000 # from some tests those outside the range aren't emojies - - -versions = VERSION_RE.findall(get(PREFIX_URL)) -lines = [] -if not os.path.isfile(OUTPUT_TXT): - with open(OUTPUT_TXT, 'w') as f: - for version in versions: - for s in SUFFIX_URL: - url = PREFIX_URL + version + s - for line in get(url).split('\n'): - line = line.strip() - if not line or line.startswith('#'): - continue - m = CODEPOINT_RE.search(line) - if m and m.start() == 0: - f.write(m.group(1) + '\n') - - -points = set() -with open(OUTPUT_TXT) as f: - for line in f: - line = line.strip() - if ' ' in line: - for p in line.split(): - i = int(p, 16) - if i > 255: - points.add(i) - elif '.' in line: - s, e = line.split('..') - for i in range(int(s, 16), int(e, 16) + 1): - if i > 255: - points.add(i) - else: - i = int(line, 16) - if i > 255: - points.add(int(line, 16)) - - -ranges = [] -points = tuple(sorted(points)) -start = points[0] -last = start -for point in points: - if point - last > 1: - if start == last or not (EMOJI_START < start < EMOJI_END): - eprint( - 'Dropping', last - start + 1, - 'character(s) from', hex(start), ':', chr(start) - ) - else: - ranges.append((start, last)) - start = point - - last = point - - -if start == last or not (EMOJI_START < start < EMOJI_END): - eprint( - 'Dropping', last - start + 1, - 'character(s) from', hex(start), ':', chr(start) - ) -else: - ranges.append((start, last)) - - -print('EMOJI_RANGES = ({})'.format(', '.join(repr(r) for r in ranges))) From 4f8042921571779ba6f163a33ffd62272a2230fa Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 6 Nov 2017 10:29:32 +0100 Subject: [PATCH 28/93] Work on byte level when parsing markdown Reasoning: instead encoding every character one by one as we encounter them to use half their length as the correct offset, we can simply encode the whole string at once as utf-16le and work with that directly. --- telethon/extensions/markdown.py | 37 +++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index d4f7ce22..f78fcb2f 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -11,8 +11,6 @@ from ..tl.types import ( MessageEntityPre, MessageEntityTextUrl ) -def tg_string_len(s): - return len(s.encode('utf-16le')) // 2 class Mode(Enum): """Different modes supported by Telegram's Markdown""" @@ -31,7 +29,10 @@ DEFAULT_DELIMITERS = { '```': Mode.PRE } -DEFAULT_URL_RE = re.compile(r'\[(.+?)\]\((.+?)\)') +# Regex used to match utf-16le encoded r'\[(.+?)\]\((.+?)\)', +# reason why there's '\0' after every match-literal character. +DEFAULT_URL_RE = re.compile(b'\\[\0(.+)\\]\0\\(\0(.+?)\\)\0') + def parse(message, delimiters=None, url_re=None): """ @@ -40,40 +41,45 @@ def parse(message, delimiters=None, url_re=None): dictionary (or default if None). The url_re(gex) must contain two matching groups: the text to be - clickable and the URL itself. + clickable and the URL itself, and be utf-16le encoded. """ + # Work on byte level with the utf-16le encoding to get the offsets right. + # The offset will just be half the index we're at. if url_re is None: url_re = DEFAULT_URL_RE elif url_re: if isinstance(url_re, str): - url_re = re.compile(url_re) + url_re = re.compile(url_re.encode('utf-16le')) if not delimiters: if delimiters is not None: return message, [] delimiters = DEFAULT_DELIMITERS + delimiters = {k.encode('utf-16le'): v for k, v in delimiters.items()} + + i = 0 result = [] current = Mode.NONE - offset = 0 - i = 0 + message = message.encode('utf-16le') while i < len(message): url_match = None if url_re and current == Mode.NONE: url_match = url_re.match(message, pos=i) if url_match: - message = ''.join(( + message = b''.join(( message[:url_match.start()], url_match.group(1), message[url_match.end():] )) result.append(( - offset, - offset + tg_string_len(url_match.group(1)), - (Mode.URL, url_match.group(2)) + i // 2, + (i + len(url_match.group(1))) // 2, + (Mode.URL, url_match.group(2).decode('utf-16le')) )) i += len(url_match.group(1)) + if not url_match: for d, m in delimiters.items(): if message[i:i + len(d)] == d and current in (Mode.NONE, m): @@ -82,21 +88,20 @@ def parse(message, delimiters=None, url_re=None): message = message[:i] + message[i + len(d):] if current == Mode.NONE: - result.append(offset) + result.append(i // 2) current = m else: - result[-1] = (result[-1], offset, current) + result[-1] = (result[-1], i // 2, current) current = Mode.NONE break if i < len(message): - offset += tg_string_len(message[i]) - i += 1 + i += 2 if result and not isinstance(result[-1], tuple): result.pop() - return message, result + return message.decode('utf-16le'), result def parse_tg(message, delimiters=None): From 07ece83aba9ff57acf7b40e5611ab4f54686805f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 6 Nov 2017 10:37:22 +0100 Subject: [PATCH 29/93] Fix overlapping markdown entities being skipped --- telethon/extensions/markdown.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index f78fcb2f..2451505e 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -78,7 +78,10 @@ def parse(message, delimiters=None, url_re=None): (i + len(url_match.group(1))) // 2, (Mode.URL, url_match.group(2).decode('utf-16le')) )) - i += len(url_match.group(1)) + # We matched the delimiter which is now gone, and we'll add + # +2 before next iteration which will make us skip a character. + # Go back by one utf-16 encoded character (-2) to avoid it. + i += len(url_match.group(1)) - 2 if not url_match: for d, m in delimiters.items(): @@ -90,9 +93,12 @@ def parse(message, delimiters=None, url_re=None): if current == Mode.NONE: result.append(i // 2) current = m + # No need to i -= 2 here because it's been already + # checked that next character won't be a delimiter. else: result[-1] = (result[-1], i // 2, current) current = Mode.NONE + i -= 2 # Delimiter matched and gone, go back 1 char break if i < len(message): From 3a2c3a9497a6606a2fc634f49eab12e6286ae450 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 6 Nov 2017 11:22:58 +0100 Subject: [PATCH 30/93] Fix URL regex for markdown was greedy (fix-up) --- telethon/extensions/markdown.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index 2451505e..9641caa0 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -31,7 +31,7 @@ DEFAULT_DELIMITERS = { # Regex used to match utf-16le encoded r'\[(.+?)\]\((.+?)\)', # reason why there's '\0' after every match-literal character. -DEFAULT_URL_RE = re.compile(b'\\[\0(.+)\\]\0\\(\0(.+?)\\)\0') +DEFAULT_URL_RE = re.compile(b'\\[\0(.+?)\\]\0\\(\0(.+?)\\)\0') def parse(message, delimiters=None, url_re=None): From 83af705cc8fba30bf3f05d8a4c3ba243edc475ca Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 6 Nov 2017 11:32:40 +0100 Subject: [PATCH 31/93] Add more comments to the markdown parser --- telethon/extensions/markdown.py | 36 ++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index 9641caa0..ef9c118c 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -43,13 +43,11 @@ def parse(message, delimiters=None, url_re=None): The url_re(gex) must contain two matching groups: the text to be clickable and the URL itself, and be utf-16le encoded. """ - # Work on byte level with the utf-16le encoding to get the offsets right. - # The offset will just be half the index we're at. if url_re is None: url_re = DEFAULT_URL_RE elif url_re: - if isinstance(url_re, str): - url_re = re.compile(url_re.encode('utf-16le')) + if isinstance(url_re, bytes): + url_re = re.compile(url_re) if not delimiters: if delimiters is not None: @@ -58,15 +56,22 @@ def parse(message, delimiters=None, url_re=None): delimiters = {k.encode('utf-16le'): v for k, v in delimiters.items()} + # Cannot use a for loop because we need to skip some indices i = 0 result = [] current = Mode.NONE + + # Work on byte level with the utf-16le encoding to get the offsets right. + # The offset will just be half the index we're at. message = message.encode('utf-16le') while i < len(message): url_match = None if url_re and current == Mode.NONE: + # If we're not inside a previous match since Telegram doesn't allow + # nested message entities, try matching the URL from the i'th pos. url_match = url_re.match(message, pos=i) if url_match: + # Replace the whole match with only the inline URL text. message = b''.join(( message[:url_match.start()], url_match.group(1), @@ -85,10 +90,20 @@ def parse(message, delimiters=None, url_re=None): if not url_match: for d, m in delimiters.items(): - if message[i:i + len(d)] == d and current in (Mode.NONE, m): - if message[i + len(d):i + 2 * len(d)] == d: - continue # ignore two consecutive delimiters + # Slice the string at the current i'th position to see if + # it matches the current delimiter d. + if message[i:i + len(d)] == d: + if current != Mode.NONE and current != m: + # We were inside another delimiter/mode, ignore this. + continue + if message[i + len(d):i + 2 * len(d)] == d: + # The same delimiter can't be right afterwards, if + # this were the case we would match empty strings + # like `` which we don't want to. + continue + + # Get rid of the delimiter by slicing it away message = message[:i] + message[i + len(d):] if current == Mode.NONE: result.append(i // 2) @@ -101,10 +116,13 @@ def parse(message, delimiters=None, url_re=None): i -= 2 # Delimiter matched and gone, go back 1 char break - if i < len(message): - i += 2 + # Next iteration, utf-16 encoded characters need 2 bytes. + i += 2 if result and not isinstance(result[-1], tuple): + # We may have found some a delimiter but not its ending pair. If + # that's the case we want to get rid of it before returning. + # TODO Should probably insert such delimiter back in the string. result.pop() return message.decode('utf-16le'), result From f65322af1811c763031246bf0e74f5b06ec78fe1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 7 Nov 2017 10:15:55 +0100 Subject: [PATCH 32/93] Fix entity database not using the phone on {phone: id} Closes #412 --- telethon/tl/entity_database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/tl/entity_database.py b/telethon/tl/entity_database.py index 0c92c75f..2273627b 100644 --- a/telethon/tl/entity_database.py +++ b/telethon/tl/entity_database.py @@ -132,7 +132,7 @@ class EntityDatabase: phone = getattr(entity, 'phone', None) if phone: - self._username_id[phone] = marked_id + self._phone_id[phone] = marked_id def _parse_key(self, key): """Parses the given string, integer or TLObject key into a From cad1e883a6baa989661a5643cf88a2cb86e3a3bf Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 10 Nov 2017 09:32:40 +0100 Subject: [PATCH 33/93] Don't save full entities unless they have access_hash --- telethon/tl/entity_database.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/telethon/tl/entity_database.py b/telethon/tl/entity_database.py index 2273627b..2b7e0501 100644 --- a/telethon/tl/entity_database.py +++ b/telethon/tl/entity_database.py @@ -76,17 +76,20 @@ class EntityDatabase: p = utils.get_input_peer(e, allow_self=False) marked_id = utils.get_peer_id(p, add_mark=True) + has_hash = False if isinstance(p, InputPeerChat): # Chats don't have a hash new_input[marked_id] = 0 + has_hash = True elif p.access_hash: # Some users and channels seem to be returned without # an 'access_hash', meaning Telegram doesn't want you # to access them. This is the reason behind ensuring # that the 'access_hash' is non-zero. See issue #354. new_input[marked_id] = p.access_hash + has_hash = True - if self.enabled_full: + if self.enabled_full and has_hash: if isinstance(e, (User, Chat, Channel)): new.append(e) except ValueError: From 7d75eebdabbc380d380ebee796752cbe6fdf6f0f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 10 Nov 2017 11:01:02 +0100 Subject: [PATCH 34/93] Make markdown parser use only Telegram's MessageEntity's --- telethon/extensions/markdown.py | 80 ++++++++++----------------------- telethon/telegram_client.py | 2 +- 2 files changed, 24 insertions(+), 58 deletions(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index ef9c118c..c650fdfc 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -4,29 +4,17 @@ for use within the library, which attempts to handle emojies correctly, since they seem to count as two characters and it's a bit strange. """ import re -from enum import Enum - from ..tl.types import ( MessageEntityBold, MessageEntityItalic, MessageEntityCode, MessageEntityPre, MessageEntityTextUrl ) -class Mode(Enum): - """Different modes supported by Telegram's Markdown""" - NONE = 0 - BOLD = 1 - ITALIC = 2 - CODE = 3 - PRE = 4 - URL = 5 - - DEFAULT_DELIMITERS = { - '**': Mode.BOLD, - '__': Mode.ITALIC, - '`': Mode.CODE, - '```': Mode.PRE + '**': MessageEntityBold, + '__': MessageEntityItalic, + '`': MessageEntityCode, + '```': MessageEntityPre } # Regex used to match utf-16le encoded r'\[(.+?)\]\((.+?)\)', @@ -37,8 +25,8 @@ DEFAULT_URL_RE = re.compile(b'\\[\0(.+?)\\]\0\\(\0(.+?)\\)\0') def parse(message, delimiters=None, url_re=None): """ Parses the given message and returns the stripped message and a list - of tuples containing (start, end, mode) using the specified delimiters - dictionary (or default if None). + of MessageEntity* using the specified delimiters dictionary (or default + if None). The dictionary should be a mapping {delimiter: entity class}. The url_re(gex) must contain two matching groups: the text to be clickable and the URL itself, and be utf-16le encoded. @@ -59,14 +47,14 @@ def parse(message, delimiters=None, url_re=None): # Cannot use a for loop because we need to skip some indices i = 0 result = [] - current = Mode.NONE + current = None # Work on byte level with the utf-16le encoding to get the offsets right. # The offset will just be half the index we're at. message = message.encode('utf-16le') while i < len(message): url_match = None - if url_re and current == Mode.NONE: + if url_re and current is None: # If we're not inside a previous match since Telegram doesn't allow # nested message entities, try matching the URL from the i'th pos. url_match = url_re.match(message, pos=i) @@ -78,10 +66,9 @@ def parse(message, delimiters=None, url_re=None): message[url_match.end():] )) - result.append(( - i // 2, - (i + len(url_match.group(1))) // 2, - (Mode.URL, url_match.group(2).decode('utf-16le')) + result.append(MessageEntityTextUrl( + offset=i // 2, length=len(url_match.group(1)) // 2, + url=url_match.group(2).decode('utf-16le') )) # We matched the delimiter which is now gone, and we'll add # +2 before next iteration which will make us skip a character. @@ -93,7 +80,7 @@ def parse(message, delimiters=None, url_re=None): # Slice the string at the current i'th position to see if # it matches the current delimiter d. if message[i:i + len(d)] == d: - if current != Mode.NONE and current != m: + if current is not None and not isinstance(current, m): # We were inside another delimiter/mode, ignore this. continue @@ -105,46 +92,25 @@ def parse(message, delimiters=None, url_re=None): # Get rid of the delimiter by slicing it away message = message[:i] + message[i + len(d):] - if current == Mode.NONE: - result.append(i // 2) - current = m + if current is None: + if m == MessageEntityPre: + # Special case, also has 'lang' + current = MessageEntityPre(i // 2, None, '') + else: + current = m(i // 2, None) # No need to i -= 2 here because it's been already # checked that next character won't be a delimiter. else: - result[-1] = (result[-1], i // 2, current) - current = Mode.NONE + current.length = (i // 2) - current.offset + result.append(current) + current = None i -= 2 # Delimiter matched and gone, go back 1 char break # Next iteration, utf-16 encoded characters need 2 bytes. i += 2 - if result and not isinstance(result[-1], tuple): - # We may have found some a delimiter but not its ending pair. If - # that's the case we want to get rid of it before returning. - # TODO Should probably insert such delimiter back in the string. - result.pop() + # We may have found some a delimiter but not its ending pair. + # TODO Should probably insert such delimiter back in the string. return message.decode('utf-16le'), result - - -def parse_tg(message, delimiters=None): - """Similar to parse(), but returns a list of MessageEntity's""" - message, tuples = parse(message, delimiters=delimiters) - result = [] - for start, end, mode in tuples: - extra = None - if isinstance(mode, tuple): - mode, extra = mode - - if mode == Mode.BOLD: - result.append(MessageEntityBold(start, end - start)) - elif mode == Mode.ITALIC: - result.append(MessageEntityItalic(start, end - start)) - elif mode == Mode.CODE: - result.append(MessageEntityCode(start, end - start)) - elif mode == Mode.PRE: - result.append(MessageEntityPre(start, end - start, '')) - elif mode == Mode.URL: - result.append(MessageEntityTextUrl(start, end - start, extra)) - return message, result diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 59bf4a9a..a90214b0 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -388,7 +388,7 @@ class TelegramClient(TelegramBareClient): if parse_mode: parse_mode = parse_mode.lower() if parse_mode in {'md', 'markdown'}: - message, msg_entities = markdown.parse_tg(message) + message, msg_entities = markdown.parse(message) else: raise ValueError('Unknown parsing mode', parse_mode) else: From cb3f20db654ca5789ee5c697b36917464cf3c9a3 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 10 Nov 2017 11:41:49 +0100 Subject: [PATCH 35/93] Clean up markdown parsing since tuples aren't used anymore --- telethon/extensions/markdown.py | 68 +++++++++++++++++---------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index c650fdfc..566a1d45 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -48,12 +48,12 @@ def parse(message, delimiters=None, url_re=None): i = 0 result = [] current = None + end_delimiter = None # Work on byte level with the utf-16le encoding to get the offsets right. # The offset will just be half the index we're at. message = message.encode('utf-16le') while i < len(message): - url_match = None if url_re and current is None: # If we're not inside a previous match since Telegram doesn't allow # nested message entities, try matching the URL from the i'th pos. @@ -70,42 +70,46 @@ def parse(message, delimiters=None, url_re=None): offset=i // 2, length=len(url_match.group(1)) // 2, url=url_match.group(2).decode('utf-16le') )) - # We matched the delimiter which is now gone, and we'll add - # +2 before next iteration which will make us skip a character. - # Go back by one utf-16 encoded character (-2) to avoid it. - i += len(url_match.group(1)) - 2 + i += len(url_match.group(1)) + # Next loop iteration, don't check delimiters, since + # a new inline URL might be right after this one. + continue - if not url_match: + if end_delimiter is None: + # We're not expecting any delimiter, so check them all for d, m in delimiters.items(): # Slice the string at the current i'th position to see if - # it matches the current delimiter d. - if message[i:i + len(d)] == d: - if current is not None and not isinstance(current, m): - # We were inside another delimiter/mode, ignore this. - continue + # it matches the current delimiter d, otherwise skip it. + if message[i:i + len(d)] != d: + continue - if message[i + len(d):i + 2 * len(d)] == d: - # The same delimiter can't be right afterwards, if - # this were the case we would match empty strings - # like `` which we don't want to. - continue + if message[i + len(d):i + 2 * len(d)] == d: + # The same delimiter can't be right afterwards, if + # this were the case we would match empty strings + # like `` which we don't want to. + continue - # Get rid of the delimiter by slicing it away - message = message[:i] + message[i + len(d):] - if current is None: - if m == MessageEntityPre: - # Special case, also has 'lang' - current = MessageEntityPre(i // 2, None, '') - else: - current = m(i // 2, None) - # No need to i -= 2 here because it's been already - # checked that next character won't be a delimiter. - else: - current.length = (i // 2) - current.offset - result.append(current) - current = None - i -= 2 # Delimiter matched and gone, go back 1 char - break + # Get rid of the delimiter by slicing it away + message = message[:i] + message[i + len(d):] + if m == MessageEntityPre: + # Special case, also has 'lang' + current = m(i // 2, None, '') + else: + current = m(i // 2, None) + + end_delimiter = d # We expect the same delimiter. + break + + elif message[i:i + len(end_delimiter)] == end_delimiter: + message = message[:i] + message[i + len(end_delimiter):] + current.length = (i // 2) - current.offset + result.append(current) + current, end_delimiter = None, None + # Don't increment i here as we matched a delimiter, + # and there may be a new one right after. This is + # different than when encountering the first delimiter, + # as we already know there won't be the same right after. + continue # Next iteration, utf-16 encoded characters need 2 bytes. i += 2 From c4e07cff57d22a5f20af5420da6752236641aab9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 10 Nov 2017 11:44:27 +0100 Subject: [PATCH 36/93] Fix unfinished markdown delimiters being stripped away --- telethon/extensions/markdown.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index 566a1d45..3cdf95f7 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -115,6 +115,9 @@ def parse(message, delimiters=None, url_re=None): i += 2 # We may have found some a delimiter but not its ending pair. - # TODO Should probably insert such delimiter back in the string. + # If this is the case, we want to insert the delimiter character back. + if current is not None: + message = \ + message[:current.offset] + end_delimiter + message[current.offset:] return message.decode('utf-16le'), result From 81baced12b8d6ee0e69215f3b0357bbd35f84fc1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 10 Nov 2017 13:27:51 +0100 Subject: [PATCH 37/93] Support t.me/ links when resolving usernames/joinchat links Closes #419 --- telethon/telegram_client.py | 23 +++++++++++++++++------ telethon/tl/entity_database.py | 21 ++++++++++++++++++++- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index a90214b0..b1eec2b2 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -30,8 +30,9 @@ from .tl.functions.contacts import ( ) from .tl.functions.messages import ( GetDialogsRequest, GetHistoryRequest, ReadHistoryRequest, SendMediaRequest, - SendMessageRequest, GetChatsRequest, - GetAllDraftsRequest) + SendMessageRequest, GetChatsRequest, GetAllDraftsRequest, + CheckChatInviteRequest +) from .tl.functions import channels from .tl.functions import messages @@ -49,13 +50,13 @@ from .tl.types import ( Message, MessageMediaContact, MessageMediaDocument, MessageMediaPhoto, InputUserSelf, UserProfilePhoto, ChatPhoto, UpdateMessageID, UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage, - PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty + PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty, + ChatInvite, ChatInviteAlready ) from .tl.types.messages import DialogsSlice from .extensions import markdown - class TelegramClient(TelegramBareClient): """Full featured TelegramClient meant to extend the basic functionality - @@ -1053,8 +1054,18 @@ class TelegramClient(TelegramBareClient): entity = phone self(GetContactsRequest(0)) else: - entity = string.strip('@').lower() - self(ResolveUsernameRequest(entity)) + entity, is_join_chat = EntityDatabase.parse_username(string) + if is_join_chat: + invite = self(CheckChatInviteRequest(entity)) + if isinstance(invite, ChatInvite): + # If it's an invite to a chat, the user must join before + # for the link to be resolved and work, otherwise raise. + if invite.channel: + return invite.channel + elif isinstance(invite, ChatInviteAlready): + return invite.chat + else: + self(ResolveUsernameRequest(entity)) # MtProtoSender will call .process_entities on the requests made try: return self.session.entities[entity] diff --git a/telethon/tl/entity_database.py b/telethon/tl/entity_database.py index 2b7e0501..9002ebd8 100644 --- a/telethon/tl/entity_database.py +++ b/telethon/tl/entity_database.py @@ -9,6 +9,11 @@ from ..tl.types import ( from .. import utils # Keep this line the last to maybe fix #357 +USERNAME_RE = re.compile( + r'@|(?:https?://)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?' +) + + class EntityDatabase: def __init__(self, input_list=None, enabled=True, enabled_full=True): """Creates a new entity database with an initial load of "Input" @@ -153,7 +158,8 @@ class EntityDatabase: if phone: return self._phone_id[phone] else: - return self._username_id[key.lstrip('@').lower()] + username, _ = EntityDatabase.parse_username(key) + return self._username_id[username.lower()] except KeyError as e: raise ValueError() from e @@ -206,6 +212,19 @@ class EntityDatabase: if phone.isdigit(): return phone + @staticmethod + def parse_username(username): + """Parses the given username or channel access hash, given + a string, username or URL. Returns a tuple consisting of + both the stripped username and whether it is a joinchat/ hash. + """ + username = username.strip() + m = USERNAME_RE.match(username) + if m: + return username[m.end():], bool(m.group(1)) + else: + return username, False + def get_input_entity(self, peer): try: i = utils.get_peer_id(peer, add_mark=True) From 5a57a8a498b1461d8aa041cc9adb7ec71eb88a33 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 11 Nov 2017 19:35:57 +0100 Subject: [PATCH 38/93] Fix message history failing if sender fwd from channel Closes #424 --- telethon/telegram_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index b1eec2b2..5a5d99ec 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -51,7 +51,7 @@ from .tl.types import ( InputUserSelf, UserProfilePhoto, ChatPhoto, UpdateMessageID, UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage, PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty, - ChatInvite, ChatInviteAlready + ChatInvite, ChatInviteAlready, PeerChannel ) from .tl.types.messages import DialogsSlice from .extensions import markdown @@ -558,7 +558,7 @@ class TelegramClient(TelegramBareClient): elif getattr(m, 'fwd_from', None): # .from_id is optional, so this is the sanest fallback. who = entities[utils.get_peer_id( - m.fwd_from.from_id or m.fwd_from.channel_id, + m.fwd_from.from_id or PeerChannel(m.fwd_from.channel_id), add_mark=True )] else: From 99512875a223d564c4de112f10f034f33cf78954 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 12 Nov 2017 16:25:56 +0100 Subject: [PATCH 39/93] Reconnect if invoking failed (#270) --- telethon/telegram_bare_client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 7ebf5ec1..161e6b30 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -428,7 +428,10 @@ class TelegramBareClient: result = self._invoke( sender, call_receive, update_state, *requests ) - if result is not None: + if result is None: + sleep(1) + self._reconnect() + else: return result raise ValueError('Number of retries reached 0.') From 84d48ef7bf8fef4834f942372639f6826e56601d Mon Sep 17 00:00:00 2001 From: Andrey Egorov Date: Sun, 12 Nov 2017 18:51:32 +0300 Subject: [PATCH 40/93] Safer check to determine whether sockets are connected (#427) --- telethon/extensions/tcp_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index 5255513a..3f803e63 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -59,7 +59,7 @@ class TcpClient: raise def _get_connected(self): - return self._socket is not None + return self._socket is not None and self._socket.fileno() > 0 connected = property(fget=_get_connected) From 08abef78d6f5877882a98ab09e70ce92265ccf03 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 12 Nov 2017 18:03:42 +0100 Subject: [PATCH 41/93] Add missing InputUserSelf case to .get_input_peer() --- telethon/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/telethon/utils.py b/telethon/utils.py index a05a4990..5d5bb953 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -99,6 +99,9 @@ def get_input_peer(entity, allow_self=True): if isinstance(entity, InputUser): return InputPeerUser(entity.user_id, entity.access_hash) + if isinstance(entity, InputUserSelf): + return InputPeerSelf() + if isinstance(entity, UserFull): return get_input_peer(entity.user) From f3e2887452cae38234467a269a39c532c13a217f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 12 Nov 2017 18:15:32 +0100 Subject: [PATCH 42/93] Add missing ChannelFull case to .get_peer_id() --- telethon/utils.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/telethon/utils.py b/telethon/utils.py index 5d5bb953..3259c8e2 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -19,7 +19,7 @@ from .tl.types import ( DocumentEmpty, InputDocumentEmpty, Message, GeoPoint, InputGeoPoint, GeoPointEmpty, InputGeoPointEmpty, Photo, InputPhoto, PhotoEmpty, InputPhotoEmpty, FileLocation, ChatPhotoEmpty, UserProfilePhotoEmpty, - FileLocationUnavailable, InputMediaUploadedDocument, + FileLocationUnavailable, InputMediaUploadedDocument, ChannelFull, InputMediaUploadedPhoto, DocumentAttributeFilename, photos ) @@ -325,8 +325,13 @@ def get_peer_id(peer, add_mark=False): return peer.user_id elif isinstance(peer, (PeerChat, InputPeerChat)): return -peer.chat_id if add_mark else peer.chat_id - elif isinstance(peer, (PeerChannel, InputPeerChannel)): - i = peer.channel_id + elif isinstance(peer, (PeerChannel, InputPeerChannel, ChannelFull)): + if isinstance(peer, ChannelFull): + # Special case: .get_input_peer can't return InputChannel from + # ChannelFull since it doesn't have an .access_hash attribute. + i = peer.id + else: + i = peer.channel_id if add_mark: # Concat -100 through math tricks, .to_supergroup() on Madeline # IDs will be strictly positive -> log works From 07cb001854f614def335b670862e75ef4e29f59e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 13 Nov 2017 10:31:32 +0100 Subject: [PATCH 43/93] Attempt at cleaning up reconnection logic --- telethon/telegram_bare_client.py | 57 ++++++++++++++------------------ 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 161e6b30..48c58c1d 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -254,16 +254,13 @@ class TelegramBareClient: connects to the new data center. """ if new_dc is None: - # Assume we are disconnected due to some error, so connect again - with self._reconnect_lock: - # Another thread may have connected again, so check that first - if self.is_connected(): - return True + if self.is_connected(): + return True - try: - return self.connect() - except ConnectionResetError: - return False + try: + return self.connect() + except ConnectionResetError: + return False else: # Since we're reconnecting possibly due to a UserMigrateError, # we need to first know the Data Centers we can connect to. Do @@ -430,7 +427,16 @@ class TelegramBareClient: ) if result is None: sleep(1) - self._reconnect() + self._logger.debug('RPC failed. Attempting reconnection.') + # The ReadThread has priority when attempting reconnection, + # since this thread is constantly running while __call__ is + # only done sometimes. Here try connecting only once/retry. + if sender == self._sender: + if not self._reconnect_lock.locked(): + with self._reconnect_lock: + self._reconnect() + else: + sender.connect() else: return result @@ -494,21 +500,12 @@ class TelegramBareClient: pass # We will just retry except ConnectionResetError: - if not self._user_connected or self._reconnect_lock.locked(): - # Only attempt reconnecting if the user called connect and not - # reconnecting already. - raise - - self._logger.debug('Server disconnected us. Reconnecting and ' - 'resending request...') - - if sender != self._sender: - # TODO Try reconnecting forever too? - sender.connect() + if self._user_connected: + # Server disconnected us, __call__ will try reconnecting. + return None else: - while self._user_connected and not self._reconnect(): - sleep(0.1) # Retry forever until we can send the request - return None + # User never called .connect(), so raise this error. + raise if init_connection: # We initialized the connection successfully, even if @@ -828,8 +825,9 @@ class TelegramBareClient: pass except ConnectionResetError: self._logger.debug('Server disconnected us. Reconnecting...') - while self._user_connected and not self._reconnect(): - sleep(0.1) # Retry forever, this is instant messaging + with self._reconnect_lock: + 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 @@ -864,12 +862,7 @@ class TelegramBareClient: self.disconnect() break except ImportError: - "Not using PySocks, so it can't be a socket error" - - # If something strange happens we don't want to enter an - # 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) + "Not using PySocks, so it can't be a proxy error" self._recv_thread = None From 4ac88a150570179ad65d13f4ff98f1e076b8b538 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 13 Nov 2017 10:58:10 +0100 Subject: [PATCH 44/93] Use ._logger.exception when .connect fails (#373) --- telethon/telegram_bare_client.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 48c58c1d..67ba0c88 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -204,12 +204,10 @@ class TelegramBareClient: self.disconnect() return self.connect(_sync_updates=_sync_updates) - except (RPCError, ConnectionError) as error: + except (RPCError, ConnectionError): # Probably errors from the previous session, ignore them self.disconnect() - self._logger.debug( - 'Could not stabilise initial connection: {}'.format(error) - ) + self._logger.exception('Could not stabilise initial connection.') return False def is_connected(self): From bfc408b00add6e0a807a514b2681c2b8120fcf95 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 13 Nov 2017 10:59:43 +0100 Subject: [PATCH 45/93] Use NullHandler as default for the library --- telethon/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/telethon/__init__.py b/telethon/__init__.py index 1210fa90..2f984bf1 100644 --- a/telethon/__init__.py +++ b/telethon/__init__.py @@ -1,3 +1,4 @@ +import logging from .telegram_bare_client import TelegramBareClient from .telegram_client import TelegramClient from .network import ConnectionMode @@ -5,3 +6,4 @@ from . import tl, version __version__ = version.__version__ +logging.getLogger(__name__).addHandler(logging.NullHandler()) From d59b17c6fc9a12a743d4694062dde02cae1333ec Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 14 Nov 2017 09:48:40 +0100 Subject: [PATCH 46/93] Clear up confusing error and trailing brace (closes #429) --- telethon_examples/interactive_telegram_client.py | 2 +- telethon_generator/error_descriptions | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/telethon_examples/interactive_telegram_client.py b/telethon_examples/interactive_telegram_client.py index ee179a42..52c2c356 100644 --- a/telethon_examples/interactive_telegram_client.py +++ b/telethon_examples/interactive_telegram_client.py @@ -354,7 +354,7 @@ class InteractiveTelegramClient(TelegramClient): update.message, get_display_name(who) )) else: - sprint('<< {} sent "{}"]'.format( + sprint('<< {} sent "{}"'.format( get_display_name(who), update.message )) diff --git a/telethon_generator/error_descriptions b/telethon_generator/error_descriptions index 500504d7..65894ba1 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]{3,30}[a-zA-Z\d]" +USERNAME_INVALID=Nobody is using this username, or the username is unacceptable. If the latter, it 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 48e96ca15fdd2a2d38d0aadc39cb6e9f6e711016 Mon Sep 17 00:00:00 2001 From: Lonami Date: Tue, 14 Nov 2017 12:01:33 +0100 Subject: [PATCH 47/93] Fix ._get_connected failing when .fileno() == 0 ( #427) --- telethon/extensions/tcp_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index 3f803e63..164429f3 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -59,7 +59,7 @@ class TcpClient: raise def _get_connected(self): - return self._socket is not None and self._socket.fileno() > 0 + return self._socket is not None and self._socket.fileno() >= 0 connected = property(fget=_get_connected) From a1c669333e22b501beced8d1be56406e374d0814 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 15 Nov 2017 12:22:18 +0100 Subject: [PATCH 48/93] Update scheme to layer 73 --- telethon_generator/scheme.tl | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/telethon_generator/scheme.tl b/telethon_generator/scheme.tl index ae6544a1..2ecb31b4 100644 --- a/telethon_generator/scheme.tl +++ b/telethon_generator/scheme.tl @@ -159,7 +159,7 @@ inputMediaUploadedPhoto#2f37e231 flags:# file:InputFile caption:string stickers: inputMediaPhoto#81fa373a flags:# id:InputPhoto caption:string ttl_seconds:flags.0?int = InputMedia; inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia; inputMediaContact#a6e45987 phone_number:string first_name:string last_name:string = InputMedia; -inputMediaUploadedDocument#e39621fd flags:# file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector caption:string stickers:flags.0?Vector ttl_seconds:flags.1?int = InputMedia; +inputMediaUploadedDocument#e39621fd flags:# nosound_video:flags.3?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector caption:string stickers:flags.0?Vector ttl_seconds:flags.1?int = InputMedia; inputMediaDocument#5acb668e flags:# id:InputDocument caption:string ttl_seconds:flags.0?int = InputMedia; inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string = InputMedia; inputMediaGifExternal#4843b0fd url:string q:string = InputMedia; @@ -169,6 +169,8 @@ inputMediaGame#d33f43f3 id:InputGame = InputMedia; inputMediaInvoice#92153685 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string start_param:string = InputMedia; inputMediaGeoLive#7b1a118f geo_point:InputGeoPoint period:int = InputMedia; +inputSingleMedia#5eaa7809 media:InputMedia random_id:long = InputSingleMedia; + inputChatPhotoEmpty#1ca48f57 = InputChatPhoto; inputChatUploadedPhoto#927c55b4 file:InputFile = InputChatPhoto; inputChatPhoto#8953ad37 id:InputPhoto = InputChatPhoto; @@ -219,7 +221,7 @@ userStatusLastMonth#77ebc742 = UserStatus; chatEmpty#9ba2d800 id:int = Chat; chat#d91cdd54 flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true admins_enabled:flags.3?true admin:flags.4?true deactivated:flags.5?true id:int title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel = Chat; chatForbidden#7328bdb id:int title:string = Chat; -channel#cb44b1c flags:# creator:flags.0?true left:flags.2?true editor:flags.3?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true democracy:flags.10?true signatures:flags.11?true min:flags.12?true id:int access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int version:int restriction_reason:flags.9?string admin_rights:flags.14?ChannelAdminRights banned_rights:flags.15?ChannelBannedRights = Chat; +channel#450b7115 flags:# creator:flags.0?true left:flags.2?true editor:flags.3?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true democracy:flags.10?true signatures:flags.11?true min:flags.12?true id:int access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int version:int restriction_reason:flags.9?string admin_rights:flags.14?ChannelAdminRights banned_rights:flags.15?ChannelBannedRights participants_count:flags.17?int = Chat; channelForbidden#289da732 flags:# broadcast:flags.5?true megagroup:flags.8?true id:int access_hash:long title:string until_date:flags.16?int = Chat; chatFull#2e02a614 id:int participants:ChatParticipants chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector = ChatFull; @@ -236,7 +238,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#6153276a photo_small:FileLocation photo_big:FileLocation = ChatPhoto; messageEmpty#83e5de54 id:int = Message; -message#90dddc11 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int edit_date:flags.15?int post_author:flags.16?string = Message; +message#44f9b43d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long = Message; messageService#9e19a1f6 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer reply_to_msg_id:flags.3?int date:int action:MessageAction = Message; messageMediaEmpty#3ded6320 = MessageMedia; @@ -710,7 +712,7 @@ messages.botResults#947ca848 flags:# gallery:flags.0?true query_id:long next_off exportedMessageLink#1f486803 link:string = ExportedMessageLink; -messageFwdHeader#fadff4ac flags:# from_id:flags.0?int date:int channel_id:flags.1?int channel_post:flags.2?int post_author:flags.3?string = MessageFwdHeader; +messageFwdHeader#559ebe6d flags:# from_id:flags.0?int date:int channel_id:flags.1?int channel_post:flags.2?int post_author:flags.3?string saved_from_peer:flags.4?Peer saved_from_msg_id:flags.4?int = MessageFwdHeader; auth.codeTypeSms#72a3158c = auth.CodeType; auth.codeTypeCall#741cd3e3 = auth.CodeType; @@ -1017,7 +1019,7 @@ messages.receivedMessages#5a954c0 max_id:int = Vector; messages.setTyping#a3825e50 peer:InputPeer action:SendMessageAction = Bool; messages.sendMessage#fa88427a flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Updates; messages.sendMedia#c8f16791 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int media:InputMedia random_id:long reply_markup:flags.2?ReplyMarkup = Updates; -messages.forwardMessages#708e0195 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer = Updates; +messages.forwardMessages#708e0195 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true grouped:flags.9?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.hideReportSpam#a8f1709b peer:InputPeer = Bool; messages.getPeerSettings#3672e09c peer:InputPeer = PeerSettings; @@ -1098,6 +1100,7 @@ messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; messages.getUnreadMentions#46578472 peer:InputPeer offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.getRecentLocations#249431e2 peer:InputPeer limit:int = messages.Messages; messages.readMentions#f0189d3 peer:InputPeer = messages.AffectedHistory; +messages.sendMultiMedia#2095512f flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int multi_media:Vector = Updates; updates.getState#edd4882a = updates.State; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; @@ -1190,4 +1193,4 @@ langpack.getStrings#2e1ee318 lang_code:string keys:Vector = Vector; -// LAYER 72 +// LAYER 73 From b346561f898b79712b8cfe7017e6ce33787f72e4 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 16 Nov 2017 13:24:32 +0100 Subject: [PATCH 49/93] Remove unnecessary call to .get_input_entity() --- 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 5a5d99ec..a5ce4572 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -504,7 +504,7 @@ class TelegramClient(TelegramBareClient): if limit == 0: # No messages, but we still need to know the total message count result = self(GetHistoryRequest( - peer=self.get_input_entity(entity), limit=1, + peer=entity, limit=1, offset_date=None, offset_id=0, max_id=0, min_id=0, add_offset=0 )) return getattr(result, 'count', len(result.messages)), [], [] From 778c844a649e050b472d547c3b7ad1ce72c5ecc0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 16 Nov 2017 13:25:13 +0100 Subject: [PATCH 50/93] Use logger.exception instead logger.error on ReadThread --- telethon/telegram_bare_client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 67ba0c88..87661e94 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -843,9 +843,8 @@ class TelegramBareClient: self.idle(stop_signals=tuple()) except Exception as error: # Unknown exception, pass it to the main thread - self._logger.error( - 'Unknown error on the read thread, please report', - error + self._logger.exception( + 'Unknown error on the read thread, please report' ) try: From 959e824c1c0be549b92d0ca9a2a87c3abf62f88b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 16 Nov 2017 13:26:49 +0100 Subject: [PATCH 51/93] Reduce indent level to simplify flow on __call__ --- telethon/telegram_bare_client.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 87661e94..a9e2fb30 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -423,21 +423,21 @@ class TelegramBareClient: result = self._invoke( sender, call_receive, update_state, *requests ) - if result is None: - sleep(1) - self._logger.debug('RPC failed. Attempting reconnection.') - # The ReadThread has priority when attempting reconnection, - # since this thread is constantly running while __call__ is - # only done sometimes. Here try connecting only once/retry. - if sender == self._sender: - if not self._reconnect_lock.locked(): - with self._reconnect_lock: - self._reconnect() - else: - sender.connect() - else: + if result is not None: return result + self._logger.debug('RPC failed. Attempting reconnection.') + sleep(1) + # The ReadThread has priority when attempting reconnection, + # since this thread is constantly running while __call__ is + # only done sometimes. Here try connecting only once/retry. + if sender == self._sender: + if not self._reconnect_lock.locked(): + with self._reconnect_lock: + self._reconnect() + else: + sender.connect() + raise ValueError('Number of retries reached 0.') finally: if sender != self._sender: From ee5915e86db20edbe65c6744d44d122b46b1a954 Mon Sep 17 00:00:00 2001 From: Vladislav Kolesnichenko Date: Thu, 16 Nov 2017 15:30:18 +0300 Subject: [PATCH 52/93] Add support for connecting through IPv6 (#425 for #112) --- telethon/telegram_bare_client.py | 19 ++++++++++++++++--- telethon/telegram_client.py | 2 ++ telethon/tl/session.py | 4 ++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index a9e2fb30..cd01f009 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -39,6 +39,11 @@ from .update_state import UpdateState from .utils import get_appropriated_part_size +DEFAULT_IPV4_IP = '149.154.167.51' +DEFAULT_IPV6_IP = '[2001:67c:4e8:f002::a]' +DEFAULT_PORT = 443 + + class TelegramBareClient: """Bare Telegram Client with just the minimum - @@ -69,6 +74,7 @@ class TelegramBareClient: def __init__(self, session, api_id, api_hash, connection_mode=ConnectionMode.TCP_FULL, + use_ipv6=False, proxy=None, update_workers=None, spawn_read_thread=False, @@ -80,6 +86,8 @@ class TelegramBareClient: "Your API ID or Hash cannot be empty or None. " "Refer to Telethon's README.rst for more information.") + self._use_ipv6 = use_ipv6 + # Determine what session object we have if isinstance(session, str) or session is None: session = Session.try_load_or_create_new(session) @@ -88,6 +96,11 @@ class TelegramBareClient: 'The given session must be a str or a Session instance.' ) + if not session.server_address: + session.port = DEFAULT_PORT + session.server_address = \ + DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP + self.session = session self.api_id = int(api_id) self.api_hash = api_hash @@ -282,7 +295,7 @@ class TelegramBareClient: return self._recv_thread is not None and \ threading.get_ident() == self._recv_thread.ident - def _get_dc(self, dc_id, ipv6=False, cdn=False): + def _get_dc(self, dc_id, cdn=False): """Gets the Data Center (DC) associated to 'dc_id'""" if not TelegramBareClient._config: TelegramBareClient._config = self(GetConfigRequest()) @@ -295,7 +308,7 @@ class TelegramBareClient: return next( dc for dc in TelegramBareClient._config.dc_options - if dc.id == dc_id and bool(dc.ipv6) == ipv6 and bool(dc.cdn) == cdn + if dc.id == dc_id and bool(dc.ipv6) == self._use_ipv6 and bool(dc.cdn) == cdn ) except StopIteration: if not cdn: @@ -303,7 +316,7 @@ class TelegramBareClient: # New configuration, perhaps a new CDN was added? TelegramBareClient._config = self(GetConfigRequest()) - return self._get_dc(dc_id, ipv6=ipv6, cdn=cdn) + return self._get_dc(dc_id, cdn=cdn) def _get_exported_client(self, dc_id): """Creates and connects a new TelegramBareClient for the desired DC. diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index a5ce4572..ae864ea6 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -69,6 +69,7 @@ class TelegramClient(TelegramBareClient): def __init__(self, session, api_id, api_hash, connection_mode=ConnectionMode.TCP_FULL, + use_ipv6=False, proxy=None, update_workers=None, timeout=timedelta(seconds=5), @@ -113,6 +114,7 @@ class TelegramClient(TelegramBareClient): super().__init__( session, api_id, api_hash, connection_mode=connection_mode, + use_ipv6=use_ipv6, proxy=proxy, update_workers=update_workers, spawn_read_thread=spawn_read_thread, diff --git a/telethon/tl/session.py b/telethon/tl/session.py index f597048f..bbdcc590 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -64,8 +64,8 @@ class Session: self._last_msg_id = 0 # Long # These values will be saved - self.server_address = '91.108.56.165' - self.port = 443 + self.server_address = None + self.port = None self.auth_key = None self.layer = 0 self.salt = 0 # Unsigned long From edd73ed69aca8703e76bd808001bcfaa019cc30f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 16 Nov 2017 13:40:25 +0100 Subject: [PATCH 53/93] Allow switching from IPv4 to IPv6 and vice versa --- telethon/telegram_bare_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index cd01f009..97251547 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -96,7 +96,9 @@ class TelegramBareClient: 'The given session must be a str or a Session instance.' ) - if not session.server_address: + # ':' in session.server_address is True if it's an IPv6 address + if (not session.server_address or + (':' in session.server_address) != use_ipv6): session.port = DEFAULT_PORT session.server_address = \ DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP From 4ddbc78699700796ba9013cd2841461e0f28883b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 16 Nov 2017 13:47:15 +0100 Subject: [PATCH 54/93] Ensure IPv6 addresses are surrounded by '[]' (#425) --- telethon/extensions/tcp_client.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index 164429f3..af9bfbfe 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -37,6 +37,12 @@ class TcpClient: 'timeout' must be given in seconds """ if ':' in ip: # IPv6 + # The address needs to be surrounded by [] as discussed on PR#425 + if not ip.startswith('['): + ip = '[' + ip + if not ip.endswith(']'): + ip = ip + ']' + mode, address = socket.AF_INET6, (ip, port, 0, 0) else: mode, address = socket.AF_INET, (ip, port) From e5deaf5db8428c45f727a391f4f4acff2b61cb55 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 16 Nov 2017 19:07:53 +0100 Subject: [PATCH 55/93] Fix c4e07cf, md parsing adding unfinished entity at wrong offset --- telethon/extensions/markdown.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index 3cdf95f7..dbbdb5fe 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -117,7 +117,10 @@ def parse(message, delimiters=None, url_re=None): # We may have found some a delimiter but not its ending pair. # If this is the case, we want to insert the delimiter character back. if current is not None: - message = \ - message[:current.offset] + end_delimiter + message[current.offset:] + message = ( + message[:2 * current.offset] + + end_delimiter + + message[2 * current.offset:] + ) return message.decode('utf-16le'), result From 346c5bb3030d4dfeb0dde3e26cc90373e3f0b996 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 16 Nov 2017 19:13:13 +0100 Subject: [PATCH 56/93] Add method to md parser to extract text surrounded by entities --- telethon/extensions/markdown.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index dbbdb5fe..a11b1aab 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -4,6 +4,9 @@ for use within the library, which attempts to handle emojies correctly, since they seem to count as two characters and it's a bit strange. """ import re + +from telethon.tl import TLObject + from ..tl.types import ( MessageEntityBold, MessageEntityItalic, MessageEntityCode, MessageEntityPre, MessageEntityTextUrl @@ -124,3 +127,23 @@ def parse(message, delimiters=None, url_re=None): ) return message.decode('utf-16le'), result + + +def get_inner_text(text, entity): + """Gets the inner text that's surrounded by the given entity or entities. + For instance: text = 'hey!', entity = MessageEntityBold(2, 2) -> 'y!'. + """ + if not isinstance(entity, TLObject) and hasattr(entity, '__iter__'): + multiple = True + else: + entity = [entity] + multiple = False + + text = text.encode('utf-16le') + result = [] + for e in entity: + start = e.offset * 2 + end = (e.offset + e.length) * 2 + result.append(text[start:end].decode('utf-16le')) + + return result if multiple else result[0] From 01f55200f29f59bf2125ed768daa2f9d0c508d95 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 16 Nov 2017 19:18:26 +0100 Subject: [PATCH 57/93] Update to v0.15.5 --- telethon/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/version.py b/telethon/version.py index bafdcc72..096fbd6c 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.15.4' +__version__ = '0.15.5' From 976777414756bfc0e14a056cdd160e56089ed1ae Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 17 Nov 2017 15:57:48 +0100 Subject: [PATCH 58/93] Fix import in markdown parser not being relative --- telethon/extensions/markdown.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index a11b1aab..f4f3f740 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -5,7 +5,7 @@ since they seem to count as two characters and it's a bit strange. """ import re -from telethon.tl import TLObject +from ..tl import TLObject from ..tl.types import ( MessageEntityBold, MessageEntityItalic, MessageEntityCode, From 231ecf3f2a4497d2d1a2cd423efcc1005a956b86 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 20 Nov 2017 00:56:05 +0800 Subject: [PATCH 59/93] add readthedocs files --- readthedocs/Makefile | 20 ++ readthedocs/conf.py | 174 ++++++++++++++++++ readthedocs/extra/examples-signing-in.rst | 54 ++++++ .../extra/examples-working-with-messages.rst | 99 ++++++++++ readthedocs/extra/examples.rst | 66 +++++++ readthedocs/extra/getting_started.rst | 103 +++++++++++ readthedocs/index.rst | 40 ++++ readthedocs/make.bat | 36 ++++ readthedocs/modules.rst | 7 + readthedocs/telethon.crypto.rst | 61 ++++++ readthedocs/telethon.errors.rst | 21 +++ readthedocs/telethon.extensions.rst | 29 +++ readthedocs/telethon.network.rst | 37 ++++ readthedocs/telethon.rst | 89 +++++++++ readthedocs/telethon.tl.custom.rst | 12 ++ readthedocs/telethon.tl.rst | 57 ++++++ 16 files changed, 905 insertions(+) create mode 100644 readthedocs/Makefile create mode 100644 readthedocs/conf.py create mode 100644 readthedocs/extra/examples-signing-in.rst create mode 100644 readthedocs/extra/examples-working-with-messages.rst create mode 100644 readthedocs/extra/examples.rst create mode 100644 readthedocs/extra/getting_started.rst create mode 100644 readthedocs/index.rst create mode 100644 readthedocs/make.bat create mode 100644 readthedocs/modules.rst create mode 100644 readthedocs/telethon.crypto.rst create mode 100644 readthedocs/telethon.errors.rst create mode 100644 readthedocs/telethon.extensions.rst create mode 100644 readthedocs/telethon.network.rst create mode 100644 readthedocs/telethon.rst create mode 100644 readthedocs/telethon.tl.custom.rst create mode 100644 readthedocs/telethon.tl.rst diff --git a/readthedocs/Makefile b/readthedocs/Makefile new file mode 100644 index 00000000..fd6e0d0a --- /dev/null +++ b/readthedocs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = Telethon +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/readthedocs/conf.py b/readthedocs/conf.py new file mode 100644 index 00000000..18ff1a17 --- /dev/null +++ b/readthedocs/conf.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Telethon documentation build configuration file, created by +# sphinx-quickstart on Fri Nov 17 15:36:11 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'Telethon' +copyright = '2017, Lonami' +author = 'Lonami' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.15' +# The full version, including alpha/beta/rc tags. +release = '0.15.5' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +html_theme_options = { + 'collapse_navigation': True, + 'display_version': True, + 'navigation_depth': 3, +} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# This is required for the alabaster theme +# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars +html_sidebars = { + '**': [ + 'globaltoc.html', + 'relations.html', # needs 'show_related': True theme option to display + 'searchbox.html', + ] +} + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Telethondoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'Telethon.tex', 'Telethon Documentation', + 'Jeff', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'telethon', 'Telethon Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'Telethon', 'Telethon Documentation', + author, 'Telethon', 'One line description of project.', + 'Miscellaneous'), +] + + + diff --git a/readthedocs/extra/examples-signing-in.rst b/readthedocs/extra/examples-signing-in.rst new file mode 100644 index 00000000..cade3649 --- /dev/null +++ b/readthedocs/extra/examples-signing-in.rst @@ -0,0 +1,54 @@ +========================= +Signing In +========================= + +Two Factor Authorization (2FA) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you have Two Factor Authorization (from now on, 2FA) enabled on your account, calling +:meth:`telethon.TelegramClient.sign_in` will raise a `SessionPasswordNeededError`. +When this happens, just :meth:`telethon.TelegramClient.sign_in` again with a ``password=``: + + .. code-block:: python + + import getpass + from telethon.errors import SessionPasswordNeededError + + client.sign_in(phone) + try: + client.sign_in(code=input('Enter code: ')) + except SessionPasswordNeededError: + client.sign_in(password=getpass.getpass()) + +Enabling 2FA +************* + +If you don't have 2FA enabled, but you would like to do so through Telethon, take as example the following code snippet: + + .. code-block:: python + + import os + from hashlib import sha256 + from telethon.tl.functions import account + from telethon.tl.types.account import PasswordInputSettings + + new_salt = client(account.GetPasswordRequest()).new_salt + salt = new_salt + os.urandom(8) # new random salt + + pw = 'secret'.encode('utf-8') # type your new password here + hint = 'hint' + + pw_salted = salt + pw + salt + pw_hash = sha256(pw_salted).digest() + + result = client(account.UpdatePasswordSettingsRequest( + current_password_hash=salt, + new_settings=PasswordInputSettings( + new_salt=salt, + new_password_hash=pw_hash, + hint=hint + ) + )) + +Thanks to `Issue 259 `_ for the tip! + diff --git a/readthedocs/extra/examples-working-with-messages.rst b/readthedocs/extra/examples-working-with-messages.rst new file mode 100644 index 00000000..1c47d328 --- /dev/null +++ b/readthedocs/extra/examples-working-with-messages.rst @@ -0,0 +1,99 @@ +========================= +Working with messages +========================= + +Forwarding messages +******************* + +Note that ForwardMessageRequest_ (note it's Message, singular) will *not* work if channels are involved. +This is because channel (and megagroups) IDs are not unique, so you also need to know who the sender is +(a parameter this request doesn't have). + +Either way, you are encouraged to use ForwardMessagesRequest_ (note it's Message*s*, plural) *always*, +since it is more powerful, as follows: + + .. code-block:: python + + from telethon.tl.functions.messages import ForwardMessagesRequest + # note the s ^ + + messages = foo() # retrieve a few messages (or even one, in a list) + from_entity = bar() + to_entity = baz() + + client(ForwardMessagesRequest( + from_peer=from_entity, # who sent these messages? + id=[msg.id for msg in messages], # which are the messages? + to_peer=to_entity # who are we forwarding them to? + )) + +The named arguments are there for clarity, although they're not needed because they appear in order. +You can obviously just wrap a single message on the list too, if that's all you have. + + +Searching Messages +******************* + +Messages are searched through the obvious SearchRequest_, but you may run into issues_. A valid example would be: + + .. code-block:: python + + result = client(SearchRequest( + entity, 'query', InputMessagesFilterEmpty(), None, None, 0, 0, 100 + )) + +It's important to note that the optional parameter ``from_id`` has been left omitted and thus defaults to ``None``. +Changing it to InputUserEmpty_, as one could think to specify "no user", won't work because this parameter is a flag, +and it being unspecified has a different meaning. + +If one were to set ``from_id=InputUserEmpty()``, it would filter messages from "empty" senders, +which would likely match no users. + +If you get a ``ChatAdminRequiredError`` on a channel, it's probably because you tried setting the ``from_id`` filter, +and as the error says, you can't do that. Leave it set to ``None`` and it should work. + +As with every method, make sure you use the right ID/hash combination for your ``InputUser`` or ``InputChat``, +or you'll likely run into errors like ``UserIdInvalidError``. + + +Sending stickers +***************** + +Stickers are nothing else than ``files``, and when you successfully retrieve the stickers for a certain sticker set, +all you will have are ``handles`` to these files. Remember, the files Telegram holds on their servers can be referenced +through this pair of ID/hash (unique per user), and you need to use this handle when sending a "document" message. +This working example will send yourself the very first sticker you have: + + .. code-block:: python + + # Get all the sticker sets this user has + sticker_sets = client(GetAllStickersRequest(0)) + + # Choose a sticker set + sticker_set = sticker_sets.sets[0] + + # Get the stickers for this sticker set + stickers = client(GetStickerSetRequest( + stickerset=InputStickerSetID( + id=sticker_set.id, access_hash=sticker_set.access_hash + ) + )) + + # Stickers are nothing more than files, so send that + client(SendMediaRequest( + peer=client.get_me(), + media=InputMediaDocument( + id=InputDocument( + id=stickers.documents[0].id, + access_hash=stickers.documents[0].access_hash + ), + caption='' + ) + )) + + +.. _ForwardMessageRequest: https://lonamiwebs.github.io/Telethon/methods/messages/forward_message.html +.. _ForwardMessagesRequest: https://lonamiwebs.github.io/Telethon/methods/messages/forward_messages.html +.. _SearchRequest: https://lonamiwebs.github.io/Telethon/methods/messages/search.html +.. _issues: https://github.com/LonamiWebs/Telethon/issues/215 +.. _InputUserEmpty: https://lonamiwebs.github.io/Telethon/constructors/input_user_empty.html diff --git a/readthedocs/extra/examples.rst b/readthedocs/extra/examples.rst new file mode 100644 index 00000000..c068f2fc --- /dev/null +++ b/readthedocs/extra/examples.rst @@ -0,0 +1,66 @@ + + +***************** +Examples +***************** + +Prelude +--------- + +Before reading any specific example, make sure to read the following common steps: + +All the examples assume that you have successfully created a client and you're authorized as follows: + + .. code-block:: python + + from telethon import TelegramClient + + # Use your own values here + api_id = 12345 + api_hash = '0123456789abcdef0123456789abcdef' + phone_number = '+34600000000' + + client = TelegramClient('some_name', api_id, api_hash) + client.connect() # Must return True, otherwise, try again + + if not client.is_user_authorized(): + client.send_code_request(phone_number) + # .sign_in() may raise PhoneNumberUnoccupiedError + # In that case, you need to call .sign_up() to get a new account + client.sign_in(phone_number, input('Enter code: ')) + + # The `client´ is now ready + +Although Python will probably clean up the resources used by the ``TelegramClient``, +you should always ``.disconnect()`` it once you're done: + + .. code-block:: python + + try: + # Code using the client goes here + except: + # No matter what happens, always disconnect in the end + client.disconnect() + +If the examples aren't enough, you're strongly advised to read the source code +for the InteractiveTelegramClient_ for an overview on how you could build your next script. +This example shows a basic usage more than enough in most cases. Even reading the source +for the TelegramClient_ may help a lot! + + +Signing In +-------------- + +.. toctree:: + examples-signing-in + + +Working with messages +----------------------- + +.. toctree:: + examples-working-with-messages + + +.. _InteractiveTelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon_examples/interactive_telegram_client.py +.. _TelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon/telegram_client.py diff --git a/readthedocs/extra/getting_started.rst b/readthedocs/extra/getting_started.rst new file mode 100644 index 00000000..3a6b36a8 --- /dev/null +++ b/readthedocs/extra/getting_started.rst @@ -0,0 +1,103 @@ +.. Telethon documentation master file, created by + sphinx-quickstart on Fri Nov 17 15:36:11 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + + +================= +Getting Started! +================= + +Installation +************** + +To install Telethon, simply do: + + ``pip install telethon`` + +If you get something like ``"SyntaxError: invalid syntax"`` or any other error while installing, it's probably because ``pip`` defaults to Python 2, which is not supported. Use ``pip3`` instead. + +If you already have the library installed, upgrade with: + + ``pip install --upgrade telethon``. + +You can also install the library directly from GitHub or a fork: + + .. code-block:: python + + # pip install git+https://github.com/LonamiWebs/Telethon.git + or + $ git clone https://github.com/LonamiWebs/Telethon.git + $ cd Telethon/ + # pip install -Ue . + +If you don't have root access, simply pass the ``--user`` flag to the pip command. + + + + +Creating a client +************** +Before working with Telegram's API, you need to get your own API ID and hash: + +1. Follow `this link `_ and login with your phone number. + +2. Click under API Development tools. + +3. A *Create new application* window will appear. Fill in your application details. There is no need to enter any *URL*, and only the first two fields (*App title* and *Short name*) can be changed later as far as I'm aware. + +4. Click on *Create application* at the end. Remember that your **API hash is secret** and Telegram won't let you revoke it. Don't post it anywhere! + +Once that's ready, the next step is to create a ``TelegramClient``. This class will be your main interface with Telegram's API, and creating one is very simple: + + .. code-block:: python + + from telethon import TelegramClient + + # These example values won't work. You must get your own api_id and + # api_hash from https://my.telegram.org, under API Development. + api_id = 12345 + api_hash = '0123456789abcdef0123456789abcdef' + phone = '+34600000000' + + client = TelegramClient('session_name', api_id, api_hash) + client.connect() + + # If you already have a previous 'session_name.session' file, skip this. + client.sign_in(phone=phone) + me = client.sign_in(code=77777) # Put whatever code you received here. + +**More details**: `Click here `_ + + +Simple Stuff +************** + .. code-block:: python + + print(me.stringify()) + + client.send_message('username', 'Hello! Talking to you from Telethon') + client.send_file('username', '/home/myself/Pictures/holidays.jpg') + + client.download_profile_photo(me) + total, messages, senders = client.get_message_history('username') + client.download_media(messages[0]) + + +Diving In +************** + +.. note:: More info in our Wiki! + +Sending Requests +^^^^^^^^^^^^^^^^^^^^^ + `Here `__ + +Working with updates +^^^^^^^^^^^^^^^^^^^^^ + `Here `__ + +Accessing the full API +^^^^^^^^^^^^^^^^^^^^^^^ + `Here `__ + diff --git a/readthedocs/index.rst b/readthedocs/index.rst new file mode 100644 index 00000000..8f036adc --- /dev/null +++ b/readthedocs/index.rst @@ -0,0 +1,40 @@ +.. Telethon documentation master file, created by + sphinx-quickstart on Fri Nov 17 15:36:11 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Telethon's documentation! +==================================== + +Pure Python 3 Telegram client library. Official Site `here `_. + + + +**************** +Getting Started +**************** +.. toctree:: + extra/getting_started + + +*************** +Examples +*************** +.. toctree:: + extra/examples + + +*************** +Modules +*************** +.. toctree:: + telethon + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/readthedocs/make.bat b/readthedocs/make.bat new file mode 100644 index 00000000..f51f7234 --- /dev/null +++ b/readthedocs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=Telethon + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/readthedocs/modules.rst b/readthedocs/modules.rst new file mode 100644 index 00000000..f710574a --- /dev/null +++ b/readthedocs/modules.rst @@ -0,0 +1,7 @@ +telethon +======== + +.. toctree:: + :maxdepth: 3 + + telethon diff --git a/readthedocs/telethon.crypto.rst b/readthedocs/telethon.crypto.rst new file mode 100644 index 00000000..3c11416d --- /dev/null +++ b/readthedocs/telethon.crypto.rst @@ -0,0 +1,61 @@ +telethon\.crypto package +======================== + + +telethon\.crypto\.aes module +---------------------------- + +.. automodule:: telethon.crypto.aes + :members: + :undoc-members: + :show-inheritance: + +telethon\.crypto\.aes\_ctr module +--------------------------------- + +.. automodule:: telethon.crypto.aes_ctr + :members: + :undoc-members: + :show-inheritance: + +telethon\.crypto\.auth\_key module +---------------------------------- + +.. automodule:: telethon.crypto.auth_key + :members: + :undoc-members: + :show-inheritance: + +telethon\.crypto\.cdn\_decrypter module +--------------------------------------- + +.. automodule:: telethon.crypto.cdn_decrypter + :members: + :undoc-members: + :show-inheritance: + +telethon\.crypto\.factorization module +-------------------------------------- + +.. automodule:: telethon.crypto.factorization + :members: + :undoc-members: + :show-inheritance: + +telethon\.crypto\.libssl module +------------------------------- + +.. automodule:: telethon.crypto.libssl + :members: + :undoc-members: + :show-inheritance: + +telethon\.crypto\.rsa module +---------------------------- + +.. automodule:: telethon.crypto.rsa + :members: + :undoc-members: + :show-inheritance: + + diff --git a/readthedocs/telethon.errors.rst b/readthedocs/telethon.errors.rst new file mode 100644 index 00000000..2e94fe33 --- /dev/null +++ b/readthedocs/telethon.errors.rst @@ -0,0 +1,21 @@ +telethon\.errors package +======================== + + +telethon\.errors\.common module +------------------------------- + +.. automodule:: telethon.errors.common + :members: + :undoc-members: + :show-inheritance: + +telethon\.errors\.rpc\_base\_errors module +------------------------------------------ + +.. automodule:: telethon.errors.rpc_base_errors + :members: + :undoc-members: + :show-inheritance: + + diff --git a/readthedocs/telethon.extensions.rst b/readthedocs/telethon.extensions.rst new file mode 100644 index 00000000..578728b5 --- /dev/null +++ b/readthedocs/telethon.extensions.rst @@ -0,0 +1,29 @@ +telethon\.extensions package +============================ + + +telethon\.extensions\.binary\_reader module +------------------------------------------- + +.. automodule:: telethon.extensions.binary_reader + :members: + :undoc-members: + :show-inheritance: + +telethon\.extensions\.markdown module +------------------------------------- + +.. automodule:: telethon.extensions.markdown + :members: + :undoc-members: + :show-inheritance: + +telethon\.extensions\.tcp\_client module +---------------------------------------- + +.. automodule:: telethon.extensions.tcp_client + :members: + :undoc-members: + :show-inheritance: + + diff --git a/readthedocs/telethon.network.rst b/readthedocs/telethon.network.rst new file mode 100644 index 00000000..3600e985 --- /dev/null +++ b/readthedocs/telethon.network.rst @@ -0,0 +1,37 @@ +telethon\.network package +========================= + + +telethon\.network\.authenticator module +--------------------------------------- + +.. automodule:: telethon.network.authenticator + :members: + :undoc-members: + :show-inheritance: + +telethon\.network\.connection module +------------------------------------ + +.. automodule:: telethon.network.connection + :members: + :undoc-members: + :show-inheritance: + +telethon\.network\.mtproto\_plain\_sender module +------------------------------------------------ + +.. automodule:: telethon.network.mtproto_plain_sender + :members: + :undoc-members: + :show-inheritance: + +telethon\.network\.mtproto\_sender module +----------------------------------------- + +.. automodule:: telethon.network.mtproto_sender + :members: + :undoc-members: + :show-inheritance: + + diff --git a/readthedocs/telethon.rst b/readthedocs/telethon.rst new file mode 100644 index 00000000..2d3c269c --- /dev/null +++ b/readthedocs/telethon.rst @@ -0,0 +1,89 @@ +telethon package +================ + + +telethon\.helpers module +------------------------ + +.. automodule:: telethon.helpers + :members: + :undoc-members: + :show-inheritance: + +telethon\.telegram\_bare\_client module +--------------------------------------- + +.. automodule:: telethon.telegram_bare_client + :members: + :undoc-members: + :show-inheritance: + +telethon\.telegram\_client module +--------------------------------- + +.. automodule:: telethon.telegram_client + :members: + :undoc-members: + :show-inheritance: + +telethon\.update\_state module +------------------------------ + +.. automodule:: telethon.update_state + :members: + :undoc-members: + :show-inheritance: + +telethon\.utils module +---------------------- + +.. automodule:: telethon.utils + :members: + :undoc-members: + :show-inheritance: + + +telethon\.cryto package +------------------------ + +.. toctree:: + + telethon.crypto + +telethon\.errors package +------------------------ + +.. toctree:: + + telethon.errors + +telethon\.extensions package +------------------------ + +.. toctree:: + + telethon.extensions + +telethon\.network package +------------------------ + +.. toctree:: + + telethon.network + +telethon\.tl package +------------------------ + +.. toctree:: + + telethon.tl + + + +Module contents +--------------- + +.. automodule:: telethon + :members: + :undoc-members: + :show-inheritance: diff --git a/readthedocs/telethon.tl.custom.rst b/readthedocs/telethon.tl.custom.rst new file mode 100644 index 00000000..a1290869 --- /dev/null +++ b/readthedocs/telethon.tl.custom.rst @@ -0,0 +1,12 @@ +telethon\.tl\.custom package +============================ + + +telethon\.tl\.custom\.draft module +---------------------------------- + +.. automodule:: telethon.tl.custom.draft + :members: + :undoc-members: + :show-inheritance: + diff --git a/readthedocs/telethon.tl.rst b/readthedocs/telethon.tl.rst new file mode 100644 index 00000000..6fbb1f00 --- /dev/null +++ b/readthedocs/telethon.tl.rst @@ -0,0 +1,57 @@ +telethon\.tl package +==================== + + +.. toctree:: + + telethon.tl.custom + + +telethon\.tl\.entity\_database module +------------------------------------- + +.. automodule:: telethon.tl.entity_database + :members: + :undoc-members: + :show-inheritance: + +telethon\.tl\.gzip\_packed module +--------------------------------- + +.. automodule:: telethon.tl.gzip_packed + :members: + :undoc-members: + :show-inheritance: + +telethon\.tl\.message\_container module +--------------------------------------- + +.. automodule:: telethon.tl.message_container + :members: + :undoc-members: + :show-inheritance: + +telethon\.tl\.session module +---------------------------- + +.. automodule:: telethon.tl.session + :members: + :undoc-members: + :show-inheritance: + +telethon\.tl\.tl\_message module +-------------------------------- + +.. automodule:: telethon.tl.tl_message + :members: + :undoc-members: + :show-inheritance: + +telethon\.tl\.tlobject module +----------------------------- + +.. automodule:: telethon.tl.tlobject + :members: + :undoc-members: + :show-inheritance: + From 34e7ae026ec638875a7351cce3ad93f253c41280 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 20 Nov 2017 01:04:51 +0800 Subject: [PATCH 60/93] add requirements.txt for readthedocs --- readthedocs/requirements.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 readthedocs/requirements.txt diff --git a/readthedocs/requirements.txt b/readthedocs/requirements.txt new file mode 100644 index 00000000..97c7493d --- /dev/null +++ b/readthedocs/requirements.txt @@ -0,0 +1 @@ +telethon \ No newline at end of file From f6ec7e47a7802e003cb06f03e89bb53c9268ea5e Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 20 Nov 2017 12:12:31 +0800 Subject: [PATCH 61/93] add a bunch more readthedocs from wiki --- readthedocs/extra/advanced-usage/bots.rst | 59 ++++ .../signing-in.rst} | 4 + .../extra/advanced-usage/users-and-chats.rst | 323 ++++++++++++++++++ .../working-with-messages.rst} | 4 + .../extra/{examples.rst => advanced.rst} | 20 +- .../extra/basic/accessing-the-full-api.rst | 115 +++++++ readthedocs/extra/basic/creating-a-client.rst | 76 +++++ readthedocs/extra/basic/getting-started.rst | 54 +++ readthedocs/extra/basic/installation.rst | 82 +++++ readthedocs/extra/basic/sending-requests.rst | 55 +++ readthedocs/extra/basic/sessions.rst | 48 +++ .../extra/basic/working-with-updates.rst | 133 ++++++++ readthedocs/extra/getting_started.rst | 103 ------ ...eleted-limited-or-deactivated-accounts.rst | 26 ++ .../extra/troubleshooting/enable-logging.rst | 24 ++ .../extra/troubleshooting/rpc-errors.rst | 27 ++ readthedocs/index.rst | 47 ++- 17 files changed, 1067 insertions(+), 133 deletions(-) create mode 100644 readthedocs/extra/advanced-usage/bots.rst rename readthedocs/extra/{examples-signing-in.rst => advanced-usage/signing-in.rst} (95%) create mode 100644 readthedocs/extra/advanced-usage/users-and-chats.rst rename readthedocs/extra/{examples-working-with-messages.rst => advanced-usage/working-with-messages.rst} (98%) rename readthedocs/extra/{examples.rst => advanced.rst} (89%) create mode 100644 readthedocs/extra/basic/accessing-the-full-api.rst create mode 100644 readthedocs/extra/basic/creating-a-client.rst create mode 100644 readthedocs/extra/basic/getting-started.rst create mode 100644 readthedocs/extra/basic/installation.rst create mode 100644 readthedocs/extra/basic/sending-requests.rst create mode 100644 readthedocs/extra/basic/sessions.rst create mode 100644 readthedocs/extra/basic/working-with-updates.rst delete mode 100644 readthedocs/extra/getting_started.rst create mode 100644 readthedocs/extra/troubleshooting/deleted-limited-or-deactivated-accounts.rst create mode 100644 readthedocs/extra/troubleshooting/enable-logging.rst create mode 100644 readthedocs/extra/troubleshooting/rpc-errors.rst diff --git a/readthedocs/extra/advanced-usage/bots.rst b/readthedocs/extra/advanced-usage/bots.rst new file mode 100644 index 00000000..091eada1 --- /dev/null +++ b/readthedocs/extra/advanced-usage/bots.rst @@ -0,0 +1,59 @@ +====== +Bots +====== + +Talking to Inline Bots +^^^^^^^^^^^^^^^^^^^^^^ + +You can query an inline bot, such as `@VoteBot`__ +(note, *query*, not *interact* with a voting message), by making use of +the `GetInlineBotResultsRequest`__ request: + + .. code-block:: python + + from telethon.tl.functions.messages import GetInlineBotResultsRequest + + bot_results = client(GetInlineBotResultsRequest( + bot, user_or_chat, 'query', '' + )) + +And you can select any of their results by using +`SendInlineBotResultRequest`__: + + .. code-block:: python + + from telethon.tl.functions.messages import SendInlineBotResultRequest + + client(SendInlineBotResultRequest( + get_input_peer(user_or_chat), + obtained_query_id, + obtained_str_id + )) + + +Talking to Bots with special reply markup +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To interact with a message that has a special reply markup, such as +`@VoteBot`__ polls, you would use +`GetBotCallbackAnswerRequest`__: + + .. code-block:: python + + from telethon.tl.functions.messages import GetBotCallbackAnswerRequest + + client(GetBotCallbackAnswerRequest( + user_or_chat, + msg.id, + data=msg.reply_markup.rows[wanted_row].buttons[wanted_button].data + )) + +It’s a bit verbose, but it has all the information you would need to +show it visually (button rows, and buttons within each row, each with +its own data). + +__ https://t.me/vote +__ https://lonamiwebs.github.io/Telethon/methods/messages/get_inline_bot_results.html +__ https://lonamiwebs.github.io/Telethon/methods/messages/send_inline_bot_result.html +__ https://lonamiwebs.github.io/Telethon/methods/messages/get_bot_callback_answer.html +__ https://t.me/vote \ No newline at end of file diff --git a/readthedocs/extra/examples-signing-in.rst b/readthedocs/extra/advanced-usage/signing-in.rst similarity index 95% rename from readthedocs/extra/examples-signing-in.rst rename to readthedocs/extra/advanced-usage/signing-in.rst index cade3649..08f4fe3d 100644 --- a/readthedocs/extra/examples-signing-in.rst +++ b/readthedocs/extra/advanced-usage/signing-in.rst @@ -2,6 +2,10 @@ Signing In ========================= +.. note:: + Make sure you have gone through :ref:`prelude` already! + + Two Factor Authorization (2FA) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/readthedocs/extra/advanced-usage/users-and-chats.rst b/readthedocs/extra/advanced-usage/users-and-chats.rst new file mode 100644 index 00000000..3f3e0729 --- /dev/null +++ b/readthedocs/extra/advanced-usage/users-and-chats.rst @@ -0,0 +1,323 @@ +========================= +Users and Chats +========================= + +.. note:: + Make sure you have gone through :ref:`prelude` already! + +.. contents:: + +.. _retrieving-an-entity: + +Retrieving an entity (user or group) +************************************** +An “entity” is used to refer to either an `User`__ or a `Chat`__ +(which includes a `Channel`__). The most straightforward way to get +an entity is to use ``TelegramClient.get_entity()``. This method accepts +either a string, which can be a username, phone number or `t.me`__-like +link, or an integer that will be the ID of an **user**. You can use it +like so: + + .. code-block:: python + + # all of these work + lonami = client.get_entity('lonami') + lonami = client.get_entity('t.me/lonami') + lonami = client.get_entity('https://telegram.dog/lonami') + + # other kind of entities + channel = client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg') + contact = client.get_entity('+34xxxxxxxxx') + friend = client.get_entity(friend_id) + +For the last one to work, the library must have “seen” the user at least +once. The library will “see” the user as long as any request contains +them, so if you’ve called ``.get_dialogs()`` for instance, and your +friend was there, the library will know about them. For more, read about +the :ref:`sessions`. + +If you want to get a channel or chat by ID, you need to specify that +they are a channel or a chat. The library can’t infer what they are by +just their ID (unless the ID is marked, but this is only done +internally), so you need to wrap the ID around a `Peer`__ object: + + .. code-block:: python + + from telethon.tl.types import PeerUser, PeerChat, PeerChannel + my_user = client.get_entity(PeerUser(some_id)) + my_chat = client.get_entity(PeerChat(some_id)) + my_channel = client.get_entity(PeerChannel(some_id)) + +**Note** that most requests don’t ask for an ``User``, or a ``Chat``, +but rather for ``InputUser``, ``InputChat``, and so on. If this is the +case, you should prefer ``.get_input_entity()`` over ``.get_entity()``, +as it will be immediate if you provide an ID (whereas ``.get_entity()`` +may need to find who the entity is first). + +Via your open “chats” (dialogs) +------------------------------- + +.. note:: + Please read here: :ref:`retrieving-all-dialogs`. + +Via ResolveUsernameRequest +-------------------------- + +This is the request used by ``.get_entity`` internally, but you can also +use it by hand: + +.. code-block:: python + + from telethon.tl.functions.contacts import ResolveUsernameRequest + + result = client(ResolveUsernameRequest('username')) + found_chats = result.chats + found_users = result.users + # result.peer may be a PeerUser, PeerChat or PeerChannel + +See `Peer`__ for more information about this result. + +Via MessageFwdHeader +-------------------- + +If all you have is a `MessageFwdHeader`__ after you retrieved a bunch +of messages, this gives you access to the ``from_id`` (if forwarded from +an user) and ``channel_id`` (if forwarded from a channel). Invoking +`GetMessagesRequest`__ also returns a list of ``chats`` and +``users``, and you can find the desired entity there: + + .. code-block:: python + + # Logic to retrieve messages with `GetMessagesRequest´ + messages = foo() + fwd_header = bar() + + user = next(u for u in messages.users if u.id == fwd_header.from_id) + channel = next(c for c in messages.chats if c.id == fwd_header.channel_id) + +Or you can just call ``.get_entity()`` with the ID, as you should have +seen that user or channel before. A call to ``GetMessagesRequest`` may +still be neeed. + +Via GetContactsRequest +---------------------- + +The library will call this for you if you pass a phone number to +``.get_entity``, but again, it can be done manually. If the user you +want to talk to is a contact, you can use `GetContactsRequest`__: + + .. code-block:: python + + from telethon.tl.functions.contacts import GetContactsRequest + from telethon.tl.types.contacts import Contacts + + contacts = client(GetContactsRequest(0)) + if isinstance(contacts, Contacts): + users = contacts.users + contacts = contacts.contacts + +__ https://lonamiwebs.github.io/Telethon/types/user.html +__ https://lonamiwebs.github.io/Telethon/types/chat.html +__ https://lonamiwebs.github.io/Telethon/constructors/channel.html +__ https://t.me +__ https://lonamiwebs.github.io/Telethon/types/peer.html +__ https://lonamiwebs.github.io/Telethon/types/peer.html +__ https://lonamiwebs.github.io/Telethon/constructors/message_fwd_header.html +__ https://lonamiwebs.github.io/Telethon/methods/messages/get_messages.html +__ https://lonamiwebs.github.io/Telethon/methods/contacts/get_contacts.html + + +.. _retrieving-all-dialogs: + +Retrieving all dialogs +*********************** + +There are several ``offset_xyz=`` parameters that have no effect at all, +but there's not much one can do since this is something the server should handle. +Currently, the only way to get all dialogs +(open chats, conversations, etc.) is by using the ``offset_date``: + + .. code-block:: python + + from telethon.tl.functions.messages import GetDialogsRequest + from telethon.tl.types import InputPeerEmpty + from time import sleep + + dialogs = [] + users = [] + chats = [] + + last_date = None + chunk_size = 20 + while True: + result = client(GetDialogsRequest( + offset_date=last_date, + offset_id=0, + offset_peer=InputPeerEmpty(), + limit=chunk_size + )) + dialogs.extend(result.dialogs) + users.extend(result.users) + chats.extend(result.chats) + if not result.messages: + break + last_date = min(msg.date for msg in result.messages) + sleep(2) + + +Joining a chat or channel +******************************* + +Note that `Chat`__\ s are normal groups, and `Channel`__\ s are a +special form of `Chat`__\ s, +which can also be super-groups if their ``megagroup`` member is +``True``. + +Joining a public channel +------------------------ + +Once you have the :ref:`entity ` +of the channel you want to join to, you can +make use of the `JoinChannelRequest`__ to join such channel: + + .. code-block:: python + + from telethon.tl.functions.channels import JoinChannelRequest + client(JoinChannelRequest(channel)) + + # In the same way, you can also leave such channel + from telethon.tl.functions.channels import LeaveChannelRequest + client(LeaveChannelRequest(input_channel)) + +For more on channels, check the `channels namespace`__. + +Joining a private chat or channel +--------------------------------- + +If all you have is a link like this one: +``https://t.me/joinchat/AAAAAFFszQPyPEZ7wgxLtd``, you already have +enough information to join! The part after the +``https://t.me/joinchat/``, this is, ``AAAAAFFszQPyPEZ7wgxLtd`` on this +example, is the ``hash`` of the chat or channel. Now you can use +`ImportChatInviteRequest`__ as follows: + + .. -block:: python + + from telethon.tl.functions.messages import ImportChatInviteRequest + updates = client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg')) + +Adding someone else to such chat or channel +------------------------------------------- + +If you don’t want to add yourself, maybe because you’re already in, you +can always add someone else with the `AddChatUserRequest`__, which +use is very straightforward: + + .. code-block:: python + + from telethon.tl.functions.messages import AddChatUserRequest + + client(AddChatUserRequest( + chat_id, + user_to_add, + fwd_limit=10 # allow the user to see the 10 last messages + )) + +Checking a link without joining +------------------------------- + +If you don’t need to join but rather check whether it’s a group or a +channel, you can use the `CheckChatInviteRequest`__, which takes in +the `hash`__ of said channel or group. + +__ https://lonamiwebs.github.io/Telethon/constructors/chat.html +__ https://lonamiwebs.github.io/Telethon/constructors/channel.html +__ https://lonamiwebs.github.io/Telethon/types/chat.html +__ https://lonamiwebs.github.io/Telethon/methods/channels/join_channel.html +__ https://lonamiwebs.github.io/Telethon/methods/channels/index.html +__ https://lonamiwebs.github.io/Telethon/methods/messages/import_chat_invite.html +__ https://lonamiwebs.github.io/Telethon/methods/messages/add_chat_user.html +__ https://lonamiwebs.github.io/Telethon/methods/messages/check_chat_invite.html +__ https://github.com/LonamiWebs/Telethon/wiki/Joining-a-chat-or-channel#joining-a-private-chat-or-channel + + +Retrieving all chat members (channels too) +****************************************** + +In order to get all the members from a mega-group or channel, you need +to use `GetParticipantsRequest`__. As we can see it needs an +`InputChannel`__, (passing the mega-group or channel you’re going to +use will work), and a mandatory `ChannelParticipantsFilter`__. The +closest thing to “no filter” is to simply use +`ChannelParticipantsSearch`__ with an empty ``'q'`` string. + +If we want to get *all* the members, we need to use a moving offset and +a fixed limit: + + .. code-block:: python + + from telethon.tl.functions.channels import GetParticipantsRequest + from telethon.tl.types import ChannelParticipantsSearch + from time import sleep + + offset = 0 + limit = 100 + all_participants = [] + + while True: + participants = client.invoke(GetParticipantsRequest( + channel, ChannelParticipantsSearch(''), offset, limit + )) + if not participants.users: + break + all_participants.extend(participants.users) + offset += len(participants.users) + # sleep(1) # This line seems to be optional, no guarantees! + +Note that ``GetParticipantsRequest`` returns `ChannelParticipants`__, +which may have more information you need (like the role of the +participants, total count of members, etc.) + +__ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html +__ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html +__ https://lonamiwebs.github.io/Telethon/types/channel_participants_filter.html +__ https://lonamiwebs.github.io/Telethon/constructors/channel_participants_search.html +__ https://lonamiwebs.github.io/Telethon/constructors/channels/channel_participants.html + + +Recent Actions +******************** + +“Recent actions” is simply the name official applications have given to +the “admin log”. Simply use `GetAdminLogRequest`__ for that, and +you’ll get AdminLogResults.events in return which in turn has the final +`.action`__. + +__ https://lonamiwebs.github.io/Telethon/methods/channels/get_admin_log.html +__ https://lonamiwebs.github.io/Telethon/types/channel_admin_log_event_action.html + + +Increasing View Count in a Channel +**************************************** + +It has been asked `quite`__ `a few`__ `times`__ (really, `many`__), and +while I don’t understand why so many people ask this, the solution is to +use `GetMessagesViewsRequest`__, setting ``increment=True``: + + .. code-block:: python + + + # Obtain `channel' through dialogs or through client.get_entity() or anyhow. + # Obtain `msg_ids' through `.get_message_history()` or anyhow. Must be a list. + + client(GetMessagesViewsRequest( + peer=channel, + id=msg_ids, + increment=True + )) + +__ https://github.com/LonamiWebs/Telethon/issues/233 +__ https://github.com/LonamiWebs/Telethon/issues/305 +__ https://github.com/LonamiWebs/Telethon/issues/409 +__ https://github.com/LonamiWebs/Telethon/issues/447 +__ https://lonamiwebs.github.io/Telethon/methods/messages/get_messages_views.html \ No newline at end of file diff --git a/readthedocs/extra/examples-working-with-messages.rst b/readthedocs/extra/advanced-usage/working-with-messages.rst similarity index 98% rename from readthedocs/extra/examples-working-with-messages.rst rename to readthedocs/extra/advanced-usage/working-with-messages.rst index 1c47d328..2c141406 100644 --- a/readthedocs/extra/examples-working-with-messages.rst +++ b/readthedocs/extra/advanced-usage/working-with-messages.rst @@ -2,6 +2,10 @@ Working with messages ========================= +.. note:: + Make sure you have gone through :ref:`prelude` already! + + Forwarding messages ******************* diff --git a/readthedocs/extra/examples.rst b/readthedocs/extra/advanced.rst similarity index 89% rename from readthedocs/extra/examples.rst rename to readthedocs/extra/advanced.rst index c068f2fc..4433116d 100644 --- a/readthedocs/extra/examples.rst +++ b/readthedocs/extra/advanced.rst @@ -1,8 +1,4 @@ - - -***************** -Examples -***************** +.. _prelude: Prelude --------- @@ -48,19 +44,5 @@ This example shows a basic usage more than enough in most cases. Even reading th for the TelegramClient_ may help a lot! -Signing In --------------- - -.. toctree:: - examples-signing-in - - -Working with messages ------------------------ - -.. toctree:: - examples-working-with-messages - - .. _InteractiveTelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon_examples/interactive_telegram_client.py .. _TelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon/telegram_client.py diff --git a/readthedocs/extra/basic/accessing-the-full-api.rst b/readthedocs/extra/basic/accessing-the-full-api.rst new file mode 100644 index 00000000..ceeea7d8 --- /dev/null +++ b/readthedocs/extra/basic/accessing-the-full-api.rst @@ -0,0 +1,115 @@ +========================== +Accessing the Full API +========================== + +The ``TelegramClient`` doesn’t offer a method for every single request +the Telegram API supports. However, it’s very simple to ``.invoke()`` +any request. Whenever you need something, don’t forget to `check the +documentation`__ and look for the `method you need`__. There you can go +through a sorted list of everything you can do. + +You should also refer to the documentation to see what the objects +(constructors) Telegram returns look like. Every constructor inherits +from a common type, and that’s the reason for this distinction. + +Say ``client.send_message()`` didn’t exist, we could use the `search`__ +to look for “message”. There we would find `SendMessageRequest`__, +which we can work with. + +Every request is a Python class, and has the parameters needed for you +to invoke it. You can also call ``help(request)`` for information on +what input parameters it takes. Remember to “Copy import to the +clipboard”, or your script won’t be aware of this class! Now we have: + + .. code-block:: python + + from telethon.tl.functions.messages import SendMessageRequest + +If you’re going to use a lot of these, you may do: + + .. code-block:: python + + import telethon.tl.functions as tl + # We now have access to 'tl.messages.SendMessageRequest' + +We see that this request must take at least two parameters, a ``peer`` +of type `InputPeer`__, and a ``message`` which is just a Python +``str``\ ing. + +How can we retrieve this ``InputPeer``? We have two options. We manually +`construct one`__, for instance: + + .. code-block:: python + + from telethon.tl.types import InputPeerUser + + peer = InputPeerUser(user_id, user_hash) + +Or we call ``.get_input_entity()``: + + .. code-block:: python + + peer = client.get_input_entity('someone') + +When you’re going to invoke an API method, most require you to pass an +``InputUser``, ``InputChat``, or so on, this is why using +``.get_input_entity()`` is more straightforward (and sometimes +immediate, if you know the ID of the user for instance). If you also +need to have information about the whole user, use ``.get_entity()`` +instead: + + .. code-block:: python + + entity = client.get_entity('someone') + +In the later case, when you use the entity, the library will cast it to +its “input” version for you. If you already have the complete user and +want to cache its input version so the library doesn’t have to do this +every time its used, simply call ``.get_input_peer``: + + .. code-block:: python + + from telethon import utils + peer = utils.get_input_user(entity) + +After this small parenthesis about ``.get_entity`` versus +``.get_input_entity``, we have everything we need. To ``.invoke()`` our +request we do: + + .. code-block:: python + + result = client(SendMessageRequest(peer, 'Hello there!')) + # __call__ is an alias for client.invoke(request). Both will work + +Message sent! Of course, this is only an example. +There are nearly 250 methods available as of layer 73, +and you can use every single of them as you wish. +Remember to use the right types! To sum up: + + .. code-block:: python + + result = client(SendMessageRequest( + client.get_input_entity('username'), 'Hello there!' + )) + + +.. note:: + + Note that some requests have a "hash" parameter. This is **not** your ``api_hash``! + It likely isn't your self-user ``.access_hash`` either. + It's a special hash used by Telegram to only send a difference of new data + that you don't already have with that request, + so you can leave it to 0, and it should work (which means no hash is known yet). + + For those requests having a "limit" parameter, + you can often set it to zero to signify "return as many items as possible". + This won't work for all of them though, + for instance, in "messages.search" it will actually return 0 items. + + +__ https://lonamiwebs.github.io/Telethon +__ https://lonamiwebs.github.io/Telethon/methods/index.html +__ https://lonamiwebs.github.io/Telethon/?q=message +__ https://lonamiwebs.github.io/Telethon/methods/messages/send_message.html +__ https://lonamiwebs.github.io/Telethon/types/input_peer.html +__ https://lonamiwebs.github.io/Telethon/constructors/input_peer_user.html \ No newline at end of file diff --git a/readthedocs/extra/basic/creating-a-client.rst b/readthedocs/extra/basic/creating-a-client.rst new file mode 100644 index 00000000..997386db --- /dev/null +++ b/readthedocs/extra/basic/creating-a-client.rst @@ -0,0 +1,76 @@ +.. _creating-a-client: + +=================== +Creating a Client +=================== + +Before working with Telegram's API, you need to get your own API ID and hash: + +1. Follow `this link `_ and login with your phone number. + +2. Click under API Development tools. + +3. A *Create new application* window will appear. Fill in your application details. +There is no need to enter any *URL*, and only the first two fields (*App title* and *Short name*) +can be changed later as far as I'm aware. + +4. Click on *Create application* at the end. Remember that your **API hash is secret** +and Telegram won't let you revoke it. Don't post it anywhere! + +Once that's ready, the next step is to create a ``TelegramClient``. +This class will be your main interface with Telegram's API, and creating one is very simple: + + .. code-block:: python + + from telethon import TelegramClient + + # Use your own values here + api_id = 12345 + api_hash = '0123456789abcdef0123456789abcdef' + phone_number = '+34600000000' + + client = TelegramClient('some_name', api_id, api_hash) + +Note that ``'some_name'`` will be used to save your session (persistent information such as access key and others) +as ``'some_name.session'`` in your disk. This is simply a JSON file which you can (but shouldn't) modify. + +Before using the client, you must be connected to Telegram. Doing so is very easy: + + ``client.connect() # Must return True, otherwise, try again`` + +You may or may not be authorized yet. You must be authorized before you're able to send any request: + + ``client.is_user_authorized() # Returns True if you can send requests`` + +If you're not authorized, you need to ``.sign_in()``: + + .. code-block:: python + + client.send_code_request(phone_number) + myself = client.sign_in(phone_number, input('Enter code: ')) + # If .sign_in raises PhoneNumberUnoccupiedError, use .sign_up instead + # If .sign_in raises SessionPasswordNeeded error, call .sign_in(password=...) + # You can import both exceptions from telethon.errors. + +``myself`` is your Telegram user. +You can view all the information about yourself by doing ``print(myself.stringify())``. +You're now ready to use the client as you wish! + +.. note:: + If you want to use a **proxy**, you have to `install PySocks`__ (via pip or manual) + and then set the appropriated parameters: + + .. code-block:: python + + import socks + client = TelegramClient('session_id', + api_id=12345, api_hash='0123456789abcdef0123456789abcdef', + proxy=(socks.SOCKS5, 'localhost', 4444) + ) + + The ``proxy=`` argument should be a tuple, a list or a dict, + consisting of parameters described `here`__. + + +__ https://github.com/Anorov/PySocks#installation +__ https://github.com/Anorov/PySocks#usage-1%3E \ No newline at end of file diff --git a/readthedocs/extra/basic/getting-started.rst b/readthedocs/extra/basic/getting-started.rst new file mode 100644 index 00000000..ce4354fd --- /dev/null +++ b/readthedocs/extra/basic/getting-started.rst @@ -0,0 +1,54 @@ +.. Telethon documentation master file, created by + sphinx-quickstart on Fri Nov 17 15:36:11 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + + +================= +Getting Started! +================= + +Simple Installation +********************* + + ``pip install telethon`` + +**More details**: :ref:`installation` + + +Creating a client +************** + + .. code-block:: python + + from telethon import TelegramClient + + # These example values won't work. You must get your own api_id and + # api_hash from https://my.telegram.org, under API Development. + api_id = 12345 + api_hash = '0123456789abcdef0123456789abcdef' + phone = '+34600000000' + + client = TelegramClient('session_name', api_id, api_hash) + client.connect() + + # If you already have a previous 'session_name.session' file, skip this. + client.sign_in(phone=phone) + me = client.sign_in(code=77777) # Put whatever code you received here. + +**More details**: `Click here `_ + + +Simple Stuff +************** + .. code-block:: python + + print(me.stringify()) + + client.send_message('username', 'Hello! Talking to you from Telethon') + client.send_file('username', '/home/myself/Pictures/holidays.jpg') + + client.download_profile_photo(me) + total, messages, senders = client.get_message_history('username') + client.download_media(messages[0]) + diff --git a/readthedocs/extra/basic/installation.rst b/readthedocs/extra/basic/installation.rst new file mode 100644 index 00000000..f302e8f7 --- /dev/null +++ b/readthedocs/extra/basic/installation.rst @@ -0,0 +1,82 @@ +.. _installation: + +================= +Installation +================= + + +Automatic Installation +^^^^^^^^^^^^^^^^^^^^^^^ +To install Telethon, simply do: + + ``pip install telethon`` + +If you get something like ``"SyntaxError: invalid syntax"`` or any other error while installing, it's probably because ``pip`` defaults to Python 2, which is not supported. Use ``pip3`` instead. + +If you already have the library installed, upgrade with: + + ``pip install --upgrade telethon``. + +You can also install the library directly from GitHub or a fork: + + .. code-block:: python + + # pip install git+https://github.com/LonamiWebs/Telethon.git + or + $ git clone https://github.com/LonamiWebs/Telethon.git + $ cd Telethon/ + # pip install -Ue . + +If you don't have root access, simply pass the ``--user`` flag to the pip command. + + +Manual Installation +^^^^^^^^^^^^^^^^^^^^ + +1. Install the required ``pyaes`` (`GitHub`__ | `PyPi`__) and ``rsa`` (`GitHub`__ | `PyPi`__) modules: + + ``sudo -H pip install pyaes rsa`` + +2. Clone Telethon's GitHub repository: ``git clone https://github.com/LonamiWebs/Telethon.git`` + +3. Enter the cloned repository: ``cd Telethon`` + +4. Run the code generator: ``python3 setup.py gen_tl`` + +5. Done! + +To generate the documentation, ``cd docs`` and then ``python3 generate.py``. + + +Optional dependencies +^^^^^^^^^^^^^^^^^^^^^^^^ + +If you're using the library under ARM (or even if you aren't), +you may want to install ``sympy`` through ``pip`` for a substantial speed-up +when generating the keys required to connect to Telegram +(you can of course do this on desktop too). See `issue #199`__ for more. + +If ``libssl`` is available on your system, it will also be used wherever encryption is needed. + +If neither of these are available, a pure Python callback will be used instead, +so you can still run the library wherever Python is available! + + +Sending Requests +***************** + `Here `__ + +Working with updates +********************** + `Here `__ + +Accessing the full API +*********************** + `Here `__ + + +__ https://github.com/ricmoo/pyaes +__ https://pypi.python.org/pypi/pyaes +__ https://github.com/sybrenstuvel/python-rsa/ +__ https://pypi.python.org/pypi/rsa/3.4.2 +__ https://github.com/LonamiWebs/Telethon/issues/199 \ No newline at end of file diff --git a/readthedocs/extra/basic/sending-requests.rst b/readthedocs/extra/basic/sending-requests.rst new file mode 100644 index 00000000..160e2259 --- /dev/null +++ b/readthedocs/extra/basic/sending-requests.rst @@ -0,0 +1,55 @@ +.. _sending-requests: + +================== +Sending Requests +================== + +Since we're working with Python, one must not forget that they can do ``help(client)`` or ``help(TelegramClient)`` +at any time for a more detailed description and a list of all the available methods. +Calling ``help()`` from an interactive Python session will always list all the methods for any object, even yours! + +Interacting with the Telegram API is done through sending **requests**, +this is, any "method" listed on the API. There are a few methods on the ``TelegramClient`` class +that abstract you from the need of manually importing the requests you need. + +For instance, retrieving your own user can be done in a single line: + + ``myself = client.get_me()`` + +Internally, this method has sent a request to Telegram, who replied with the information about your own user. + +If you want to retrieve any other user, chat or channel (channels are a special subset of chats), +you want to retrieve their "entity". This is how the library refers to either of these: + + .. code-block:: python + + # The method will infer that you've passed an username + # It also accepts phone numbers, and will get the user + # from your contact list. + lonami = client.get_entity('lonami') + +Note that saving and using these entities will be more important when Accessing the Full API. +For now, this is a good way to get information about an user or chat. + +Other common methods for quick scripts are also available: + + .. code-block:: python + + # Sending a message (use an entity/username/etc) + client.send_message('TheAyyBot', 'ayy') + + # Sending a photo, or a file + client.send_file(myself, '/path/to/the/file.jpg', force_document=True) + + # Downloading someone's profile photo. File is saved to 'where' + where = client.download_profile_photo(someone) + + # Retrieving the message history + total, messages, senders = client.get_message_history(someone) + + # Downloading the media from a specific message + # You can specify either a directory, a filename, or nothing at all + where = client.download_media(message, '/path/to/output') + +Remember that you can call ``.stringify()`` to any object Telegram returns to pretty print it. +Calling ``str(result)`` does the same operation, but on a single line. diff --git a/readthedocs/extra/basic/sessions.rst b/readthedocs/extra/basic/sessions.rst new file mode 100644 index 00000000..0f9d458a --- /dev/null +++ b/readthedocs/extra/basic/sessions.rst @@ -0,0 +1,48 @@ +.. _sessions: + +============== +Session Files +============== + +The first parameter you pass the the constructor of the +``TelegramClient`` is the ``session``, and defaults to be the session +name (or full path). That is, if you create a ``TelegramClient('anon')`` +instance and connect, an ``anon.session`` file will be created on the +working directory. + +These JSON session files contain the required information to talk to the +Telegram servers, such as to which IP the client should connect, port, +authorization key so that messages can be encrypted, and so on. + +These files will by default also save all the input entities that you’ve +seen, so that you can get information about an user or channel by just +their ID. Telegram will **not** send their ``access_hash`` required to +retrieve more information about them, if it thinks you have already seem +them. For this reason, the library needs to store this information +offline. + +The library will by default too save all the entities (users with their +name, username, chats and so on) **in memory**, not to disk, so that you +can quickly access them by username or phone number. This can be +disabled too. Run ``help(client.session.entities)`` to see the available +methods (or ``help(EntityDatabase)``). + +If you’re not going to work without updates, or don’t need to cache the +``access_hash`` associated with the entities’ ID, you can disable this +by setting ``client.session.save_entities = False``. + +If you don’t want to save the files as JSON, you can also create your +custom ``Session`` subclass and override the ``.save()`` and ``.load()`` +methods. For example, you could save it on a database: + + .. code-block:: python + + class DatabaseSession(Session): + def save(): + # serialize relevant data to the database + + def load(): + # load relevant data to the database + +You should read the ``session.py`` source file to know what “relevant +data” you need to keep track of. \ No newline at end of file diff --git a/readthedocs/extra/basic/working-with-updates.rst b/readthedocs/extra/basic/working-with-updates.rst new file mode 100644 index 00000000..4367bbc4 --- /dev/null +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -0,0 +1,133 @@ +==================== +Working with Updates +==================== + +.. contents:: + + +The library can run in four distinguishable modes: + +- With no extra threads at all. +- With an extra thread that receives everything as soon as possible (default). +- With several worker threads that run your update handlers. +- A mix of the above. + +Since this section is about updates, we'll describe the simplest way to work with them. + +.. warning:: + Remember that you should always call ``client.disconnect()`` once you're done. + + +Using multiple workers +^^^^^^^^^^^^^^^^^^^^^^^ + +When you create your client, simply pass a number to the ``update_workers`` parameter: + + ``client = TelegramClient('session', api_id, api_hash, update_workers=4)`` + +4 workers should suffice for most cases (this is also the default on `Python Telegram Bot`__). +You can set this value to more, or even less if you need. + +The next thing you want to do is to add a method that will be called when an `Update`__ arrives: + + .. code-block:: python + + def callback(update): + print('I received', update) + + client.add_update_handler(callback) + # do more work here, or simply sleep! + +That's it! Now let's do something more interesting. +Every time an user talks to use, let's reply to them with the same text reversed: + + .. code-block:: python + + from telethon.tl.types import UpdateShortMessage, PeerUser + + def replier(update): + if isinstance(update, UpdateShortMessage) and not update.out: + client.send_message(PeerUser(update.user_id), update.message[::-1]) + + + client.add_update_handler(replier) + input('Press enter to stop this!') + client.disconnect() + +We only ask you one thing: don't keep this running for too long, or your contacts will go mad. + + +Spawning no worker at all +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +All the workers do is loop forever and poll updates from a queue that is filled from the ``ReadThread``, +responsible for reading every item off the network. +If you only need a worker and the ``MainThread`` would be doing no other job, +this is the preferred way. You can easily do the same as the workers like so: + + .. code-block:: python + + while True: + try: + update = client.updates.poll() + if not update: + continue + + print('I received', update) + except KeyboardInterrupt: + break + + client.disconnect() + +Note that ``poll`` accepts a ``timeout=`` parameter, +and it will return ``None`` if other thread got the update before you could or if the timeout expired, +so it's important to check ``if not update``. + +This can coexist with the rest of ``N`` workers, or you can set it to ``0`` additional workers: + + ``client = TelegramClient('session', api_id, api_hash, update_workers=0)`` + +You **must** set it to ``0`` (or other number), as it defaults to ``None`` and there is a different. +``None`` workers means updates won't be processed *at all*, +so you must set it to some value (0 or greater) if you want ``client.updates.poll()`` to work. + + +Using the main thread instead the ``ReadThread`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you have no work to do on the ``MainThread`` and you were planning to have a ``while True: sleep(1)``, +don't do that. Instead, don't spawn the secondary ``ReadThread`` at all like so: + + .. code-block:: python + + client = TelegramClient( + ... + spawn_read_thread=False + ) + +And then ``.idle()`` from the ``MainThread``: + + ``client.idle()`` + +You can stop it with :kbd:`Control+C`, +and you can configure the signals to be used in a similar fashion to `Python Telegram Bot`__. + +As a complete example: + + .. code-block:: python + + def callback(update): + print('I received', update) + + client = TelegramClient('session', api_id, api_hash, + update_workers=1, spawn_read_thread=False) + + client.connect() + client.add_update_handler(callback) + client.idle() # ends with Ctrl+C + client.disconnect() + + +__ https://python-telegram-bot.org/ +__ https://lonamiwebs.github.io/Telethon/types/update.html +__ https://github.com/python-telegram-bot/python-telegram-bot/blob/4b3315db6feebafb94edcaa803df52bb49999ced/telegram/ext/updater.py#L460 \ No newline at end of file diff --git a/readthedocs/extra/getting_started.rst b/readthedocs/extra/getting_started.rst deleted file mode 100644 index 3a6b36a8..00000000 --- a/readthedocs/extra/getting_started.rst +++ /dev/null @@ -1,103 +0,0 @@ -.. Telethon documentation master file, created by - sphinx-quickstart on Fri Nov 17 15:36:11 2017. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - - -================= -Getting Started! -================= - -Installation -************** - -To install Telethon, simply do: - - ``pip install telethon`` - -If you get something like ``"SyntaxError: invalid syntax"`` or any other error while installing, it's probably because ``pip`` defaults to Python 2, which is not supported. Use ``pip3`` instead. - -If you already have the library installed, upgrade with: - - ``pip install --upgrade telethon``. - -You can also install the library directly from GitHub or a fork: - - .. code-block:: python - - # pip install git+https://github.com/LonamiWebs/Telethon.git - or - $ git clone https://github.com/LonamiWebs/Telethon.git - $ cd Telethon/ - # pip install -Ue . - -If you don't have root access, simply pass the ``--user`` flag to the pip command. - - - - -Creating a client -************** -Before working with Telegram's API, you need to get your own API ID and hash: - -1. Follow `this link `_ and login with your phone number. - -2. Click under API Development tools. - -3. A *Create new application* window will appear. Fill in your application details. There is no need to enter any *URL*, and only the first two fields (*App title* and *Short name*) can be changed later as far as I'm aware. - -4. Click on *Create application* at the end. Remember that your **API hash is secret** and Telegram won't let you revoke it. Don't post it anywhere! - -Once that's ready, the next step is to create a ``TelegramClient``. This class will be your main interface with Telegram's API, and creating one is very simple: - - .. code-block:: python - - from telethon import TelegramClient - - # These example values won't work. You must get your own api_id and - # api_hash from https://my.telegram.org, under API Development. - api_id = 12345 - api_hash = '0123456789abcdef0123456789abcdef' - phone = '+34600000000' - - client = TelegramClient('session_name', api_id, api_hash) - client.connect() - - # If you already have a previous 'session_name.session' file, skip this. - client.sign_in(phone=phone) - me = client.sign_in(code=77777) # Put whatever code you received here. - -**More details**: `Click here `_ - - -Simple Stuff -************** - .. code-block:: python - - print(me.stringify()) - - client.send_message('username', 'Hello! Talking to you from Telethon') - client.send_file('username', '/home/myself/Pictures/holidays.jpg') - - client.download_profile_photo(me) - total, messages, senders = client.get_message_history('username') - client.download_media(messages[0]) - - -Diving In -************** - -.. note:: More info in our Wiki! - -Sending Requests -^^^^^^^^^^^^^^^^^^^^^ - `Here `__ - -Working with updates -^^^^^^^^^^^^^^^^^^^^^ - `Here `__ - -Accessing the full API -^^^^^^^^^^^^^^^^^^^^^^^ - `Here `__ - diff --git a/readthedocs/extra/troubleshooting/deleted-limited-or-deactivated-accounts.rst b/readthedocs/extra/troubleshooting/deleted-limited-or-deactivated-accounts.rst new file mode 100644 index 00000000..1ad3da19 --- /dev/null +++ b/readthedocs/extra/troubleshooting/deleted-limited-or-deactivated-accounts.rst @@ -0,0 +1,26 @@ +========================================= +Deleted, Limited or Deactivated Accounts +========================================= + +If you're from Iran or Russian, we have bad news for you. +Telegram is much more likely to ban these numbers, +as they are often used to spam other accounts, +likely through the use of libraries like this one. +The best advice we can give you is to not abuse the API, +like calling many requests really quickly, +and to sign up with these phones through an official application. + +Telegram may also ban virtual (VoIP) phone numbers, +as again, they're likely to be used for spam. + +If you want to check if your account has been limited, +simply send a private message to `@SpamBot`__ through Telegram itself. +You should notice this by getting errors like ``PeerFloodError``, +which means you're limited, for instance, +when sending a message to some accounts but not others. + +For more discussion, please see `issue 297`__. + + +__ https://t.me/SpamBot +__ https://github.com/LonamiWebs/Telethon/issues/297 \ No newline at end of file diff --git a/readthedocs/extra/troubleshooting/enable-logging.rst b/readthedocs/extra/troubleshooting/enable-logging.rst new file mode 100644 index 00000000..a6d45d00 --- /dev/null +++ b/readthedocs/extra/troubleshooting/enable-logging.rst @@ -0,0 +1,24 @@ +================ +Enable Logging +================ + +Telethon makes use of the `logging`__ module, and you can enable it as follows: + + .. code-block:: python + + import logging + logging.basicConfig(level=logging.DEBUG) + +You can also use it in your own project very easily: + + .. code-block:: python + + import logging + logger = logging.getLogger(__name__) + + logger.debug('Debug messages') + logger.info('Useful information') + logger.warning('This is a warning!') + + +__ https://docs.python.org/3/library/logging.html \ No newline at end of file diff --git a/readthedocs/extra/troubleshooting/rpc-errors.rst b/readthedocs/extra/troubleshooting/rpc-errors.rst new file mode 100644 index 00000000..6e8a59f0 --- /dev/null +++ b/readthedocs/extra/troubleshooting/rpc-errors.rst @@ -0,0 +1,27 @@ +========== +RPC Errors +========== + +RPC stands for Remote Procedure Call, and when Telethon raises an +``RPCError``, it’s most likely because you have invoked some of the API +methods incorrectly (wrong parameters, wrong permissions, or even +something went wrong on Telegram’s server). The most common are: + +- ``FloodError`` (420), the same request was repeated many times. Must + wait ``.seconds``. +- ``SessionPasswordNeededError``, if you have setup two-steps + verification on Telegram. +- ``CdnFileTamperedError``, if the media you were trying to download + from a CDN has been altered. +- ``ChatAdminRequiredError``, you don’t have permissions to perform + said operation on a chat or channel. Try avoiding filters, i.e. when + searching messages. + +The generic classes for different error codes are: \* ``InvalidDCError`` +(303), the request must be repeated on another DC. \* +``BadRequestError`` (400), the request contained errors. \* +``UnauthorizedError`` (401), the user is not authorized yet. \* +``ForbiddenError`` (403), privacy violation error. \* ``NotFoundError`` +(404), make sure you’re invoking ``Request``\ ’s! + +If the error is not recognised, it will only be an ``RPCError``. \ No newline at end of file diff --git a/readthedocs/index.rst b/readthedocs/index.rst index 8f036adc..b5c77e6b 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -10,24 +10,49 @@ Pure Python 3 Telegram client library. Official Site `here Date: Mon, 20 Nov 2017 12:19:53 +0800 Subject: [PATCH 62/93] final fix for readthedocs --- .../extra/advanced-usage/users-and-chats.rst | 1 + readthedocs/extra/basic/accessing-the-full-api.rst | 2 ++ readthedocs/extra/basic/installation.rst | 14 +++++++++++--- readthedocs/extra/basic/working-with-updates.rst | 2 ++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/readthedocs/extra/advanced-usage/users-and-chats.rst b/readthedocs/extra/advanced-usage/users-and-chats.rst index 3f3e0729..a48a2857 100644 --- a/readthedocs/extra/advanced-usage/users-and-chats.rst +++ b/readthedocs/extra/advanced-usage/users-and-chats.rst @@ -6,6 +6,7 @@ Users and Chats Make sure you have gone through :ref:`prelude` already! .. contents:: + :depth: 2 .. _retrieving-an-entity: diff --git a/readthedocs/extra/basic/accessing-the-full-api.rst b/readthedocs/extra/basic/accessing-the-full-api.rst index ceeea7d8..ab6682db 100644 --- a/readthedocs/extra/basic/accessing-the-full-api.rst +++ b/readthedocs/extra/basic/accessing-the-full-api.rst @@ -1,3 +1,5 @@ +.. _accessing-the-full-api: + ========================== Accessing the Full API ========================== diff --git a/readthedocs/extra/basic/installation.rst b/readthedocs/extra/basic/installation.rst index f302e8f7..ab9b44a3 100644 --- a/readthedocs/extra/basic/installation.rst +++ b/readthedocs/extra/basic/installation.rst @@ -64,15 +64,23 @@ so you can still run the library wherever Python is available! Sending Requests ***************** - `Here `__ + + .. note:: + Read here: :ref:`sending-requests` + Working with updates ********************** - `Here `__ + + .. note:: + Read here: :ref:`working-with-updates` + Accessing the full API *********************** - `Here `__ + + .. note:: + Read here: :ref:`accessing-the-full-api` __ https://github.com/ricmoo/pyaes diff --git a/readthedocs/extra/basic/working-with-updates.rst b/readthedocs/extra/basic/working-with-updates.rst index 4367bbc4..c5d9e919 100644 --- a/readthedocs/extra/basic/working-with-updates.rst +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -1,3 +1,5 @@ +.. _working-with-updates: + ==================== Working with Updates ==================== From 07d642a354829693d1eb79cb3d7da962a48bbfcd Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 20 Nov 2017 13:58:42 +0800 Subject: [PATCH 63/93] tiny fix for readthedocs --- readthedocs/extra/basic/installation.rst | 25 +++--------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/readthedocs/extra/basic/installation.rst b/readthedocs/extra/basic/installation.rst index ab9b44a3..ecad699b 100644 --- a/readthedocs/extra/basic/installation.rst +++ b/readthedocs/extra/basic/installation.rst @@ -11,11 +11,12 @@ To install Telethon, simply do: ``pip install telethon`` -If you get something like ``"SyntaxError: invalid syntax"`` or any other error while installing, it's probably because ``pip`` defaults to Python 2, which is not supported. Use ``pip3`` instead. +If you get something like ``"SyntaxError: invalid syntax"`` or any other error while installing, +it's probably because ``pip`` defaults to Python 2, which is not supported. Use ``pip3`` instead. If you already have the library installed, upgrade with: - ``pip install --upgrade telethon``. + ``pip install --upgrade telethon`` You can also install the library directly from GitHub or a fork: @@ -62,26 +63,6 @@ If neither of these are available, a pure Python callback will be used instead, so you can still run the library wherever Python is available! -Sending Requests -***************** - - .. note:: - Read here: :ref:`sending-requests` - - -Working with updates -********************** - - .. note:: - Read here: :ref:`working-with-updates` - - -Accessing the full API -*********************** - - .. note:: - Read here: :ref:`accessing-the-full-api` - __ https://github.com/ricmoo/pyaes __ https://pypi.python.org/pypi/pyaes From 9f033be05a9af4a7804d0bcb19c3ab6235c56c4a Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 20 Nov 2017 17:26:31 +0800 Subject: [PATCH 64/93] fix in-docs links --- readthedocs/extra/basic/getting-started.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readthedocs/extra/basic/getting-started.rst b/readthedocs/extra/basic/getting-started.rst index ce4354fd..bad3ea30 100644 --- a/readthedocs/extra/basic/getting-started.rst +++ b/readthedocs/extra/basic/getting-started.rst @@ -13,7 +13,7 @@ Simple Installation ``pip install telethon`` -**More details**: :ref:`installation` + **More details**: :ref:`installation` Creating a client @@ -36,7 +36,7 @@ Creating a client client.sign_in(phone=phone) me = client.sign_in(code=77777) # Put whatever code you received here. -**More details**: `Click here `_ + **More details**: :ref:`creating-a-client` Simple Stuff From 152856dfbc9e3f8b4b29b171bad306236d7fc754 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 20 Nov 2017 10:58:11 +0100 Subject: [PATCH 65/93] Add a force_fetch parameter to .get_entity --- telethon/telegram_client.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index ae864ea6..fc4b4342 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -996,7 +996,7 @@ class TelegramClient(TelegramBareClient): # region Small utilities to make users' life easier - def get_entity(self, entity): + def get_entity(self, entity, force_fetch=False): """ Turns the given entity into a valid Telegram user or chat. @@ -1014,12 +1014,20 @@ class TelegramClient(TelegramBareClient): If the entity is neither, and it's not a TLObject, an error will be raised. + + :param force_fetch: + If True, the entity cache is bypassed and the entity is fetched + again with an API call. Defaults to False to avoid unnecessary + calls, but since a cached version would be returned, the entity + may be out of date. :return: """ - try: - return self.session.entities[entity] - except KeyError: - pass + if not force_fetch: + # Try to use cache unless we want to force a fetch + try: + return self.session.entities[entity] + except KeyError: + pass if isinstance(entity, int) or ( isinstance(entity, TLObject) and From 7d4453351babd62df70f9ebe96d59e016eff1737 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 21 Nov 2017 12:56:53 +0100 Subject: [PATCH 66/93] Update .gitignore to include docs/_build --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 156d23e3..f2090e85 100755 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ -.idea +# Docs +_build/ + +# Generated code telethon/tl/functions/ telethon/tl/types/ telethon/tl/all_tlobjects.py From f99d14558fc964f37fa321f9f598e7fef83adba4 Mon Sep 17 00:00:00 2001 From: "Dmitry D. Chernov" Date: Thu, 23 Nov 2017 02:06:43 +1000 Subject: [PATCH 67/93] binary_reader: Parse TL 'date' to UTC datetime instead of local --- telethon/extensions/binary_reader.py | 2 +- telethon/tl/tlobject.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/telethon/extensions/binary_reader.py b/telethon/extensions/binary_reader.py index 2355c6a4..c5abcbf9 100644 --- a/telethon/extensions/binary_reader.py +++ b/telethon/extensions/binary_reader.py @@ -113,7 +113,7 @@ class BinaryReader: into a Python datetime object """ value = self.read_int() - return None if value == 0 else datetime.fromtimestamp(value) + return None if value == 0 else datetime.utcfromtimestamp(value) def tgread_object(self): """Reads a Telegram object""" diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index 1bd57878..2ba6ef14 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -36,7 +36,7 @@ class TLObject: ', '.join(TLObject.pretty_format(x) for x in obj) ) elif isinstance(obj, datetime): - return 'datetime.fromtimestamp({})'.format(obj.timestamp()) + return 'datetime.utcfromtimestamp({})'.format(obj.timestamp()) else: return repr(obj) else: @@ -81,7 +81,7 @@ class TLObject: result.append(']') elif isinstance(obj, datetime): - result.append('datetime.fromtimestamp(') + result.append('datetime.utcfromtimestamp(') result.append(repr(obj.timestamp())) result.append(')') From 5a4d6d4a570cc71afb553e914bf7fc7d2366544f Mon Sep 17 00:00:00 2001 From: "Dmitry D. Chernov" Date: Thu, 23 Nov 2017 22:39:35 +1000 Subject: [PATCH 68/93] tlobject: Represent timestamp as 'int' instead of 'float' --- telethon/tl/tlobject.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index 2ba6ef14..68c5e741 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -36,7 +36,9 @@ class TLObject: ', '.join(TLObject.pretty_format(x) for x in obj) ) elif isinstance(obj, datetime): - return 'datetime.utcfromtimestamp({})'.format(obj.timestamp()) + return 'datetime.utcfromtimestamp({})'.format( + int(obj.timestamp()) + ) else: return repr(obj) else: @@ -82,7 +84,7 @@ class TLObject: elif isinstance(obj, datetime): result.append('datetime.utcfromtimestamp(') - result.append(repr(obj.timestamp())) + result.append(repr(int(obj.timestamp()))) result.append(')') else: From b42b4bb3267bcbce297d79fc2f292f0fc548fb87 Mon Sep 17 00:00:00 2001 From: Maxim Smirnov Date: Fri, 24 Nov 2017 20:47:36 +0300 Subject: [PATCH 69/93] Add OpenSSL assertion on authenticator.py (#453) aes_ige.c(88): OpenSSL internal error, assertion failed --- telethon/network/authenticator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/telethon/network/authenticator.py b/telethon/network/authenticator.py index 1accf493..8c508ef9 100644 --- a/telethon/network/authenticator.py +++ b/telethon/network/authenticator.py @@ -113,6 +113,8 @@ def _do_authentication(connection): key, iv = utils.generate_key_data_from_nonce( res_pq.server_nonce, new_nonce ) + if server_dh_params.encrypted_answer % 16 != 0: + raise SecurityError('AES block size missmatch') plain_text_answer = AES.decrypt_ige( server_dh_params.encrypted_answer, key, iv ) From 74ec6391d92fc1ee75b738a4af74ae5e47f494f4 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 24 Nov 2017 19:05:18 +0100 Subject: [PATCH 70/93] Fix-up security assertion (b42b4bb for #453) --- telethon/network/authenticator.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/telethon/network/authenticator.py b/telethon/network/authenticator.py index 8c508ef9..00a28fdf 100644 --- a/telethon/network/authenticator.py +++ b/telethon/network/authenticator.py @@ -113,8 +113,10 @@ def _do_authentication(connection): key, iv = utils.generate_key_data_from_nonce( res_pq.server_nonce, new_nonce ) - if server_dh_params.encrypted_answer % 16 != 0: - raise SecurityError('AES block size missmatch') + if len(server_dh_params.encrypted_answer) % 16 != 0: + # See PR#453 + raise SecurityError('AES block size mismatch') + plain_text_answer = AES.decrypt_ige( server_dh_params.encrypted_answer, key, iv ) From a932fb64701500f6aa50d0cca7a534a42a655a23 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 26 Nov 2017 16:57:40 +0100 Subject: [PATCH 71/93] Document the crypto/ module --- telethon/crypto/__init__.py | 5 ++++ telethon/crypto/aes.py | 17 +++++++++--- telethon/crypto/aes_ctr.py | 21 +++++++++++++++ telethon/crypto/auth_key.py | 20 ++++++++++++-- telethon/crypto/cdn_decrypter.py | 45 ++++++++++++++++++++++++-------- telethon/crypto/factorization.py | 19 ++++++++++++++ telethon/crypto/libssl.py | 16 ++++++++++++ telethon/crypto/rsa.py | 20 +++++++++++--- 8 files changed, 142 insertions(+), 21 deletions(-) diff --git a/telethon/crypto/__init__.py b/telethon/crypto/__init__.py index d151a96c..aa470adf 100644 --- a/telethon/crypto/__init__.py +++ b/telethon/crypto/__init__.py @@ -1,3 +1,8 @@ +""" +This module contains several utilities regarding cryptographic purposes, +such as the AES IGE mode used by Telegram, the authorization key bound with +their data centers, and so on. +""" from .aes import AES from .aes_ctr import AESModeCTR from .auth_key import AuthKey diff --git a/telethon/crypto/aes.py b/telethon/crypto/aes.py index c09add56..191cde15 100644 --- a/telethon/crypto/aes.py +++ b/telethon/crypto/aes.py @@ -1,3 +1,6 @@ +""" +AES IGE implementation in Python. This module may use libssl if available. +""" import os import pyaes from . import libssl @@ -9,10 +12,15 @@ if libssl.AES is not None: else: # Fallback to a pure Python implementation class AES: + """ + Class that servers as an interface to encrypt and decrypt + text through the AES IGE mode. + """ @staticmethod def decrypt_ige(cipher_text, key, iv): - """Decrypts the given text in 16-bytes blocks by using the - given key and 32-bytes initialization vector + """ + Decrypts the given text in 16-bytes blocks by using the + given key and 32-bytes initialization vector. """ iv1 = iv[:len(iv) // 2] iv2 = iv[len(iv) // 2:] @@ -42,8 +50,9 @@ else: @staticmethod def encrypt_ige(plain_text, key, iv): - """Encrypts the given text in 16-bytes blocks by using the - given key and 32-bytes initialization vector + """ + Encrypts the given text in 16-bytes blocks by using the + given key and 32-bytes initialization vector. """ # Add random padding iff it's not evenly divisible by 16 already diff --git a/telethon/crypto/aes_ctr.py b/telethon/crypto/aes_ctr.py index 7bd7b79a..34422904 100644 --- a/telethon/crypto/aes_ctr.py +++ b/telethon/crypto/aes_ctr.py @@ -1,3 +1,6 @@ +""" +This module holds the AESModeCTR wrapper class. +""" import pyaes @@ -6,6 +9,12 @@ class AESModeCTR: # TODO Maybe make a pull request to pyaes to support iv on CTR def __init__(self, key, iv): + """ + Initializes the AES CTR mode with the given key/iv pair. + + :param key: the key to be used as bytes. + :param iv: the bytes initialization vector. Must have a length of 16. + """ # TODO Use libssl if available assert isinstance(key, bytes) self._aes = pyaes.AESModeOfOperationCTR(key) @@ -15,7 +24,19 @@ class AESModeCTR: self._aes._counter._counter = list(iv) def encrypt(self, data): + """ + Encrypts the given plain text through AES CTR. + + :param data: the plain text to be encrypted. + :return: the encrypted cipher text. + """ return self._aes.encrypt(data) def decrypt(self, data): + """ + Decrypts the given cipher text through AES CTR + + :param data: the cipher text to be decrypted. + :return: the decrypted plain text. + """ return self._aes.decrypt(data) diff --git a/telethon/crypto/auth_key.py b/telethon/crypto/auth_key.py index 17a7f8ca..679e62ff 100644 --- a/telethon/crypto/auth_key.py +++ b/telethon/crypto/auth_key.py @@ -1,3 +1,6 @@ +""" +This module holds the AuthKey class. +""" import struct from hashlib import sha1 @@ -6,7 +9,16 @@ from ..extensions import BinaryReader class AuthKey: + """ + Represents an authorization key, used to encrypt and decrypt + messages sent to Telegram's data centers. + """ def __init__(self, data): + """ + Initializes a new authorization key. + + :param data: the data in bytes that represent this auth key. + """ self.key = data with BinaryReader(sha1(self.key).digest()) as reader: @@ -15,8 +27,12 @@ class AuthKey: self.key_id = reader.read_long(signed=False) def calc_new_nonce_hash(self, new_nonce, number): - """Calculates the new nonce hash based on - the current class fields' values + """ + Calculates the new nonce hash based on the current attributes. + + :param new_nonce: the new nonce to be hashed. + :param number: number to prepend before the hash. + :return: the hash for the given new nonce. """ new_nonce = new_nonce.to_bytes(32, 'little', signed=True) data = new_nonce + struct.pack(' Date: Sun, 26 Nov 2017 17:06:09 +0100 Subject: [PATCH 72/93] Document the errors/ module --- telethon/errors/__init__.py | 19 +++++++ telethon/errors/common.py | 29 ++++++++--- telethon/errors/rpc_base_errors.py | 79 ++++++++++++++++-------------- 3 files changed, 84 insertions(+), 43 deletions(-) diff --git a/telethon/errors/__init__.py b/telethon/errors/__init__.py index 6e62bfb9..fbb2f424 100644 --- a/telethon/errors/__init__.py +++ b/telethon/errors/__init__.py @@ -1,3 +1,7 @@ +""" +This module holds all the base and automatically generated errors that the +Telegram API has. See telethon_generator/errors.json for more. +""" import urllib.request import re from threading import Thread @@ -13,6 +17,13 @@ from .rpc_error_list import * def report_error(code, message, report_method): + """ + Reports an RPC error to pwrtelegram. + + :param code: the integer code of the error (like 400). + :param message: the message representing the error. + :param report_method: the constructor ID of the function that caused it. + """ try: # Ensure it's signed report_method = int.from_bytes( @@ -30,6 +41,14 @@ def report_error(code, message, report_method): def rpc_message_to_error(code, message, report_method=None): + """ + Converts a Telegram's RPC Error to a Python error. + + :param code: the integer code of the error (like 400). + :param message: the message representing the error. + :param report_method: if present, the ID of the method that caused it. + :return: the RPCError as a Python exception that represents this error. + """ if report_method is not None: Thread( target=report_error, diff --git a/telethon/errors/common.py b/telethon/errors/common.py index be3b1d93..f2f21840 100644 --- a/telethon/errors/common.py +++ b/telethon/errors/common.py @@ -2,20 +2,23 @@ class ReadCancelledError(Exception): - """Occurs when a read operation was cancelled""" + """Occurs when a read operation was cancelled.""" def __init__(self): super().__init__(self, 'The read operation was cancelled.') class InvalidParameterError(Exception): - """Occurs when an invalid parameter is given, for example, - when either A or B are required but none is given""" + """ + Occurs when an invalid parameter is given, for example, + when either A or B are required but none is given. + """ class TypeNotFoundError(Exception): - """Occurs when a type is not found, for example, - when trying to read a TLObject with an invalid constructor code""" - + """ + Occurs when a type is not found, for example, + when trying to read a TLObject with an invalid constructor code. + """ def __init__(self, invalid_constructor_id): super().__init__( self, 'Could not find a matching Constructor ID for the TLObject ' @@ -27,6 +30,10 @@ class TypeNotFoundError(Exception): class InvalidChecksumError(Exception): + """ + Occurs when using the TCP full mode and the checksum of a received + packet doesn't match the expected checksum. + """ def __init__(self, checksum, valid_checksum): super().__init__( self, @@ -39,6 +46,9 @@ class InvalidChecksumError(Exception): class BrokenAuthKeyError(Exception): + """ + Occurs when the authorization key for a data center is not valid. + """ def __init__(self): super().__init__( self, @@ -47,6 +57,9 @@ class BrokenAuthKeyError(Exception): class SecurityError(Exception): + """ + Generic security error, mostly used when generating a new AuthKey. + """ def __init__(self, *args): if not args: args = ['A security check failed.'] @@ -54,6 +67,10 @@ class SecurityError(Exception): class CdnFileTamperedError(SecurityError): + """ + Occurs when there's a hash mismatch between the decrypted CDN file + and its expected hash. + """ def __init__(self): super().__init__( 'The CDN file has been altered and its download cancelled.' diff --git a/telethon/errors/rpc_base_errors.py b/telethon/errors/rpc_base_errors.py index 5c938641..9e6eed1a 100644 --- a/telethon/errors/rpc_base_errors.py +++ b/telethon/errors/rpc_base_errors.py @@ -1,11 +1,12 @@ class RPCError(Exception): + """Base class for all Remote Procedure Call errors.""" code = None message = None class InvalidDCError(RPCError): """ - The request must be repeated, but directed to a different data center. + The request must be repeated, but directed to a different data center. """ code = 303 message = 'ERROR_SEE_OTHER' @@ -13,9 +14,9 @@ class InvalidDCError(RPCError): class BadRequestError(RPCError): """ - The query contains errors. In the event that a request was created - using a form and contains user generated data, the user should be - notified that the data must be corrected before the query is repeated. + The query contains errors. In the event that a request was created + using a form and contains user generated data, the user should be + notified that the data must be corrected before the query is repeated. """ code = 400 message = 'BAD_REQUEST' @@ -23,8 +24,8 @@ class BadRequestError(RPCError): class UnauthorizedError(RPCError): """ - There was an unauthorized attempt to use functionality available only - to authorized users. + There was an unauthorized attempt to use functionality available only + to authorized users. """ code = 401 message = 'UNAUTHORIZED' @@ -32,8 +33,8 @@ class UnauthorizedError(RPCError): class ForbiddenError(RPCError): """ - Privacy violation. For example, an attempt to write a message to - someone who has blacklisted the current user. + Privacy violation. For example, an attempt to write a message to + someone who has blacklisted the current user. """ code = 403 message = 'FORBIDDEN' @@ -45,7 +46,7 @@ class ForbiddenError(RPCError): class NotFoundError(RPCError): """ - An attempt to invoke a non-existent object, such as a method. + An attempt to invoke a non-existent object, such as a method. """ code = 404 message = 'NOT_FOUND' @@ -57,10 +58,10 @@ class NotFoundError(RPCError): class FloodError(RPCError): """ - The maximum allowed number of attempts to invoke the given method - with the given input parameters has been exceeded. For example, in an - attempt to request a large number of text messages (SMS) for the same - phone number. + The maximum allowed number of attempts to invoke the given method + with the given input parameters has been exceeded. For example, in an + attempt to request a large number of text messages (SMS) for the same + phone number. """ code = 420 message = 'FLOOD' @@ -68,9 +69,9 @@ class FloodError(RPCError): class ServerError(RPCError): """ - An internal server error occurred while a request was being processed - for example, there was a disruption while accessing a database or file - storage. + An internal server error occurred while a request was being processed + for example, there was a disruption while accessing a database or file + storage. """ code = 500 message = 'INTERNAL' @@ -81,38 +82,42 @@ class ServerError(RPCError): class BadMessageError(Exception): - """Occurs when handling a bad_message_notification""" + """Occurs when handling a bad_message_notification.""" ErrorMessages = { 16: - 'msg_id too low (most likely, client time is wrong it would be worthwhile to ' - 'synchronize it using msg_id notifications and re-send the original message ' - 'with the "correct" msg_id or wrap it in a container with a new msg_id if the ' - 'original message had waited too long on the client to be transmitted).', + 'msg_id too low (most likely, client time is wrong it would be ' + 'worthwhile to synchronize it using msg_id notifications and re-send ' + 'the original message with the "correct" msg_id or wrap it in a ' + 'container with a new msg_id if the original message had waited too ' + 'long on the client to be transmitted).', 17: - 'msg_id too high (similar to the previous case, the client time has to be ' - 'synchronized, and the message re-sent with the correct msg_id).', + 'msg_id too high (similar to the previous case, the client time has ' + 'to be synchronized, and the message re-sent with the correct msg_id).', 18: - 'Incorrect two lower order msg_id bits (the server expects client message msg_id ' - 'to be divisible by 4).', + 'Incorrect two lower order msg_id bits (the server expects client ' + 'message msg_id to be divisible by 4).', 19: - 'Container msg_id is the same as msg_id of a previously received message ' - '(this must never happen).', + 'Container msg_id is the same as msg_id of a previously received ' + 'message (this must never happen).', 20: - 'Message too old, and it cannot be verified whether the server has received a ' - 'message with this msg_id or not.', + 'Message too old, and it cannot be verified whether the server has ' + 'received a message with this msg_id or not.', 32: - 'msg_seqno too low (the server has already received a message with a lower ' - 'msg_id but with either a higher or an equal and odd seqno).', + 'msg_seqno too low (the server has already received a message with a ' + 'lower msg_id but with either a higher or an equal and odd seqno).', 33: - 'msg_seqno too high (similarly, there is a message with a higher msg_id but with ' - 'either a lower or an equal and odd seqno).', + 'msg_seqno too high (similarly, there is a message with a higher ' + 'msg_id but with either a lower or an equal and odd seqno).', 34: 'An even msg_seqno expected (irrelevant message), but odd received.', - 35: 'Odd msg_seqno expected (relevant message), but even received.', + 35: + 'Odd msg_seqno expected (relevant message), but even received.', 48: - 'Incorrect server salt (in this case, the bad_server_salt response is received with ' - 'the correct salt, and the message is to be re-sent with it).', - 64: 'Invalid container.' + 'Incorrect server salt (in this case, the bad_server_salt response ' + 'is received with the correct salt, and the message is to be re-sent ' + 'with it).', + 64: + 'Invalid container.' } def __init__(self, code): From 57a70d0d47c294f28c02a0f1664e5b717a6f73c2 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 26 Nov 2017 17:14:28 +0100 Subject: [PATCH 73/93] Document the extensions/ module --- telethon/extensions/binary_reader.py | 44 ++++++++++++++++------------ telethon/extensions/markdown.py | 20 ++++++++----- telethon/extensions/tcp_client.py | 38 +++++++++++++++++------- 3 files changed, 65 insertions(+), 37 deletions(-) diff --git a/telethon/extensions/binary_reader.py b/telethon/extensions/binary_reader.py index c5abcbf9..19fb608b 100644 --- a/telethon/extensions/binary_reader.py +++ b/telethon/extensions/binary_reader.py @@ -1,3 +1,6 @@ +""" +This module contains the BinaryReader utility class. +""" import os from datetime import datetime from io import BufferedReader, BytesIO @@ -30,32 +33,32 @@ class BinaryReader: # "All numbers are written as little endian." # https://core.telegram.org/mtproto def read_byte(self): - """Reads a single byte value""" + """Reads a single byte value.""" return self.read(1)[0] def read_int(self, signed=True): - """Reads an integer (4 bytes) value""" + """Reads an integer (4 bytes) value.""" return int.from_bytes(self.read(4), byteorder='little', signed=signed) def read_long(self, signed=True): - """Reads a long integer (8 bytes) value""" + """Reads a long integer (8 bytes) value.""" return int.from_bytes(self.read(8), byteorder='little', signed=signed) def read_float(self): - """Reads a real floating point (4 bytes) value""" + """Reads a real floating point (4 bytes) value.""" return unpack(' 'y!'. + """ + Gets the inner text that's surrounded by the given entity or entities. + For instance: text = 'hey!', entity = MessageEntityBold(2, 2) -> 'y!'. + + :param text: the original text. + :param entity: the entity or entities that must be matched. + :return: a single result or a list of the text surrounded by the entities. """ if not isinstance(entity, TLObject) and hasattr(entity, '__iter__'): multiple = True diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index af9bfbfe..3941a4d6 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -1,4 +1,6 @@ -# Python rough implementation of a C# TCP client +""" +This module holds a rough implementation of the C# TCP client. +""" import errno import socket from datetime import timedelta @@ -7,7 +9,14 @@ from threading import Lock class TcpClient: + """A simple TCP client to ease the work with sockets and proxies.""" def __init__(self, proxy=None, timeout=timedelta(seconds=5)): + """ + Initializes the TCP client. + + :param proxy: the proxy to be used, if any. + :param timeout: the timeout for connect, read and write operations. + """ self.proxy = proxy self._socket = None self._closing_lock = Lock() @@ -33,8 +42,11 @@ class TcpClient: self._socket.settimeout(self.timeout) def connect(self, ip, port): - """Connects to the specified IP and port number. - 'timeout' must be given in seconds + """ + Tries connecting forever to IP:port unless an OSError is raised. + + :param ip: the IP to connect to. + :param port: the port to connect to. """ if ':' in ip: # IPv6 # The address needs to be surrounded by [] as discussed on PR#425 @@ -65,12 +77,13 @@ class TcpClient: raise def _get_connected(self): + """Determines whether the client is connected or not.""" return self._socket is not None and self._socket.fileno() >= 0 connected = property(fget=_get_connected) def close(self): - """Closes the connection""" + """Closes the connection.""" if self._closing_lock.locked(): # Already closing, no need to close again (avoid None.close()) return @@ -86,7 +99,11 @@ class TcpClient: self._socket = None def write(self, data): - """Writes (sends) the specified bytes to the connected peer""" + """ + Writes (sends) the specified bytes to the connected peer. + + :param data: the data to send. + """ if self._socket is None: raise ConnectionResetError() @@ -105,13 +122,11 @@ class TcpClient: raise def read(self, size): - """Reads (receives) a whole block of 'size bytes - from the connected peer. + """ + Reads (receives) a whole block of size bytes from the connected peer. - A timeout can be specified, which will cancel the operation if - no data has been read in the specified time. If data was read - and it's waiting for more, the timeout will NOT cancel the - operation. Set to None for no timeout + :param size: the size of the block to be read. + :return: the read data with len(data) == size. """ if self._socket is None: raise ConnectionResetError() @@ -141,5 +156,6 @@ class TcpClient: return buffer.raw.getvalue() def _raise_connection_reset(self): + """Disconnects the client and raises ConnectionResetError.""" self.close() # Connection reset -> flag as socket closed raise ConnectionResetError('The server has closed the connection.') From 605c103f298636c970af2eea65ea0a07e231aedd Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 26 Nov 2017 17:16:59 +0100 Subject: [PATCH 74/93] Add unparse markdown method --- telethon/extensions/markdown.py | 65 ++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index a960bb34..24ae5aa7 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -24,6 +24,12 @@ DEFAULT_DELIMITERS = { # reason why there's '\0' after every match-literal character. DEFAULT_URL_RE = re.compile(b'\\[\0(.+?)\\]\0\\(\0(.+?)\\)\0') +# Reverse operation for DEFAULT_URL_RE. {0} for text, {1} for URL. +DEFAULT_URL_FORMAT = '[{0}]({1})' + +# Encoding to be used +ENC = 'utf-16le' + def parse(message, delimiters=None, url_re=None): """ @@ -46,7 +52,7 @@ def parse(message, delimiters=None, url_re=None): return message, [] delimiters = DEFAULT_DELIMITERS - delimiters = {k.encode('utf-16le'): v for k, v in delimiters.items()} + delimiters = {k.encode(ENC): v for k, v in delimiters.items()} # Cannot use a for loop because we need to skip some indices i = 0 @@ -56,7 +62,7 @@ def parse(message, delimiters=None, url_re=None): # Work on byte level with the utf-16le encoding to get the offsets right. # The offset will just be half the index we're at. - message = message.encode('utf-16le') + message = message.encode(ENC) while i < len(message): if url_re and current is None: # If we're not inside a previous match since Telegram doesn't allow @@ -72,7 +78,7 @@ def parse(message, delimiters=None, url_re=None): result.append(MessageEntityTextUrl( offset=i // 2, length=len(url_match.group(1)) // 2, - url=url_match.group(2).decode('utf-16le') + url=url_match.group(2).decode(ENC) )) i += len(url_match.group(1)) # Next loop iteration, don't check delimiters, since @@ -127,7 +133,54 @@ def parse(message, delimiters=None, url_re=None): + message[2 * current.offset:] ) - return message.decode('utf-16le'), result + return message.decode(ENC), result + + +def unparse(text, entities, delimiters=None, url_fmt=None): + """ + Performs the reverse operation to .parse(), effectively returning + markdown-like syntax given a normal text and its MessageEntity's. + + :param text: the text to be reconverted into markdown. + :param entities: the MessageEntity's applied to the text. + :return: a markdown-like text representing the combination of both inputs. + """ + if not delimiters: + if delimiters is not None: + return text + delimiters = DEFAULT_DELIMITERS + + if url_fmt is None: + url_fmt = DEFAULT_URL_FORMAT + + if isinstance(entities, TLObject): + entities = (entities,) + else: + entities = tuple(sorted(entities, key=lambda e: e.offset, reverse=True)) + + # Reverse the delimiters, and encode them as utf16 + delimiters = {v: k.encode(ENC) for k, v in delimiters.items()} + text = text.encode(ENC) + for entity in entities: + s = entity.offset * 2 + e = (entity.offset + entity.length) * 2 + delimiter = delimiters.get(type(entity), None) + if delimiter: + text = text[:s] + delimiter + text[s:e] + delimiter + text[e:] + elif isinstance(entity, MessageEntityTextUrl) and url_fmt: + # If byte-strings supported .format(), we, could have converted + # the str url_fmt to a byte-string with the following regex: + # re.sub(b'{\0\s*(?:([01])\0)?\s*}\0',rb'{\1}',url_fmt.encode(ENC)) + # + # This would preserve {}, {0} and {1}. + # Alternatively (as it's done), we can decode/encode it every time. + text = ( + text[:s] + + url_fmt.format(text[s:e].decode(ENC), entity.url).encode(ENC) + + text[e:] + ) + + return text.decode(ENC) def get_inner_text(text, entity): @@ -145,11 +198,11 @@ def get_inner_text(text, entity): entity = [entity] multiple = False - text = text.encode('utf-16le') + text = text.encode(ENC) result = [] for e in entity: start = e.offset * 2 end = (e.offset + e.length) * 2 - result.append(text[start:end].decode('utf-16le')) + result.append(text[start:end].decode(ENC)) return result if multiple else result[0] From 7509ba906796a49995345d72dc2effaa8a178f11 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 29 Nov 2017 12:34:15 +0100 Subject: [PATCH 75/93] Assert that module was generated correctly on setup.py pypi --- setup.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/setup.py b/setup.py index 3f8ee7a6..0c531d70 100755 --- a/setup.py +++ b/setup.py @@ -71,6 +71,16 @@ def main(): print('Done.') elif len(argv) >= 2 and argv[1] == 'pypi': + # (Re)generate the code to make sure we don't push without it + gen_tl() + + # Try importing the telethon module to assert it has no errors + try: + import telethon + except: + print('Packaging for PyPi aborted, importing the module failed.') + return + # Need python3.5 or higher, but Telethon is supposed to support 3.x # Place it here since noone should be running ./setup.py pypi anyway from subprocess import run From 9046b46fcd24b6fa083476c043021e8193e1638a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 30 Nov 2017 13:20:51 +0100 Subject: [PATCH 76/93] Document the network/ module --- telethon/network/__init__.py | 4 + telethon/network/authenticator.py | 30 +++- telethon/network/connection.py | 102 ++++++++++++- telethon/network/mtproto_plain_sender.py | 31 +++- telethon/network/mtproto_sender.py | 179 +++++++++++++++++++---- 5 files changed, 300 insertions(+), 46 deletions(-) diff --git a/telethon/network/__init__.py b/telethon/network/__init__.py index 77bd4406..d2538924 100644 --- a/telethon/network/__init__.py +++ b/telethon/network/__init__.py @@ -1,3 +1,7 @@ +""" +This module contains several classes regarding network, low level connection +with Telegram's servers and the protocol used (TCP full, abridged, etc.). +""" from .mtproto_plain_sender import MtProtoPlainSender from .authenticator import do_authentication from .mtproto_sender import MtProtoSender diff --git a/telethon/network/authenticator.py b/telethon/network/authenticator.py index 00a28fdf..a73bae38 100644 --- a/telethon/network/authenticator.py +++ b/telethon/network/authenticator.py @@ -1,3 +1,7 @@ +""" +This module contains several functions that authenticate the client machine +with Telegram's servers, effectively creating an authorization key. +""" import os import time from hashlib import sha1 @@ -18,6 +22,14 @@ from ..tl.functions import ( def do_authentication(connection, retries=5): + """ + Performs the authentication steps on the given connection. + Raises an error if all attempts fail. + + :param connection: the connection to be used (must be connected). + :param retries: how many times should we retry on failure. + :return: + """ if not retries or retries < 0: retries = 1 @@ -32,9 +44,11 @@ def do_authentication(connection, retries=5): def _do_authentication(connection): - """Executes the authentication process with the Telegram servers. - If no error is raised, returns both the authorization key and the - time offset. + """ + Executes the authentication process with the Telegram servers. + + :param connection: the connection to be used (must be connected). + :return: returns a (authorization key, time offset) tuple. """ sender = MtProtoPlainSender(connection) @@ -195,8 +209,12 @@ def _do_authentication(connection): def get_int(byte_array, signed=True): - """Gets the specified integer from its byte array. - This should be used by the authenticator, - who requires the data to be in big endian + """ + Gets the specified integer from its byte array. + This should be used by this module alone, as it works with big endian. + + :param byte_array: the byte array representing th integer. + :param signed: whether the number is signed or not. + :return: the integer representing the given byte array. """ return int.from_bytes(byte_array, byteorder='big', signed=signed) diff --git a/telethon/network/connection.py b/telethon/network/connection.py index fe04352f..ff255d00 100644 --- a/telethon/network/connection.py +++ b/telethon/network/connection.py @@ -1,3 +1,7 @@ +""" +This module holds both the Connection class and the ConnectionMode enum, +which specifies the protocol to be used by the Connection. +""" import os import struct from datetime import timedelta @@ -35,16 +39,24 @@ class ConnectionMode(Enum): class Connection: - """Represents an abstract connection (TCP, TCP abridged...). - 'mode' must be any of the ConnectionMode enumeration. + """ + Represents an abstract connection (TCP, TCP abridged...). + 'mode' must be any of the ConnectionMode enumeration. - Note that '.send()' and '.recv()' refer to messages, which - will be packed accordingly, whereas '.write()' and '.read()' - work on plain bytes, with no further additions. + Note that '.send()' and '.recv()' refer to messages, which + will be packed accordingly, whereas '.write()' and '.read()' + work on plain bytes, with no further additions. """ def __init__(self, mode=ConnectionMode.TCP_FULL, proxy=None, timeout=timedelta(seconds=5)): + """ + Initializes a new connection. + + :param mode: the ConnectionMode to be used. + :param proxy: whether to use a proxy or not. + :param timeout: timeout to be used for all operations. + """ self._mode = mode self._send_counter = 0 self._aes_encrypt, self._aes_decrypt = None, None @@ -75,6 +87,12 @@ class Connection: setattr(self, 'read', self._read_plain) def connect(self, ip, port): + """ + Estabilishes a connection to IP:port. + + :param ip: the IP to connect to. + :param port: the port to connect to. + """ try: self.conn.connect(ip, port) except OSError as e: @@ -92,9 +110,13 @@ class Connection: self._setup_obfuscation() def get_timeout(self): + """Returns the timeout used by the connection.""" return self.conn.timeout def _setup_obfuscation(self): + """ + Sets up the obfuscated protocol. + """ # Obfuscated messages secrets cannot start with any of these keywords = (b'PVrG', b'GET ', b'POST', b'\xee' * 4) while True: @@ -122,13 +144,19 @@ class Connection: self.conn.write(bytes(random)) def is_connected(self): + """ + Determines whether the connection is alive or not. + + :return: true if it's connected. + """ return self.conn.connected def close(self): + """Closes the connection.""" self.conn.close() def clone(self): - """Creates a copy of this Connection""" + """Creates a copy of this Connection.""" return Connection( mode=self._mode, proxy=self.conn.proxy, timeout=self.conn.timeout ) @@ -141,6 +169,15 @@ class Connection: raise ValueError('Invalid connection mode specified: ' + str(self._mode)) def _recv_tcp_full(self): + """ + Receives a message from the network, + internally encoded using the TCP full protocol. + + May raise InvalidChecksumError if the received data doesn't + match its valid checksum. + + :return: the read message payload. + """ packet_len_seq = self.read(8) # 4 and 4 packet_len, seq = struct.unpack('= 127: length = struct.unpack('> 2 if length < 127: length = struct.pack('B', length) @@ -201,9 +268,21 @@ class Connection: raise ValueError('Invalid connection mode specified: ' + str(self._mode)) def _read_plain(self, length): + """ + Reads data from the socket connection. + + :param length: how many bytes should be read. + :return: a byte sequence with len(data) == length + """ return self.conn.read(length) def _read_obfuscated(self, length): + """ + Reads data and decrypts from the socket connection. + + :param length: how many bytes should be read. + :return: the decrypted byte sequence with len(data) == length + """ return self._aes_decrypt.encrypt( self.conn.read(length) ) @@ -216,9 +295,20 @@ class Connection: raise ValueError('Invalid connection mode specified: ' + str(self._mode)) def _write_plain(self, data): + """ + Writes the given data through the socket connection. + + :param data: the data in bytes to be written. + """ self.conn.write(data) def _write_obfuscated(self, data): + """ + Writes the given data through the socket connection, + using the obfuscated mode (AES encryption is applied on top). + + :param data: the data in bytes to be written. + """ self.conn.write(self._aes_encrypt.encrypt(data)) # endregion diff --git a/telethon/network/mtproto_plain_sender.py b/telethon/network/mtproto_plain_sender.py index c7c021be..cb6d63af 100644 --- a/telethon/network/mtproto_plain_sender.py +++ b/telethon/network/mtproto_plain_sender.py @@ -1,3 +1,7 @@ +""" +This module contains the class used to communicate with Telegram's servers +in plain text, when no authorization key has been created yet. +""" import struct import time @@ -6,32 +10,47 @@ from ..extensions import BinaryReader class MtProtoPlainSender: - """MTProto Mobile Protocol plain sender - (https://core.telegram.org/mtproto/description#unencrypted-messages) + """ + MTProto Mobile Protocol plain sender + (https://core.telegram.org/mtproto/description#unencrypted-messages) """ def __init__(self, connection): + """ + Initializes the MTProto plain sender. + + :param connection: the Connection to be used. + """ self._sequence = 0 self._time_offset = 0 self._last_msg_id = 0 self._connection = connection def connect(self): + """Connects to Telegram's servers.""" self._connection.connect() def disconnect(self): + """Disconnects from Telegram's servers.""" self._connection.close() def send(self, data): - """Sends a plain packet (auth_key_id = 0) containing the - given message body (data) + """ + Sends a plain packet (auth_key_id = 0) containing the + given message body (data). + + :param data: the data to be sent. """ self._connection.send( struct.pack(' Date: Thu, 30 Nov 2017 13:34:55 +0100 Subject: [PATCH 77/93] Fix TLParser not stripping inline comments --- telethon_generator/parser/tl_parser.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/telethon_generator/parser/tl_parser.py b/telethon_generator/parser/tl_parser.py index a08521db..8c24cbf4 100644 --- a/telethon_generator/parser/tl_parser.py +++ b/telethon_generator/parser/tl_parser.py @@ -17,11 +17,13 @@ class TLParser: # Read all the lines from the .tl file for line in file: + # Strip comments from the line + comment_index = line.find('//') + if comment_index != -1: + line = line[:comment_index] + line = line.strip() - - # Ensure that the line is not a comment - if line and not line.startswith('//'): - + if line: # Check whether the line is a type change # (types <-> functions) or not match = re.match('---(\w+)---', line) From 7d7b2cb1fa769393de2e3b0e2e7b9cc606e0b846 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 30 Nov 2017 20:40:35 +0100 Subject: [PATCH 78/93] Remove redundant checks from UpdateState --- telethon/update_state.py | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/telethon/update_state.py b/telethon/update_state.py index 9410125e..302d4ab8 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -163,27 +163,22 @@ class UpdateState: self._latest_updates.append(data) - if type(update).SUBCLASS_OF_ID == 0x8af52aac: # crc32(b'Updates') - # 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. - if isinstance(update, tl.UpdateShort): - self._updates.append(update.update) - self._updates_available.set() + if isinstance(update, tl.UpdateShort): + self._updates.append(update.update) + self._updates_available.set() - elif isinstance(update, (tl.Updates, tl.UpdatesCombined)): - self._updates.extend(update.updates) - self._updates_available.set() + # 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)): + self._updates.extend(update.updates) + self._updates_available.set() - elif not isinstance(update, tl.UpdatesTooLong): - # TODO Handle "Updates too long" - self._updates.append(update) - self._updates_available.set() - - elif type(update).SUBCLASS_OF_ID == 0x9f89304e: # crc32(b'Update') + elif not isinstance(update, tl.UpdatesTooLong): + # TODO Handle "Updates too long" self._updates.append(update) self._updates_available.set() + else: - self._logger.debug('Ignoring "update" of type {}'.format( - type(update).__name__) - ) + self._updates.append(update) + self._updates_available.set() From 21a93d58ec1949a55c31364658f390d685c2ddb9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 30 Nov 2017 21:09:34 +0100 Subject: [PATCH 79/93] Use a synchronized queue instead event/deque pair --- telethon/update_state.py | 42 ++++++++++++++-------------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/telethon/update_state.py b/telethon/update_state.py index 302d4ab8..b7c43ba3 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -1,8 +1,9 @@ import logging import pickle from collections import deque +from queue import Queue, Empty from datetime import datetime -from threading import RLock, Event, Thread +from threading import RLock, Thread from .tl import types as tl @@ -26,8 +27,7 @@ class UpdateState: self.handlers = [] self._updates_lock = RLock() - self._updates_available = Event() - self._updates = deque() + self._updates = Queue() self._latest_updates = deque(maxlen=10) self._logger = logging.getLogger(__name__) @@ -37,24 +37,18 @@ class UpdateState: def can_poll(self): """Returns True if a call to .poll() won't lock""" - return self._updates_available.is_set() + return not self._updates.empty() def poll(self, timeout=None): """Polls an update or blocks until an update object is available. If 'timeout is not None', it should be a floating point value, and the method will 'return None' if waiting times out. """ - if not self._updates_available.wait(timeout=timeout): + try: + update = self._updates.get(timeout=timeout) + except Empty: return - with self._updates_lock: - if not self._updates_available.is_set(): - return - - update = self._updates.popleft() - if not self._updates: - self._updates_available.clear() - if isinstance(update, Exception): raise update # Some error was set through (surely StopIteration) @@ -70,7 +64,8 @@ class UpdateState: self.stop_workers() self._workers = n if n is None: - self._updates.clear() + while self._updates: + self._updates.get() else: self.setup_workers() @@ -86,8 +81,7 @@ class UpdateState: # on all the worker threads # TODO Should this reset the pts and such? for _ in range(self._workers): - self._updates.appendleft(StopIteration()) - self._updates_available.set() + self._updates.put(StopIteration()) for t in self._worker_threads: t.join() @@ -164,21 +158,15 @@ class UpdateState: self._latest_updates.append(data) if isinstance(update, tl.UpdateShort): - self._updates.append(update.update) - self._updates_available.set() - + 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)): - self._updates.extend(update.updates) - self._updates_available.set() - + for u in update.updates: + self._updates.put(u) elif not isinstance(update, tl.UpdatesTooLong): # TODO Handle "Updates too long" - self._updates.append(update) - self._updates_available.set() - + self._updates.put(update) else: - self._updates.append(update) - self._updates_available.set() + self._updates.put(update) From 6662f49bcbf7b7a1f0db855fe8c6a2fd883ac0b7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 30 Nov 2017 21:10:02 +0100 Subject: [PATCH 80/93] Remove another redundant if --- telethon/update_state.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/telethon/update_state.py b/telethon/update_state.py index b7c43ba3..c3768fbd 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -165,8 +165,6 @@ class UpdateState: elif isinstance(update, (tl.Updates, tl.UpdatesCombined)): for u in update.updates: self._updates.put(u) - elif not isinstance(update, tl.UpdatesTooLong): - # TODO Handle "Updates too long" - self._updates.put(update) + # TODO Handle "tl.UpdatesTooLong" else: self._updates.put(update) From d4d7aa9063a7d53b80a0d3eb8bc0f1e5434d0506 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 3 Dec 2017 21:10:22 +0100 Subject: [PATCH 81/93] Use signed salt --- telethon/network/mtproto_sender.py | 7 ++----- telethon/tl/session.py | 9 +++++++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 117c6f68..33794167 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -155,7 +155,7 @@ class MtProtoSender: :param message: the TLMessage to be sent. """ plain_text = \ - struct.pack(' Date: Mon, 4 Dec 2017 20:34:35 +0100 Subject: [PATCH 82/93] Don't ignore NewSessionCreated salt --- telethon/network/mtproto_sender.py | 1 + 1 file changed, 1 insertion(+) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 33794167..b775ae92 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -490,6 +490,7 @@ class MtProtoSender: """ new_session = reader.tgread_object() assert isinstance(new_session, NewSessionCreated) + self.session.salt = new_session.server_salt # TODO https://goo.gl/LMyN7A return True From 0e0bc6ecbc84e2703aa75bc7e626ff13a2bf4032 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 7 Dec 2017 12:22:40 +0100 Subject: [PATCH 83/93] Fix session ID is also signed since d4d7aa9 --- telethon/network/mtproto_sender.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index b775ae92..41c791d9 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -155,7 +155,7 @@ class MtProtoSender: :param message: the TLMessage to be sent. """ plain_text = \ - struct.pack(' Date: Thu, 14 Dec 2017 14:46:57 +0330 Subject: [PATCH 84/93] Fix typo in sessions.rst (#491) --- readthedocs/extra/basic/sessions.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readthedocs/extra/basic/sessions.rst b/readthedocs/extra/basic/sessions.rst index 0f9d458a..f55d9703 100644 --- a/readthedocs/extra/basic/sessions.rst +++ b/readthedocs/extra/basic/sessions.rst @@ -4,7 +4,7 @@ Session Files ============== -The first parameter you pass the the constructor of the +The first parameter you pass the constructor of the ``TelegramClient`` is the ``session``, and defaults to be the session name (or full path). That is, if you create a ``TelegramClient('anon')`` instance and connect, an ``anon.session`` file will be created on the @@ -45,4 +45,4 @@ methods. For example, you could save it on a database: # load relevant data to the database You should read the ``session.py`` source file to know what “relevant -data” you need to keep track of. \ No newline at end of file +data” you need to keep track of. From 7d189119f40a6a30f3647a23f832ae378e32ae47 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 15 Dec 2017 19:46:17 +0100 Subject: [PATCH 85/93] Fix salt migration failing with valid signed salts --- telethon/tl/session.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index dfbfffb3..e530cc83 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -128,8 +128,9 @@ class Session: result.port = data.get('port', result.port) result.salt = data.get('salt', result.salt) # Keep while migrating from unsigned to signed salt - result.salt = struct.unpack( - 'q', struct.pack('Q', result.salt))[0] + if result.salt > 0: + result.salt = struct.unpack( + 'q', struct.pack('Q', result.salt))[0] result.layer = data.get('layer', result.layer) result.server_address = \ From 5842d3741bcda3d54d4fc69b39adf0f1233c768e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 20 Dec 2017 12:47:10 +0100 Subject: [PATCH 86/93] Make a proper use of the logging module --- telethon/network/mtproto_sender.py | 34 ++++------- telethon/telegram_bare_client.py | 96 +++++++++++++++++++++--------- telethon/update_state.py | 11 ++-- 3 files changed, 87 insertions(+), 54 deletions(-) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 41c791d9..d76d44ae 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -21,7 +21,7 @@ from ..tl.types import ( ) from ..tl.functions.auth import LogOutRequest -logging.getLogger(__name__).addHandler(logging.NullHandler()) +__log__ = logging.getLogger(__name__) class MtProtoSender: @@ -46,7 +46,6 @@ class MtProtoSender: """ self.session = session self.connection = connection - self._logger = logging.getLogger(__name__) # Message IDs that need confirmation self._need_confirmation = set() @@ -137,6 +136,9 @@ class MtProtoSender: # "This packet should be skipped"; since this may have # been a result for a request, invalidate every request # and just re-invoke them to avoid problems + __log__.exception('Error while receiving server response. ' + '%d pending request(s) will be ignored', + len(self._pending_receive)) self._clear_all_pending() return @@ -218,7 +220,7 @@ class MtProtoSender: code = reader.read_int(signed=False) reader.seek(-4) - # The following codes are "parsed manually" + __log__.debug('Processing server message with ID %s', hex(code)) if code == 0xf35c6d01: # rpc_result, (response of an RPC call) return self._handle_rpc_result(msg_id, sequence, reader) @@ -257,7 +259,6 @@ class MtProtoSender: if r: r.result = True # Telegram won't send this value r.confirm_received.set() - self._logger.debug('Message ack confirmed', r) return True @@ -270,11 +271,9 @@ class MtProtoSender: return True - self._logger.debug( - '[WARN] Unknown message: {}, data left in the buffer: {}' - .format( - hex(code), repr(reader.get_bytes()[reader.tell_position():]) - ) + __log__.warning( + 'Unknown message with ID %d, data left in the buffer %s', + hex(code), repr(reader.get_bytes()[reader.tell_position():]) ) return False @@ -351,13 +350,11 @@ class MtProtoSender: :param reader: the reader containing the Pong. :return: true, as it always succeeds. """ - self._logger.debug('Handling pong') pong = reader.tgread_object() assert isinstance(pong, Pong) request = self._pop_request(pong.msg_id) if request: - self._logger.debug('Pong confirmed a request') request.result = pong request.confirm_received.set() @@ -372,7 +369,6 @@ class MtProtoSender: :param reader: the reader containing the MessageContainer. :return: true, as it always succeeds. """ - self._logger.debug('Handling container') for inner_msg_id, _, inner_len in MessageContainer.iter_read(reader): begin_position = reader.tell_position() @@ -397,7 +393,6 @@ class MtProtoSender: :param reader: the reader containing the BadServerSalt. :return: true, as it always succeeds. """ - self._logger.debug('Handling bad server salt') bad_salt = reader.tgread_object() assert isinstance(bad_salt, BadServerSalt) @@ -418,28 +413,29 @@ class MtProtoSender: :param reader: the reader containing the BadMessageError. :return: true, as it always succeeds. """ - self._logger.debug('Handling bad message notification') bad_msg = reader.tgread_object() assert isinstance(bad_msg, BadMsgNotification) error = BadMessageError(bad_msg.error_code) + __log__.warning('Read bad msg notification %s: %s', bad_msg, error) if bad_msg.error_code in (16, 17): # sent msg_id too low or too high (respectively). # Use the current msg_id to determine the right time offset. 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.') + __log__.info('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 + __log__.info('Attempting to set the right higher sequence') 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 + __log__.info('Attempting to set the right lower sequence') self._resend_request(bad_msg.bad_msg_id) return True else: @@ -504,7 +500,6 @@ class MtProtoSender: :return: true if the request ID to which this result belongs is found, false otherwise (meaning nothing was read). """ - self._logger.debug('Handling RPC result') reader.read_int(signed=False) # code request_id = reader.read_long() inner_code = reader.read_int(signed=False) @@ -530,11 +525,9 @@ class MtProtoSender: request.confirm_received.set() # else TODO Where should this error be reported? # Read may be async. Can an error not-belong to a request? - self._logger.debug('Read RPC error: %s', str(error)) return True # All contents were read okay elif request: - self._logger.debug('Reading request response') if inner_code == 0x3072cfa1: # GZip packed unpacked_data = gzip.decompress(reader.tgread_bytes()) with BinaryReader(unpacked_data) as compressed_reader: @@ -549,7 +542,7 @@ class MtProtoSender: # If it's really a result for RPC from previous connection # session, it will be skipped by the handle_container() - self._logger.debug('Lost request will be skipped.') + __log__.warning('Lost request will be skipped') return False def _handle_gzip_packed(self, msg_id, sequence, reader, state): @@ -561,7 +554,6 @@ class MtProtoSender: :param reader: the reader containing the GzipPacked. :return: the result of processing the packed message. """ - self._logger.debug('Handling gzip packed data') with BinaryReader(GzipPacked.read(reader)) as compressed_reader: # We are reentering process_msg, which seemingly the same msg_id # to the self._need_confirmation set. Remove it from there first diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 97251547..6c7d3ab0 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -43,6 +43,8 @@ DEFAULT_IPV4_IP = '149.154.167.51' DEFAULT_IPV6_IP = '[2001:67c:4e8:f002::a]' DEFAULT_PORT = 443 +__log__ = logging.getLogger(__name__) + class TelegramBareClient: """Bare Telegram Client with just the minimum - @@ -117,8 +119,6 @@ class TelegramBareClient: mode=connection_mode, proxy=proxy, timeout=timeout )) - self._logger = logging.getLogger(__name__) - # Two threads may be calling reconnect() when the connection is lost, # we only want one to actually perform the reconnection. self._reconnect_lock = Lock() @@ -191,11 +191,15 @@ class TelegramBareClient: native data center, raising a "UserMigrateError", and calling .disconnect() in the process. """ + __log__.info('Connecting to %s:%d...', + self.session.server_address, self.session.port) + self._main_thread_ident = threading.get_ident() self._background_error = None # Clear previous errors try: self._sender.connect() + __log__.info('Connection success!') # Connection was successful! Try syncing the update state # UNLESS '_sync_updates' is False (we probably are in @@ -215,14 +219,15 @@ class TelegramBareClient: except TypeNotFoundError as e: # This is fine, probably layer migration - self._logger.debug('Found invalid item, probably migrating', e) + __log__.warning('Connection failed, got unexpected type with ID ' + '%s. Migrating?', hex(e.invalid_constructor_id)) self.disconnect() return self.connect(_sync_updates=_sync_updates) - except (RPCError, ConnectionError): + except (RPCError, ConnectionError) as e: # Probably errors from the previous session, ignore them + __log__.error('Connection failed due to %s', e) self.disconnect() - self._logger.exception('Could not stabilise initial connection.') return False def is_connected(self): @@ -244,14 +249,19 @@ class TelegramBareClient: def disconnect(self): """Disconnects from the Telegram server and stops all the spawned threads""" + __log__.info('Disconnecting...') self._user_connected = False # This will stop recv_thread's loop + + __log__.debug('Stopping all workers...') self.updates.stop_workers() # This will trigger a "ConnectionResetError" on the recv_thread, # which won't attempt reconnecting as ._user_connected is False. + __log__.debug('Disconnecting the socket...') self._sender.disconnect() if self._recv_thread: + __log__.debug('Joining the read thread...') self._recv_thread.join() # TODO Shall we clear the _exported_sessions, or may be reused? @@ -268,17 +278,21 @@ class TelegramBareClient: """ if new_dc is None: if self.is_connected(): + __log__.info('Reconnection aborted: already connected') return True try: + __log__.info('Attempting reconnection...') return self.connect() - except ConnectionResetError: + except ConnectionResetError as e: + __log__.warning('Reconnection failed due to %s', e) return False else: # 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) + __log__.info('Reconnecting to new data center %s', dc) self.session.server_address = dc.ip_address self.session.port = dc.port @@ -340,6 +354,7 @@ class TelegramBareClient: dc = self._get_dc(dc_id) # Export the current authorization to the new DC. + __log__.info('Exporting authorization for data center %s', dc) export_auth = self(ExportAuthorizationRequest(dc_id)) # Create a temporary session for this IP address, which needs @@ -352,6 +367,7 @@ class TelegramBareClient: session.port = dc.port self._exported_sessions[dc_id] = session + __log__.info('Creating exported new client') client = TelegramBareClient( session, self.api_id, self.api_hash, proxy=self._sender.connection.conn.proxy, @@ -363,7 +379,7 @@ class TelegramBareClient: id=export_auth.id, bytes=export_auth.bytes )) elif export_auth is not None: - self._logger.warning('Unknown return export_auth type', export_auth) + __log__.warning('Unknown export auth type %s', export_auth) client._authorized = True # We exported the auth, so we got auth return client @@ -378,6 +394,7 @@ class TelegramBareClient: session.port = dc.port self._exported_sessions[cdn_redirect.dc_id] = session + __log__.info('Creating new CDN client') client = TelegramBareClient( session, self.api_id, self.api_hash, proxy=self._sender.connection.conn.proxy, @@ -407,12 +424,23 @@ class TelegramBareClient: x.content_related for x in requests): raise ValueError('You can only invoke requests, not types!') + # For logging purposes + if len(requests) == 1: + which = type(requests[0]).__name__ + else: + which = '{} requests ({})'.format( + len(requests), [type(x).__name__ for x in requests]) + # Determine the sender to be used (main or a new connection) on_main_thread = threading.get_ident() == self._main_thread_ident if on_main_thread or self._on_read_thread(): + __log__.debug('Invoking %s from main thread', which) sender = self._sender update_state = self.updates else: + __log__.debug('Invoking %s from background thread. ' + 'Creating temporary connection', which) + sender = self._sender.clone() sender.connect() # We're on another connection, Telegram will resend all the @@ -431,7 +459,7 @@ class TelegramBareClient: call_receive = not on_main_thread or self._recv_thread is None \ or self._reconnect_lock.locked() try: - for _ in range(retries): + for attempt in range(retries): if self._background_error and on_main_thread: raise self._background_error @@ -441,7 +469,9 @@ class TelegramBareClient: if result is not None: return result - self._logger.debug('RPC failed. Attempting reconnection.') + __log__.warning('Invoking %s failed %d times, ' + 'reconnecting and retrying', + [str(x) for x in requests], attempt + 1) sleep(1) # The ReadThread has priority when attempting reconnection, # since this thread is constantly running while __call__ is @@ -475,11 +505,13 @@ class TelegramBareClient: 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. + __log__.info('Need to generate new auth key before invoking') self.session.auth_key, self.session.time_offset = \ authenticator.do_authentication(self._sender.connection) init_connection = True if init_connection: + __log__.info('Initializing a new connection while invoking') if len(requests) == 1: requests = [self._wrap_init_connection(requests[0])] else: @@ -506,13 +538,14 @@ class TelegramBareClient: sender.receive(update_state=update_state) except BrokenAuthKeyError: - self._logger.error('Broken auth key, a new one will be generated') + __log__.error('Authorization key seems broken and was invalid!') self.session.auth_key = None except TimeoutError: - pass # We will just retry + __log__.warning('Invoking timed out') # We will just retry except ConnectionResetError: + __log__.warning('Connection was reset while invoking') if self._user_connected: # Server disconnected us, __call__ will try reconnecting. return None @@ -541,10 +574,6 @@ class TelegramBareClient: except (PhoneMigrateError, NetworkMigrateError, UserMigrateError) as e: - self._logger.debug( - 'DC error when invoking request, ' - 'attempting to reconnect at DC {}'.format(e.new_dc) - ) # TODO What happens with the background thread here? # For normal use cases, this won't happen, because this will only @@ -555,17 +584,13 @@ class TelegramBareClient: except ServerError as e: # Telegram is having some issues, just retry - self._logger.debug( - '[ERROR] Telegram is having some internal issues', e - ) + __log__.error('Telegram servers are having internal errors %s', e) except FloodWaitError as e: + __log__.warning('Request invoked too often, wait %ds', e.seconds) if e.seconds > self.session.flood_sleep_threshold | 0: raise - self._logger.debug( - 'Sleep of %d seconds below threshold, sleeping' % e.seconds - ) sleep(e.seconds) # Some really basic functionality @@ -628,6 +653,8 @@ class TelegramBareClient: file_id = utils.generate_random_long() hash_md5 = md5() + __log__.info('Uploading file of %d bytes in %d chunks of %d', + file_size, part_count, part_size) stream = open(file, 'rb') if isinstance(file, str) else BytesIO(file) try: for part_index in range(part_count): @@ -644,6 +671,7 @@ class TelegramBareClient: result = self(request) if result: + __log__.debug('Uploaded %d/%d', part_index, part_count) if not is_large: # No need to update the hash if it's a large file hash_md5.update(part) @@ -712,6 +740,7 @@ class TelegramBareClient: client = self cdn_decrypter = None + __log__.info('Downloading file in chunks of %d bytes', part_size) try: offset = 0 while True: @@ -724,12 +753,14 @@ class TelegramBareClient: )) if isinstance(result, FileCdnRedirect): + __log__.info('File lives in a CDN') cdn_decrypter, result = \ CdnDecrypter.prepare_decrypter( client, self._get_cdn_client(result), result ) except FileMigrateError as e: + __log__.info('File lives in another DC') client = self._get_exported_client(e.new_dc) continue @@ -742,6 +773,7 @@ class TelegramBareClient: return getattr(result, 'type', '') f.write(result.bytes) + __log__.debug('Saved %d more bytes', len(result.bytes)) if progress_callback: progress_callback(f.tell(), file_size) finally: @@ -803,7 +835,6 @@ class TelegramBareClient: if self._user_connected: self.disconnect() else: - self._logger.debug('Forcing exit...') os._exit(1) def idle(self, stop_signals=(SIGINT, SIGTERM, SIGABRT)): @@ -824,6 +855,11 @@ class TelegramBareClient: for sig in stop_signals: signal(sig, self._signal_handler) + if self._on_read_thread(): + __log__.info('Starting to wait for items from the network') + else: + __log__.info('Idling to receive items from the network') + while self._user_connected: try: if datetime.now() > self._last_ping + self._ping_delay: @@ -832,16 +868,21 @@ class TelegramBareClient: )) self._last_ping = datetime.now() + __log__.debug('Receiving items from the network...') self._sender.receive(update_state=self.updates) except TimeoutError: - # No problem. - pass + # No problem + __log__.info('Receiving items from the network timed out') except ConnectionResetError: - self._logger.debug('Server disconnected us. Reconnecting...') + if self._user_connected: + __log__.error('Connection was reset while receiving ' + 'items. Reconnecting') with self._reconnect_lock: while self._user_connected and not self._reconnect(): sleep(0.1) # Retry forever, this is instant messaging + __log__.info('Connection closed by the user, not reading anymore') + # 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() @@ -857,10 +898,9 @@ class TelegramBareClient: try: self.idle(stop_signals=tuple()) except Exception as error: + __log__.exception('Unknown exception in the read thread! ' + 'Disconnecting and leaving it to main thread') # Unknown exception, pass it to the main thread - self._logger.exception( - 'Unknown error on the read thread, please report' - ) try: import socks diff --git a/telethon/update_state.py b/telethon/update_state.py index c3768fbd..9f308d89 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -7,6 +7,8 @@ from threading import RLock, Thread from .tl import types as tl +__log__ = logging.getLogger(__name__) + class UpdateState: """Used to hold the current state of processed updates. @@ -30,8 +32,6 @@ class UpdateState: self._updates = Queue() self._latest_updates = deque(maxlen=10) - self._logger = logging.getLogger(__name__) - # https://core.telegram.org/api/updates self._state = tl.updates.State(0, 0, datetime.now(), 0, 0) @@ -115,9 +115,7 @@ class UpdateState: break except: # We don't want to crash a worker thread due to any reason - self._logger.exception( - '[ERROR] Unhandled exception on worker {}'.format(wid) - ) + __log__.exception('Unhandled exception on worker %d', wid) def process(self, update): """Processes an update object. This method is normally called by @@ -128,11 +126,13 @@ class UpdateState: with self._updates_lock: if isinstance(update, tl.updates.State): + __log__.debug('Saved new updates state') self._state = update return # Nothing else to be done pts = getattr(update, 'pts', self._state.pts) if hasattr(update, 'pts') and pts <= self._state.pts: + __log__.info('Ignoring %s, already have it', update) return # We already handled this update self._state.pts = pts @@ -153,6 +153,7 @@ class UpdateState: """ data = pickle.dumps(update.to_dict()) if data in self._latest_updates: + __log__.info('Ignoring %s, already have it', update) return # Duplicated too self._latest_updates.append(data) From c848ae0ace8a6d496fdb52a7f8a0519d0259d839 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 20 Dec 2017 17:45:40 +0100 Subject: [PATCH 87/93] Move tgread_object() outside specific msg processing calls --- telethon/network/mtproto_sender.py | 94 +++++++++++++----------------- 1 file changed, 40 insertions(+), 54 deletions(-) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index d76d44ae..7e4d2f18 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -220,41 +220,52 @@ class MtProtoSender: code = reader.read_int(signed=False) reader.seek(-4) - __log__.debug('Processing server message with ID %s', hex(code)) + # These are a bit of special case, not yet generated by the code gen if code == 0xf35c6d01: # rpc_result, (response of an RPC call) + __log__.debug('Processing Remote Procedure Call result') return self._handle_rpc_result(msg_id, sequence, reader) - if code == Pong.CONSTRUCTOR_ID: - return self._handle_pong(msg_id, sequence, reader) - if code == MessageContainer.CONSTRUCTOR_ID: + __log__.debug('Processing container result') return self._handle_container(msg_id, sequence, reader, state) if code == GzipPacked.CONSTRUCTOR_ID: + __log__.debug('Processing gzipped result') return self._handle_gzip_packed(msg_id, sequence, reader, state) - if code == BadServerSalt.CONSTRUCTOR_ID: - return self._handle_bad_server_salt(msg_id, sequence, reader) + if code not in tlobjects: + __log__.warning( + 'Unknown message with ID %d, data left in the buffer %s', + hex(code), repr(reader.get_bytes()[reader.tell_position():]) + ) + return False - if code == BadMsgNotification.CONSTRUCTOR_ID: - return self._handle_bad_msg_notification(msg_id, sequence, reader) + obj = reader.tgread_object() + __log__.debug('Processing %s result', type(obj).__name__) - if code == MsgDetailedInfo.CONSTRUCTOR_ID: - return self._handle_msg_detailed_info(msg_id, sequence, reader) + if isinstance(obj, Pong): + return self._handle_pong(msg_id, sequence, obj) - if code == MsgNewDetailedInfo.CONSTRUCTOR_ID: - return self._handle_msg_new_detailed_info(msg_id, sequence, reader) + if isinstance(obj, BadServerSalt): + return self._handle_bad_server_salt(msg_id, sequence, obj) - if code == NewSessionCreated.CONSTRUCTOR_ID: - return self._handle_new_session_created(msg_id, sequence, reader) + if isinstance(obj, BadMsgNotification): + return self._handle_bad_msg_notification(msg_id, sequence, obj) - if code == MsgsAck.CONSTRUCTOR_ID: # may handle the request we wanted - ack = reader.tgread_object() - assert isinstance(ack, MsgsAck) + if isinstance(obj, MsgDetailedInfo): + return self._handle_msg_detailed_info(msg_id, sequence, obj) + + if isinstance(obj, MsgNewDetailedInfo): + return self._handle_msg_new_detailed_info(msg_id, sequence, obj) + + if isinstance(obj, NewSessionCreated): + return self._handle_new_session_created(msg_id, sequence, obj) + + if isinstance(obj, MsgsAck): # may handle the request we wanted # Ignore every ack request *unless* when logging out, when it's # when it seems to only make sense. We also need to set a non-None # result since Telegram doesn't send the response for these. - for msg_id in ack.msg_ids: + for msg_id in obj.msg_ids: r = self._pop_request_of_type(msg_id, LogOutRequest) if r: r.result = True # Telegram won't send this value @@ -262,20 +273,12 @@ class MtProtoSender: return True - # If the code is not parsed manually then it should be a TLObject. - if code in tlobjects: - result = reader.tgread_object() - self.session.process_entities(result) - if state: - state.process(result) + # If the object isn't any of the above, then it should be an Update. + self.session.process_entities(obj) + if state: + state.process(obj) - return True - - __log__.warning( - 'Unknown message with ID %d, data left in the buffer %s', - hex(code), repr(reader.get_bytes()[reader.tell_position():]) - ) - return False + return True # endregion @@ -341,7 +344,7 @@ class MtProtoSender: if requests: return self.send(*requests) - def _handle_pong(self, msg_id, sequence, reader): + def _handle_pong(self, msg_id, sequence, pong): """ Handles a Pong response. @@ -350,9 +353,6 @@ class MtProtoSender: :param reader: the reader containing the Pong. :return: true, as it always succeeds. """ - pong = reader.tgread_object() - assert isinstance(pong, Pong) - request = self._pop_request(pong.msg_id) if request: request.result = pong @@ -384,7 +384,7 @@ class MtProtoSender: return True - def _handle_bad_server_salt(self, msg_id, sequence, reader): + def _handle_bad_server_salt(self, msg_id, sequence, bad_salt): """ Handles a BadServerSalt response. @@ -393,9 +393,6 @@ class MtProtoSender: :param reader: the reader containing the BadServerSalt. :return: true, as it always succeeds. """ - bad_salt = reader.tgread_object() - assert isinstance(bad_salt, BadServerSalt) - self.session.salt = bad_salt.new_server_salt self.session.save() @@ -404,7 +401,7 @@ class MtProtoSender: self._resend_request(bad_salt.bad_msg_id) return True - def _handle_bad_msg_notification(self, msg_id, sequence, reader): + def _handle_bad_msg_notification(self, msg_id, sequence, bad_msg): """ Handles a BadMessageError response. @@ -413,9 +410,6 @@ class MtProtoSender: :param reader: the reader containing the BadMessageError. :return: true, as it always succeeds. """ - bad_msg = reader.tgread_object() - assert isinstance(bad_msg, BadMsgNotification) - error = BadMessageError(bad_msg.error_code) __log__.warning('Read bad msg notification %s: %s', bad_msg, error) if bad_msg.error_code in (16, 17): @@ -441,7 +435,7 @@ class MtProtoSender: else: raise error - def _handle_msg_detailed_info(self, msg_id, sequence, reader): + def _handle_msg_detailed_info(self, msg_id, sequence, msg_new): """ Handles a MsgDetailedInfo response. @@ -450,15 +444,12 @@ class MtProtoSender: :param reader: the reader containing the MsgDetailedInfo. :return: true, as it always succeeds. """ - msg_new = reader.tgread_object() - assert isinstance(msg_new, MsgDetailedInfo) - # TODO For now, simply ack msg_new.answer_msg_id # Relevant tdesktop source code: https://goo.gl/VvpCC6 self._send_acknowledge(msg_new.answer_msg_id) return True - def _handle_msg_new_detailed_info(self, msg_id, sequence, reader): + def _handle_msg_new_detailed_info(self, msg_id, sequence, msg_new): """ Handles a MsgNewDetailedInfo response. @@ -467,15 +458,12 @@ class MtProtoSender: :param reader: the reader containing the MsgNewDetailedInfo. :return: true, as it always succeeds. """ - msg_new = reader.tgread_object() - assert isinstance(msg_new, MsgNewDetailedInfo) - # TODO For now, simply ack msg_new.answer_msg_id # Relevant tdesktop source code: https://goo.gl/G7DPsR self._send_acknowledge(msg_new.answer_msg_id) return True - def _handle_new_session_created(self, msg_id, sequence, reader): + def _handle_new_session_created(self, msg_id, sequence, new_session): """ Handles a NewSessionCreated response. @@ -484,8 +472,6 @@ class MtProtoSender: :param reader: the reader containing the NewSessionCreated. :return: true, as it always succeeds. """ - new_session = reader.tgread_object() - assert isinstance(new_session, NewSessionCreated) self.session.salt = new_session.server_salt # TODO https://goo.gl/LMyN7A return True From 23ab70fc29048d8a0cc135fb731d8ab4124170ec Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 20 Dec 2017 17:48:41 +0100 Subject: [PATCH 88/93] Remove unused request_msg_id from the TLObject class --- telethon/tl/tlobject.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index 68c5e741..e2b23018 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -4,8 +4,6 @@ from threading import Event class TLObject: def __init__(self): - self.request_msg_id = 0 # Long - self.confirm_received = Event() self.rpc_error = None From 992017ddf8eb3d9fcc70ff82761acdea4260ecb6 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 22 Dec 2017 11:27:57 +0100 Subject: [PATCH 89/93] Except ConnectionAbortedError on TcpClient --- telethon/extensions/tcp_client.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index 3941a4d6..9a007dcd 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -113,7 +113,7 @@ class TcpClient: self._socket.sendall(data) except socket.timeout as e: raise TimeoutError() from e - except BrokenPipeError: + except (BrokenPipeError, ConnectionAbortedError): self._raise_connection_reset() except OSError as e: if e.errno == errno.EBADF: @@ -139,6 +139,11 @@ class TcpClient: partial = self._socket.recv(bytes_left) except socket.timeout as e: raise TimeoutError() from e + except ConnectionAbortedError: + # ConnectionAbortedError: [WinError 10053] + # An established connection was aborted by + # the software in your host machine. + self._raise_connection_reset() except OSError as e: if e.errno == errno.EBADF or e.errno == errno.ENOTSOCK: self._raise_connection_reset() From 4a2a64ce2f6f5b3b89d3dbfcd0af29725bb03732 Mon Sep 17 00:00:00 2001 From: "Dmitry D. Chernov" Date: Sat, 23 Dec 2017 05:45:23 +1000 Subject: [PATCH 90/93] TcpClient: Catch ConnectionError instead of its particular cases That can be more reliable, especially in the case of using PySocks. --- telethon/extensions/tcp_client.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index 9a007dcd..f59bb9f0 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -113,7 +113,7 @@ class TcpClient: self._socket.sendall(data) except socket.timeout as e: raise TimeoutError() from e - except (BrokenPipeError, ConnectionAbortedError): + except ConnectionError: self._raise_connection_reset() except OSError as e: if e.errno == errno.EBADF: @@ -139,10 +139,7 @@ class TcpClient: partial = self._socket.recv(bytes_left) except socket.timeout as e: raise TimeoutError() from e - except ConnectionAbortedError: - # ConnectionAbortedError: [WinError 10053] - # An established connection was aborted by - # the software in your host machine. + except ConnectionError: self._raise_connection_reset() except OSError as e: if e.errno == errno.EBADF or e.errno == errno.ENOTSOCK: From fb9813ae61bd6657293c867bf17af102cb8c37c2 Mon Sep 17 00:00:00 2001 From: "Dmitry D. Chernov" Date: Sun, 24 Dec 2017 21:21:14 +1000 Subject: [PATCH 91/93] TelegramClient.send_code_request(): Change logic of methods invocation Before: First call, force_sms=False: SendCodeRequest Next call, force_sms=False: SendCodeRequest First call, force_sms=True: raise ValueError Next call, force_sms=True: ResendCodeRequest That's inconvenient because the user must remember whether the code requested at all and whether the request was successful. In addition, the repeated invocation of SendCodeRequest does nothing. This commit changes logic to this: First call, force_sms=False: SendCodeRequest Next call, force_sms=False: ResendCodeRequest First call, force_sms=True: SendCodeRequest, ResendCodeRequest Next call, force_sms=True: ResendCodeRequest --- telethon/telegram_client.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index fc4b4342..8546c377 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -138,23 +138,24 @@ class TelegramClient(TelegramBareClient): :param str | int phone: The phone to which the code will be sent. :param bool force_sms: - Whether to force sending as SMS. You should call it at least - once before without this set to True first. + Whether to force sending as SMS. :return auth.SentCode: Information about the result of the request. """ phone = EntityDatabase.parse_phone(phone) or self._phone - if force_sms: - if not self._phone_code_hash: - raise ValueError( - 'You must call this method without force_sms at least once.' - ) - result = self(ResendCodeRequest(phone, self._phone_code_hash)) - else: + + if not self._phone_code_hash: result = self(SendCodeRequest(phone, self.api_id, self.api_hash)) self._phone_code_hash = result.phone_code_hash + else: + force_sms = True self._phone = phone + + if force_sms: + result = self(ResendCodeRequest(phone, self._phone_code_hash)) + self._phone_code_hash = result.phone_code_hash + return result def sign_in(self, phone=None, code=None, From 9c66f0b2b48dc8ed526b4a253719aa974f71254d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 24 Dec 2017 15:14:54 +0100 Subject: [PATCH 92/93] Fix empty strings not working as expected for flag parameters --- telethon_generator/tl_generator.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index 60f07bd6..f8a9e873 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -464,9 +464,11 @@ class TLGenerator: # Vector flags are special since they consist of 3 values, # so we need an extra join here. Note that empty vector flags # should NOT be sent either! - builder.write("b'' if not {} else b''.join((".format(name)) + builder.write("b'' if {0} is None or {0} is False " + "else b''.join((".format(name)) else: - builder.write("b'' if not {} else (".format(name)) + builder.write("b'' if {0} is None or {0} is False " + "else (".format(name)) if arg.is_vector: if arg.use_vector_id: @@ -495,11 +497,14 @@ class TLGenerator: # There's a flag indicator, but no flag arguments so it's 0 builder.write(r"b'\0\0\0\0'") else: - builder.write("struct.pack(' Date: Sun, 24 Dec 2017 16:18:09 +0100 Subject: [PATCH 93/93] Create a convenient class to wrap Dialog instances --- telethon/telegram_client.py | 42 ++++++++++++---------------------- telethon/tl/custom/__init__.py | 1 + telethon/tl/custom/dialog.py | 37 ++++++++++++++++++++++++++++++ telethon/utils.py | 6 ++--- 4 files changed, 55 insertions(+), 31 deletions(-) create mode 100644 telethon/tl/custom/dialog.py diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 8546c377..32ade1a9 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1,5 +1,7 @@ +import itertools import os import time +from collections import OrderedDict from datetime import datetime, timedelta from mimetypes import guess_type @@ -16,7 +18,7 @@ from .errors import ( ) from .network import ConnectionMode from .tl import TLObject -from .tl.custom import Draft +from .tl.custom import Draft, Dialog from .tl.entity_database import EntityDatabase from .tl.functions.account import ( GetPasswordRequest @@ -294,15 +296,14 @@ class TelegramClient(TelegramBareClient): 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]). + + :return List[telethon.tl.custom.Dialog]: A list dialogs. """ limit = float('inf') if limit is None else int(limit) if limit == 0: return [], [] - dialogs = {} # Use peer id as identifier to avoid dupes - messages = {} # Used later for sorting TODO also return these? - entities = {} + dialogs = OrderedDict() # Use peer id as identifier to avoid dupes while len(dialogs) < limit: real_limit = min(limit - len(dialogs), 100) r = self(GetDialogsRequest( @@ -312,16 +313,13 @@ class TelegramClient(TelegramBareClient): limit=real_limit )) - for d in r.dialogs: - dialogs[utils.get_peer_id(d.peer, True)] = d - for m in r.messages: - messages[m.id] = m + messages = {m.id: m for m in r.messages} + entities = {utils.get_peer_id(x, add_mark=True): x + for x in itertools.chain(r.users, r.chats)} - # We assume users can't have the same ID as a chat - for u in r.users: - entities[u.id] = u - for c in r.chats: - entities[c.id] = c + for d in r.dialogs: + dialogs[utils.get_peer_id(d.peer, add_mark=True)] = \ + Dialog(self, d, entities, messages) if len(r.dialogs) < real_limit or not isinstance(r, DialogsSlice): # Less than we requested means we reached the end, or @@ -334,20 +332,8 @@ class TelegramClient(TelegramBareClient): ) offset_id = r.messages[-1].id & 4294967296 # Telegram/danog magic - # Sort by message date. Windows will raise if timestamp is 0, - # so we need to set at least one day ahead while still being - # the smallest date possible. - no_date = datetime.fromtimestamp(86400) - ds = list(sorted( - dialogs.values(), - key=lambda d: getattr(messages[d.top_message], 'date', no_date) - )) - if limit < float('inf'): - ds = ds[:limit] - return ( - ds, - [utils.find_user_or_chat(d.peer, entities, entities) for d in ds] - ) + dialogs = list(dialogs.values()) + return dialogs[:limit] if limit < float('inf') else dialogs def get_drafts(self): # TODO: Ability to provide a `filter` """ diff --git a/telethon/tl/custom/__init__.py b/telethon/tl/custom/__init__.py index 40914f16..5b6bf44d 100644 --- a/telethon/tl/custom/__init__.py +++ b/telethon/tl/custom/__init__.py @@ -1 +1,2 @@ from .draft import Draft +from .dialog import Dialog diff --git a/telethon/tl/custom/dialog.py b/telethon/tl/custom/dialog.py new file mode 100644 index 00000000..bac8b0de --- /dev/null +++ b/telethon/tl/custom/dialog.py @@ -0,0 +1,37 @@ +from . import Draft +from ... import utils + + +class Dialog: + """ + Custom class that encapsulates a dialog (an open "conversation" with + someone, a group or a channel) providing an abstraction to easily + access the input version/normal entity/message etc. The library will + return instances of this class when calling `client.get_dialogs()`. + """ + def __init__(self, client, dialog, entities, messages): + # Both entities and messages being dicts {ID: item} + self._client = client + self.dialog = dialog + self.pinned = bool(dialog.pinned) + self.message = messages.get(dialog.top_message, None) + self.date = getattr(self.message, 'date', None) + + self.entity = entities[utils.get_peer_id(dialog.peer, add_mark=True)] + self.input_entity = utils.get_input_peer(self.entity) + self.name = utils.get_display_name(self.entity) + + self.unread_count = dialog.unread_count + self.unread_mentions_count = dialog.unread_mentions_count + + if dialog.draft: + self.draft = Draft(client, dialog.peer, dialog.draft) + else: + self.draft = None + + def send_message(self, *args, **kwargs): + """ + Sends a message to this dialog. This is just a wrapper around + client.send_message(dialog.input_entity, *args, **kwargs). + """ + return self._client.send_message(self.input_entity, *args, **kwargs) diff --git a/telethon/utils.py b/telethon/utils.py index 3259c8e2..5e92b13d 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -35,12 +35,12 @@ def get_display_name(entity): elif entity.last_name: return entity.last_name else: - return '(No name)' + return '' - if isinstance(entity, (Chat, Channel)): + elif isinstance(entity, (Chat, Channel)): return entity.title - return '(unknown)' + return '' # For some reason, .webp (stickers' format) is not registered add_type('image/webp', '.webp')