├── Poloniex.py ├── PoloniexThread.py ├── README.md ├── config.ini ├── main.py ├── setup.py ├── setup.sh ├── telethon ├── __init__.py ├── crypto │ ├── __init__.py │ ├── aes.py │ ├── auth_key.py │ ├── factorization.py │ └── rsa.py ├── errors │ ├── __init__.py │ ├── common.py │ ├── rpc_errors.py │ ├── rpc_errors_303.py │ ├── rpc_errors_400.py │ ├── rpc_errors_401.py │ └── rpc_errors_420.py ├── extensions │ ├── __init__.py │ ├── binary_reader.py │ ├── binary_writer.py │ ├── tcp_client.py │ └── threaded_tcp_client.py ├── helpers.py ├── network │ ├── __init__.py │ ├── authenticator.py │ ├── mtproto_plain_sender.py │ ├── mtproto_sender.py │ └── tcp_transport.py ├── telegram_bare_client.py ├── telegram_client.py ├── tl │ ├── __init__.py │ ├── session.py │ └── tlobject.py └── utils.py ├── telethon_examples └── interactive_telegram_client.py ├── telethon_generator ├── parser │ ├── __init__.py │ ├── source_builder.py │ ├── tl_object.py │ └── tl_parser.py ├── scheme.tl └── tl_generator.py ├── telethon_tests ├── __init__.py ├── crypto_test.py ├── network_test.py ├── parser_test.py ├── tl_test.py └── utils_test.py └── trader.py /Poloniex.py: -------------------------------------------------------------------------------- 1 | 2 | import urllib 3 | import urllib2 4 | import json 5 | import time 6 | import hmac, hashlib 7 | 8 | 9 | def createTimeStamp(datestr, format="%Y-%m-%d %H:%M:%S"): 10 | return time.mktime(time.strptime(datestr, format)) 11 | 12 | 13 | class Poloniex: 14 | def __init__(self, APIKey, Secret): 15 | self.APIKey = APIKey 16 | self.Secret = Secret 17 | 18 | def post_process(self, before): 19 | after = before 20 | 21 | # Add timestamps if there isnt one but is a datetime 22 | if ('return' in after): 23 | if (isinstance(after['return'], list)): 24 | for x in xrange(0, len(after['return'])): 25 | if (isinstance(after['return'][x], dict)): 26 | if ('datetime' in after['return'][x] and 'timestamp' not in after['return'][x]): 27 | after['return'][x]['timestamp'] = float(createTimeStamp(after['return'][x]['datetime'])) 28 | 29 | return after 30 | 31 | def api_query(self, command, req={}): 32 | 33 | if (command == "returnTicker" or command == "return24Volume"): 34 | ret = urllib2.urlopen(urllib2.Request('https://poloniex.com/public?command=' + command)) 35 | return json.loads(ret.read()) 36 | elif (command == "returnOrderBook"): 37 | ret = urllib2.urlopen(urllib2.Request( 38 | 'https://poloniex.com/public?command=' + command + '¤cyPair=' + str(req['currencyPair']))) 39 | return json.loads(ret.read()) 40 | elif (command == "returnMarketTradeHistory"): 41 | ret = urllib2.urlopen(urllib2.Request( 42 | 'https://poloniex.com/public?command=' + "returnTradeHistory" + '¤cyPair=' + str( 43 | req['currencyPair']))) 44 | return json.loads(ret.read()) 45 | else: 46 | req['command'] = command 47 | req['nonce'] = int(time.time() * 1000) 48 | post_data = urllib.urlencode(req) 49 | 50 | sign = hmac.new(self.Secret, post_data, hashlib.sha512).hexdigest() 51 | headers = { 52 | 'Sign': sign, 53 | 'Key': self.APIKey 54 | } 55 | 56 | ret = urllib2.urlopen(urllib2.Request('https://poloniex.com/tradingApi', post_data, headers)) 57 | jsonRet = json.loads(ret.read()) 58 | return self.post_process(jsonRet) 59 | 60 | def returnTicker(self): 61 | return self.api_query("returnTicker") 62 | 63 | def return24Volume(self): 64 | return self.api_query("return24Volume") 65 | 66 | def returnOrderBook(self, currencyPair): 67 | return self.api_query("returnOrderBook", {'currencyPair': currencyPair}) 68 | 69 | def returnMarketTradeHistory(self, currencyPair): 70 | return self.api_query("returnMarketTradeHistory", {'currencyPair': currencyPair}) 71 | 72 | # Returns all of your balances. 73 | # Outputs: 74 | # {"BTC":"0.59098578","LTC":"3.31117268", ... } 75 | def returnBalances(self): 76 | return self.api_query('returnBalances') 77 | 78 | # Returns your open orders for a given market, specified by the "currencyPair" POST parameter, e.g. "BTC_XCP" 79 | # Inputs: 80 | # currencyPair The currency pair e.g. "BTC_XCP" 81 | # Outputs: 82 | # orderNumber The order number 83 | # type sell or buy 84 | # rate Price the order is selling or buying at 85 | # Amount Quantity of order 86 | # total Total value of order (price * quantity) 87 | def returnOpenOrders(self, currencyPair): 88 | return self.api_query('returnOpenOrders', {"currencyPair": currencyPair}) 89 | 90 | # Returns your trade history for a given market, specified by the "currencyPair" POST parameter 91 | # Inputs: 92 | # currencyPair The currency pair e.g. "BTC_XCP" 93 | # Outputs: 94 | # date Date in the form: "2014-02-19 03:44:59" 95 | # rate Price the order is selling or buying at 96 | # amount Quantity of order 97 | # total Total value of order (price * quantity) 98 | # type sell or buy 99 | def returnTradeHistory(self, currencyPair): 100 | return self.api_query('returnTradeHistory', {"currencyPair": currencyPair}) 101 | 102 | # Places a buy order in a given market. Required POST parameters are "currencyPair", "rate", and "amount". If successful, the method will return the order number. 103 | # Inputs: 104 | # currencyPair The curreny pair 105 | # rate price the order is buying at 106 | # amount Amount of coins to buy 107 | # Outputs: 108 | # orderNumber The order number 109 | def buy(self, currencyPair, rate, amount): 110 | return self.api_query('buy', {"currencyPair": currencyPair, "rate": rate, "amount": amount}) 111 | 112 | # Places a sell order in a given market. Required POST parameters are "currencyPair", "rate", and "amount". If successful, the method will return the order number. 113 | # Inputs: 114 | # currencyPair The curreny pair 115 | # rate price the order is selling at 116 | # amount Amount of coins to sell 117 | # Outputs: 118 | # orderNumber The order number 119 | def sell(self, currencyPair, rate, amount): 120 | return self.api_query('sell', {"currencyPair": currencyPair, "rate": rate, "amount": amount}) 121 | 122 | # Cancels an order you have placed in a given market. Required POST parameters are "currencyPair" and "orderNumber". 123 | # Inputs: 124 | # currencyPair The curreny pair 125 | # orderNumber The order number to cancel 126 | # Outputs: 127 | # succes 1 or 0 128 | def cancel(self, currencyPair, orderNumber): 129 | return self.api_query('cancelOrder', {"currencyPair": currencyPair, "orderNumber": orderNumber}) 130 | 131 | # Immediately places a withdrawal for a given currency, with no email confirmation. In order to use this method, the withdrawal privilege must be enabled for your API key. Required POST parameters are "currency", "amount", and "address". Sample output: {"response":"Withdrew 2398 NXT."} 132 | # Inputs: 133 | # currency The currency to withdraw 134 | # amount The amount of this coin to withdraw 135 | # address The withdrawal address 136 | # Outputs: 137 | # response Text containing message about the withdrawal 138 | def withdraw(self, currency, amount, address): 139 | return self.api_query('withdraw', {"currency": currency, "amount": amount, "address": address}) 140 | -------------------------------------------------------------------------------- /PoloniexThread.py: -------------------------------------------------------------------------------- 1 | import ConfigParser 2 | from Poloniex import Poloniex 3 | import json 4 | import time 5 | 6 | 7 | class PoloniexThread: 8 | 9 | def __init__(self, coin): 10 | self.config = ConfigParser.ConfigParser() 11 | self.config.read("config.ini") 12 | 13 | self.exchange = Poloniex(self.config.get('poloniex', 'key'), self.config.get('poloniex', 'secret')) 14 | self.coin = "BTC_" + coin 15 | self.plainCoin = coin 16 | 17 | self.quantity = self.config.getfloat('general', 'AMOUNT_BTC_PER_TRADE_P') 18 | 19 | self.balance = float( self.exchange.returnBalances()[coin] ) 20 | self.coinBalance = float( self.balance ) 21 | 22 | self.buyCompleted = False 23 | 24 | self.handle() 25 | 26 | def getMarket(self): 27 | cars = self.exchange.returnTicker() 28 | #for keys, values in cars.items(): 29 | # print(keys) 30 | # print(values) 31 | return round(float(cars[self.coin]['lowestAsk']) + float(cars[self.coin]['lowestAsk']) * self.config.getfloat('general', 'PERCENT_ADD_ASK'), 8) 32 | 33 | def buy(self): 34 | self.ask = round(self.config.getfloat('general', 'AMOUNT_BTC_PER_TRADE_P') / self.rate, 8) 35 | return self.exchange.buy(self.coin, self.rate, self.ask) 36 | 37 | 38 | def awaitBuy(self): 39 | c = 0 40 | while True: 41 | balance = float( self.exchange.returnBalances()[self.plainCoin] ) 42 | if balance > self.coinBalance: 43 | self.buyCompleted = True 44 | self.coinBalance = balance 45 | break 46 | 47 | if c > self.config.getint('general', 'TIME_FOR_BUY_CANCEL'): 48 | break 49 | c = c + 1 50 | time.sleep(1) 51 | 52 | def waitForHighestBid(self): 53 | 54 | lastPrice = 0 55 | stopLossOrder = 0 56 | iterator = 0 57 | 58 | while True: 59 | cPrice = round( float(self.exchange.returnTicker()[self.coin]['lowestAsk']), 8) 60 | tmpStopLossOrder = (1 - self.config.getfloat('general', 'STOP_LOSS_ORDER_PERCENT')) * cPrice 61 | if tmpStopLossOrder > stopLossOrder: 62 | if iterator % self.config.getint('general', 'RECALCULATE_STOP_LOSS_SECONDS') == 0: 63 | stopLossOrder = tmpStopLossOrder 64 | 65 | if cPrice <= stopLossOrder: 66 | lastPrice = cPrice 67 | break 68 | print("======================================") 69 | print("=> WAITING FOR SELL SINCE : " + str(iterator) + "s") 70 | print("=> BUY:" + self.coin + " @ Poloniex") 71 | print("=> Current Bid Price: ", cPrice) 72 | print("=> Current Stop Loss: ", stopLossOrder) 73 | print("======================================") 74 | lastPrice = cPrice 75 | iterator = iterator + 1 76 | time.sleep(self.config.getint('general', 'WAIT_FOR_HIGHEST_BID_SLEEP_SECONDS')) 77 | return lastPrice 78 | 79 | def handle(self): 80 | self.rate = self.getMarket() 81 | self.buyReturn = self.buy() 82 | self.awaitBuy() 83 | if self.buyCompleted: 84 | self.latestPrice = self.waitForHighestBid() 85 | print("======================================") 86 | print("TIME TO SELL") 87 | print( self.exchange.sell(self.coin, self.latestPrice * (1 + self.config.getfloat('general', 'PROFITMARGIN')), round( self.coinBalance, 8 )) ) 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #
BitBot

2 | A bot to monitor Cryptoalert, and instant buy on Poloniex Pings on Telegram.
3 | After buy, it monitor the price, and add a trailing stop.
4 | Right now its a very simple trailingstop, so it need some improvement 5 | -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [general] 2 | PROFITMARGIN: 0.0 3 | AMOUNT_BTC_PER_TRADE_B: 0.01 4 | AMOUNT_BTC_PER_TRADE_P: 0.01 5 | PERCENT_ADD_ASK: 0.015 6 | TIME_FOR_BUY_CANCEL: 120 7 | STOP_LOSS_ORDER_PERCENT: 0.005 8 | WAIT_FOR_HIGHEST_BID_SLEEP_SECONDS: 1 9 | RECALCULATE_STOP_LOSS_SECONDS: 60 10 | 11 | [poloniex] 12 | key: KEY 13 | secret: SECRET 14 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | 2 | import time 3 | import datetime 4 | import random 5 | from bs4 import BeautifulSoup 6 | import pandas as pd 7 | import HandlerThread, PoloniexThread 8 | import ConfigParser 9 | import thread 10 | 11 | 12 | config = ConfigParser.ConfigParser() 13 | config.read("config.ini") 14 | 15 | firstRun = True 16 | 17 | lastpair = "" 18 | currpair = "" 19 | 20 | cryptoping = Cryptoping.Cryptoping() 21 | 22 | while True: 23 | currency, exchange = cryptoping.refreshAndGetData() 24 | if firstRun: 25 | lastpair=currency+exchange 26 | firstRun = False 27 | currpair=currency+exchange 28 | 29 | 30 | if(lastpair!=currpair): 31 | legitExchange = False 32 | # Pruefen ob Poloniex oder Bittrex Wenn ja Transaktion freigeben 33 | if (exchange == "Poloniex"): 34 | legitExchange = True 35 | 36 | # Transaktion freigegeben 37 | if legitExchange: 38 | if (exchange == "Poloniex"): 39 | thread.start_new(PoloniexThread.PoloniexThread, (currency,)) 40 | 41 | lastpair = currpair 42 | time.sleep(random.randint(1,3)) 43 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """A setuptools based setup module. 3 | 4 | See: 5 | https://packaging.python.org/en/latest/distributing.html 6 | https://github.com/pypa/sampleproject 7 | 8 | Extra supported commands are: 9 | * gen_tl, to generate the classes required for Telethon to run 10 | * clean_tl, to clean these generated classes 11 | """ 12 | 13 | # To use a consistent encoding 14 | from codecs import open 15 | from sys import argv 16 | from os import path 17 | 18 | # Always prefer setuptools over distutils 19 | from setuptools import find_packages, setup 20 | 21 | try: 22 | from telethon import TelegramClient 23 | except ImportError: 24 | TelegramClient = None 25 | 26 | 27 | if __name__ == '__main__': 28 | if len(argv) >= 2 and argv[1] == 'gen_tl': 29 | from telethon_generator.tl_generator import TLGenerator 30 | generator = TLGenerator('telethon/tl') 31 | if generator.tlobjects_exist(): 32 | print('Detected previous TLObjects. Cleaning...') 33 | generator.clean_tlobjects() 34 | 35 | print('Generating TLObjects...') 36 | generator.generate_tlobjects( 37 | 'telethon_generator/scheme.tl', import_depth=2 38 | ) 39 | print('Done.') 40 | 41 | elif len(argv) >= 2 and argv[1] == 'clean_tl': 42 | from telethon_generator.tl_generator import TLGenerator 43 | print('Cleaning...') 44 | TLGenerator('telethon/tl').clean_tlobjects() 45 | print('Done.') 46 | 47 | else: 48 | if not TelegramClient: 49 | print('Run `python3', argv[0], 'gen_tl` first.') 50 | quit() 51 | 52 | here = path.abspath(path.dirname(__file__)) 53 | 54 | # Get the long description from the README file 55 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f: 56 | long_description = f.read() 57 | 58 | setup( 59 | name='Telethon', 60 | 61 | # Versions should comply with PEP440. 62 | version=TelegramClient.__version__, 63 | description="Full-featured Telegram client library for Python 3", 64 | long_description=long_description, 65 | 66 | url='https://github.com/LonamiWebs/Telethon', 67 | download_url='https://github.com/LonamiWebs/Telethon/releases', 68 | 69 | author='Lonami Exo', 70 | author_email='totufals@hotmail.com', 71 | 72 | license='MIT', 73 | 74 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 75 | classifiers=[ 76 | # 3 - Alpha 77 | # 4 - Beta 78 | # 5 - Production/Stable 79 | 'Development Status :: 3 - Alpha', 80 | 81 | 'Intended Audience :: Developers', 82 | 'Topic :: Communications :: Chat', 83 | 84 | 'License :: OSI Approved :: MIT License', 85 | 86 | 'Programming Language :: Python :: 3', 87 | 'Programming Language :: Python :: 3.3', 88 | 'Programming Language :: Python :: 3.4', 89 | 'Programming Language :: Python :: 3.5', 90 | 'Programming Language :: Python :: 3.6' 91 | ], 92 | keywords='telegram api chat client library messaging mtproto', 93 | packages=find_packages(exclude=[ 94 | 'telethon_generator', 'telethon_tests', 'run_tests.py', 95 | 'try_telethon.py' 96 | ]), 97 | install_requires=['pyaes'] 98 | ) 99 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | 2 | sudo apt update 3 | sudo apt install python3-setuptools python3 python3-pip mongodb libcurl4-openssl-dev libssl-dev dialog jq curl build-essential node-gyp 4 | sudo curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - 5 | sudo apt-get install -y nodejs 6 | sudo dpkg --configure -a 7 | sudo apt install -f 8 | sudo pip3 install pycurl 9 | sudo pip3 install pyyaml 10 | sudo pip3 install pyparse 11 | sudo pip3 install parse 12 | sudo pip3 install pyaes 13 | sudo pip3 install pandas 14 | sudo pip3 install pymongo 15 | sudo python3 setup.py gen_tl 16 | sudo python3 setup.py install 17 | sudo npm install 18 | sudo npm link 19 | 20 | echo "First, Create a telegram account... For more info read the README.md and README2.md" 21 | echo "...And Then... Go here and get an API key and hash under Dev Tools: https://my.telegram.org/auth" 22 | echo "...And Then... Input your hash, key, and phone into trader.py..." 23 | echo "then input your poloniex (preferred) Key and hash into conf-example.js" 24 | echo "...Then git clone this: https://github.com/TheRoboKitten/python-poloniex.git" 25 | echo "...move everything inside that folder into the zenaii directory" 26 | echo "Then run sudo python3 setup.py install" 27 | 28 | echo "If you get errors with forex.analytics module, do su... then install sudo npm install forex.analytics" 29 | 30 | -------------------------------------------------------------------------------- /telethon/__init__.py: -------------------------------------------------------------------------------- 1 | from .telegram_bare_client import TelegramBareClient 2 | from .telegram_client import TelegramClient 3 | from . import tl 4 | -------------------------------------------------------------------------------- /telethon/crypto/__init__.py: -------------------------------------------------------------------------------- 1 | from .aes import AES 2 | from .rsa import RSA, RSAServerKey 3 | from .auth_key import AuthKey 4 | from .factorization import Factorization 5 | -------------------------------------------------------------------------------- /telethon/crypto/aes.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pyaes 3 | 4 | 5 | class AES: 6 | @staticmethod 7 | def decrypt_ige(cipher_text, key, iv): 8 | """Decrypts the given text in 16-bytes blocks by using the given key and 32-bytes initialization vector""" 9 | iv1 = iv[:len(iv) // 2] 10 | iv2 = iv[len(iv) // 2:] 11 | 12 | aes = pyaes.AES(key) 13 | 14 | plain_text = [] 15 | blocks_count = len(cipher_text) // 16 16 | 17 | cipher_text_block = [0] * 16 18 | for block_index in range(blocks_count): 19 | for i in range(16): 20 | cipher_text_block[i] = cipher_text[block_index * 16 + i] ^ iv2[ 21 | i] 22 | 23 | plain_text_block = aes.decrypt(cipher_text_block) 24 | 25 | for i in range(16): 26 | plain_text_block[i] ^= iv1[i] 27 | 28 | iv1 = cipher_text[block_index * 16:block_index * 16 + 16] 29 | iv2 = plain_text_block[:] 30 | 31 | plain_text.extend(plain_text_block[:]) 32 | 33 | return bytes(plain_text) 34 | 35 | @staticmethod 36 | def encrypt_ige(plain_text, key, iv): 37 | """Encrypts the given text in 16-bytes blocks by using the given key and 32-bytes initialization vector""" 38 | 39 | # Add random padding if and only if it's not evenly divisible by 16 already 40 | if len(plain_text) % 16 != 0: 41 | padding_count = 16 - len(plain_text) % 16 42 | plain_text += os.urandom(padding_count) 43 | 44 | iv1 = iv[:len(iv) // 2] 45 | iv2 = iv[len(iv) // 2:] 46 | 47 | aes = pyaes.AES(key) 48 | 49 | cipher_text = [] 50 | blocks_count = len(plain_text) // 16 51 | 52 | for block_index in range(blocks_count): 53 | plain_text_block = list(plain_text[block_index * 16:block_index * 54 | 16 + 16]) 55 | for i in range(16): 56 | plain_text_block[i] ^= iv1[i] 57 | 58 | cipher_text_block = aes.encrypt(plain_text_block) 59 | 60 | for i in range(16): 61 | cipher_text_block[i] ^= iv2[i] 62 | 63 | iv1 = cipher_text_block[:] 64 | iv2 = plain_text[block_index * 16:block_index * 16 + 16] 65 | 66 | cipher_text.extend(cipher_text_block[:]) 67 | 68 | return bytes(cipher_text) 69 | -------------------------------------------------------------------------------- /telethon/crypto/auth_key.py: -------------------------------------------------------------------------------- 1 | from hashlib import sha1 2 | 3 | from .. import helpers as utils 4 | from ..extensions import BinaryReader, BinaryWriter 5 | 6 | 7 | class AuthKey: 8 | def __init__(self, data): 9 | self.key = data 10 | 11 | with BinaryReader(sha1(self.key).digest()) as reader: 12 | self.aux_hash = reader.read_long(signed=False) 13 | reader.read(4) 14 | self.key_id = reader.read_long(signed=False) 15 | 16 | def calc_new_nonce_hash(self, new_nonce, number): 17 | """Calculates the new nonce hash based on the current class fields' values""" 18 | with BinaryWriter() as writer: 19 | writer.write(new_nonce) 20 | writer.write_byte(number) 21 | writer.write_long(self.aux_hash, signed=False) 22 | 23 | new_nonce_hash = utils.calc_msg_key(writer.get_bytes()) 24 | return new_nonce_hash 25 | -------------------------------------------------------------------------------- /telethon/crypto/factorization.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | 3 | 4 | class Factorization: 5 | @staticmethod 6 | def find_small_multiplier_lopatin(what): 7 | """Finds the small multiplier by using Lopatin's method""" 8 | g = 0 9 | for i in range(3): 10 | q = (randint(0, 127) & 15) + 17 11 | x = randint(0, 1000000000) + 1 12 | y = x 13 | lim = 1 << (i + 18) 14 | for j in range(1, lim): 15 | a, b, c = x, x, q 16 | while b != 0: 17 | if (b & 1) != 0: 18 | c += a 19 | if c >= what: 20 | c -= what 21 | a += a 22 | if a >= what: 23 | a -= what 24 | b >>= 1 25 | 26 | x = c 27 | z = y - x if x < y else x - y 28 | g = Factorization.gcd(z, what) 29 | if g != 1: 30 | break 31 | 32 | if (j & (j - 1)) == 0: 33 | y = x 34 | 35 | if g > 1: 36 | break 37 | 38 | p = what // g 39 | return min(p, g) 40 | 41 | @staticmethod 42 | def gcd(a, b): 43 | """Calculates the greatest common divisor""" 44 | while a != 0 and b != 0: 45 | while b & 1 == 0: 46 | b >>= 1 47 | 48 | while a & 1 == 0: 49 | a >>= 1 50 | 51 | if a > b: 52 | a -= b 53 | else: 54 | b -= a 55 | 56 | return a if b == 0 else b 57 | 58 | @staticmethod 59 | def factorize(pq): 60 | """Factorizes the given number and returns both the divisor and the number divided by the divisor""" 61 | divisor = Factorization.find_small_multiplier_lopatin(pq) 62 | return divisor, pq // divisor 63 | -------------------------------------------------------------------------------- /telethon/crypto/rsa.py: -------------------------------------------------------------------------------- 1 | import os 2 | from hashlib import sha1 3 | 4 | from ..extensions import BinaryWriter 5 | 6 | 7 | class RSAServerKey: 8 | def __init__(self, fingerprint, m, e): 9 | self.fingerprint = fingerprint 10 | self.m = m 11 | self.e = e 12 | 13 | def encrypt(self, data, offset=None, length=None): 14 | """Encrypts the given data with the current key""" 15 | if offset is None: 16 | offset = 0 17 | if length is None: 18 | length = len(data) 19 | 20 | with BinaryWriter() as writer: 21 | # Write SHA 22 | writer.write(sha1(data[offset:offset + length]).digest()) 23 | # Write data 24 | writer.write(data[offset:offset + length]) 25 | # Add padding if required 26 | if length < 235: 27 | writer.write(os.urandom(235 - length)) 28 | 29 | result = int.from_bytes(writer.get_bytes(), byteorder='big') 30 | result = pow(result, self.e, self.m) 31 | 32 | # If the result byte count is less than 256, since the byte order is big, 33 | # the non-used bytes on the left will be 0 and act as padding, 34 | # without need of any additional checks 35 | return int.to_bytes( 36 | result, length=256, byteorder='big', signed=False) 37 | 38 | 39 | class RSA: 40 | _server_keys = { 41 | '216be86c022bb4c3': RSAServerKey('216be86c022bb4c3', int( 42 | 'C150023E2F70DB7985DED064759CFECF0AF328E69A41DAF4D6F01B538135A6F9' 43 | '1F8F8B2A0EC9BA9720CE352EFCF6C5680FFC424BD634864902DE0B4BD6D49F4E' 44 | '580230E3AE97D95C8B19442B3C0A10D8F5633FECEDD6926A7F6DAB0DDB7D457F' 45 | '9EA81B8465FCD6FFFEED114011DF91C059CAEDAF97625F6C96ECC74725556934' 46 | 'EF781D866B34F011FCE4D835A090196E9A5F0E4449AF7EB697DDB9076494CA5F' 47 | '81104A305B6DD27665722C46B60E5DF680FB16B210607EF217652E60236C255F' 48 | '6A28315F4083A96791D7214BF64C1DF4FD0DB1944FB26A2A57031B32EEE64AD1' 49 | '5A8BA68885CDE74A5BFC920F6ABF59BA5C75506373E7130F9042DA922179251F', 50 | 16), int('010001', 16)) 51 | } 52 | 53 | @staticmethod 54 | def encrypt(fingerprint, data, offset=None, length=None): 55 | """Encrypts the given data given a fingerprint""" 56 | if fingerprint.lower() not in RSA._server_keys: 57 | return None 58 | 59 | key = RSA._server_keys[fingerprint.lower()] 60 | return key.encrypt(data, offset, length) 61 | -------------------------------------------------------------------------------- /telethon/errors/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from .common import ( 4 | ReadCancelledError, InvalidParameterError, TypeNotFoundError, 5 | InvalidChecksumError 6 | ) 7 | 8 | from .rpc_errors import ( 9 | RPCError, InvalidDCError, BadRequestError, UnauthorizedError, 10 | ForbiddenError, NotFoundError, FloodError, ServerError, BadMessageError 11 | ) 12 | 13 | from .rpc_errors_303 import * 14 | from .rpc_errors_400 import * 15 | from .rpc_errors_401 import * 16 | from .rpc_errors_420 import * 17 | 18 | 19 | def rpc_message_to_error(code, message): 20 | errors = { 21 | 303: rpc_errors_303_all, 22 | 400: rpc_errors_400_all, 23 | 401: rpc_errors_401_all, 24 | 420: rpc_errors_420_all 25 | }.get(code, None) 26 | 27 | if errors is not None: 28 | for msg, cls in errors.items(): 29 | m = re.match(msg, message) 30 | if m: 31 | extra = int(m.group(1)) if m.groups() else None 32 | return cls(extra=extra) 33 | 34 | elif code == 403: 35 | return ForbiddenError(message) 36 | 37 | elif code == 404: 38 | return NotFoundError(message) 39 | 40 | elif code == 500: 41 | return ServerError(message) 42 | 43 | return RPCError('{} (code {})'.format(message, code)) 44 | -------------------------------------------------------------------------------- /telethon/errors/common.py: -------------------------------------------------------------------------------- 1 | """Errors not related to the Telegram API itself""" 2 | 3 | 4 | class ReadCancelledError(Exception): 5 | """Occurs when a read operation was cancelled""" 6 | def __init__(self): 7 | super().__init__(self, 'The read operation was cancelled.') 8 | 9 | 10 | class InvalidParameterError(Exception): 11 | """Occurs when an invalid parameter is given, for example, 12 | when either A or B are required but none is given""" 13 | 14 | 15 | class TypeNotFoundError(Exception): 16 | """Occurs when a type is not found, for example, 17 | when trying to read a TLObject with an invalid constructor code""" 18 | 19 | def __init__(self, invalid_constructor_id): 20 | super().__init__( 21 | self, 'Could not find a matching Constructor ID for the TLObject ' 22 | 'that was supposed to be read with ID {}. Most likely, a TLObject ' 23 | 'was trying to be read when it should not be read.' 24 | .format(hex(invalid_constructor_id))) 25 | 26 | self.invalid_constructor_id = invalid_constructor_id 27 | 28 | 29 | class InvalidChecksumError(Exception): 30 | def __init__(self, checksum, valid_checksum): 31 | super().__init__( 32 | self, 33 | 'Invalid checksum ({} when {} was expected). This packet should be skipped.' 34 | .format(checksum, valid_checksum)) 35 | 36 | self.checksum = checksum 37 | self.valid_checksum = valid_checksum 38 | -------------------------------------------------------------------------------- /telethon/errors/rpc_errors.py: -------------------------------------------------------------------------------- 1 | class RPCError(Exception): 2 | code = None 3 | message = None 4 | 5 | 6 | class InvalidDCError(RPCError): 7 | """ 8 | The request must be repeated, but directed to a different data center. 9 | """ 10 | code = 303 11 | message = 'ERROR_SEE_OTHER' 12 | 13 | 14 | class BadRequestError(RPCError): 15 | """ 16 | The query contains errors. In the event that a request was created 17 | using a form and contains user generated data, the user should be 18 | notified that the data must be corrected before the query is repeated. 19 | """ 20 | code = 400 21 | message = 'BAD_REQUEST' 22 | 23 | 24 | class UnauthorizedError(RPCError): 25 | """ 26 | There was an unauthorized attempt to use functionality available only 27 | to authorized users. 28 | """ 29 | code = 401 30 | message = 'UNAUTHORIZED' 31 | 32 | 33 | class ForbiddenError(RPCError): 34 | """ 35 | Privacy violation. For example, an attempt to write a message to 36 | someone who has blacklisted the current user. 37 | """ 38 | code = 403 39 | message = 'FORBIDDEN' 40 | 41 | def __init__(self, message): 42 | super().__init__(self, message) 43 | self.message = message 44 | 45 | 46 | class NotFoundError(RPCError): 47 | """ 48 | An attempt to invoke a non-existent object, such as a method. 49 | """ 50 | code = 404 51 | message = 'NOT_FOUND' 52 | 53 | def __init__(self, message): 54 | super().__init__(self, message) 55 | self.message = message 56 | 57 | 58 | class FloodError(RPCError): 59 | """ 60 | The maximum allowed number of attempts to invoke the given method 61 | with the given input parameters has been exceeded. For example, in an 62 | attempt to request a large number of text messages (SMS) for the same 63 | phone number. 64 | """ 65 | code = 420 66 | message = 'FLOOD' 67 | 68 | 69 | class ServerError(RPCError): 70 | """ 71 | An internal server error occurred while a request was being processed 72 | for example, there was a disruption while accessing a database or file 73 | storage. 74 | """ 75 | code = 500 76 | message = 'INTERNAL' 77 | 78 | def __init__(self, message): 79 | super().__init__(self, message) 80 | self.message = message 81 | 82 | 83 | class BadMessageError(Exception): 84 | """Occurs when handling a bad_message_notification""" 85 | ErrorMessages = { 86 | 16: 87 | 'msg_id too low (most likely, client time is wrong it would be worthwhile to ' 88 | 'synchronize it using msg_id notifications and re-send the original message ' 89 | 'with the "correct" msg_id or wrap it in a container with a new msg_id if the ' 90 | 'original message had waited too long on the client to be transmitted).', 91 | 17: 92 | 'msg_id too high (similar to the previous case, the client time has to be ' 93 | 'synchronized, and the message re-sent with the correct msg_id).', 94 | 18: 95 | 'Incorrect two lower order msg_id bits (the server expects client message msg_id ' 96 | 'to be divisible by 4).', 97 | 19: 98 | 'Container msg_id is the same as msg_id of a previously received message ' 99 | '(this must never happen).', 100 | 20: 101 | 'Message too old, and it cannot be verified whether the server has received a ' 102 | 'message with this msg_id or not.', 103 | 32: 104 | 'msg_seqno too low (the server has already received a message with a lower ' 105 | 'msg_id but with either a higher or an equal and odd seqno).', 106 | 33: 107 | 'msg_seqno too high (similarly, there is a message with a higher msg_id but with ' 108 | 'either a lower or an equal and odd seqno).', 109 | 34: 110 | 'An even msg_seqno expected (irrelevant message), but odd received.', 111 | 35: 'Odd msg_seqno expected (relevant message), but even received.', 112 | 48: 113 | 'Incorrect server salt (in this case, the bad_server_salt response is received with ' 114 | 'the correct salt, and the message is to be re-sent with it).', 115 | 64: 'Invalid container.' 116 | } 117 | 118 | def __init__(self, code): 119 | super().__init__(self, self.ErrorMessages.get( 120 | code, 121 | 'Unknown error code (this should not happen): {}.'.format(code))) 122 | 123 | self.code = code 124 | -------------------------------------------------------------------------------- /telethon/errors/rpc_errors_303.py: -------------------------------------------------------------------------------- 1 | from . import InvalidDCError 2 | 3 | 4 | class FileMigrateError(InvalidDCError): 5 | def __init__(self, **kwargs): 6 | self.new_dc = kwargs['extra'] 7 | super(Exception, self).__init__( 8 | self, 9 | 'The file to be accessed is currently stored in DC {}.' 10 | .format(self.new_dc) 11 | ) 12 | 13 | 14 | class PhoneMigrateError(InvalidDCError): 15 | def __init__(self, **kwargs): 16 | self.new_dc = kwargs['extra'] 17 | super(Exception, self).__init__( 18 | self, 19 | 'The phone number a user is trying to use for authorization is ' 20 | 'associated with DC {}.' 21 | .format(self.new_dc) 22 | ) 23 | 24 | 25 | class NetworkMigrateError(InvalidDCError): 26 | def __init__(self, **kwargs): 27 | self.new_dc = kwargs['extra'] 28 | super(Exception, self).__init__( 29 | self, 30 | 'The source IP address is associated with DC {}.' 31 | .format(self.new_dc) 32 | ) 33 | 34 | 35 | class UserMigrateError(InvalidDCError): 36 | def __init__(self, **kwargs): 37 | self.new_dc = kwargs['extra'] 38 | super(Exception, self).__init__( 39 | self, 40 | 'The user whose identity is being used to execute queries is ' 41 | 'associated with DC {}.' 42 | .format(self.new_dc) 43 | ) 44 | 45 | 46 | rpc_errors_303_all = { 47 | 'FILE_MIGRATE_(\d+)': FileMigrateError, 48 | 'PHONE_MIGRATE_(\d+)': PhoneMigrateError, 49 | 'NETWORK_MIGRATE_(\d+)': NetworkMigrateError, 50 | 'USER_MIGRATE_(\d+)': UserMigrateError 51 | } 52 | -------------------------------------------------------------------------------- /telethon/errors/rpc_errors_400.py: -------------------------------------------------------------------------------- 1 | from . import BadRequestError 2 | 3 | 4 | class ApiIdInvalidError(BadRequestError): 5 | def __init__(self, **kwargs): 6 | super(Exception, self).__init__( 7 | self, 8 | 'The api_id/api_hash combination is invalid.' 9 | ) 10 | 11 | 12 | class BotMethodInvalidError(BadRequestError): 13 | def __init__(self, **kwargs): 14 | super(Exception, self).__init__( 15 | self, 16 | 'The API access for bot users is restricted. The method you ' 17 | 'tried to invoke cannot be executed as a bot.' 18 | ) 19 | 20 | 21 | class ChannelInvalidError(BadRequestError): 22 | def __init__(self, **kwargs): 23 | super(Exception, self).__init__( 24 | self, 25 | 'Invalid channel object. Make sure to pass the right types.' 26 | ) 27 | 28 | 29 | class ChatAdminRequiredError(BadRequestError): 30 | def __init__(self, **kwargs): 31 | super(Exception, self).__init__( 32 | self, 33 | 'Chat admin privileges are required to do that in the specified ' 34 | 'chat (for example, to send a message in a channel which is not ' 35 | 'yours).' 36 | ) 37 | 38 | 39 | class ChatIdInvalidError(BadRequestError): 40 | def __init__(self, **kwargs): 41 | super(Exception, self).__init__( 42 | self, 43 | 'Invalid object ID for a chat. Make sure to pass the right types.' 44 | ) 45 | 46 | 47 | class ConnectionLangPackInvalid(BadRequestError): 48 | def __init__(self, **kwargs): 49 | super(Exception, self).__init__( 50 | self, 51 | 'The specified language pack is not valid.' 52 | ) 53 | 54 | 55 | class ConnectionLayerInvalidError(BadRequestError): 56 | def __init__(self, **kwargs): 57 | super(Exception, self).__init__( 58 | self, 59 | 'The very first request must always be InvokeWithLayerRequest.' 60 | ) 61 | 62 | 63 | class DcIdInvalidError(BadRequestError): 64 | def __init__(self, **kwargs): 65 | super(Exception, self).__init__( 66 | self, 67 | 'This occurs when an authorization is tried to be exported for ' 68 | 'the same data center one is currently connected to.' 69 | ) 70 | 71 | 72 | class FieldNameEmptyError(BadRequestError): 73 | def __init__(self, **kwargs): 74 | super(Exception, self).__init__( 75 | self, 76 | 'The field with the name FIELD_NAME is missing.' 77 | ) 78 | 79 | 80 | class FieldNameInvalidError(BadRequestError): 81 | def __init__(self, **kwargs): 82 | super(Exception, self).__init__( 83 | self, 84 | 'The field with the name FIELD_NAME is invalid.' 85 | ) 86 | 87 | 88 | class FilePartsInvalidError(BadRequestError): 89 | def __init__(self, **kwargs): 90 | super(Exception, self).__init__( 91 | self, 92 | 'The number of file parts is invalid.' 93 | ) 94 | 95 | 96 | class FilePartMissingError(BadRequestError): 97 | def __init__(self, **kwargs): 98 | self.which = kwargs['extra'] 99 | super(Exception, self).__init__( 100 | self, 101 | 'Part {} of the file is missing from storage.'.format(self.which) 102 | ) 103 | 104 | 105 | class FilePartInvalidError(BadRequestError): 106 | def __init__(self, **kwargs): 107 | super(Exception, self).__init__( 108 | self, 109 | 'The file part number is invalid.' 110 | ) 111 | 112 | 113 | class FirstNameInvalidError(BadRequestError): 114 | def __init__(self, **kwargs): 115 | super(Exception, self).__init__( 116 | self, 117 | 'The first name is invalid.' 118 | ) 119 | 120 | 121 | class InputMethodInvalidError(BadRequestError): 122 | def __init__(self, **kwargs): 123 | super(Exception, self).__init__( 124 | self, 125 | 'The invoked method does not exist anymore or has never existed.' 126 | ) 127 | 128 | 129 | class LastNameInvalidError(BadRequestError): 130 | def __init__(self, **kwargs): 131 | super(Exception, self).__init__( 132 | self, 133 | 'The last name is invalid.' 134 | ) 135 | 136 | 137 | class Md5ChecksumInvalidError(BadRequestError): 138 | def __init__(self, **kwargs): 139 | super(Exception, self).__init__( 140 | self, 141 | 'The MD5 check-sums do not match.' 142 | ) 143 | 144 | 145 | class MessageEmptyError(BadRequestError): 146 | def __init__(self, **kwargs): 147 | super(Exception, self).__init__( 148 | self, 149 | 'Empty or invalid UTF-8 message was sent.' 150 | ) 151 | 152 | 153 | class MessageIdInvalidError(BadRequestError): 154 | def __init__(self, **kwargs): 155 | super(Exception, self).__init__( 156 | self, 157 | 'The specified message ID is invalid.' 158 | ) 159 | 160 | 161 | class MessageTooLongError(BadRequestError): 162 | def __init__(self, **kwargs): 163 | super(Exception, self).__init__( 164 | self, 165 | 'Message was too long. Current maximum length is 4096 UTF-8 ' 166 | 'characters.' 167 | ) 168 | 169 | 170 | class MsgWaitFailedError(BadRequestError): 171 | def __init__(self, **kwargs): 172 | super(Exception, self).__init__( 173 | self, 174 | 'A waiting call returned an error.' 175 | ) 176 | 177 | 178 | class PasswordHashInvalidError(BadRequestError): 179 | def __init__(self, **kwargs): 180 | super(Exception, self).__init__( 181 | self, 182 | 'The password (and thus its hash value) you entered is invalid.' 183 | ) 184 | 185 | 186 | class PeerIdInvalidError(BadRequestError): 187 | def __init__(self, **kwargs): 188 | super(Exception, self).__init__( 189 | self, 190 | 'An invalid Peer was used. Make sure to pass the right peer type.' 191 | ) 192 | 193 | 194 | class PhoneCodeEmptyError(BadRequestError): 195 | def __init__(self, **kwargs): 196 | super(Exception, self).__init__( 197 | self, 198 | 'The phone code is missing.' 199 | ) 200 | 201 | 202 | class PhoneCodeExpiredError(BadRequestError): 203 | def __init__(self, **kwargs): 204 | super(Exception, self).__init__( 205 | self, 206 | 'The confirmation code has expired.' 207 | ) 208 | 209 | 210 | class PhoneCodeHashEmptyError(BadRequestError): 211 | def __init__(self, **kwargs): 212 | super(Exception, self).__init__( 213 | self, 214 | 'The phone code hash is missing.' 215 | ) 216 | 217 | 218 | class PhoneCodeInvalidError(BadRequestError): 219 | def __init__(self, **kwargs): 220 | super(Exception, self).__init__( 221 | self, 222 | 'The phone code entered was invalid.' 223 | ) 224 | 225 | 226 | class PhoneNumberBannedError(BadRequestError): 227 | def __init__(self, **kwargs): 228 | super(Exception, self).__init__( 229 | self, 230 | 'The used phone number has been banned from Telegram and cannot ' 231 | 'be used anymore. Maybe check https://www.telegram.org/faq_spam.' 232 | ) 233 | 234 | 235 | class PhoneNumberInvalidError(BadRequestError): 236 | def __init__(self, **kwargs): 237 | super(Exception, self).__init__( 238 | self, 239 | 'The phone number is invalid.' 240 | ) 241 | 242 | 243 | class PhoneNumberOccupiedError(BadRequestError): 244 | def __init__(self, **kwargs): 245 | super(Exception, self).__init__( 246 | self, 247 | 'The phone number is already in use.' 248 | ) 249 | 250 | 251 | class PhoneNumberUnoccupiedError(BadRequestError): 252 | def __init__(self, **kwargs): 253 | super(Exception, self).__init__( 254 | self, 255 | 'The phone number is not yet being used.' 256 | ) 257 | 258 | 259 | class PhotoInvalidDimensionsError(BadRequestError): 260 | def __init__(self, **kwargs): 261 | super(Exception, self).__init__( 262 | self, 263 | 'The photo dimensions are invalid.' 264 | ) 265 | 266 | 267 | class TypeConstructorInvalidError(BadRequestError): 268 | def __init__(self, **kwargs): 269 | super(Exception, self).__init__( 270 | self, 271 | 'The type constructor is invalid.' 272 | ) 273 | 274 | 275 | class UsernameInvalidError(BadRequestError): 276 | def __init__(self, **kwargs): 277 | super(Exception, self).__init__( 278 | self, 279 | 'Unacceptable username. Must match r"[a-zA-Z][\w\d]{4,32}"' 280 | ) 281 | 282 | 283 | class UsernameNotModifiedError(BadRequestError): 284 | def __init__(self, **kwargs): 285 | super(Exception, self).__init__( 286 | self, 287 | 'The username is not different from the current username' 288 | ) 289 | 290 | 291 | class UsernameNotOccupiedError(BadRequestError): 292 | def __init__(self, **kwargs): 293 | super(Exception, self).__init__( 294 | self, 295 | 'See issue #96 for Telethon - try upgrading the library.' 296 | ) 297 | 298 | 299 | class UsernameOccupiedError(BadRequestError): 300 | def __init__(self, **kwargs): 301 | super(Exception, self).__init__( 302 | self, 303 | 'The username is already taken.' 304 | ) 305 | 306 | 307 | class UsersTooFewError(BadRequestError): 308 | def __init__(self, **kwargs): 309 | super(Exception, self).__init__( 310 | self, 311 | 'Not enough users (to create a chat, for example).' 312 | ) 313 | 314 | 315 | class UsersTooMuchError(BadRequestError): 316 | def __init__(self, **kwargs): 317 | super(Exception, self).__init__( 318 | self, 319 | 'The maximum number of users has been exceeded (to create a ' 320 | 'chat, for example).' 321 | ) 322 | 323 | 324 | class UserIdInvalidError(BadRequestError): 325 | def __init__(self, **kwargs): 326 | super(Exception, self).__init__( 327 | self, 328 | 'Invalid object ID for an user. Make sure to pass the right types.' 329 | ) 330 | 331 | 332 | rpc_errors_400_all = { 333 | 'API_ID_INVALID': ApiIdInvalidError, 334 | 'BOT_METHOD_INVALID': BotMethodInvalidError, 335 | 'CHANNEL_INVALID': ChannelInvalidError, 336 | 'CHAT_ADMIN_REQUIRED': ChatAdminRequiredError, 337 | 'CHAT_ID_INVALID': ChatIdInvalidError, 338 | 'CONNECTION_LAYER_INVALID': ConnectionLayerInvalidError, 339 | 'DC_ID_INVALID': DcIdInvalidError, 340 | 'FIELD_NAME_EMPTY': FieldNameEmptyError, 341 | 'FIELD_NAME_INVALID': FieldNameInvalidError, 342 | 'FILE_PARTS_INVALID': FilePartsInvalidError, 343 | 'FILE_PART_(\d+)_MISSING': FilePartMissingError, 344 | 'FILE_PART_INVALID': FilePartInvalidError, 345 | 'FIRSTNAME_INVALID': FirstNameInvalidError, 346 | 'INPUT_METHOD_INVALID': InputMethodInvalidError, 347 | 'LASTNAME_INVALID': LastNameInvalidError, 348 | 'MD5_CHECKSUM_INVALID': Md5ChecksumInvalidError, 349 | 'MESSAGE_EMPTY': MessageEmptyError, 350 | 'MESSAGE_ID_INVALID': MessageIdInvalidError, 351 | 'MESSAGE_TOO_LONG': MessageTooLongError, 352 | 'MSG_WAIT_FAILED': MsgWaitFailedError, 353 | 'PASSWORD_HASH_INVALID': PasswordHashInvalidError, 354 | 'PEER_ID_INVALID': PeerIdInvalidError, 355 | 'PHONE_CODE_EMPTY': PhoneCodeEmptyError, 356 | 'PHONE_CODE_EXPIRED': PhoneCodeExpiredError, 357 | 'PHONE_CODE_HASH_EMPTY': PhoneCodeHashEmptyError, 358 | 'PHONE_CODE_INVALID': PhoneCodeInvalidError, 359 | 'PHONE_NUMBER_BANNED': PhoneNumberBannedError, 360 | 'PHONE_NUMBER_INVALID': PhoneNumberInvalidError, 361 | 'PHONE_NUMBER_OCCUPIED': PhoneNumberOccupiedError, 362 | 'PHONE_NUMBER_UNOCCUPIED': PhoneNumberUnoccupiedError, 363 | 'PHOTO_INVALID_DIMENSIONS': PhotoInvalidDimensionsError, 364 | 'TYPE_CONSTRUCTOR_INVALID': TypeConstructorInvalidError, 365 | 'USERNAME_INVALID': UsernameInvalidError, 366 | 'USERNAME_NOT_MODIFIED': UsernameNotModifiedError, 367 | 'USERNAME_NOT_OCCUPIED': UsernameNotOccupiedError, 368 | 'USERNAME_OCCUPIED': UsernameOccupiedError, 369 | 'USERS_TOO_FEW': UsersTooFewError, 370 | 'USERS_TOO_MUCH': UsersTooMuchError, 371 | 'USER_ID_INVALID': UserIdInvalidError, 372 | } 373 | -------------------------------------------------------------------------------- /telethon/errors/rpc_errors_401.py: -------------------------------------------------------------------------------- 1 | from . import UnauthorizedError 2 | 3 | 4 | class ActiveUserRequiredError(UnauthorizedError): 5 | def __init__(self, **kwargs): 6 | super(Exception, self).__init__( 7 | self, 8 | 'The method is only available to already activated users.' 9 | ) 10 | 11 | 12 | class AuthKeyInvalidError(UnauthorizedError): 13 | def __init__(self, **kwargs): 14 | super(Exception, self).__init__( 15 | self, 16 | 'The key is invalid.' 17 | ) 18 | 19 | 20 | class AuthKeyPermEmptyError(UnauthorizedError): 21 | def __init__(self, **kwargs): 22 | super(Exception, self).__init__( 23 | self, 24 | 'The method is unavailable for temporary authorization key, not ' 25 | 'bound to permanent.' 26 | ) 27 | 28 | 29 | class AuthKeyUnregisteredError(UnauthorizedError): 30 | def __init__(self, **kwargs): 31 | super(Exception, self).__init__( 32 | self, 33 | 'The key is not registered in the system.' 34 | ) 35 | 36 | 37 | class InviteHashExpiredError(UnauthorizedError): 38 | def __init__(self, **kwargs): 39 | super(Exception, self).__init__( 40 | self, 41 | 'The chat the user tried to join has expired and is not valid ' 42 | 'anymore.' 43 | ) 44 | 45 | 46 | class SessionExpiredError(UnauthorizedError): 47 | def __init__(self, **kwargs): 48 | super(Exception, self).__init__( 49 | self, 50 | 'The authorization has expired.' 51 | ) 52 | 53 | 54 | class SessionPasswordNeededError(UnauthorizedError): 55 | def __init__(self, **kwargs): 56 | super(Exception, self).__init__( 57 | self, 58 | 'Two-steps verification is enabled and a password is required.' 59 | ) 60 | 61 | 62 | class SessionRevokedError(UnauthorizedError): 63 | def __init__(self, **kwargs): 64 | super(Exception, self).__init__( 65 | self, 66 | 'The authorization has been invalidated, because of the user ' 67 | 'terminating all sessions.' 68 | ) 69 | 70 | 71 | class UserAlreadyParticipantError(UnauthorizedError): 72 | def __init__(self, **kwargs): 73 | super(Exception, self).__init__( 74 | self, 75 | 'The authenticated user is already a participant of the chat.' 76 | ) 77 | 78 | 79 | class UserDeactivatedError(UnauthorizedError): 80 | def __init__(self, **kwargs): 81 | super(Exception, self).__init__( 82 | self, 83 | 'The user has been deleted/deactivated.' 84 | ) 85 | 86 | 87 | rpc_errors_401_all = { 88 | 'ACTIVE_USER_REQUIRED': ActiveUserRequiredError, 89 | 'AUTH_KEY_INVALID': AuthKeyInvalidError, 90 | 'AUTH_KEY_PERM_EMPTY': AuthKeyPermEmptyError, 91 | 'AUTH_KEY_UNREGISTERED': AuthKeyUnregisteredError, 92 | 'INVITE_HASH_EXPIRED': InviteHashExpiredError, 93 | 'SESSION_EXPIRED': SessionExpiredError, 94 | 'SESSION_PASSWORD_NEEDED': SessionPasswordNeededError, 95 | 'SESSION_REVOKED': SessionRevokedError, 96 | 'USER_ALREADY_PARTICIPANT': UserAlreadyParticipantError, 97 | 'USER_DEACTIVATED': UserDeactivatedError, 98 | } 99 | -------------------------------------------------------------------------------- /telethon/errors/rpc_errors_420.py: -------------------------------------------------------------------------------- 1 | from . import FloodError 2 | 3 | 4 | class FloodWaitError(FloodError): 5 | def __init__(self, **kwargs): 6 | self.seconds = kwargs['extra'] 7 | super(Exception, self).__init__( 8 | self, 9 | 'A wait of {} seconds is required.' 10 | .format(self.seconds) 11 | ) 12 | 13 | 14 | rpc_errors_420_all = { 15 | 'FLOOD_WAIT_(\d+)': FloodWaitError 16 | } 17 | -------------------------------------------------------------------------------- /telethon/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Several extensions Python is missing, such as a proper class to handle a TCP 3 | communication with support for cancelling the operation, and an utility class 4 | to work with arbitrary binary data in a more comfortable way (writing ints, 5 | strings, bytes, etc.) 6 | """ 7 | from .binary_writer import BinaryWriter 8 | from .binary_reader import BinaryReader 9 | from .tcp_client import TcpClient -------------------------------------------------------------------------------- /telethon/extensions/binary_reader.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | from io import BufferedReader, BytesIO 4 | from struct import unpack 5 | 6 | from ..errors import InvalidParameterError, TypeNotFoundError 7 | from ..tl.all_tlobjects import tlobjects 8 | 9 | 10 | class BinaryReader: 11 | """ 12 | Small utility class to read binary data. 13 | Also creates a "Memory Stream" if necessary 14 | """ 15 | 16 | def __init__(self, data=None, stream=None): 17 | if data: 18 | self.stream = BytesIO(data) 19 | elif stream: 20 | self.stream = stream 21 | else: 22 | raise InvalidParameterError( 23 | 'Either bytes or a stream must be provided') 24 | 25 | self.reader = BufferedReader(self.stream) 26 | 27 | # region Reading 28 | 29 | # "All numbers are written as little endian." |> Source: https://core.telegram.org/mtproto 30 | def read_byte(self): 31 | """Reads a single byte value""" 32 | return self.read(1)[0] 33 | 34 | def read_int(self, signed=True): 35 | """Reads an integer (4 bytes) value""" 36 | return int.from_bytes(self.read(4), byteorder='little', signed=signed) 37 | 38 | def read_long(self, signed=True): 39 | """Reads a long integer (8 bytes) value""" 40 | return int.from_bytes(self.read(8), byteorder='little', signed=signed) 41 | 42 | def read_float(self): 43 | """Reads a real floating point (4 bytes) value""" 44 | return unpack(' 0: 85 | padding = 4 - padding 86 | self.read(padding) 87 | 88 | return data 89 | 90 | def tgread_string(self): 91 | """Reads a Telegram-encoded string""" 92 | return str(self.tgread_bytes(), encoding='utf-8', errors='replace') 93 | 94 | def tgread_bool(self): 95 | """Reads a Telegram boolean value""" 96 | value = self.read_int(signed=False) 97 | if value == 0x997275b5: # boolTrue 98 | return True 99 | elif value == 0xbc799737: # boolFalse 100 | return False 101 | else: 102 | raise ValueError('Invalid boolean code {}'.format(hex(value))) 103 | 104 | def tgread_date(self): 105 | """Reads and converts Unix time (used by Telegram) into a Python datetime object""" 106 | value = self.read_int() 107 | return None if value == 0 else datetime.fromtimestamp(value) 108 | 109 | def tgread_object(self): 110 | """Reads a Telegram object""" 111 | constructor_id = self.read_int(signed=False) 112 | clazz = tlobjects.get(constructor_id, None) 113 | if clazz is None: 114 | # The class was None, but there's still a 115 | # chance of it being a manually parsed value like bool! 116 | value = constructor_id 117 | if value == 0x997275b5: # boolTrue 118 | return True 119 | elif value == 0xbc799737: # boolFalse 120 | return False 121 | 122 | # If there was still no luck, give up 123 | raise TypeNotFoundError(constructor_id) 124 | 125 | # Create an empty instance of the class and 126 | # fill it with the read attributes 127 | result = clazz.empty() 128 | result.on_response(self) 129 | return result 130 | 131 | def tgread_vector(self): 132 | """Reads a vector (a list) of Telegram objects""" 133 | if 0x1cb5c415 != self.read_int(signed=False): 134 | raise ValueError('Invalid constructor code, vector was expected') 135 | 136 | count = self.read_int() 137 | return [self.tgread_object() for _ in range(count)] 138 | 139 | # endregion 140 | 141 | def close(self): 142 | self.reader.close() 143 | 144 | # region Position related 145 | 146 | def tell_position(self): 147 | """Tells the current position on the stream""" 148 | return self.reader.tell() 149 | 150 | def set_position(self, position): 151 | """Sets the current position on the stream""" 152 | self.reader.seek(position) 153 | 154 | def seek(self, offset): 155 | """Seeks the stream position given an offset from the current position. May be negative""" 156 | self.reader.seek(offset, os.SEEK_CUR) 157 | 158 | # endregion 159 | 160 | # region with block 161 | 162 | def __enter__(self): 163 | return self 164 | 165 | def __exit__(self, exc_type, exc_val, exc_tb): 166 | self.close() 167 | 168 | # endregion 169 | -------------------------------------------------------------------------------- /telethon/extensions/binary_writer.py: -------------------------------------------------------------------------------- 1 | from io import BufferedWriter, BytesIO 2 | from struct import pack 3 | 4 | 5 | class BinaryWriter: 6 | """ 7 | Small utility class to write binary data. 8 | Also creates a "Memory Stream" if necessary 9 | """ 10 | 11 | def __init__(self, stream=None): 12 | if not stream: 13 | stream = BytesIO() 14 | 15 | self.writer = BufferedWriter(stream) 16 | self.written_count = 0 17 | 18 | # region Writing 19 | 20 | # "All numbers are written as little endian." |> Source: https://core.telegram.org/mtproto 21 | def write_byte(self, value): 22 | """Writes a single byte value""" 23 | self.writer.write(pack('B', value)) 24 | self.written_count += 1 25 | 26 | def write_int(self, value, signed=True): 27 | """Writes an integer value (4 bytes), which can or cannot be signed""" 28 | self.writer.write( 29 | int.to_bytes( 30 | value, length=4, byteorder='little', signed=signed)) 31 | self.written_count += 4 32 | 33 | def write_long(self, value, signed=True): 34 | """Writes a long integer value (8 bytes), which can or cannot be signed""" 35 | self.writer.write( 36 | int.to_bytes( 37 | value, length=8, byteorder='little', signed=signed)) 38 | self.written_count += 8 39 | 40 | def write_float(self, value): 41 | """Writes a floating point value (4 bytes)""" 42 | self.writer.write(pack('> 8) % 256])) 84 | self.write(bytes([(len(data) >> 16) % 256])) 85 | self.write(data) 86 | 87 | self.write(bytes(padding)) 88 | 89 | def tgwrite_string(self, string): 90 | """Write a string by using Telegram guidelines""" 91 | self.tgwrite_bytes(string.encode('utf-8')) 92 | 93 | def tgwrite_bool(self, boolean): 94 | """Write a boolean value by using Telegram guidelines""" 95 | # boolTrue boolFalse 96 | self.write_int(0x997275b5 if boolean else 0xbc799737, signed=False) 97 | 98 | def tgwrite_date(self, datetime): 99 | """Converts a Python datetime object into Unix time (used by Telegram) and writes it""" 100 | value = 0 if datetime is None else int(datetime.timestamp()) 101 | self.write_int(value) 102 | 103 | def tgwrite_object(self, tlobject): 104 | """Writes a Telegram object""" 105 | tlobject.on_send(self) 106 | 107 | def tgwrite_vector(self, vector): 108 | """Writes a vector of Telegram objects""" 109 | self.write_int(0x1cb5c415, signed=False) # Vector's constructor ID 110 | self.write_int(len(vector)) 111 | for item in vector: 112 | self.tgwrite_object(item) 113 | 114 | # endregion 115 | 116 | def flush(self): 117 | """Flush the current stream to "update" changes""" 118 | self.writer.flush() 119 | 120 | def close(self): 121 | """Close the current stream""" 122 | self.writer.close() 123 | 124 | def get_bytes(self, flush=True): 125 | """Get the current bytes array content from the buffer, optionally flushing first""" 126 | if flush: 127 | self.writer.flush() 128 | return self.writer.raw.getvalue() 129 | 130 | def get_written_bytes_count(self): 131 | """Gets the count of bytes written in the buffer. 132 | This may NOT be equal to the stream length if one was provided when initializing the writer""" 133 | return self.written_count 134 | 135 | # with block 136 | def __enter__(self): 137 | return self 138 | 139 | def __exit__(self, exc_type, exc_val, exc_tb): 140 | self.close() 141 | -------------------------------------------------------------------------------- /telethon/extensions/tcp_client.py: -------------------------------------------------------------------------------- 1 | # Python rough implementation of a C# TCP client 2 | import socket 3 | import time 4 | from datetime import datetime, timedelta 5 | from io import BytesIO, BufferedWriter 6 | from threading import Event, Lock 7 | 8 | from ..errors import ReadCancelledError 9 | 10 | 11 | class TcpClient: 12 | def __init__(self, proxy=None): 13 | self.connected = False 14 | self._proxy = proxy 15 | self._recreate_socket() 16 | 17 | # Support for multi-threading advantages and safety 18 | self.cancelled = Event() # Has the read operation been cancelled? 19 | self.delay = 0.1 # Read delay when there was no data available 20 | self._lock = Lock() 21 | 22 | def _recreate_socket(self): 23 | if self._proxy is None: 24 | self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 25 | else: 26 | import socks 27 | self._socket = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM) 28 | if type(self._proxy) is dict: 29 | self._socket.set_proxy(**self._proxy) 30 | else: # tuple, list, etc. 31 | self._socket.set_proxy(*self._proxy) 32 | 33 | def connect(self, ip, port, timeout): 34 | """Connects to the specified IP and port number. 35 | 'timeout' must be given in seconds 36 | """ 37 | if not self.connected: 38 | self._socket.settimeout(timeout) 39 | self._socket.connect((ip, port)) 40 | self._socket.setblocking(False) 41 | self.connected = True 42 | 43 | def close(self): 44 | """Closes the connection""" 45 | if self.connected: 46 | self._socket.shutdown(socket.SHUT_RDWR) 47 | self._socket.close() 48 | self.connected = False 49 | self._recreate_socket() 50 | 51 | def write(self, data): 52 | """Writes (sends) the specified bytes to the connected peer""" 53 | 54 | # Ensure that only one thread can send data at once 55 | with self._lock: 56 | view = memoryview(data) 57 | total_sent, total = 0, len(data) 58 | while total_sent < total: 59 | try: 60 | sent = self._socket.send(view[total_sent:]) 61 | if sent == 0: 62 | raise ConnectionResetError( 63 | 'The server has closed the connection.') 64 | total_sent += sent 65 | 66 | except BlockingIOError: 67 | time.sleep(self.delay) 68 | 69 | def read(self, size, timeout=timedelta(seconds=5)): 70 | """Reads (receives) a whole block of 'size bytes 71 | from the connected peer. 72 | 73 | A timeout can be specified, which will cancel the operation if 74 | no data has been read in the specified time. If data was read 75 | and it's waiting for more, the timeout will NOT cancel the 76 | operation. Set to None for no timeout 77 | """ 78 | 79 | # Ensure that only one thread can receive data at once 80 | with self._lock: 81 | # Ensure it is not cancelled at first, so we can enter the loop 82 | self.cancelled.clear() 83 | 84 | # Set the starting time so we can 85 | # calculate whether the timeout should fire 86 | start_time = datetime.now() if timeout is not None else None 87 | 88 | with BufferedWriter(BytesIO(), buffer_size=size) as buffer: 89 | bytes_left = size 90 | while bytes_left != 0: 91 | # Only do cancel if no data was read yet 92 | # Otherwise, carry on reading and finish 93 | if self.cancelled.is_set() and bytes_left == size: 94 | raise ReadCancelledError() 95 | 96 | try: 97 | partial = self._socket.recv(bytes_left) 98 | if len(partial) == 0: 99 | self.connected = False 100 | raise ConnectionResetError( 101 | 'The server has closed the connection.') 102 | 103 | buffer.write(partial) 104 | bytes_left -= len(partial) 105 | 106 | except BlockingIOError as error: 107 | # No data available yet, sleep a bit 108 | time.sleep(self.delay) 109 | 110 | # Check if the timeout finished 111 | if timeout is not None: 112 | time_passed = datetime.now() - start_time 113 | if time_passed > timeout: 114 | raise TimeoutError( 115 | 'The read operation exceeded the timeout.') from error 116 | 117 | # If everything went fine, return the read bytes 118 | buffer.flush() 119 | return buffer.raw.getvalue() 120 | 121 | def cancel_read(self): 122 | """Cancels the read operation IF it hasn't yet 123 | started, raising a ReadCancelledError""" 124 | self.cancelled.set() 125 | -------------------------------------------------------------------------------- /telethon/extensions/threaded_tcp_client.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import time 3 | from datetime import datetime, timedelta 4 | from io import BytesIO, BufferedWriter 5 | from threading import Event, Lock, Thread, Condition 6 | 7 | from ..errors import ReadCancelledError 8 | 9 | 10 | class ThreadedTcpClient: 11 | """The main difference with the TcpClient class is that this one 12 | will spawn a secondary thread that will be constantly reading 13 | from the network and putting everything on another buffer. 14 | """ 15 | def __init__(self, proxy=None): 16 | self.connected = False 17 | self._proxy = proxy 18 | self._recreate_socket() 19 | 20 | # Support for multi-threading advantages and safety 21 | self.cancelled = Event() # Has the read operation been cancelled? 22 | self.delay = 0.1 # Read delay when there was no data available 23 | self._lock = Lock() 24 | 25 | self._buffer = [] 26 | self._read_thread = Thread(target=self._reading_thread, daemon=True) 27 | self._cv = Condition() # Condition Variable 28 | 29 | def _recreate_socket(self): 30 | if self._proxy is None: 31 | self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 32 | else: 33 | import socks 34 | self._socket = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM) 35 | if type(self._proxy) is dict: 36 | self._socket.set_proxy(**self._proxy) 37 | else: # tuple, list, etc. 38 | self._socket.set_proxy(*self._proxy) 39 | 40 | def connect(self, ip, port, timeout): 41 | """Connects to the specified IP and port number. 42 | 'timeout' must be given in seconds 43 | """ 44 | if not self.connected: 45 | self._socket.settimeout(timeout) 46 | self._socket.connect((ip, port)) 47 | self._socket.setblocking(False) 48 | self.connected = True 49 | 50 | def close(self): 51 | """Closes the connection""" 52 | if self.connected: 53 | self._socket.shutdown(socket.SHUT_RDWR) 54 | self._socket.close() 55 | self.connected = False 56 | self._recreate_socket() 57 | 58 | def write(self, data): 59 | """Writes (sends) the specified bytes to the connected peer""" 60 | self._socket.sendall(data) 61 | 62 | def read(self, size, timeout=timedelta(seconds=5)): 63 | """Reads (receives) a whole block of 'size bytes 64 | from the connected peer. 65 | 66 | A timeout can be specified, which will cancel the operation if 67 | no data has been read in the specified time. If data was read 68 | and it's waiting for more, the timeout will NOT cancel the 69 | operation. Set to None for no timeout 70 | """ 71 | with self._cv: 72 | print('wait for...') 73 | self._cv.wait_for(lambda: len(self._buffer) >= size, timeout=timeout.seconds) 74 | print('got', size) 75 | result, self._buffer = self._buffer[:size], self._buffer[size:] 76 | return result 77 | 78 | def _reading_thread(self): 79 | while True: 80 | partial = self._socket.recv(4096) 81 | if len(partial) == 0: 82 | self.connected = False 83 | raise ConnectionResetError( 84 | 'The server has closed the connection.') 85 | 86 | with self._cv: 87 | print('extended', len(partial)) 88 | self._buffer.extend(partial) 89 | self._cv.notify() 90 | 91 | def cancel_read(self): 92 | """Cancels the read operation IF it hasn't yet 93 | started, raising a ReadCancelledError""" 94 | self.cancelled.set() 95 | -------------------------------------------------------------------------------- /telethon/helpers.py: -------------------------------------------------------------------------------- 1 | """Various helpers not related to the Telegram API itself""" 2 | from hashlib import sha1, sha256 3 | import os 4 | 5 | # region Multiple utilities 6 | 7 | 8 | def generate_random_long(signed=True): 9 | """Generates a random long integer (8 bytes), which is optionally signed""" 10 | return int.from_bytes(os.urandom(8), signed=signed, byteorder='little') 11 | 12 | 13 | def ensure_parent_dir_exists(file_path): 14 | """Ensures that the parent directory exists""" 15 | parent = os.path.dirname(file_path) 16 | if parent: 17 | os.makedirs(parent, exist_ok=True) 18 | 19 | # endregion 20 | 21 | # region Cryptographic related utils 22 | 23 | 24 | def calc_key(shared_key, msg_key, client): 25 | """Calculate the key based on Telegram guidelines, specifying whether it's the client or not""" 26 | x = 0 if client else 8 27 | 28 | sha1a = sha1(msg_key + shared_key[x:x + 32]).digest() 29 | sha1b = sha1(shared_key[x + 32:x + 48] + msg_key + 30 | shared_key[x + 48:x + 64]).digest() 31 | 32 | sha1c = sha1(shared_key[x + 64:x + 96] + msg_key).digest() 33 | sha1d = sha1(msg_key + shared_key[x + 96:x + 128]).digest() 34 | 35 | key = sha1a[0:8] + sha1b[8:20] + sha1c[4:16] 36 | iv = sha1a[8:20] + sha1b[0:8] + sha1c[16:20] + sha1d[0:8] 37 | 38 | return key, iv 39 | 40 | 41 | def calc_msg_key(data): 42 | """Calculates the message key from the given data""" 43 | return sha1(data).digest()[4:20] 44 | 45 | 46 | def generate_key_data_from_nonce(server_nonce, new_nonce): 47 | """Generates the key data corresponding to the given nonce""" 48 | hash1 = sha1(bytes(new_nonce + server_nonce)).digest() 49 | hash2 = sha1(bytes(server_nonce + new_nonce)).digest() 50 | hash3 = sha1(bytes(new_nonce + new_nonce)).digest() 51 | 52 | key = hash1 + hash2[:12] 53 | iv = hash2[12:20] + hash3 + new_nonce[:4] 54 | return key, iv 55 | 56 | 57 | def get_password_hash(pw, current_salt): 58 | """Gets the password hash for the two-step verification. 59 | current_salt should be the byte array provided by invoking GetPasswordRequest()""" 60 | 61 | # Passwords are encoded as UTF-8 62 | # At https://github.com/DrKLO/Telegram/blob/e31388 63 | # src/main/java/org/telegram/ui/LoginActivity.java#L2003 64 | data = pw.encode('utf-8') 65 | 66 | pw_hash = current_salt + data + current_salt 67 | return sha256(pw_hash).digest() 68 | 69 | # endregion 70 | -------------------------------------------------------------------------------- /telethon/network/__init__.py: -------------------------------------------------------------------------------- 1 | from .mtproto_plain_sender import MtProtoPlainSender 2 | from .authenticator import do_authentication 3 | from .mtproto_sender import MtProtoSender 4 | from .tcp_transport import TcpTransport 5 | -------------------------------------------------------------------------------- /telethon/network/authenticator.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from hashlib import sha1 4 | 5 | from .. import helpers as utils 6 | from ..crypto import AES, RSA, AuthKey, Factorization 7 | from ..network import MtProtoPlainSender 8 | from ..extensions import BinaryReader, BinaryWriter 9 | 10 | 11 | def do_authentication(transport): 12 | """Executes the authentication process with the Telegram servers. 13 | If no error is raised, returns both the authorization key and the 14 | time offset. 15 | """ 16 | sender = MtProtoPlainSender(transport) 17 | sender.connect() 18 | 19 | # Step 1 sending: PQ Request 20 | nonce = os.urandom(16) 21 | with BinaryWriter() as writer: 22 | writer.write_int(0x60469778, signed=False) # Constructor number 23 | writer.write(nonce) 24 | sender.send(writer.get_bytes()) 25 | 26 | # Step 1 response: PQ Request 27 | pq, pq_bytes, server_nonce, fingerprints = None, None, None, [] 28 | with BinaryReader(sender.receive()) as reader: 29 | response_code = reader.read_int(signed=False) 30 | if response_code != 0x05162463: 31 | raise AssertionError('Invalid response code: {}'.format( 32 | hex(response_code))) 33 | 34 | nonce_from_server = reader.read(16) 35 | if nonce_from_server != nonce: 36 | raise AssertionError('Invalid nonce from server') 37 | 38 | server_nonce = reader.read(16) 39 | 40 | pq_bytes = reader.tgread_bytes() 41 | pq = get_int(pq_bytes) 42 | 43 | vector_id = reader.read_int() 44 | if vector_id != 0x1cb5c415: 45 | raise AssertionError('Invalid vector constructor ID: {}'.format( 46 | hex(response_code))) 47 | 48 | fingerprints = [] 49 | fingerprint_count = reader.read_int() 50 | for _ in range(fingerprint_count): 51 | fingerprints.append(reader.read(8)) 52 | 53 | # Step 2 sending: DH Exchange 54 | new_nonce = os.urandom(32) 55 | p, q = Factorization.factorize(pq) 56 | with BinaryWriter() as pq_inner_data_writer: 57 | pq_inner_data_writer.write_int( 58 | 0x83c95aec, signed=False) # PQ Inner Data 59 | pq_inner_data_writer.tgwrite_bytes(get_byte_array(pq, signed=False)) 60 | pq_inner_data_writer.tgwrite_bytes( 61 | get_byte_array( 62 | min(p, q), signed=False)) 63 | pq_inner_data_writer.tgwrite_bytes( 64 | get_byte_array( 65 | max(p, q), signed=False)) 66 | pq_inner_data_writer.write(nonce) 67 | pq_inner_data_writer.write(server_nonce) 68 | pq_inner_data_writer.write(new_nonce) 69 | 70 | cipher_text, target_fingerprint = None, None 71 | for fingerprint in fingerprints: 72 | cipher_text = RSA.encrypt( 73 | get_fingerprint_text(fingerprint), 74 | pq_inner_data_writer.get_bytes()) 75 | 76 | if cipher_text is not None: 77 | target_fingerprint = fingerprint 78 | break 79 | 80 | if cipher_text is None: 81 | raise AssertionError( 82 | 'Could not find a valid key for fingerprints: {}' 83 | .format(', '.join([get_fingerprint_text(f) 84 | for f in fingerprints]))) 85 | 86 | with BinaryWriter() as req_dh_params_writer: 87 | req_dh_params_writer.write_int( 88 | 0xd712e4be, signed=False) # Req DH Params 89 | req_dh_params_writer.write(nonce) 90 | req_dh_params_writer.write(server_nonce) 91 | req_dh_params_writer.tgwrite_bytes( 92 | get_byte_array( 93 | min(p, q), signed=False)) 94 | req_dh_params_writer.tgwrite_bytes( 95 | get_byte_array( 96 | max(p, q), signed=False)) 97 | req_dh_params_writer.write(target_fingerprint) 98 | req_dh_params_writer.tgwrite_bytes(cipher_text) 99 | 100 | req_dh_params_bytes = req_dh_params_writer.get_bytes() 101 | sender.send(req_dh_params_bytes) 102 | 103 | # Step 2 response: DH Exchange 104 | encrypted_answer = None 105 | with BinaryReader(sender.receive()) as reader: 106 | response_code = reader.read_int(signed=False) 107 | 108 | if response_code == 0x79cb045d: 109 | raise AssertionError('Server DH params fail: TODO') 110 | 111 | if response_code != 0xd0e8075c: 112 | raise AssertionError('Invalid response code: {}'.format( 113 | hex(response_code))) 114 | 115 | nonce_from_server = reader.read(16) 116 | if nonce_from_server != nonce: 117 | raise NotImplementedError('Invalid nonce from server') 118 | 119 | server_nonce_from_server = reader.read(16) 120 | if server_nonce_from_server != server_nonce: 121 | raise NotImplementedError('Invalid server nonce from server') 122 | 123 | encrypted_answer = reader.tgread_bytes() 124 | 125 | # Step 3 sending: Complete DH Exchange 126 | key, iv = utils.generate_key_data_from_nonce(server_nonce, new_nonce) 127 | plain_text_answer = AES.decrypt_ige(encrypted_answer, key, iv) 128 | 129 | g, dh_prime, ga, time_offset = None, None, None, None 130 | with BinaryReader(plain_text_answer) as dh_inner_data_reader: 131 | dh_inner_data_reader.read(20) # hash sum 132 | code = dh_inner_data_reader.read_int(signed=False) 133 | if code != 0xb5890dba: 134 | raise AssertionError('Invalid DH Inner Data code: {}'.format(code)) 135 | 136 | nonce_from_server1 = dh_inner_data_reader.read(16) 137 | if nonce_from_server1 != nonce: 138 | raise AssertionError('Invalid nonce in encrypted answer') 139 | 140 | server_nonce_from_server1 = dh_inner_data_reader.read(16) 141 | if server_nonce_from_server1 != server_nonce: 142 | raise AssertionError('Invalid server nonce in encrypted answer') 143 | 144 | g = dh_inner_data_reader.read_int() 145 | dh_prime = get_int(dh_inner_data_reader.tgread_bytes(), signed=False) 146 | ga = get_int(dh_inner_data_reader.tgread_bytes(), signed=False) 147 | 148 | server_time = dh_inner_data_reader.read_int() 149 | time_offset = server_time - int(time.time()) 150 | 151 | b = get_int(os.urandom(256), signed=False) 152 | gb = pow(g, b, dh_prime) 153 | gab = pow(ga, b, dh_prime) 154 | 155 | # Prepare client DH Inner Data 156 | with BinaryWriter() as client_dh_inner_data_writer: 157 | client_dh_inner_data_writer.write_int( 158 | 0x6643b654, signed=False) # Client DH Inner Data 159 | client_dh_inner_data_writer.write(nonce) 160 | client_dh_inner_data_writer.write(server_nonce) 161 | client_dh_inner_data_writer.write_long(0) # TODO retry_id 162 | client_dh_inner_data_writer.tgwrite_bytes( 163 | get_byte_array( 164 | gb, signed=False)) 165 | 166 | with BinaryWriter() as client_dh_inner_data_with_hash_writer: 167 | client_dh_inner_data_with_hash_writer.write( 168 | sha1(client_dh_inner_data_writer.get_bytes()).digest()) 169 | 170 | client_dh_inner_data_with_hash_writer.write( 171 | client_dh_inner_data_writer.get_bytes()) 172 | 173 | client_dh_inner_data_bytes = \ 174 | client_dh_inner_data_with_hash_writer.get_bytes() 175 | 176 | # Encryption 177 | client_dh_inner_data_encrypted_bytes = AES.encrypt_ige( 178 | client_dh_inner_data_bytes, key, iv) 179 | 180 | # Prepare Set client DH params 181 | with BinaryWriter() as set_client_dh_params_writer: 182 | set_client_dh_params_writer.write_int(0xf5045f1f, signed=False) 183 | set_client_dh_params_writer.write(nonce) 184 | set_client_dh_params_writer.write(server_nonce) 185 | set_client_dh_params_writer.tgwrite_bytes( 186 | client_dh_inner_data_encrypted_bytes) 187 | 188 | set_client_dh_params_bytes = set_client_dh_params_writer.get_bytes() 189 | sender.send(set_client_dh_params_bytes) 190 | 191 | # Step 3 response: Complete DH Exchange 192 | with BinaryReader(sender.receive()) as reader: 193 | # Everything read from the server, disconnect now 194 | sender.disconnect() 195 | 196 | code = reader.read_int(signed=False) 197 | if code == 0x3bcbf734: # DH Gen OK 198 | nonce_from_server = reader.read(16) 199 | if nonce_from_server != nonce: 200 | raise NotImplementedError('Invalid nonce from server') 201 | 202 | server_nonce_from_server = reader.read(16) 203 | if server_nonce_from_server != server_nonce: 204 | raise NotImplementedError('Invalid server nonce from server') 205 | 206 | new_nonce_hash1 = reader.read(16) 207 | auth_key = AuthKey(get_byte_array(gab, signed=False)) 208 | 209 | new_nonce_hash_calculated = auth_key.calc_new_nonce_hash(new_nonce, 210 | 1) 211 | if new_nonce_hash1 != new_nonce_hash_calculated: 212 | raise AssertionError('Invalid new nonce hash') 213 | 214 | return auth_key, time_offset 215 | 216 | elif code == 0x46dc1fb9: # DH Gen Retry 217 | raise NotImplementedError('dh_gen_retry') 218 | 219 | elif code == 0xa69dae02: # DH Gen Fail 220 | raise NotImplementedError('dh_gen_fail') 221 | 222 | else: 223 | raise AssertionError('DH Gen unknown: {}'.format(hex(code))) 224 | 225 | 226 | def get_fingerprint_text(fingerprint): 227 | """Gets a fingerprint text in 01-23-45-67-89-AB-CD-EF format (no hyphens)""" 228 | return ''.join(hex(b)[2:].rjust(2, '0').upper() for b in fingerprint) 229 | 230 | 231 | # The following methods operate in big endian (unlike most of Telegram API) because: 232 | # > "...pq is a representation of a natural number (in binary *big endian* format)..." 233 | # > "...current value of dh_prime equals (in *big-endian* byte order)..." 234 | # Reference: https://core.telegram.org/mtproto/auth_key 235 | def get_byte_array(integer, signed): 236 | """Gets the arbitrary-length byte array corresponding to the given integer""" 237 | bits = integer.bit_length() 238 | byte_length = (bits + 8 - 1) // 8 # 8 bits per byte 239 | return int.to_bytes( 240 | integer, length=byte_length, byteorder='big', signed=signed) 241 | 242 | 243 | def get_int(byte_array, signed=True): 244 | """Gets the specified integer from its byte array. This should be used by the authenticator, 245 | who requires the data to be in big endian""" 246 | return int.from_bytes(byte_array, byteorder='big', signed=signed) 247 | -------------------------------------------------------------------------------- /telethon/network/mtproto_plain_sender.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from ..extensions import BinaryReader, BinaryWriter 4 | 5 | 6 | class MtProtoPlainSender: 7 | """MTProto Mobile Protocol plain sender (https://core.telegram.org/mtproto/description#unencrypted-messages)""" 8 | 9 | def __init__(self, transport): 10 | self._sequence = 0 11 | self._time_offset = 0 12 | self._last_msg_id = 0 13 | self._transport = transport 14 | 15 | def connect(self): 16 | self._transport.connect() 17 | 18 | def disconnect(self): 19 | self._transport.close() 20 | 21 | def send(self, data): 22 | """Sends a plain packet (auth_key_id = 0) containing the given message body (data)""" 23 | with BinaryWriter() as writer: 24 | writer.write_long(0) 25 | writer.write_long(self._get_new_msg_id()) 26 | writer.write_int(len(data)) 27 | writer.write(data) 28 | 29 | packet = writer.get_bytes() 30 | self._transport.send(packet) 31 | 32 | def receive(self): 33 | """Receives a plain packet, returning the body of the response""" 34 | seq, body = self._transport.receive() 35 | with BinaryReader(body) as reader: 36 | reader.read_long() # auth_key_id 37 | reader.read_long() # msg_id 38 | message_length = reader.read_int() 39 | 40 | response = reader.read(message_length) 41 | return response 42 | 43 | def _get_new_msg_id(self): 44 | """Generates a new message ID based on the current time since epoch""" 45 | # See core.telegram.org/mtproto/description#message-identifier-msg-id 46 | now = time.time() 47 | nanoseconds = int((now - int(now)) * 1e+9) 48 | # "message identifiers are divisible by 4" 49 | new_msg_id = (int(now) << 32) | (nanoseconds << 2) 50 | if self._last_msg_id >= new_msg_id: 51 | new_msg_id = self._last_msg_id + 4 52 | 53 | self._last_msg_id = new_msg_id 54 | return new_msg_id 55 | -------------------------------------------------------------------------------- /telethon/network/mtproto_sender.py: -------------------------------------------------------------------------------- 1 | import gzip 2 | from datetime import timedelta 3 | from threading import RLock 4 | 5 | from .. import helpers as utils 6 | from ..crypto import AES 7 | from ..errors import BadMessageError, InvalidDCError, rpc_message_to_error 8 | from ..tl.all_tlobjects import tlobjects 9 | from ..tl.types import MsgsAck 10 | from ..extensions import BinaryReader, BinaryWriter 11 | 12 | import logging 13 | logging.getLogger(__name__).addHandler(logging.NullHandler()) 14 | 15 | 16 | class MtProtoSender: 17 | """MTProto Mobile Protocol sender (https://core.telegram.org/mtproto/description)""" 18 | 19 | def __init__(self, transport, session): 20 | self.transport = transport 21 | self.session = session 22 | self._logger = logging.getLogger(__name__) 23 | 24 | self._need_confirmation = [] # Message IDs that need confirmation 25 | self._pending_receive = [] # Requests sent waiting to be received 26 | 27 | # Store an RLock instance to make this class safely multi-threaded 28 | self._lock = RLock() 29 | 30 | # Used when logging out, the only request that seems to use 'ack' requests 31 | # TODO There might be a better way to handle msgs_ack requests 32 | self.logging_out = False 33 | 34 | def connect(self): 35 | """Connects to the server""" 36 | self.transport.connect() 37 | 38 | def is_connected(self): 39 | return self.transport.is_connected() 40 | 41 | def disconnect(self): 42 | """Disconnects from the server""" 43 | self.transport.close() 44 | 45 | # region Send and receive 46 | 47 | def send(self, request): 48 | """Sends the specified MTProtoRequest, previously sending any message 49 | which needed confirmation.""" 50 | 51 | # Now only us can be using this method 52 | with self._lock: 53 | self._logger.debug('send() acquired the lock') 54 | 55 | # If any message needs confirmation send an AckRequest first 56 | self._send_acknowledges() 57 | 58 | # Finally send our packed request 59 | with BinaryWriter() as writer: 60 | request.on_send(writer) 61 | self._send_packet(writer.get_bytes(), request) 62 | self._pending_receive.append(request) 63 | 64 | # And update the saved session 65 | self.session.save() 66 | 67 | self._logger.debug('send() released the lock') 68 | 69 | def _send_acknowledges(self): 70 | """Sends a messages acknowledge for all those who _need_confirmation""" 71 | if self._need_confirmation: 72 | msgs_ack = MsgsAck(self._need_confirmation) 73 | with BinaryWriter() as writer: 74 | msgs_ack.on_send(writer) 75 | self._send_packet(writer.get_bytes(), msgs_ack) 76 | 77 | del self._need_confirmation[:] 78 | 79 | def receive(self, request=None, updates=None, **kwargs): 80 | """Receives the specified MTProtoRequest ("fills in it" 81 | the received data). This also restores the updates thread. 82 | 83 | An optional named parameter 'timeout' can be specified if 84 | one desires to override 'self.transport.timeout'. 85 | 86 | If 'request' is None, a single item will be read into 87 | the 'updates' list (which cannot be None). 88 | 89 | If 'request' is not None, any update received before 90 | reading the request's result will be put there unless 91 | it's None, in which case updates will be ignored. 92 | """ 93 | if request is None and updates is None: 94 | raise ValueError('Both the "request" and "updates"' 95 | 'parameters cannot be None at the same time.') 96 | 97 | with self._lock: 98 | self._logger.debug('receive() acquired the lock') 99 | # Don't stop trying to receive until we get the request we wanted 100 | # or, if there is no request, until we read an update 101 | while (request and not request.confirm_received) or \ 102 | (not request and not updates): 103 | self._logger.debug('Trying to .receive() the request result...') 104 | seq, body = self.transport.receive(**kwargs) 105 | message, remote_msg_id, remote_seq = self._decode_msg(body) 106 | 107 | with BinaryReader(message) as reader: 108 | self._process_msg( 109 | remote_msg_id, remote_seq, reader, updates) 110 | 111 | # We're done receiving, remove the request from pending, if any 112 | if request: 113 | try: 114 | self._pending_receive.remove(request) 115 | except ValueError: pass 116 | 117 | self._logger.debug('Request result received') 118 | self._logger.debug('receive() released the lock') 119 | 120 | def receive_updates(self, **kwargs): 121 | """Wrapper for .receive(request=None, updates=[])""" 122 | updates = [] 123 | self.receive(updates=updates, **kwargs) 124 | return updates 125 | 126 | def cancel_receive(self): 127 | """Cancels any pending receive operation 128 | by raising a ReadCancelledError""" 129 | self.transport.cancel_receive() 130 | 131 | # endregion 132 | 133 | # region Low level processing 134 | 135 | def _send_packet(self, packet, request): 136 | """Sends the given packet bytes with the additional 137 | information of the original request. This does NOT lock the threads!""" 138 | request.request_msg_id = self.session.get_new_msg_id() 139 | 140 | # First calculate plain_text to encrypt it 141 | with BinaryWriter() as plain_writer: 142 | plain_writer.write_long(self.session.salt, signed=False) 143 | plain_writer.write_long(self.session.id, signed=False) 144 | plain_writer.write_long(request.request_msg_id) 145 | plain_writer.write_int( 146 | self.session.generate_sequence(request.content_related)) 147 | 148 | plain_writer.write_int(len(packet)) 149 | plain_writer.write(packet) 150 | 151 | msg_key = utils.calc_msg_key(plain_writer.get_bytes()) 152 | 153 | key, iv = utils.calc_key(self.session.auth_key.key, msg_key, True) 154 | cipher_text = AES.encrypt_ige(plain_writer.get_bytes(), key, iv) 155 | 156 | # And then finally send the encrypted packet 157 | with BinaryWriter() as cipher_writer: 158 | cipher_writer.write_long( 159 | self.session.auth_key.key_id, signed=False) 160 | cipher_writer.write(msg_key) 161 | cipher_writer.write(cipher_text) 162 | self.transport.send(cipher_writer.get_bytes()) 163 | 164 | def _decode_msg(self, body): 165 | """Decodes an received encrypted message body bytes""" 166 | message = None 167 | remote_msg_id = None 168 | remote_sequence = None 169 | 170 | with BinaryReader(body) as reader: 171 | if len(body) < 8: 172 | raise BufferError("Can't decode packet ({})".format(body)) 173 | 174 | # TODO Check for both auth key ID and msg_key correctness 175 | reader.read_long() # remote_auth_key_id 176 | msg_key = reader.read(16) 177 | 178 | key, iv = utils.calc_key(self.session.auth_key.key, msg_key, False) 179 | plain_text = AES.decrypt_ige( 180 | reader.read(len(body) - reader.tell_position()), key, iv) 181 | 182 | with BinaryReader(plain_text) as plain_text_reader: 183 | plain_text_reader.read_long() # remote_salt 184 | plain_text_reader.read_long() # remote_session_id 185 | remote_msg_id = plain_text_reader.read_long() 186 | remote_sequence = plain_text_reader.read_int() 187 | msg_len = plain_text_reader.read_int() 188 | message = plain_text_reader.read(msg_len) 189 | 190 | return message, remote_msg_id, remote_sequence 191 | 192 | def _process_msg(self, msg_id, sequence, reader, updates): 193 | """Processes and handles a Telegram message. 194 | 195 | Returns True if the message was handled correctly and doesn't 196 | need to be skipped. Returns False otherwise. 197 | """ 198 | 199 | # TODO Check salt, session_id and sequence_number 200 | self._need_confirmation.append(msg_id) 201 | 202 | code = reader.read_int(signed=False) 203 | reader.seek(-4) 204 | 205 | # The following codes are "parsed manually" 206 | if code == 0xf35c6d01: # rpc_result, (response of an RPC call, i.e., we sent a request) 207 | return self._handle_rpc_result(msg_id, sequence, reader) 208 | 209 | if code == 0x347773c5: # pong 210 | return self._handle_pong(msg_id, sequence, reader) 211 | 212 | if code == 0x73f1f8dc: # msg_container 213 | return self._handle_container(msg_id, sequence, reader, updates) 214 | 215 | if code == 0x3072cfa1: # gzip_packed 216 | return self._handle_gzip_packed(msg_id, sequence, reader, updates) 217 | 218 | if code == 0xedab447b: # bad_server_salt 219 | return self._handle_bad_server_salt(msg_id, sequence, reader) 220 | 221 | if code == 0xa7eff811: # bad_msg_notification 222 | return self._handle_bad_msg_notification(msg_id, sequence, reader) 223 | 224 | # msgs_ack, it may handle the request we wanted 225 | if code == 0x62d6b459: 226 | ack = reader.tgread_object() 227 | for r in self._pending_receive: 228 | if r.request_msg_id in ack.msg_ids: 229 | self._logger.debug('Ack found for the a request') 230 | 231 | if self.logging_out: 232 | self._logger.debug('Message ack confirmed a request') 233 | r.confirm_received = True 234 | 235 | return True 236 | 237 | # If the code is not parsed manually, then it was parsed by the code generator! 238 | # In this case, we will simply treat the incoming TLObject as an Update, 239 | # if we can first find a matching TLObject 240 | if code in tlobjects: 241 | result = reader.tgread_object() 242 | if updates is None: 243 | self._logger.debug('Ignored update for %s', repr(result)) 244 | else: 245 | self._logger.debug('Read update for %s', repr(result)) 246 | updates.append(result) 247 | 248 | return True 249 | 250 | self._logger.debug('Unknown message: {}'.format(hex(code))) 251 | return False 252 | 253 | # endregion 254 | 255 | # region Message handling 256 | 257 | def _handle_pong(self, msg_id, sequence, reader): 258 | self._logger.debug('Handling pong') 259 | reader.read_int(signed=False) # code 260 | received_msg_id = reader.read_long() 261 | 262 | try: 263 | request = next(r for r in self._pending_receive 264 | if r.request_msg_id == received_msg_id) 265 | 266 | self._logger.debug('Pong confirmed a request') 267 | request.confirm_received = True 268 | except StopIteration: pass 269 | 270 | return True 271 | 272 | def _handle_container(self, msg_id, sequence, reader, updates): 273 | self._logger.debug('Handling container') 274 | reader.read_int(signed=False) # code 275 | size = reader.read_int() 276 | for _ in range(size): 277 | inner_msg_id = reader.read_long() 278 | reader.read_int() # inner_sequence 279 | inner_length = reader.read_int() 280 | begin_position = reader.tell_position() 281 | 282 | # Note that this code is IMPORTANT for skipping RPC results of 283 | # lost requests (i.e., ones from the previous connection session) 284 | try: 285 | if not self._process_msg( 286 | inner_msg_id, sequence, reader, updates): 287 | reader.set_position(begin_position + inner_length) 288 | except: 289 | # If any error is raised, something went wrong; skip the packet 290 | reader.set_position(begin_position + inner_length) 291 | raise 292 | 293 | return True 294 | 295 | def _handle_bad_server_salt(self, msg_id, sequence, reader): 296 | self._logger.debug('Handling bad server salt') 297 | reader.read_int(signed=False) # code 298 | bad_msg_id = reader.read_long() 299 | reader.read_int() # bad_msg_seq_no 300 | reader.read_int() # error_code 301 | new_salt = reader.read_long(signed=False) 302 | self.session.salt = new_salt 303 | 304 | try: 305 | request = next(r for r in self._pending_receive 306 | if r.request_msg_id == bad_msg_id) 307 | 308 | self.send(request) 309 | except StopIteration: pass 310 | 311 | return True 312 | 313 | def _handle_bad_msg_notification(self, msg_id, sequence, reader): 314 | self._logger.debug('Handling bad message notification') 315 | reader.read_int(signed=False) # code 316 | reader.read_long() # request_id 317 | reader.read_int() # request_sequence 318 | 319 | error_code = reader.read_int() 320 | error = BadMessageError(error_code) 321 | if error_code in (16, 17): 322 | # sent msg_id too low or too high (respectively). 323 | # Use the current msg_id to determine the right time offset. 324 | self.session.update_time_offset(correct_msg_id=msg_id) 325 | self.session.save() 326 | self._logger.debug('Read Bad Message error: ' + str(error)) 327 | self._logger.debug('Attempting to use the correct time offset.') 328 | return True 329 | else: 330 | raise error 331 | 332 | def _handle_rpc_result(self, msg_id, sequence, reader): 333 | self._logger.debug('Handling RPC result') 334 | reader.read_int(signed=False) # code 335 | request_id = reader.read_long() 336 | inner_code = reader.read_int(signed=False) 337 | 338 | try: 339 | request = next(r for r in self._pending_receive 340 | if r.request_msg_id == request_id) 341 | 342 | request.confirm_received = True 343 | except StopIteration: 344 | request = None 345 | 346 | if inner_code == 0x2144ca19: # RPC Error 347 | error = rpc_message_to_error( 348 | reader.read_int(), reader.tgread_string()) 349 | 350 | # Acknowledge that we received the error 351 | self._need_confirmation.append(request_id) 352 | self._send_acknowledges() 353 | 354 | self._logger.debug('Read RPC error: %s', str(error)) 355 | if isinstance(error, InvalidDCError): 356 | # Must resend this request, if any 357 | if request: 358 | request.confirm_received = False 359 | 360 | raise error 361 | else: 362 | if request: 363 | self._logger.debug('Reading request response') 364 | if inner_code == 0x3072cfa1: # GZip packed 365 | unpacked_data = gzip.decompress(reader.tgread_bytes()) 366 | with BinaryReader(unpacked_data) as compressed_reader: 367 | request.on_response(compressed_reader) 368 | else: 369 | reader.seek(-4) 370 | request.on_response(reader) 371 | 372 | return True 373 | else: 374 | # If it's really a result for RPC from previous connection 375 | # session, it will be skipped by the handle_container() 376 | self._logger.debug('Lost request will be skipped.') 377 | return False 378 | 379 | def _handle_gzip_packed(self, msg_id, sequence, reader, updates): 380 | self._logger.debug('Handling gzip packed data') 381 | reader.read_int(signed=False) # code 382 | packed_data = reader.tgread_bytes() 383 | unpacked_data = gzip.decompress(packed_data) 384 | 385 | with BinaryReader(unpacked_data) as compressed_reader: 386 | return self._process_msg( 387 | msg_id, sequence, compressed_reader, updates) 388 | 389 | # endregion 390 | -------------------------------------------------------------------------------- /telethon/network/tcp_transport.py: -------------------------------------------------------------------------------- 1 | from zlib import crc32 2 | from datetime import timedelta 3 | 4 | from ..errors import InvalidChecksumError 5 | from ..extensions import TcpClient 6 | from ..extensions import BinaryWriter 7 | 8 | 9 | class TcpTransport: 10 | def __init__(self, ip_address, port, 11 | proxy=None, timeout=timedelta(seconds=5)): 12 | self.ip = ip_address 13 | self.port = port 14 | self.tcp_client = TcpClient(proxy) 15 | self.timeout = timeout 16 | self.send_counter = 0 17 | 18 | def connect(self): 19 | """Connects to the specified IP address and port""" 20 | self.send_counter = 0 21 | self.tcp_client.connect(self.ip, self.port, 22 | timeout=round(self.timeout.seconds)) 23 | 24 | def is_connected(self): 25 | return self.tcp_client.connected 26 | 27 | # Original reference: https://core.telegram.org/mtproto#tcp-transport 28 | # The packets are encoded as: 29 | # total length, sequence number, packet and checksum (CRC32) 30 | def send(self, packet): 31 | """Sends the given packet (bytes array) to the connected peer""" 32 | if not self.tcp_client.connected: 33 | raise ConnectionError('Client not connected to server.') 34 | 35 | with BinaryWriter() as writer: 36 | writer.write_int(len(packet) + 12) # 12 = size_of (integer) * 3 37 | writer.write_int(self.send_counter) 38 | writer.write(packet) 39 | 40 | crc = crc32(writer.get_bytes()) 41 | writer.write_int(crc, signed=False) 42 | 43 | self.send_counter += 1 44 | self.tcp_client.write(writer.get_bytes()) 45 | 46 | def receive(self, **kwargs): 47 | """Receives a TCP message (tuple(sequence number, body)) from the 48 | connected peer. 49 | 50 | If a named 'timeout' parameter is present, it will override 51 | 'self.timeout', and this can be a 'timedelta' or 'None'. 52 | """ 53 | timeout = kwargs.get('timeout', self.timeout) 54 | 55 | # First read everything we need 56 | packet_length_bytes = self.tcp_client.read(4, timeout) 57 | packet_length = int.from_bytes(packet_length_bytes, byteorder='little') 58 | 59 | seq_bytes = self.tcp_client.read(4, timeout) 60 | seq = int.from_bytes(seq_bytes, byteorder='little') 61 | 62 | body = self.tcp_client.read(packet_length - 12, timeout) 63 | 64 | checksum = int.from_bytes( 65 | self.tcp_client.read(4, timeout), byteorder='little', signed=False) 66 | 67 | # Then perform the checks 68 | rv = packet_length_bytes + seq_bytes + body 69 | valid_checksum = crc32(rv) 70 | 71 | if checksum != valid_checksum: 72 | raise InvalidChecksumError(checksum, valid_checksum) 73 | 74 | # If we passed the tests, we can then return a valid TCP message 75 | return seq, body 76 | 77 | def close(self): 78 | self.tcp_client.close() 79 | 80 | def cancel_receive(self): 81 | """Cancels (stops) trying to receive from the 82 | remote peer and raises a ReadCancelledError""" 83 | self.tcp_client.cancel_read() 84 | 85 | def get_client_delay(self): 86 | """Gets the client read delay""" 87 | return self.tcp_client.delay 88 | -------------------------------------------------------------------------------- /telethon/telegram_bare_client.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import timedelta 3 | from hashlib import md5 4 | from os import path 5 | 6 | # Import some externalized utilities to work with the Telegram types and more 7 | from . import helpers as utils 8 | from .errors import ( 9 | RPCError, FloodWaitError, FileMigrateError, TypeNotFoundError 10 | ) 11 | from .network import authenticator, MtProtoSender, TcpTransport 12 | from .utils import get_appropriated_part_size 13 | 14 | # For sending and receiving requests 15 | from .tl import TLObject, JsonSession 16 | from .tl.all_tlobjects import layer 17 | from .tl.functions import (InitConnectionRequest, InvokeWithLayerRequest) 18 | 19 | # Initial request 20 | from .tl.functions.help import GetConfigRequest 21 | from .tl.functions.auth import ( 22 | ImportAuthorizationRequest, ExportAuthorizationRequest 23 | ) 24 | 25 | # Easier access for working with media 26 | from .tl.functions.upload import ( 27 | GetFileRequest, SaveBigFilePartRequest, SaveFilePartRequest 28 | ) 29 | 30 | # All the types we need to work with 31 | from .tl.types import InputFile, InputFileBig 32 | 33 | 34 | class TelegramBareClient: 35 | """Bare Telegram Client with just the minimum - 36 | 37 | The reason to distinguish between a MtProtoSender and a 38 | TelegramClient itself is because the sender is just that, 39 | a sender, which should know nothing about Telegram but 40 | rather how to handle this specific connection. 41 | 42 | The TelegramClient itself should know how to initialize 43 | a proper connection to the servers, as well as other basic 44 | methods such as disconnection and reconnection. 45 | 46 | This distinction between a bare client and a full client 47 | makes it possible to create clones of the bare version 48 | (by using the same session, IP address and port) to be 49 | able to execute queries on either, without the additional 50 | cost that would involve having the methods for signing in, 51 | logging out, and such. 52 | """ 53 | 54 | # Current TelegramClient version 55 | __version__ = '0.11.5' 56 | 57 | # region Initialization 58 | 59 | def __init__(self, session, api_id, api_hash, 60 | proxy=None, timeout=timedelta(seconds=5)): 61 | """Initializes the Telegram client with the specified API ID and Hash. 62 | Session must always be a Session instance, and an optional proxy 63 | can also be specified to be used on the connection. 64 | """ 65 | self.session = session 66 | self.api_id = int(api_id) 67 | self.api_hash = api_hash 68 | self.proxy = proxy 69 | self._timeout = timeout 70 | self._logger = logging.getLogger(__name__) 71 | 72 | # Cache "exported" senders 'dc_id: TelegramBareClient' and 73 | # their corresponding sessions not to recreate them all 74 | # the time since it's a (somewhat expensive) process. 75 | self._cached_clients = {} 76 | 77 | # These will be set later 78 | self.dc_options = None 79 | self._sender = None 80 | 81 | # endregion 82 | 83 | # region Connecting 84 | 85 | def connect(self, exported_auth=None): 86 | """Connects to the Telegram servers, executing authentication if 87 | required. Note that authenticating to the Telegram servers is 88 | not the same as authenticating the desired user itself, which 89 | may require a call (or several) to 'sign_in' for the first time. 90 | 91 | If 'exported_auth' is not None, it will be used instead to 92 | determine the authorization key for the current session. 93 | """ 94 | if self._sender and self._sender.is_connected(): 95 | self._logger.debug( 96 | 'Attempted to connect when the client was already connected.' 97 | ) 98 | return 99 | 100 | transport = TcpTransport(self.session.server_address, 101 | self.session.port, 102 | proxy=self.proxy, 103 | timeout=self._timeout) 104 | 105 | try: 106 | if not self.session.auth_key: 107 | self.session.auth_key, self.session.time_offset = \ 108 | authenticator.do_authentication(transport) 109 | 110 | self.session.save() 111 | 112 | self._sender = MtProtoSender(transport, self.session) 113 | self._sender.connect() 114 | 115 | # Now it's time to send an InitConnectionRequest 116 | # This must always be invoked with the layer we'll be using 117 | if exported_auth is None: 118 | query = GetConfigRequest() 119 | else: 120 | query = ImportAuthorizationRequest( 121 | exported_auth.id, exported_auth.bytes) 122 | 123 | request = InitConnectionRequest( 124 | api_id=self.api_id, 125 | device_model=self.session.device_model, 126 | system_version=self.session.system_version, 127 | app_version=self.session.app_version, 128 | lang_code=self.session.lang_code, 129 | system_lang_code=self.session.system_lang_code, 130 | lang_pack='', # "langPacks are for official apps only" 131 | query=query) 132 | 133 | result = self(InvokeWithLayerRequest( 134 | layer=layer, query=request 135 | )) 136 | 137 | if exported_auth is not None: 138 | result = self(GetConfigRequest()) 139 | 140 | # We're only interested in the DC options, 141 | # although many other options are available! 142 | self.dc_options = result.dc_options 143 | return True 144 | 145 | except TypeNotFoundError as e: 146 | # This is fine, probably layer migration 147 | self._logger.debug('Found invalid item, probably migrating', e) 148 | self.disconnect() 149 | self.connect(exported_auth=exported_auth) 150 | 151 | except (RPCError, ConnectionError) as error: 152 | # Probably errors from the previous session, ignore them 153 | self.disconnect() 154 | self._logger.debug('Could not stabilise initial connection: {}' 155 | .format(error)) 156 | return False 157 | 158 | def disconnect(self): 159 | """Disconnects from the Telegram server""" 160 | if self._sender: 161 | self._sender.disconnect() 162 | self._sender = None 163 | 164 | def reconnect(self, new_dc=None): 165 | """Disconnects and connects again (effectively reconnecting). 166 | 167 | If 'new_dc' is not None, the current authorization key is 168 | removed, the DC used is switched, and a new connection is made. 169 | """ 170 | self.disconnect() 171 | 172 | if new_dc is not None: 173 | self.session.auth_key = None # Force creating new auth_key 174 | dc = self._get_dc(new_dc) 175 | self.session.server_address = dc.ip_address 176 | self.session.port = dc.port 177 | self.session.save() 178 | 179 | self.connect() 180 | 181 | # endregion 182 | 183 | # region Properties 184 | 185 | def set_timeout(self, timeout): 186 | if timeout is None: 187 | self._timeout = None 188 | elif isinstance(timeout, int) or isinstance(timeout, float): 189 | self._timeout = timedelta(seconds=timeout) 190 | elif isinstance(timeout, timedelta): 191 | self._timeout = timeout 192 | else: 193 | raise ValueError( 194 | '{} is not a valid type for a timeout'.format(type(timeout)) 195 | ) 196 | 197 | if self._sender: 198 | self._sender.transport.timeout = self._timeout 199 | 200 | def get_timeout(self): 201 | return self._timeout 202 | 203 | timeout = property(get_timeout, set_timeout) 204 | 205 | # endregion 206 | 207 | # region Working with different Data Centers 208 | 209 | def _get_dc(self, dc_id): 210 | """Gets the Data Center (DC) associated to 'dc_id'""" 211 | if not self.dc_options: 212 | raise ConnectionError( 213 | 'Cannot determine the required data center IP address. ' 214 | 'Stabilise a successful initial connection first.') 215 | 216 | return next(dc for dc in self.dc_options if dc.id == dc_id) 217 | 218 | def _get_exported_client(self, dc_id, 219 | init_connection=False, 220 | bypass_cache=False): 221 | """Gets a cached exported TelegramBareClient for the desired DC. 222 | 223 | If it's the first time retrieving the TelegramBareClient, the 224 | current authorization is exported to the new DC so that 225 | it can be used there, and the connection is initialized. 226 | 227 | If after using the sender a ConnectionResetError is raised, 228 | this method should be called again with init_connection=True 229 | in order to perform the reconnection. 230 | 231 | If bypass_cache is True, a new client will be exported and 232 | it will not be cached. 233 | """ 234 | # Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt 235 | # for clearly showing how to export the authorization! ^^ 236 | client = self._cached_clients.get(dc_id) 237 | if client and not bypass_cache: 238 | if init_connection: 239 | client.reconnect() 240 | return client 241 | else: 242 | dc = self._get_dc(dc_id) 243 | 244 | # Export the current authorization to the new DC. 245 | export_auth = self(ExportAuthorizationRequest(dc_id)) 246 | 247 | # Create a temporary session for this IP address, which needs 248 | # to be different because each auth_key is unique per DC. 249 | # 250 | # Construct this session with the connection parameters 251 | # (system version, device model...) from the current one. 252 | session = JsonSession(self.session) 253 | session.server_address = dc.ip_address 254 | session.port = dc.port 255 | client = TelegramBareClient( 256 | session, self.api_id, self.api_hash, 257 | timeout=self._timeout 258 | ) 259 | client.connect(exported_auth=export_auth) 260 | 261 | if not bypass_cache: 262 | # Don't go through this expensive process every time. 263 | self._cached_clients[dc_id] = client 264 | return client 265 | 266 | # endregion 267 | 268 | # region Invoking Telegram requests 269 | 270 | def invoke(self, request, updates=None): 271 | """Invokes (sends) a MTProtoRequest and returns (receives) its result. 272 | 273 | If 'updates' is not None, all read update object will be put 274 | in such list. Otherwise, update objects will be ignored. 275 | """ 276 | if not isinstance(request, TLObject) and not request.content_related: 277 | raise ValueError('You can only invoke requests, not types!') 278 | 279 | if not self._sender: 280 | raise ValueError('You must be connected to invoke requests!') 281 | 282 | try: 283 | self._sender.send(request) 284 | self._sender.receive(request, updates=updates) 285 | return request.result 286 | 287 | except ConnectionResetError: 288 | self._logger.debug('Server disconnected us. Reconnecting and ' 289 | 'resending request...') 290 | self.reconnect() 291 | return self.invoke(request) 292 | 293 | except FloodWaitError: 294 | self.disconnect() 295 | raise 296 | 297 | # Let people use client(SomeRequest()) instead client.invoke(...) 298 | __call__ = invoke 299 | 300 | # endregion 301 | 302 | # region Uploading media 303 | 304 | def upload_file(self, 305 | file_path, 306 | part_size_kb=None, 307 | file_name=None, 308 | progress_callback=None): 309 | """Uploads the specified file_path and returns a handle (an instance 310 | of InputFile or InputFileBig, as required) which can be later used. 311 | 312 | If 'progress_callback' is not None, it should be a function that 313 | takes two parameters, (bytes_uploaded, total_bytes). 314 | 315 | Default values for the optional parameters if left as None are: 316 | part_size_kb = get_appropriated_part_size(file_size) 317 | file_name = path.basename(file_path) 318 | """ 319 | file_size = path.getsize(file_path) 320 | if not part_size_kb: 321 | part_size_kb = get_appropriated_part_size(file_size) 322 | 323 | if part_size_kb > 512: 324 | raise ValueError('The part size must be less or equal to 512KB') 325 | 326 | part_size = int(part_size_kb * 1024) 327 | if part_size % 1024 != 0: 328 | raise ValueError('The part size must be evenly divisible by 1024') 329 | 330 | # Determine whether the file is too big (over 10MB) or not 331 | # Telegram does make a distinction between smaller or larger files 332 | is_large = file_size > 10 * 1024 * 1024 333 | part_count = (file_size + part_size - 1) // part_size 334 | 335 | file_id = utils.generate_random_long() 336 | hash_md5 = md5() 337 | 338 | with open(file_path, 'rb') as file: 339 | for part_index in range(part_count): 340 | # Read the file by in chunks of size part_size 341 | part = file.read(part_size) 342 | 343 | # The SavePartRequest is different depending on whether 344 | # the file is too large or not (over or less than 10MB) 345 | if is_large: 346 | request = SaveBigFilePartRequest(file_id, part_index, 347 | part_count, part) 348 | else: 349 | request = SaveFilePartRequest(file_id, part_index, part) 350 | 351 | result = self(request) 352 | if result: 353 | if not is_large: 354 | # No need to update the hash if it's a large file 355 | hash_md5.update(part) 356 | 357 | if progress_callback: 358 | progress_callback(file.tell(), file_size) 359 | else: 360 | raise ValueError('Failed to upload file part {}.' 361 | .format(part_index)) 362 | 363 | # Set a default file name if None was specified 364 | if not file_name: 365 | file_name = path.basename(file_path) 366 | 367 | if is_large: 368 | return InputFileBig(file_id, part_count, file_name) 369 | else: 370 | return InputFile(file_id, part_count, file_name, 371 | md5_checksum=hash_md5.hexdigest()) 372 | 373 | # endregion 374 | 375 | # region Downloading media 376 | 377 | def download_file(self, 378 | input_location, 379 | file, 380 | part_size_kb=None, 381 | file_size=None, 382 | progress_callback=None): 383 | """Downloads the given InputFileLocation to file (a stream or str). 384 | 385 | If 'progress_callback' is not None, it should be a function that 386 | takes two parameters, (bytes_downloaded, total_bytes). Note that 387 | 'total_bytes' simply equals 'file_size', and may be None. 388 | """ 389 | if not part_size_kb: 390 | if not file_size: 391 | part_size_kb = 64 # Reasonable default 392 | else: 393 | part_size_kb = get_appropriated_part_size(file_size) 394 | 395 | part_size = int(part_size_kb * 1024) 396 | if part_size % 1024 != 0: 397 | raise ValueError('The part size must be evenly divisible by 1024.') 398 | 399 | if isinstance(file, str): 400 | # Ensure that we'll be able to download the media 401 | utils.ensure_parent_dir_exists(file) 402 | f = open(file, 'wb') 403 | else: 404 | f = file 405 | 406 | # The used client will change if FileMigrateError occurs 407 | client = self 408 | 409 | try: 410 | offset_index = 0 411 | while True: 412 | offset = offset_index * part_size 413 | 414 | try: 415 | result = client( 416 | GetFileRequest(input_location, offset, part_size)) 417 | except FileMigrateError as e: 418 | client = self._get_exported_client(e.new_dc) 419 | continue 420 | 421 | offset_index += 1 422 | 423 | # If we have received no data (0 bytes), the file is over 424 | # So there is nothing left to download and write 425 | if not result.bytes: 426 | return result.type # Return some extra information 427 | 428 | f.write(result.bytes) 429 | if progress_callback: 430 | progress_callback(f.tell(), file_size) 431 | finally: 432 | if isinstance(file, str): 433 | f.close() 434 | 435 | # endregion 436 | -------------------------------------------------------------------------------- /telethon/tl/__init__.py: -------------------------------------------------------------------------------- 1 | from .tlobject import TLObject 2 | from .session import Session, JsonSession 3 | -------------------------------------------------------------------------------- /telethon/tl/session.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import pickle 4 | import platform 5 | import time 6 | from threading import Lock 7 | from base64 import b64encode, b64decode 8 | from os.path import isfile as file_exists 9 | 10 | from .. import helpers as utils 11 | 12 | 13 | class Session: 14 | def __init__(self, session_user_id): 15 | self.session_user_id = session_user_id 16 | self.server_address = '91.108.56.165' 17 | self.port = 443 18 | self.auth_key = None 19 | self.id = utils.generate_random_long(signed=False) 20 | self.sequence = 0 21 | self.salt = 0 # Unsigned long 22 | self.time_offset = 0 23 | self.last_message_id = 0 # Long 24 | # TODO Remove this now unused members, left so unpickling is happy 25 | self.user = None 26 | 27 | def save(self): 28 | """Saves the current session object as session_user_id.session""" 29 | if self.session_user_id: 30 | with open('{}.session'.format(self.session_user_id), 'wb') as file: 31 | pickle.dump(self, file) 32 | 33 | def delete(self): 34 | """Deletes the current session file""" 35 | try: 36 | os.remove('{}.session'.format(self.session_user_id)) 37 | return True 38 | except OSError: 39 | return False 40 | 41 | @staticmethod 42 | def try_load_or_create_new(session_user_id): 43 | """Loads a saved session_user_id session, or creates a new one if none existed before. 44 | If the given session_user_id is None, we assume that it is for testing purposes""" 45 | if session_user_id is None: 46 | return Session(None) 47 | else: 48 | path = '{}.session'.format(session_user_id) 49 | 50 | if file_exists(path): 51 | with open(path, 'rb') as file: 52 | return pickle.load(file) 53 | else: 54 | return Session(session_user_id) 55 | 56 | def generate_sequence(self, confirmed): 57 | """Ported from JsonSession.generate_sequence""" 58 | with Lock(): 59 | if confirmed: 60 | result = self.sequence * 2 + 1 61 | self.sequence += 1 62 | return result 63 | else: 64 | return self.sequence * 2 65 | 66 | def get_new_msg_id(self): 67 | now = time.time() 68 | nanoseconds = int((now - int(now)) * 1e+9) 69 | # "message identifiers are divisible by 4" 70 | new_msg_id = (int(now) << 32) | (nanoseconds << 2) 71 | 72 | if self.last_message_id >= new_msg_id: 73 | new_msg_id = self.last_message_id + 4 74 | 75 | self.last_message_id = new_msg_id 76 | return new_msg_id 77 | 78 | def update_time_offset(self, correct_msg_id): 79 | """Updates the time offset based on a known correct message ID""" 80 | now = int(time.time()) 81 | correct = correct_msg_id >> 32 82 | self.time_offset = correct - now 83 | 84 | 85 | # Until migration is complete, we need the original 'Session' class 86 | # for Pickle to keep working. TODO Replace 'Session' by 'JsonSession' by v1.0 87 | class JsonSession: 88 | """This session contains the required information to login into your 89 | Telegram account. NEVER give the saved JSON file to anyone, since 90 | they would gain instant access to all your messages and contacts. 91 | 92 | If you think the session has been compromised, close all the sessions 93 | through an official Telegram client to revoke the authorization. 94 | """ 95 | def __init__(self, session_user_id): 96 | """session_user_id should either be a string or another Session. 97 | Note that if another session is given, only parameters like 98 | those required to init a connection will be copied. 99 | """ 100 | # These values will NOT be saved 101 | if isinstance(session_user_id, JsonSession): 102 | self.session_user_id = None 103 | 104 | # For connection purposes 105 | session = session_user_id 106 | self.device_model = session.device_model 107 | self.system_version = session.system_version 108 | self.app_version = session.app_version 109 | self.lang_code = session.lang_code 110 | self.system_lang_code = session.system_lang_code 111 | self.lang_pack = session.lang_pack 112 | 113 | else: # str / None 114 | self.session_user_id = session_user_id 115 | 116 | system = platform.uname() 117 | self.device_model = system.system if system.system else 'Unknown' 118 | self.system_version = system.release if system.release else '1.0' 119 | self.app_version = '1.0' # '0' will provoke error 120 | self.lang_code = 'en' 121 | self.system_lang_code = self.lang_code 122 | self.lang_pack = '' 123 | 124 | # Cross-thread safety 125 | self._lock = Lock() 126 | 127 | # These values will be saved 128 | self.server_address = '91.108.56.165' 129 | self.port = 443 130 | self.auth_key = None 131 | self.id = utils.generate_random_long(signed=False) 132 | self._sequence = 0 133 | self.salt = 0 # Unsigned long 134 | self.time_offset = 0 135 | self._last_msg_id = 0 # Long 136 | 137 | def save(self): 138 | """Saves the current session object as session_user_id.session""" 139 | if self.session_user_id: 140 | with open('{}.session'.format(self.session_user_id), 'w') as file: 141 | json.dump({ 142 | 'id': self.id, 143 | 'port': self.port, 144 | 'salt': self.salt, 145 | 'sequence': self._sequence, 146 | 'time_offset': self.time_offset, 147 | 'server_address': self.server_address, 148 | 'auth_key_data': 149 | b64encode(self.auth_key.key).decode('ascii')\ 150 | if self.auth_key else None 151 | }, file) 152 | 153 | def delete(self): 154 | """Deletes the current session file""" 155 | try: 156 | os.remove('{}.session'.format(self.session_user_id)) 157 | return True 158 | except OSError: 159 | return False 160 | 161 | @staticmethod 162 | def list_sessions(): 163 | """Lists all the sessions of the users who have ever connected 164 | using this client and never logged out 165 | """ 166 | return [os.path.splitext(os.path.basename(f))[0] 167 | for f in os.listdir('.') if f.endswith('.session')] 168 | 169 | @staticmethod 170 | def try_load_or_create_new(session_user_id): 171 | """Loads a saved session_user_id.session or creates a new one. 172 | If session_user_id=None, later .save()'s will have no effect. 173 | """ 174 | if session_user_id is None: 175 | return JsonSession(None) 176 | else: 177 | path = '{}.session'.format(session_user_id) 178 | result = JsonSession(session_user_id) 179 | if not file_exists(path): 180 | return result 181 | 182 | try: 183 | with open(path, 'r') as file: 184 | data = json.load(file) 185 | result.id = data['id'] 186 | result.port = data['port'] 187 | result.salt = data['salt'] 188 | result._sequence = data['sequence'] 189 | result.time_offset = data['time_offset'] 190 | result.server_address = data['server_address'] 191 | 192 | # FIXME We need to import the AuthKey here or otherwise 193 | # we get cyclic dependencies. 194 | from ..crypto import AuthKey 195 | if data['auth_key_data'] is not None: 196 | key = b64decode(data['auth_key_data']) 197 | result.auth_key = AuthKey(data=key) 198 | 199 | except (json.decoder.JSONDecodeError, UnicodeDecodeError): 200 | # TODO Backwards-compatibility code 201 | old = Session.try_load_or_create_new(session_user_id) 202 | result.id = old.id 203 | result.port = old.port 204 | result.salt = old.salt 205 | result._sequence = old.sequence 206 | result.time_offset = old.time_offset 207 | result.server_address = old.server_address 208 | result.auth_key = old.auth_key 209 | result.save() 210 | 211 | return result 212 | 213 | def generate_sequence(self, confirmed): 214 | """Thread safe method to generates the next sequence number, 215 | based on whether it was confirmed yet or not. 216 | 217 | Note that if confirmed=True, the sequence number 218 | will be increased by one too 219 | """ 220 | with self._lock: 221 | if confirmed: 222 | result = self._sequence * 2 + 1 223 | self._sequence += 1 224 | return result 225 | else: 226 | return self._sequence * 2 227 | 228 | def get_new_msg_id(self): 229 | """Generates a new unique message ID based on the current 230 | time (in ms) since epoch""" 231 | # Refer to mtproto_plain_sender.py for the original method 232 | now = time.time() 233 | nanoseconds = int((now - int(now)) * 1e+9) 234 | # "message identifiers are divisible by 4" 235 | new_msg_id = (int(now) << 32) | (nanoseconds << 2) 236 | 237 | if self._last_msg_id >= new_msg_id: 238 | new_msg_id = self._last_msg_id + 4 239 | 240 | self._last_msg_id = new_msg_id 241 | return new_msg_id 242 | 243 | def update_time_offset(self, correct_msg_id): 244 | """Updates the time offset based on a known correct message ID""" 245 | now = int(time.time()) 246 | correct = correct_msg_id >> 32 247 | self.time_offset = correct - now 248 | -------------------------------------------------------------------------------- /telethon/tl/tlobject.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | 4 | class TLObject: 5 | def __init__(self): 6 | self.sent = False 7 | 8 | self.request_msg_id = 0 # Long 9 | self.sequence = 0 10 | 11 | self.dirty = False 12 | self.send_time = None 13 | self.confirm_received = False 14 | 15 | # These should be overrode 16 | self.constructor_id = 0 17 | self.content_related = False # Only requests/functions/queries are 18 | self.responded = False 19 | 20 | # These should not be overrode 21 | def on_send_success(self): 22 | self.send_time = datetime.now() 23 | self.sent = True 24 | 25 | def on_confirm(self): 26 | self.confirm_received = True 27 | 28 | def need_resend(self): 29 | return self.dirty or ( 30 | self.content_related and not self.confirm_received and 31 | datetime.now() - self.send_time > timedelta(seconds=3)) 32 | 33 | @staticmethod 34 | def pretty_format(obj, indent=None): 35 | """Pretty formats the given object as a string which is returned. 36 | If indent is None, a single line will be returned. 37 | """ 38 | if indent is None: 39 | if isinstance(obj, TLObject): 40 | return '{{{}: {}}}'.format( 41 | type(obj).__name__, 42 | TLObject.pretty_format(obj.to_dict()) 43 | ) 44 | if isinstance(obj, dict): 45 | return '{{{}}}'.format(', '.join( 46 | '{}: {}'.format( 47 | k, TLObject.pretty_format(v) 48 | ) for k, v in obj.items() 49 | )) 50 | elif isinstance(obj, str): 51 | return '"{}"'.format(obj) 52 | elif hasattr(obj, '__iter__'): 53 | return '[{}]'.format( 54 | ', '.join(TLObject.pretty_format(x) for x in obj) 55 | ) 56 | else: 57 | return str(obj) 58 | else: 59 | result = [] 60 | if isinstance(obj, TLObject): 61 | result.append('{') 62 | result.append(type(obj).__name__) 63 | result.append(': ') 64 | result.append(TLObject.pretty_format( 65 | obj.to_dict(), indent 66 | )) 67 | 68 | elif isinstance(obj, dict): 69 | result.append('{\n') 70 | indent += 1 71 | for k, v in obj.items(): 72 | result.append('\t' * indent) 73 | result.append(k) 74 | result.append(': ') 75 | result.append(TLObject.pretty_format(v, indent)) 76 | result.append(',\n') 77 | indent -= 1 78 | result.append('\t' * indent) 79 | result.append('}') 80 | 81 | elif isinstance(obj, str): 82 | result.append('"') 83 | result.append(obj) 84 | result.append('"') 85 | 86 | elif hasattr(obj, '__iter__'): 87 | result.append('[\n') 88 | indent += 1 89 | for x in obj: 90 | result.append('\t' * indent) 91 | result.append(TLObject.pretty_format(x, indent)) 92 | result.append(',\n') 93 | indent -= 1 94 | result.append('\t' * indent) 95 | result.append(']') 96 | 97 | else: 98 | result.append(str(obj)) 99 | 100 | return ''.join(result) 101 | 102 | # These should be overrode 103 | def to_dict(self): 104 | return {} 105 | 106 | def on_send(self, writer): 107 | pass 108 | 109 | def on_response(self, reader): 110 | pass 111 | 112 | def on_exception(self, exception): 113 | pass 114 | -------------------------------------------------------------------------------- /telethon/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities for working with the Telegram API itself (such as handy methods 3 | to convert between an entity like an User, Chat, etc. into its Input version) 4 | """ 5 | from mimetypes import add_type, guess_extension 6 | 7 | from .tl.types import ( 8 | Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull, 9 | ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, InputPeerEmpty, 10 | MessageMediaDocument, MessageMediaPhoto, PeerChannel, InputChannel, 11 | UserEmpty, InputUser, InputUserEmpty, InputUserSelf, InputPeerSelf, 12 | PeerChat, PeerUser, User, UserFull, UserProfilePhoto) 13 | 14 | 15 | def get_display_name(entity): 16 | """Gets the input peer for the given "entity" (user, chat or channel) 17 | Returns None if it was not found""" 18 | if isinstance(entity, User): 19 | if entity.last_name and entity.first_name: 20 | return '{} {}'.format(entity.first_name, entity.last_name) 21 | elif entity.first_name: 22 | return entity.first_name 23 | elif entity.last_name: 24 | return entity.last_name 25 | else: 26 | return '(No name)' 27 | 28 | if isinstance(entity, Chat) or isinstance(entity, Channel): 29 | return entity.title 30 | 31 | return '(unknown)' 32 | 33 | # For some reason, .webp (stickers' format) is not registered 34 | add_type('image/webp', '.webp') 35 | 36 | 37 | def get_extension(media): 38 | """Gets the corresponding extension for any Telegram media""" 39 | 40 | # Photos are always compressed as .jpg by Telegram 41 | if (isinstance(media, UserProfilePhoto) or isinstance(media, ChatPhoto) or 42 | isinstance(media, MessageMediaPhoto)): 43 | return '.jpg' 44 | 45 | # Documents will come with a mime type, from which we can guess their mime type 46 | if isinstance(media, MessageMediaDocument): 47 | extension = guess_extension(media.document.mime_type) 48 | return extension if extension else '' 49 | 50 | return None 51 | 52 | 53 | def get_input_peer(entity): 54 | """Gets the input peer for the given "entity" (user, chat or channel). 55 | A ValueError is raised if the given entity isn't a supported type.""" 56 | if type(entity).subclass_of_id == 0xc91c90b6: # crc32(b'InputPeer') 57 | return entity 58 | 59 | if isinstance(entity, User): 60 | if entity.is_self: 61 | return InputPeerSelf() 62 | else: 63 | return InputPeerUser(entity.id, entity.access_hash) 64 | 65 | if any(isinstance(entity, c) for c in ( 66 | Chat, ChatEmpty, ChatForbidden)): 67 | return InputPeerChat(entity.id) 68 | 69 | if any(isinstance(entity, c) for c in ( 70 | Channel, ChannelForbidden)): 71 | return InputPeerChannel(entity.id, entity.access_hash) 72 | 73 | # Less common cases 74 | if isinstance(entity, UserEmpty): 75 | return InputPeerEmpty() 76 | 77 | if isinstance(entity, InputUser): 78 | return InputPeerUser(entity.user_id, entity.access_hash) 79 | 80 | if isinstance(entity, UserFull): 81 | return get_input_peer(entity.user) 82 | 83 | if isinstance(entity, ChatFull): 84 | return InputPeerChat(entity.id) 85 | 86 | if isinstance(entity, PeerChat): 87 | return InputPeerChat(entity.chat_id) 88 | 89 | raise ValueError('Cannot cast {} to any kind of InputPeer.' 90 | .format(type(entity).__name__)) 91 | 92 | 93 | def get_input_channel(entity): 94 | """Similar to get_input_peer, but for InputChannel's alone""" 95 | if type(entity).subclass_of_id == 0x40f202fd: # crc32(b'InputChannel') 96 | return entity 97 | 98 | if isinstance(entity, Channel) or isinstance(entity, ChannelForbidden): 99 | return InputChannel(entity.id, entity.access_hash) 100 | 101 | if isinstance(entity, InputPeerChannel): 102 | return InputChannel(entity.channel_id, entity.access_hash) 103 | 104 | raise ValueError('Cannot cast {} to any kind of InputChannel.' 105 | .format(type(entity).__name__)) 106 | 107 | 108 | def get_input_user(entity): 109 | """Similar to get_input_peer, but for InputUser's alone""" 110 | if type(entity).subclass_of_id == 0xe669bf46: # crc32(b'InputUser') 111 | return entity 112 | 113 | if isinstance(entity, User): 114 | if entity.is_self: 115 | return InputUserSelf() 116 | else: 117 | return InputUser(entity.id, entity.access_hash) 118 | 119 | if isinstance(entity, UserEmpty): 120 | return InputUserEmpty() 121 | 122 | if isinstance(entity, UserFull): 123 | return get_input_user(entity.user) 124 | 125 | if isinstance(entity, InputPeerUser): 126 | return InputUser(entity.user_id, entity.access_hash) 127 | 128 | raise ValueError('Cannot cast {} to any kind of InputUser.' 129 | .format(type(entity).__name__)) 130 | 131 | 132 | def find_user_or_chat(peer, users, chats): 133 | """Finds the corresponding user or chat given a peer. 134 | Returns None if it was not found""" 135 | try: 136 | if isinstance(peer, PeerUser): 137 | return next(u for u in users if u.id == peer.user_id) 138 | 139 | elif isinstance(peer, PeerChat): 140 | return next(c for c in chats if c.id == peer.chat_id) 141 | 142 | elif isinstance(peer, PeerChannel): 143 | return next(c for c in chats if c.id == peer.channel_id) 144 | 145 | except StopIteration: return 146 | 147 | if isinstance(peer, int): 148 | try: return next(u for u in users if u.id == peer) 149 | except StopIteration: pass 150 | 151 | try: return next(c for c in chats if c.id == peer) 152 | except StopIteration: pass 153 | 154 | 155 | def get_appropriated_part_size(file_size): 156 | """Gets the appropriated part size when uploading or downloading files, 157 | given an initial file size""" 158 | if file_size <= 1048576: # 1MB 159 | return 32 160 | if file_size <= 10485760: # 10MB 161 | return 64 162 | if file_size <= 393216000: # 375MB 163 | return 128 164 | if file_size <= 786432000: # 750MB 165 | return 256 166 | if file_size <= 1572864000: # 1500MB 167 | return 512 168 | 169 | raise ValueError('File size too large') 170 | -------------------------------------------------------------------------------- /telethon_examples/interactive_telegram_client.py: -------------------------------------------------------------------------------- 1 | from getpass import getpass 2 | 3 | from telethon import TelegramClient 4 | from telethon.errors import SessionPasswordNeededError 5 | from telethon.tl.types import UpdateShortChatMessage, UpdateShortMessage 6 | from telethon.utils import get_display_name 7 | 8 | 9 | def sprint(string, *args, **kwargs): 10 | """Safe Print (handle UnicodeEncodeErrors on some terminals)""" 11 | try: 12 | print(string, *args, **kwargs) 13 | except UnicodeEncodeError: 14 | string = string.encode('utf-8', errors='ignore')\ 15 | .decode('ascii', errors='ignore') 16 | print(string, *args, **kwargs) 17 | 18 | 19 | def print_title(title): 20 | # Clear previous window 21 | print('\n') 22 | print('=={}=='.format('=' * len(title))) 23 | sprint('= {} ='.format(title)) 24 | print('=={}=='.format('=' * len(title))) 25 | 26 | 27 | def bytes_to_string(byte_count): 28 | """Converts a byte count to a string (in KB, MB...)""" 29 | suffix_index = 0 30 | while byte_count >= 1024: 31 | byte_count /= 1024 32 | suffix_index += 1 33 | 34 | return '{:.2f}{}'.format(byte_count, 35 | [' bytes', 'KB', 'MB', 'GB', 'TB'][suffix_index]) 36 | 37 | 38 | class InteractiveTelegramClient(TelegramClient): 39 | """Full featured Telegram client, meant to be used on an interactive 40 | session to see what Telethon is capable off - 41 | 42 | This client allows the user to perform some basic interaction with 43 | Telegram through Telethon, such as listing dialogs (open chats), 44 | talking to people, downloading media, and receiving updates. 45 | """ 46 | def __init__(self, session_user_id, user_phone, api_id, api_hash, 47 | proxy=None): 48 | print_title('Initialization') 49 | 50 | print('Initializing interactive example...') 51 | super().__init__(session_user_id, api_id, api_hash, proxy) 52 | 53 | # Store all the found media in memory here, 54 | # so it can be downloaded if the user wants 55 | self.found_media = set() 56 | 57 | print('Connecting to Telegram servers...') 58 | if not self.connect(): 59 | print('Initial connection failed. Retrying...') 60 | if not self.connect(): 61 | print('Could not connect to Telegram servers.') 62 | return 63 | 64 | # Then, ensure we're authorized and have access 65 | if not self.is_user_authorized(): 66 | print('First run. Sending code request...') 67 | self.send_code_request(user_phone) 68 | 69 | self_user = None 70 | while self_user is None: 71 | code = input('Enter the code you just received: ') 72 | try: 73 | self_user = self.sign_in(user_phone, code) 74 | 75 | # Two-step verification may be enabled 76 | except SessionPasswordNeededError: 77 | pw = getpass('Two step verification is enabled. ' 78 | 'Please enter your password: ') 79 | 80 | self_user = self.sign_in(password=pw) 81 | 82 | def run(self): 83 | # Listen for updates 84 | self.add_update_handler(self.update_handler) 85 | 86 | # Enter a while loop to chat as long as the user wants 87 | while True: 88 | # Retrieve the top dialogs 89 | dialog_count = 10 90 | 91 | # Entities represent the user, chat or channel 92 | # corresponding to the dialog on the same index 93 | dialogs, entities = self.get_dialogs(dialog_count) 94 | 95 | i = None 96 | while i is None: 97 | print_title('Dialogs window') 98 | 99 | # Display them so the user can choose 100 | for i, entity in enumerate(entities, start=1): 101 | sprint('{}. {}'.format(i, get_display_name(entity))) 102 | 103 | # Let the user decide who they want to talk to 104 | print() 105 | print('> Who do you want to send messages to?') 106 | print('> Available commands:') 107 | print(' !q: Quits the dialogs window and exits.') 108 | print(' !l: Logs out, terminating this session.') 109 | print() 110 | i = input('Enter dialog ID or a command: ') 111 | if i == '!q': 112 | return 113 | if i == '!l': 114 | self.log_out() 115 | return 116 | 117 | try: 118 | i = int(i if i else 0) - 1 119 | # Ensure it is inside the bounds, otherwise retry 120 | if not 0 <= i < dialog_count: 121 | i = None 122 | except ValueError: 123 | i = None 124 | 125 | # Retrieve the selected user (or chat, or channel) 126 | entity = entities[i] 127 | 128 | # Show some information 129 | print_title('Chat with "{}"'.format(get_display_name(entity))) 130 | print('Available commands:') 131 | print(' !q: Quits the current chat.') 132 | print(' !Q: Quits the current chat and exits.') 133 | print(' !h: prints the latest messages (message History).') 134 | print(' !up : Uploads and sends the Photo from path.') 135 | print(' !uf : Uploads and sends the File from path.') 136 | print(' !dm : Downloads the given message Media (if any).') 137 | print(' !dp: Downloads the current dialog Profile picture.') 138 | print() 139 | 140 | # And start a while loop to chat 141 | while True: 142 | msg = input('Enter a message: ') 143 | # Quit 144 | if msg == '!q': 145 | break 146 | elif msg == '!Q': 147 | return 148 | 149 | # History 150 | elif msg == '!h': 151 | # First retrieve the messages and some information 152 | total_count, messages, senders = self.get_message_history( 153 | entity, limit=10) 154 | 155 | # Iterate over all (in reverse order so the latest appear 156 | # the last in the console) and print them with format: 157 | # "[hh:mm] Sender: Message" 158 | for msg, sender in zip( 159 | reversed(messages), reversed(senders)): 160 | # Get the name of the sender if any 161 | if sender: 162 | name = getattr(sender, 'first_name', None) 163 | if not name: 164 | name = getattr(sender, 'title') 165 | if not name: 166 | name = '???' 167 | else: 168 | name = '???' 169 | 170 | # Format the message content 171 | if getattr(msg, 'media', None): 172 | self.found_media.add(msg) 173 | # The media may or may not have a caption 174 | caption = getattr(msg.media, 'caption', '') 175 | content = '<{}> {}'.format( 176 | type(msg.media).__name__, caption) 177 | 178 | elif hasattr(msg, 'message'): 179 | content = msg.message 180 | elif hasattr(msg, 'action'): 181 | content = str(msg.action) 182 | else: 183 | # Unknown message, simply print its class name 184 | content = type(msg).__name__ 185 | 186 | # And print it to the user 187 | sprint('[{}:{}] (ID={}) {}: {}'.format( 188 | msg.date.hour, msg.date.minute, msg.id, name, 189 | content)) 190 | 191 | # Send photo 192 | elif msg.startswith('!up '): 193 | # Slice the message to get the path 194 | self.send_photo(path=msg[len('!up '):], entity=entity) 195 | 196 | # Send file (document) 197 | elif msg.startswith('!uf '): 198 | # Slice the message to get the path 199 | self.send_document(path=msg[len('!uf '):], entity=entity) 200 | 201 | # Download media 202 | elif msg.startswith('!dm '): 203 | # Slice the message to get message ID 204 | self.download_media(msg[len('!dm '):]) 205 | 206 | # Download profile photo 207 | elif msg == '!dp': 208 | output = str('usermedia/propic_{}'.format(entity.id)) 209 | print('Downloading profile picture...') 210 | success = self.download_profile_photo(entity.photo, output) 211 | if success: 212 | print('Profile picture downloaded to {}'.format( 213 | output)) 214 | else: 215 | print('No profile picture found for this user.') 216 | 217 | # Send chat message (if any) 218 | elif msg: 219 | self.send_message( 220 | entity, msg, link_preview=False) 221 | 222 | def send_photo(self, path, entity): 223 | print('Uploading {}...'.format(path)) 224 | input_file = self.upload_file( 225 | path, progress_callback=self.upload_progress_callback) 226 | 227 | # After we have the handle to the uploaded file, send it to our peer 228 | self.send_photo_file(input_file, entity) 229 | print('Photo sent!') 230 | 231 | def send_document(self, path, entity): 232 | print('Uploading {}...'.format(path)) 233 | input_file = self.upload_file( 234 | path, progress_callback=self.upload_progress_callback) 235 | 236 | # After we have the handle to the uploaded file, send it to our peer 237 | self.send_document_file(input_file, entity) 238 | print('Document sent!') 239 | 240 | def download_media(self, media_id): 241 | try: 242 | # The user may have entered a non-integer string! 243 | msg_media_id = int(media_id) 244 | 245 | # Search the message ID 246 | for msg in self.found_media: 247 | if msg.id == msg_media_id: 248 | # Let the output be the message ID 249 | output = str('usermedia/{}'.format(msg_media_id)) 250 | print('Downloading media with name {}...'.format(output)) 251 | output = self.download_msg_media( 252 | msg.media, 253 | file=output, 254 | progress_callback=self.download_progress_callback) 255 | print('Media downloaded to {}!'.format(output)) 256 | 257 | except ValueError: 258 | print('Invalid media ID given!') 259 | 260 | @staticmethod 261 | def download_progress_callback(downloaded_bytes, total_bytes): 262 | InteractiveTelegramClient.print_progress('Downloaded', 263 | downloaded_bytes, total_bytes) 264 | 265 | @staticmethod 266 | def upload_progress_callback(uploaded_bytes, total_bytes): 267 | InteractiveTelegramClient.print_progress('Uploaded', uploaded_bytes, 268 | total_bytes) 269 | 270 | @staticmethod 271 | def print_progress(progress_type, downloaded_bytes, total_bytes): 272 | print('{} {} out of {} ({:.2%})'.format(progress_type, bytes_to_string( 273 | downloaded_bytes), bytes_to_string(total_bytes), downloaded_bytes / 274 | total_bytes)) 275 | 276 | @staticmethod 277 | def update_handler(update_object): 278 | if isinstance(update_object, UpdateShortMessage): 279 | if update_object.out: 280 | sprint('You sent {} to user #{}'.format( 281 | update_object.message, update_object.user_id)) 282 | else: 283 | sprint('[User #{} sent {}]'.format( 284 | update_object.user_id, update_object.message)) 285 | 286 | elif isinstance(update_object, UpdateShortChatMessage): 287 | if update_object.out: 288 | sprint('You sent {} to chat #{}'.format( 289 | update_object.message, update_object.chat_id)) 290 | else: 291 | sprint('[Chat #{}, user #{} sent {}]'.format( 292 | update_object.chat_id, update_object.from_id, 293 | update_object.message)) 294 | -------------------------------------------------------------------------------- /telethon_generator/parser/__init__.py: -------------------------------------------------------------------------------- 1 | from .source_builder import SourceBuilder 2 | from .tl_parser import TLParser 3 | from .tl_object import TLObject 4 | -------------------------------------------------------------------------------- /telethon_generator/parser/source_builder.py: -------------------------------------------------------------------------------- 1 | class SourceBuilder: 2 | """This class should be used to build .py source files""" 3 | 4 | def __init__(self, out_stream, indent_size=4): 5 | self.current_indent = 0 6 | self.on_new_line = False 7 | self.indent_size = indent_size 8 | self.out_stream = out_stream 9 | 10 | # Was a new line added automatically before? If so, avoid it 11 | self.auto_added_line = False 12 | 13 | def indent(self): 14 | """Indents the current source code line by the current indentation level""" 15 | self.write(' ' * (self.current_indent * self.indent_size)) 16 | 17 | def write(self, string): 18 | """Writes a string into the source code, applying indentation if required""" 19 | if self.on_new_line: 20 | self.on_new_line = False # We're not on a new line anymore 21 | if string.strip( 22 | ): # If the string was not empty, indent; Else it probably was a new line 23 | self.indent() 24 | 25 | self.out_stream.write(string) 26 | 27 | def writeln(self, string=''): 28 | """Writes a string into the source code _and_ appends a new line, applying indentation if required""" 29 | self.write(string + '\n') 30 | self.on_new_line = True 31 | 32 | # If we're writing a block, increment indent for the next time 33 | if string and string[-1] == ':': 34 | self.current_indent += 1 35 | 36 | # Clear state after the user adds a new line 37 | self.auto_added_line = False 38 | 39 | def end_block(self): 40 | """Ends an indentation block, leaving an empty line afterwards""" 41 | self.current_indent -= 1 42 | 43 | # If we did not add a new line automatically yet, now it's the time! 44 | if not self.auto_added_line: 45 | self.writeln() 46 | self.auto_added_line = True 47 | 48 | def __str__(self): 49 | self.out_stream.seek(0) 50 | return self.out_stream.read() 51 | 52 | def __enter__(self): 53 | return self 54 | 55 | def __exit__(self, exc_type, exc_val, exc_tb): 56 | self.out_stream.close() 57 | -------------------------------------------------------------------------------- /telethon_generator/parser/tl_object.py: -------------------------------------------------------------------------------- 1 | import re 2 | from zlib import crc32 3 | 4 | 5 | class TLObject: 6 | """.tl core types IDs (such as vector, booleans, etc.)""" 7 | CORE_TYPES = (0x1cb5c415, 0xbc799737, 0x997275b5, 0x3fedd339) 8 | 9 | def __init__(self, fullname, object_id, args, result, is_function): 10 | """ 11 | Initializes a new TLObject, given its properties. 12 | Usually, this will be called from `from_tl` instead 13 | :param fullname: The fullname of the TL object (namespace.name) 14 | The namespace can be omitted 15 | :param object_id: The hexadecimal string representing the object ID 16 | :param args: The arguments, if any, of the TL object 17 | :param result: The result type of the TL object 18 | :param is_function: Is the object a function or a type? 19 | """ 20 | # The name can or not have a namespace 21 | if '.' in fullname: 22 | self.namespace = fullname.split('.')[0] 23 | self.name = fullname.split('.')[1] 24 | else: 25 | self.namespace = None 26 | self.name = fullname 27 | 28 | self.args = args 29 | self.result = result 30 | self.is_function = is_function 31 | 32 | # The ID should be an hexadecimal string or None to be inferred 33 | if object_id is None: 34 | self.id = self.infer_id() 35 | else: 36 | self.id = int(object_id, base=16) 37 | assert self.id == self.infer_id(),\ 38 | 'Invalid inferred ID for ' + repr(self) 39 | 40 | @staticmethod 41 | def from_tl(tl, is_function): 42 | """Returns a TL object from the given TL scheme line""" 43 | 44 | # Regex to match the whole line 45 | match = re.match(r''' 46 | ^ # We want to match from the beginning to the end 47 | ([\w.]+) # The .tl object can contain alpha_name or namespace.alpha_name 48 | (?: 49 | \# # After the name, comes the ID of the object 50 | ([0-9a-f]+) # The constructor ID is in hexadecimal form 51 | )? # If no constructor ID was given, CRC32 the 'tl' to determine it 52 | 53 | (?:\s # After that, we want to match its arguments (name:type) 54 | {? # For handling the start of the '{X:Type}' case 55 | \w+ # The argument name will always be an alpha-only name 56 | : # Then comes the separator between name:type 57 | [\w\d<>#.?!]+ # The type is slightly more complex, since it's alphanumeric and it can 58 | # also have Vector, flags:# and flags.0?default, plus :!X as type 59 | }? # For handling the end of the '{X:Type}' case 60 | )* # Match 0 or more arguments 61 | \s # Leave a space between the arguments and the equal 62 | = 63 | \s # Leave another space between the equal and the result 64 | ([\w\d<>#.?]+) # The result can again be as complex as any argument type 65 | ;$ # Finally, the line should always end with ; 66 | ''', tl, re.IGNORECASE | re.VERBOSE) 67 | 68 | # Sub-regex to match the arguments (sadly, it cannot be embedded in the first regex) 69 | args_match = re.findall(r''' 70 | ({)? # We may or may not capture the opening brace 71 | (\w+) # First we capture any alpha name with length 1 or more 72 | : # Which is separated from its type by a colon 73 | ([\w\d<>#.?!]+) # The type is slightly more complex, since it's alphanumeric and it can 74 | # also have Vector, flags:# and flags.0?default, plus :!X as type 75 | (})? # We may or not capture the closing brace 76 | ''', tl, re.IGNORECASE | re.VERBOSE) 77 | 78 | # Retrieve the matched arguments 79 | args = [TLArg(name, arg_type, brace != '') 80 | for brace, name, arg_type, _ in args_match] 81 | 82 | # And initialize the TLObject 83 | return TLObject( 84 | fullname=match.group(1), 85 | object_id=match.group(2), 86 | args=args, 87 | result=match.group(3), 88 | is_function=is_function) 89 | 90 | def sorted_args(self): 91 | """Returns the arguments properly sorted and ready to plug-in 92 | into a Python's method header (i.e., flags and those which 93 | can be inferred will go last so they can default =None) 94 | """ 95 | return sorted(self.args, 96 | key=lambda x: x.is_flag or x.can_be_inferred) 97 | 98 | def is_core_type(self): 99 | """Determines whether the TLObject is a "core type" 100 | (and thus should be embedded in the generated code) or not""" 101 | return self.id in TLObject.CORE_TYPES 102 | 103 | def __repr__(self, ignore_id=False): 104 | fullname = ('{}.{}'.format(self.namespace, self.name) 105 | if self.namespace is not None else self.name) 106 | 107 | if getattr(self, 'id', None) is None or ignore_id: 108 | hex_id = '' 109 | else: 110 | # Skip 0x and add 0's for padding 111 | hex_id = '#' + hex(self.id)[2:].rjust(8, '0') 112 | 113 | if self.args: 114 | args = ' ' + ' '.join([repr(arg) for arg in self.args]) 115 | else: 116 | args = '' 117 | 118 | return '{}{}{} = {}'.format(fullname, hex_id, args, self.result) 119 | 120 | def infer_id(self): 121 | representation = self.__repr__(ignore_id=True) 122 | 123 | # Clean the representation 124 | representation = representation\ 125 | .replace(':bytes ', ':string ')\ 126 | .replace('?bytes ', '?string ')\ 127 | .replace('<', ' ').replace('>', '')\ 128 | .replace('{', '').replace('}', '') 129 | 130 | representation = re.sub( 131 | r' \w+:flags\.\d+\?true', 132 | r'', 133 | representation 134 | ) 135 | return crc32(representation.encode('ascii')) 136 | 137 | def __str__(self): 138 | fullname = ('{}.{}'.format(self.namespace, self.name) 139 | if self.namespace is not None else self.name) 140 | 141 | # Some arguments are not valid for being represented, such as the flag indicator or generic definition 142 | # (these have no explicit values until used) 143 | valid_args = [arg for arg in self.args 144 | if not arg.flag_indicator and not arg.generic_definition] 145 | 146 | args = ', '.join(['{}={{}}'.format(arg.name) for arg in valid_args]) 147 | 148 | # Since Python's default representation for lists is using repr(), we need to str() manually on every item 149 | args_format = ', '.join( 150 | ['str(self.{})'.format(arg.name) if not arg.is_vector else 151 | 'None if not self.{0} else [str(_) for _ in self.{0}]'.format( 152 | arg.name) for arg in valid_args]) 153 | 154 | return ("'({} (ID: {}) = ({}))'.format({})" 155 | .format(fullname, hex(self.id), args, args_format)) 156 | 157 | 158 | class TLArg: 159 | def __init__(self, name, arg_type, generic_definition): 160 | """ 161 | Initializes a new .tl argument 162 | :param name: The name of the .tl argument 163 | :param arg_type: The type of the .tl argument 164 | :param generic_definition: Is the argument a generic definition? 165 | (i.e. {X:Type}) 166 | """ 167 | if name == 'self': # This very only name is restricted 168 | self.name = 'is_self' 169 | else: 170 | self.name = name 171 | 172 | # Default values 173 | self.is_vector = False 174 | self.is_flag = False 175 | self.flag_index = -1 176 | 177 | # Special case: some types can be inferred, which makes it 178 | # less annoying to type. Currently the only type that can 179 | # be inferred is if the name is 'random_id', to which a 180 | # random ID will be assigned if left as None (the default) 181 | self.can_be_inferred = name == 'random_id' 182 | 183 | # The type can be an indicator that other arguments will be flags 184 | if arg_type == '#': 185 | self.flag_indicator = True 186 | self.type = None 187 | self.is_generic = False 188 | else: 189 | self.flag_indicator = False 190 | self.is_generic = arg_type.startswith('!') 191 | self.type = arg_type.lstrip( 192 | '!') # Strip the exclamation mark always to have only the name 193 | 194 | # The type may be a flag (flags.IDX?REAL_TYPE) 195 | # Note that 'flags' is NOT the flags name; this is determined by a previous argument 196 | # However, we assume that the argument will always be called 'flags' 197 | flag_match = re.match(r'flags.(\d+)\?([\w<>.]+)', self.type) 198 | if flag_match: 199 | self.is_flag = True 200 | self.flag_index = int(flag_match.group(1)) 201 | # Update the type to match the exact type, not the "flagged" one 202 | self.type = flag_match.group(2) 203 | 204 | # Then check if the type is a Vector 205 | vector_match = re.match(r'vector<(\w+)>', self.type, re.IGNORECASE) 206 | if vector_match: 207 | self.is_vector = True 208 | 209 | # If the type's first letter is not uppercase, then 210 | # it is a constructor and we use (read/write) its ID 211 | # as pinpointed on issue #81. 212 | self.use_vector_id = self.type[0] == 'V' 213 | 214 | # Update the type to match the one inside the vector 215 | self.type = vector_match.group(1) 216 | 217 | # The name may contain "date" in it, if this is the case and the type is "int", 218 | # we can safely assume that this should be treated as a "date" object. 219 | # Note that this is not a valid Telegram object, but it's easier to work with 220 | if self.type == 'int' and ( 221 | re.search(r'(\b|_)date\b', name) or 222 | name in ('expires', 'expires_at', 'was_online')): 223 | self.type = 'date' 224 | 225 | self.generic_definition = generic_definition 226 | 227 | def __str__(self): 228 | # Find the real type representation by updating it as required 229 | real_type = self.type 230 | if self.flag_indicator: 231 | real_type = '#' 232 | 233 | if self.is_vector: 234 | if self.use_vector_id: 235 | real_type = 'Vector<{}>'.format(real_type) 236 | else: 237 | real_type = 'vector<{}>'.format(real_type) 238 | 239 | if self.is_generic: 240 | real_type = '!{}'.format(real_type) 241 | 242 | if self.is_flag: 243 | real_type = 'flags.{}?{}'.format(self.flag_index, real_type) 244 | 245 | if self.generic_definition: 246 | return '{{{}:{}}}'.format(self.name, real_type) 247 | else: 248 | return '{}:{}'.format(self.name, real_type) 249 | 250 | def __repr__(self): 251 | # Get rid of our special type 252 | return str(self)\ 253 | .replace(':date', ':int')\ 254 | .replace('?date', '?int') 255 | -------------------------------------------------------------------------------- /telethon_generator/parser/tl_parser.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from .tl_object import TLObject 4 | 5 | 6 | class TLParser: 7 | """Class used to parse .tl files""" 8 | 9 | @staticmethod 10 | def parse_file(file_path): 11 | """This method yields TLObjects from a given .tl file""" 12 | 13 | with open(file_path, encoding='utf-8') as file: 14 | # Start by assuming that the next found line won't be a function (and will hence be a type) 15 | is_function = False 16 | 17 | # Read all the lines from the .tl file 18 | for line in file: 19 | line = line.strip() 20 | 21 | # Ensure that the line is not a comment 22 | if line and not line.startswith('//'): 23 | 24 | # Check whether the line is a type change (types ⋄ functions) or not 25 | match = re.match('---(\w+)---', line) 26 | if match: 27 | following_types = match.group(1) 28 | is_function = following_types == 'functions' 29 | 30 | else: 31 | yield TLObject.from_tl(line, is_function) 32 | 33 | @staticmethod 34 | def find_layer(file_path): 35 | """Finds the layer used on the specified scheme.tl file""" 36 | layer_regex = re.compile(r'^//\s*LAYER\s*(\d+)$') 37 | with open(file_path, encoding='utf-8') as file: 38 | for line in file: 39 | match = layer_regex.match(line) 40 | if match: 41 | return int(match.group(1)) 42 | -------------------------------------------------------------------------------- /telethon_generator/tl_generator.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shutil 4 | from zlib import crc32 5 | from collections import defaultdict 6 | 7 | from .parser import SourceBuilder, TLParser 8 | 9 | 10 | class TLGenerator: 11 | def __init__(self, output_dir): 12 | self.output_dir = output_dir 13 | 14 | def _get_file(self, *paths): 15 | return os.path.join(self.output_dir, *paths) 16 | 17 | def _rm_if_exists(self, filename): 18 | file = self._get_file(filename) 19 | if os.path.exists(file): 20 | if os.path.isdir(file): 21 | shutil.rmtree(file) 22 | else: 23 | os.remove(file) 24 | 25 | def tlobjects_exist(self): 26 | """Determines whether the TLObjects were previously 27 | generated (hence exist) or not 28 | """ 29 | return os.path.isfile(self._get_file('all_tlobjects.py')) 30 | 31 | def clean_tlobjects(self): 32 | """Cleans the automatically generated TLObjects from disk""" 33 | for name in ('functions', 'types', 'all_tlobjects.py'): 34 | self._rm_if_exists(name) 35 | 36 | def generate_tlobjects(self, scheme_file, import_depth): 37 | """Generates all the TLObjects from scheme.tl to 38 | tl/functions and tl/types 39 | """ 40 | 41 | # First ensure that the required parent directories exist 42 | os.makedirs(self._get_file('functions'), exist_ok=True) 43 | os.makedirs(self._get_file('types'), exist_ok=True) 44 | 45 | # Step 0: Cache the parsed file on a tuple 46 | tlobjects = tuple(TLParser.parse_file(scheme_file)) 47 | 48 | # Step 1: Ensure that no object has the same name as a namespace 49 | # We must check this because Python will complain if it sees a 50 | # file and a directory with the same name, which happens for 51 | # example with "updates". 52 | # 53 | # We distinguish between function and type namespaces since we 54 | # will later need to perform a relative import for them to be used 55 | function_namespaces = set() 56 | type_namespaces = set() 57 | 58 | # Make use of this iteration to also store 'Type: [Constructors]' 59 | type_constructors = defaultdict(list) 60 | for tlobject in tlobjects: 61 | if tlobject.is_function: 62 | if tlobject.namespace: 63 | function_namespaces.add(tlobject.namespace) 64 | else: 65 | type_constructors[tlobject.result].append(tlobject) 66 | if tlobject.namespace: 67 | type_namespaces.add(tlobject.namespace) 68 | 69 | # Merge both namespaces to easily check if any namespace exists, 70 | # though we could also distinguish between types and functions 71 | # here, it's not worth doing 72 | namespace_directories = function_namespaces | type_namespaces 73 | for tlobject in tlobjects: 74 | if TLGenerator.get_file_name(tlobject, add_extension=False) \ 75 | in namespace_directories: 76 | # If this TLObject isn't under the same directory as its 77 | # name (i.e. "contacts"), append "_tg" to avoid confusion 78 | # between the file and the directory (i.e. "updates") 79 | if tlobject.namespace != tlobject.name: 80 | tlobject.name += '_tg' 81 | 82 | # Step 2: Generate the actual code 83 | for tlobject in tlobjects: 84 | # Omit core types, these are embedded in the generated code 85 | if tlobject.is_core_type(): 86 | continue 87 | 88 | # Determine the output directory and create it 89 | out_dir = self._get_file('functions' 90 | if tlobject.is_function else 'types') 91 | 92 | # Path depth to perform relative import 93 | depth = import_depth 94 | if tlobject.namespace: 95 | depth += 1 96 | out_dir = os.path.join(out_dir, tlobject.namespace) 97 | 98 | os.makedirs(out_dir, exist_ok=True) 99 | 100 | # Add this object to __init__.py, so we can import * 101 | init_py = os.path.join(out_dir, '__init__.py') 102 | with open(init_py, 'a', encoding='utf-8') as file: 103 | with SourceBuilder(file) as builder: 104 | builder.writeln('from .{} import {}'.format( 105 | TLGenerator.get_file_name(tlobject, add_extension=False), 106 | TLGenerator.get_class_name(tlobject))) 107 | 108 | # Create the file for this TLObject 109 | filename = os.path.join(out_dir, TLGenerator.get_file_name( 110 | tlobject, add_extension=True 111 | )) 112 | 113 | with open(filename, 'w', encoding='utf-8') as file: 114 | with SourceBuilder(file) as builder: 115 | TLGenerator._write_source_code( 116 | tlobject, builder, depth, type_constructors) 117 | 118 | # Step 3: Add the relative imports to the namespaces on __init__.py's 119 | init_py = self._get_file('functions', '__init__.py') 120 | with open(init_py, 'a') as file: 121 | file.write('from . import {}\n' 122 | .format(', '.join(function_namespaces))) 123 | 124 | init_py = self._get_file('types', '__init__.py') 125 | with open(init_py, 'a') as file: 126 | file.write('from . import {}\n' 127 | .format(', '.join(type_namespaces))) 128 | 129 | # Step 4: Once all the objects have been generated, 130 | # we can now group them in a single file 131 | filename = os.path.join(self._get_file('all_tlobjects.py')) 132 | with open(filename, 'w', encoding='utf-8') as file: 133 | with SourceBuilder(file) as builder: 134 | builder.writeln( 135 | '"""File generated by TLObjects\' generator. All changes will be ERASED"""') 136 | builder.writeln() 137 | 138 | builder.writeln('from . import types, functions') 139 | builder.writeln() 140 | 141 | # Create a variable to indicate which layer this is 142 | builder.writeln('layer = {} # Current generated layer'.format( 143 | TLParser.find_layer(scheme_file))) 144 | builder.writeln() 145 | 146 | # Then create the dictionary containing constructor_id: class 147 | builder.writeln('tlobjects = {') 148 | builder.current_indent += 1 149 | 150 | # Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class) 151 | for tlobject in tlobjects: 152 | constructor = hex(tlobject.id) 153 | if len(constructor) != 10: 154 | # Make it a nice length 10 so it fits well 155 | constructor = '0x' + constructor[2:].zfill(8) 156 | 157 | builder.write('{}: '.format(constructor)) 158 | builder.write( 159 | 'functions' if tlobject.is_function else 'types') 160 | 161 | if tlobject.namespace: 162 | builder.write('.' + tlobject.namespace) 163 | 164 | builder.writeln('.{},'.format( 165 | TLGenerator.get_class_name(tlobject))) 166 | 167 | builder.current_indent -= 1 168 | builder.writeln('}') 169 | 170 | @staticmethod 171 | def _write_source_code(tlobject, builder, depth, type_constructors): 172 | """Writes the source code corresponding to the given TLObject 173 | by making use of the 'builder' SourceBuilder. 174 | 175 | Additional information such as file path depth and 176 | the Type: [Constructors] must be given for proper 177 | importing and documentation strings. 178 | '""" 179 | 180 | # Both types and functions inherit from the TLObject class so they 181 | # all can be serialized and sent, however, only the functions are 182 | # "content_related". 183 | builder.writeln('from {}.tl.tlobject import TLObject' 184 | .format('.' * depth)) 185 | 186 | if tlobject.is_function: 187 | util_imports = set() 188 | for a in tlobject.args: 189 | # We can automatically convert some "full" types to 190 | # "input only" (like User -> InputPeerUser, etc.) 191 | if a.type == 'InputPeer': 192 | util_imports.add('get_input_peer') 193 | elif a.type == 'InputChannel': 194 | util_imports.add('get_input_channel') 195 | elif a.type == 'InputUser': 196 | util_imports.add('get_input_user') 197 | 198 | if util_imports: 199 | builder.writeln('from {}.utils import {}'.format( 200 | '.' * depth, ', '.join(util_imports))) 201 | 202 | if any(a for a in tlobject.args if a.can_be_inferred): 203 | # Currently only 'random_id' needs 'os' to be imported 204 | builder.writeln('import os') 205 | 206 | builder.writeln() 207 | builder.writeln() 208 | builder.writeln('class {}(TLObject):'.format( 209 | TLGenerator.get_class_name(tlobject))) 210 | 211 | # Write the original .tl definition, 212 | # along with a "generated automatically" message 213 | builder.writeln( 214 | '"""Class generated by TLObjects\' generator. ' 215 | 'All changes will be ERASED. TL definition below.' 216 | ) 217 | builder.writeln('{}"""'.format(repr(tlobject))) 218 | builder.writeln() 219 | 220 | # Class-level variable to store its constructor ID 221 | builder.writeln("# Telegram's constructor (U)ID for this class") 222 | builder.writeln('constructor_id = {}'.format(hex(tlobject.id))) 223 | builder.writeln("# Also the ID of its resulting type for fast checks") 224 | builder.writeln('subclass_of_id = {}'.format( 225 | hex(crc32(tlobject.result.encode('ascii'))))) 226 | builder.writeln() 227 | 228 | # Flag arguments must go last 229 | args = [ 230 | a for a in tlobject.sorted_args() 231 | if not a.flag_indicator and not a.generic_definition 232 | ] 233 | 234 | # Convert the args to string parameters, flags having =None 235 | args = [ 236 | (a.name if not a.is_flag and not a.can_be_inferred 237 | else '{}=None'.format(a.name)) 238 | for a in args 239 | ] 240 | 241 | # Write the __init__ function 242 | if args: 243 | builder.writeln( 244 | 'def __init__(self, {}):'.format(', '.join(args)) 245 | ) 246 | else: 247 | builder.writeln('def __init__(self):') 248 | 249 | # Now update args to have the TLObject arguments, _except_ 250 | # those which are calculated on send or ignored, this is 251 | # flag indicator and generic definitions. 252 | # 253 | # We don't need the generic definitions in Python 254 | # because arguments can be any type 255 | args = [arg for arg in tlobject.args 256 | if not arg.flag_indicator and 257 | not arg.generic_definition] 258 | 259 | if args: 260 | # Write the docstring, to know the type of the args 261 | builder.writeln('"""') 262 | for arg in args: 263 | if not arg.flag_indicator: 264 | builder.write( 265 | ':param {}: Telegram type: "{}".' 266 | .format(arg.name, arg.type) 267 | ) 268 | if arg.is_vector: 269 | builder.write(' Must be a list.'.format(arg.name)) 270 | 271 | if arg.is_generic: 272 | builder.write(' Must be another TLObject request.') 273 | 274 | builder.writeln() 275 | 276 | # We also want to know what type this request returns 277 | # or to which type this constructor belongs to 278 | builder.writeln() 279 | if tlobject.is_function: 280 | builder.write(':returns {}: '.format(tlobject.result)) 281 | else: 282 | builder.write('Constructor for {}: '.format(tlobject.result)) 283 | 284 | constructors = type_constructors[tlobject.result] 285 | if not constructors: 286 | builder.writeln('This type has no constructors.') 287 | elif len(constructors) == 1: 288 | builder.writeln('Instance of {}.'.format( 289 | TLGenerator.get_class_name(constructors[0]) 290 | )) 291 | else: 292 | builder.writeln('Instance of either {}.'.format( 293 | ', '.join(TLGenerator.get_class_name(c) 294 | for c in constructors) 295 | )) 296 | 297 | builder.writeln('"""') 298 | 299 | builder.writeln('super().__init__()') 300 | # Functions have a result object and are confirmed by default 301 | if tlobject.is_function: 302 | builder.writeln('self.result = None') 303 | builder.writeln( 304 | 'self.content_related = True') 305 | 306 | # Set the arguments 307 | if args: 308 | # Leave an empty line if there are any args 309 | builder.writeln() 310 | 311 | for arg in args: 312 | if arg.can_be_inferred: 313 | # Currently the only argument that can be 314 | # inferred are those called 'random_id' 315 | if arg.name == 'random_id': 316 | builder.writeln( 317 | "self.random_id = random_id if random_id " 318 | "is not None else int.from_bytes(" 319 | "os.urandom({}), signed=True, " 320 | "byteorder='little')" 321 | .format(8 if arg.type == 'long' else 4) 322 | ) 323 | else: 324 | raise ValueError('Cannot infer a value for ', arg) 325 | 326 | # Well-known cases, auto-cast it to the right type 327 | elif arg.type == 'InputPeer' and tlobject.is_function: 328 | TLGenerator.write_get_input(builder, arg, 'get_input_peer') 329 | elif arg.type == 'InputChannel' and tlobject.is_function: 330 | TLGenerator.write_get_input(builder, arg, 'get_input_channel') 331 | elif arg.type == 'InputUser' and tlobject.is_function: 332 | TLGenerator.write_get_input(builder, arg, 'get_input_user') 333 | 334 | else: 335 | builder.writeln('self.{0} = {0}'.format(arg.name)) 336 | 337 | builder.end_block() 338 | 339 | # Write the to_dict(self) method 340 | if args: 341 | builder.writeln('def to_dict(self):') 342 | builder.writeln('return {') 343 | builder.current_indent += 1 344 | 345 | base_types = ('string', 'bytes', 'int', 'long', 'int128', 346 | 'int256', 'double', 'Bool', 'true', 'date') 347 | 348 | for arg in args: 349 | builder.write("'{}': ".format(arg.name)) 350 | if arg.type in base_types: 351 | if arg.is_vector: 352 | builder.write( 353 | '[] if self.{0} is None else self.{0}[:]' 354 | .format(arg.name) 355 | ) 356 | else: 357 | builder.write('self.{}'.format(arg.name)) 358 | else: 359 | if arg.is_vector: 360 | builder.write( 361 | '[] if self.{0} is None else [None ' 362 | 'if x is None else x.to_dict() for x in self.{0}]' 363 | .format(arg.name) 364 | ) 365 | else: 366 | builder.write( 367 | 'None if self.{0} is None else self.{0}.to_dict()' 368 | .format(arg.name) 369 | ) 370 | builder.writeln(',') 371 | 372 | builder.current_indent -= 1 373 | builder.writeln("}") 374 | else: 375 | builder.writeln('@staticmethod') 376 | builder.writeln('def to_dict():') 377 | builder.writeln('return {}') 378 | 379 | builder.end_block() 380 | 381 | # Write the on_send(self, writer) function 382 | builder.writeln('def on_send(self, writer):') 383 | builder.writeln( 384 | 'writer.write_int({}.constructor_id, signed=False)' 385 | .format(TLGenerator.get_class_name(tlobject))) 386 | 387 | for arg in tlobject.args: 388 | TLGenerator.write_onsend_code(builder, arg, 389 | tlobject.args) 390 | builder.end_block() 391 | 392 | # Write the empty() function, which returns an "empty" 393 | # instance, in which all attributes are set to None 394 | builder.writeln('@staticmethod') 395 | builder.writeln('def empty():') 396 | builder.writeln( 397 | '"""Returns an "empty" instance (attributes=None)"""') 398 | builder.writeln('return {}({})'.format( 399 | TLGenerator.get_class_name(tlobject), ', '.join( 400 | 'None' for _ in range(len(args))))) 401 | builder.end_block() 402 | 403 | # Write the on_response(self, reader) function 404 | builder.writeln('def on_response(self, reader):') 405 | # Do not read constructor's ID, since 406 | # that's already been read somewhere else 407 | if tlobject.is_function: 408 | TLGenerator.write_request_result_code(builder, tlobject) 409 | else: 410 | if tlobject.args: 411 | for arg in tlobject.args: 412 | TLGenerator.write_onresponse_code( 413 | builder, arg, tlobject.args) 414 | else: 415 | # If there were no arguments, we still need an 416 | # on_response method, and hence "pass" if empty 417 | builder.writeln('pass') 418 | builder.end_block() 419 | 420 | # Write the __repr__(self) and __str__(self) functions 421 | builder.writeln('def __repr__(self):') 422 | builder.writeln("return '{}'".format(repr(tlobject))) 423 | builder.end_block() 424 | 425 | builder.writeln('def __str__(self):') 426 | builder.writeln('return TLObject.pretty_format(self)') 427 | builder.end_block() 428 | 429 | builder.writeln('def stringify(self):') 430 | builder.writeln('return TLObject.pretty_format(self, indent=0)') 431 | # builder.end_block() # No need to end the last block 432 | 433 | @staticmethod 434 | def write_get_input(builder, arg, get_input_code): 435 | """Returns "True" if the get_input_* code was written when assigning 436 | a parameter upon creating the request. Returns False otherwise 437 | """ 438 | if arg.is_vector: 439 | builder.writeln( 440 | 'self.{0} = [{1}(_x) for _x in {0}]' 441 | .format(arg.name, get_input_code) 442 | ) 443 | pass 444 | else: 445 | builder.writeln( 446 | 'self.{0} = {1}({0})'.format(arg.name, get_input_code) 447 | ) 448 | 449 | @staticmethod 450 | def get_class_name(tlobject): 451 | """Gets the class name following the Python style guidelines, in ThisClassFormat""" 452 | 453 | # Courtesy of http://stackoverflow.com/a/31531797/4759433 454 | # Also, '_' could be replaced for ' ', then use .title(), and then remove ' ' 455 | result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), 456 | tlobject.name) 457 | result = result[:1].upper() + result[1:].replace( 458 | '_', '') # Replace again to fully ensure! 459 | # If it's a function, let it end with "Request" to identify them more easily 460 | if tlobject.is_function: 461 | result += 'Request' 462 | return result 463 | 464 | @staticmethod 465 | def get_file_name(tlobject, add_extension=False): 466 | """Gets the file name in file_name_format.py for the given TLObject""" 467 | 468 | # Courtesy of http://stackoverflow.com/a/1176023/4759433 469 | s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', tlobject.name) 470 | result = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() 471 | if add_extension: 472 | return result + '.py' 473 | else: 474 | return result 475 | 476 | @staticmethod 477 | def write_onsend_code(builder, arg, args, name=None): 478 | """ 479 | Writes the write code for the given argument 480 | :param builder: The source code builder 481 | :param arg: The argument to write 482 | :param args: All the other arguments in TLObject same on_send. This is required to determine the flags value 483 | :param name: The name of the argument. Defaults to "self.argname" 484 | This argument is an option because it's required when writing Vectors<> 485 | """ 486 | 487 | if arg.generic_definition: 488 | return # Do nothing, this only specifies a later type 489 | 490 | if name is None: 491 | name = 'self.{}'.format(arg.name) 492 | 493 | # The argument may be a flag, only write if it's not None AND if it's not a True type 494 | # True types are not actually sent, but instead only used to determine the flags 495 | if arg.is_flag: 496 | if arg.type == 'true': 497 | return # Exit, since True type is never written 498 | else: 499 | builder.writeln('if {}:'.format(name)) 500 | 501 | if arg.is_vector: 502 | if arg.use_vector_id: 503 | builder.writeln( 504 | "writer.write_int(0x1cb5c415, signed=False) # Vector's constructor ID") 505 | 506 | builder.writeln('writer.write_int(len({}))'.format(name)) 507 | builder.writeln('for _x in {}:'.format(name)) 508 | # Temporary disable .is_vector, not to enter this if again 509 | arg.is_vector = False 510 | TLGenerator.write_onsend_code(builder, arg, args, name='_x') 511 | arg.is_vector = True 512 | 513 | elif arg.flag_indicator: 514 | # Calculate the flags with those items which are not None 515 | builder.writeln( 516 | '# Calculate the flags. This equals to those flag arguments which are NOT None') 517 | builder.writeln('flags = 0') 518 | for flag in args: 519 | if flag.is_flag: 520 | builder.writeln('flags |= (1 << {}) if {} else 0'.format( 521 | flag.flag_index, 'self.{}'.format(flag.name))) 522 | 523 | builder.writeln('writer.write_int(flags)') 524 | builder.writeln() 525 | 526 | elif 'int' == arg.type: 527 | builder.writeln('writer.write_int({})'.format(name)) 528 | 529 | elif 'long' == arg.type: 530 | builder.writeln('writer.write_long({})'.format(name)) 531 | 532 | elif 'int128' == arg.type: 533 | builder.writeln('writer.write_large_int({}, bits=128)'.format( 534 | name)) 535 | 536 | elif 'int256' == arg.type: 537 | builder.writeln('writer.write_large_int({}, bits=256)'.format( 538 | name)) 539 | 540 | elif 'double' == arg.type: 541 | builder.writeln('writer.write_double({})'.format(name)) 542 | 543 | elif 'string' == arg.type: 544 | builder.writeln('writer.tgwrite_string({})'.format(name)) 545 | 546 | elif 'Bool' == arg.type: 547 | builder.writeln('writer.tgwrite_bool({})'.format(name)) 548 | 549 | elif 'true' == arg.type: # Awkwardly enough, Telegram has both bool and "true", used in flags 550 | pass # These are actually NOT written! Only used for flags 551 | 552 | elif 'bytes' == arg.type: 553 | builder.writeln('writer.tgwrite_bytes({})'.format(name)) 554 | 555 | elif 'date' == arg.type: # Custom format 556 | builder.writeln('writer.tgwrite_date({})'.format(name)) 557 | 558 | else: 559 | # Else it may be a custom type 560 | builder.writeln('{}.on_send(writer)'.format(name)) 561 | 562 | # End vector and flag blocks if required (if we opened them before) 563 | if arg.is_vector: 564 | builder.end_block() 565 | 566 | if arg.is_flag: 567 | builder.end_block() 568 | 569 | @staticmethod 570 | def write_onresponse_code(builder, arg, args, name=None): 571 | """ 572 | Writes the receive code for the given argument 573 | 574 | :param builder: The source code builder 575 | :param arg: The argument to write 576 | :param args: All the other arguments in TLObject same on_send. This is required to determine the flags value 577 | :param name: The name of the argument. Defaults to "self.argname" 578 | This argument is an option because it's required when writing Vectors<> 579 | """ 580 | 581 | if arg.generic_definition: 582 | return # Do nothing, this only specifies a later type 583 | 584 | if name is None: 585 | name = 'self.{}'.format(arg.name) 586 | 587 | # The argument may be a flag, only write that flag was given! 588 | was_flag = False 589 | if arg.is_flag: 590 | was_flag = True 591 | builder.writeln('if (flags & (1 << {})) != 0:'.format( 592 | arg.flag_index)) 593 | # Temporary disable .is_flag not to enter this if again when calling the method recursively 594 | arg.is_flag = False 595 | 596 | if arg.is_vector: 597 | if arg.use_vector_id: 598 | builder.writeln("reader.read_int() # Vector's constructor ID") 599 | 600 | builder.writeln('{} = [] # Initialize an empty list'.format(name)) 601 | builder.writeln('_len = reader.read_int()') 602 | builder.writeln('for _ in range(_len):') 603 | # Temporary disable .is_vector, not to enter this if again 604 | arg.is_vector = False 605 | TLGenerator.write_onresponse_code(builder, arg, args, name='_x') 606 | builder.writeln('{}.append(_x)'.format(name)) 607 | arg.is_vector = True 608 | 609 | elif arg.flag_indicator: 610 | # Read the flags, which will indicate what items we should read next 611 | builder.writeln('flags = reader.read_int()') 612 | builder.writeln() 613 | 614 | elif 'int' == arg.type: 615 | builder.writeln('{} = reader.read_int()'.format(name)) 616 | 617 | elif 'long' == arg.type: 618 | builder.writeln('{} = reader.read_long()'.format(name)) 619 | 620 | elif 'int128' == arg.type: 621 | builder.writeln( 622 | '{} = reader.read_large_int(bits=128)'.format(name) 623 | ) 624 | 625 | elif 'int256' == arg.type: 626 | builder.writeln( 627 | '{} = reader.read_large_int(bits=256)'.format(name) 628 | ) 629 | 630 | elif 'double' == arg.type: 631 | builder.writeln('{} = reader.read_double()'.format(name)) 632 | 633 | elif 'string' == arg.type: 634 | builder.writeln('{} = reader.tgread_string()'.format(name)) 635 | 636 | elif 'Bool' == arg.type: 637 | builder.writeln('{} = reader.tgread_bool()'.format(name)) 638 | 639 | elif 'true' == arg.type: # Awkwardly enough, Telegram has both bool and "true", used in flags 640 | builder.writeln( 641 | '{} = True # Arbitrary not-None value, no need to read since it is a flag'. 642 | format(name)) 643 | 644 | elif 'bytes' == arg.type: 645 | builder.writeln('{} = reader.tgread_bytes()'.format(name)) 646 | 647 | elif 'date' == arg.type: # Custom format 648 | builder.writeln('{} = reader.tgread_date()'.format(name)) 649 | 650 | else: 651 | # Else it may be a custom type 652 | builder.writeln('{} = reader.tgread_object()'.format(name)) 653 | 654 | # End vector and flag blocks if required (if we opened them before) 655 | if arg.is_vector: 656 | builder.end_block() 657 | 658 | if was_flag: 659 | builder.end_block() 660 | # Restore .is_flag 661 | arg.is_flag = True 662 | 663 | @staticmethod 664 | def write_request_result_code(builder, tlobject): 665 | """ 666 | Writes the receive code for the given function 667 | 668 | :param builder: The source code builder 669 | :param tlobject: The TLObject for which the 'self.result = ' will be written 670 | """ 671 | if tlobject.result.startswith('Vector<'): 672 | # Vector results are a bit special since they can also be composed 673 | # of integer values and such; however, the result of requests is 674 | # not parsed as arguments are and it's a bit harder to tell which 675 | # is which. 676 | if tlobject.result == 'Vector': 677 | builder.writeln('reader.read_int() # Vector id') 678 | builder.writeln('count = reader.read_int()') 679 | builder.writeln('self.result = [reader.read_int() for _ in range(count)]') 680 | 681 | elif tlobject.result == 'Vector': 682 | builder.writeln('reader.read_int() # Vector id') 683 | builder.writeln('count = reader.read_long()') 684 | builder.writeln('self.result = [reader.read_long() for _ in range(count)]') 685 | 686 | else: 687 | builder.writeln('self.result = reader.tgread_vector()') 688 | else: 689 | builder.writeln('self.result = reader.tgread_object()') 690 | -------------------------------------------------------------------------------- /telethon_tests/__init__.py: -------------------------------------------------------------------------------- 1 | from .crypto_test import CryptoTests 2 | from .network_test import NetworkTests 3 | from .parser_test import ParserTests 4 | from .tl_test import TLTests 5 | from .utils_test import UtilsTests 6 | -------------------------------------------------------------------------------- /telethon_tests/crypto_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from hashlib import sha1 3 | 4 | import telethon.helpers as utils 5 | from telethon.crypto import AES, Factorization 6 | 7 | 8 | class CryptoTests(unittest.TestCase): 9 | def setUp(self): 10 | # Test known values 11 | self.key = b'\xd1\xf4MXy\x0c\xf8/z,\xe9\xf9\xa4\x17\x04\xd9C\xc9\xaba\x81\xf3\xf8\xdd\xcb\x0c6\x92\x01\x1f\xc2y' 12 | self.iv = b':\x02\x91x\x90Dj\xa6\x03\x90C\x08\x9e@X\xb5E\xffwy\xf3\x1c\xde\xde\xfbo\x8dm\xd6e.Z' 13 | 14 | self.plain_text = b'Non encrypted text :D' 15 | self.plain_text_padded = b'My len is more uniform, promise!' 16 | 17 | self.cipher_text = b'\xb6\xa7\xec.\xb9\x9bG\xcb\xe9{\x91[\x12\xfc\x84D\x1c' \ 18 | b'\x93\xd9\x17\x03\xcd\xd6\xb1D?\x98\xd2\xb5\xa5U\xfd' 19 | 20 | self.cipher_text_padded = b"W\xd1\xed'\x01\xa6c\xc3\xcb\xef\xaa\xe5\x1d\x1a" \ 21 | b"[\x1b\xdf\xcdI\x1f>Z\n\t\xb9\xd2=\xbaF\xd1\x8e'" 22 | 23 | @staticmethod 24 | def test_sha1(): 25 | string = 'Example string' 26 | 27 | hash_sum = sha1(string.encode('utf-8')).digest() 28 | expected = b'\nT\x92|\x8d\x06:)\x99\x04\x8e\xf8j?\xc4\x8e\xd3}m9' 29 | 30 | assert hash_sum == expected, 'Invalid sha1 hash_sum representation (should be {}, but is {})'\ 31 | .format(expected, hash_sum) 32 | 33 | def test_aes_encrypt(self): 34 | value = AES.encrypt_ige(self.plain_text, self.key, self.iv) 35 | take = 16 # Don't take all the bytes, since latest involve are random padding 36 | assert value[:take] == self.cipher_text[:take],\ 37 | ('Ciphered text ("{}") does not equal expected ("{}")' 38 | .format(value[:take], self.cipher_text[:take])) 39 | 40 | value = AES.encrypt_ige(self.plain_text_padded, self.key, self.iv) 41 | assert value == self.cipher_text_padded, ( 42 | 'Ciphered text ("{}") does not equal expected ("{}")' 43 | .format(value, self.cipher_text_padded)) 44 | 45 | def test_aes_decrypt(self): 46 | # The ciphered text must always be padded 47 | value = AES.decrypt_ige(self.cipher_text_padded, self.key, self.iv) 48 | assert value == self.plain_text_padded, ( 49 | 'Decrypted text ("{}") does not equal expected ("{}")' 50 | .format(value, self.plain_text_padded)) 51 | 52 | @staticmethod 53 | def test_calc_key(): 54 | shared_key = b'\xbc\xd2m\xb7\xcav\xf4][\x88\x83\' \xf3\x11\x8as\xd04\x941\xae' \ 55 | b'*O\x03\x86\x9a/H#\x1a\x8c\xb5j\xe9$\xe0IvCm^\xe70\x1a5C\t\x16' \ 56 | b'\x03\xd2\x9d\xa9\x89\xd6\xce\x08P\x0fdr\xa0\xb3\xeb\xfecv\x1a' \ 57 | b'\xdfJ\x14\x96\x98\x16\xa3G\xab\x04\x14!\\\xeb\n\xbcn\xdf\xc4%' \ 58 | b'\xc6\t\xb7\x16\x14\x9c\'\x81\x15=\xb0\xaf\x0e\x0bR\xaa\x0466s' \ 59 | b'\xf0\xcf\xb7\xb8>,D\x94x\xd7\xf8\xe0\x84\xcb%\xd3\x05\xb2\xe8' \ 60 | b'\x95Mr?\xa2\xe8In\xf9\x0b[E\x9b\xaa\x0cX\x7f\x0ei\xde\xeed\x1d' \ 61 | b'x/J\xce\xea^}0;\xa83B\xbbR\xa1\xbfe\x04\xb9\x1e\xa1"f=\xa5M@' \ 62 | b'\x9e\xdd\x81\x80\xc9\xa5\xfb\xfcg\xdd\x15\x03p!\x0ffD\x16\x892' \ 63 | b'\xea\xca\xb1A\x99O\xa94P\xa9\xa2\xc6;\xb2C9\x1dC5\xd2\r\xecL' \ 64 | b'\xd9\xabw-\x03\ry\xc2v\x17]\x02\x15\x0cBa\x97\xce\xa5\xb1\xe4]' \ 65 | b'\x8e\xe0,\xcfC{o\xfa\x99f\xa4pM\x00' 66 | 67 | # Calculate key being the client 68 | msg_key = b'\xba\x1a\xcf\xda\xa8^Cbl\xfa\xb6\x0c:\x9b\xb0\xfc' 69 | 70 | key, iv = utils.calc_key(shared_key, msg_key, client=True) 71 | expected_key = b"\xaf\xe3\x84Qm\xe0!\x0c\xd91\xe4\x9a\xa0v_gc" \ 72 | b"x\xa1\xb0\xc9\xbc\x16'v\xcf,\x9dM\xae\xc6\xa5" 73 | 74 | expected_iv = b'\xb8Q\xf3\xc5\xa3]\xc6\xdf\x9e\xe0Q\xbd"\x8d' \ 75 | b'\x13\t\x0e\x9a\x9d^8\xa2\xf8\xe7\x00w\xd9\xc1' \ 76 | b'\xa7\xa0\xf7\x0f' 77 | 78 | assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format( 79 | expected_key, key) 80 | assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format( 81 | expected_iv, iv) 82 | 83 | # Calculate key being the server 84 | msg_key = b'\x86m\x92i\xcf\x8b\x93\xaa\x86K\x1fi\xd04\x83]' 85 | 86 | key, iv = utils.calc_key(shared_key, msg_key, client=False) 87 | expected_key = b'\xdd0X\xb6\x93\x8e\xc9y\xef\x83\xf8\x8cj' \ 88 | b'\xa7h\x03\xe2\xc6\xb16\xc5\xbb\xfc\xe7' \ 89 | b'\xdf\xd6\xb1g\xf7u\xcfk' 90 | 91 | expected_iv = b'\xdcL\xc2\x18\x01J"X\x86lb\xb6\xb547\xfd' \ 92 | b'\xe2a4\xb6\xaf}FS\xd7[\xe0N\r\x19\xfb\xbc' 93 | 94 | assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format( 95 | expected_key, key) 96 | assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format( 97 | expected_iv, iv) 98 | 99 | @staticmethod 100 | def test_calc_msg_key(): 101 | value = utils.calc_msg_key(b'Some random message') 102 | expected = b'\xdfAa\xfc\x10\xab\x89\xd2\xfe\x19C\xf1\xdd~\xbf\x81' 103 | assert value == expected, 'Value ("{}") does not equal expected ("{}")'.format( 104 | value, expected) 105 | 106 | @staticmethod 107 | def test_generate_key_data_from_nonce(): 108 | server_nonce = b'I am the server nonce.' 109 | new_nonce = b'I am a new calculated nonce.' 110 | 111 | key, iv = utils.generate_key_data_from_nonce(server_nonce, new_nonce) 112 | expected_key = b'?\xc4\xbd\xdf\rWU\x8a\xf5\x0f+V\xdc\x96up\x1d\xeeG\x00\x81|\x1eg\x8a\x8f{\xf0y\x80\xda\xde' 113 | expected_iv = b'Q\x9dpZ\xb7\xdd\xcb\x82_\xfa\xf4\x90\xecn\x10\x9cD\xd2\x01\x8d\x83\xa0\xa4^\xb8\x91,\x7fI am' 114 | 115 | assert key == expected_key, 'Key ("{}") does not equal expected ("{}")'.format( 116 | key, expected_key) 117 | assert iv == expected_iv, 'Key ("{}") does not equal expected ("{}")'.format( 118 | key, expected_iv) 119 | 120 | @staticmethod 121 | def test_factorize(): 122 | pq = 3118979781119966969 123 | p, q = Factorization.factorize(pq) 124 | 125 | assert p == 1719614201, 'Factorized pair did not yield the correct result' 126 | assert q == 1813767169, 'Factorized pair did not yield the correct result' 127 | -------------------------------------------------------------------------------- /telethon_tests/network_test.py: -------------------------------------------------------------------------------- 1 | import random 2 | import socket 3 | import threading 4 | import unittest 5 | 6 | import telethon.network.authenticator as authenticator 7 | from telethon.extensions import TcpClient 8 | from telethon.network import TcpTransport 9 | 10 | 11 | def run_server_echo_thread(port): 12 | def server_thread(): 13 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 14 | s.bind(('', port)) 15 | s.listen(1) 16 | connection, address = s.accept() 17 | with connection: 18 | data = connection.recv(16) 19 | connection.send(data) 20 | 21 | server = threading.Thread(target=server_thread) 22 | server.start() 23 | 24 | 25 | class NetworkTests(unittest.TestCase): 26 | @staticmethod 27 | def test_tcp_client(): 28 | port = random.randint(50000, 60000) # Arbitrary non-privileged port 29 | run_server_echo_thread(port) 30 | 31 | msg = b'Unit testing...' 32 | client = TcpClient() 33 | client.connect('localhost', port) 34 | client.write(msg) 35 | assert msg == client.read( 36 | 15), 'Read message does not equal sent message' 37 | client.close() 38 | 39 | @staticmethod 40 | def test_authenticator(): 41 | transport = TcpTransport('149.154.167.91', 443) 42 | authenticator.do_authentication(transport) 43 | transport.close() 44 | -------------------------------------------------------------------------------- /telethon_tests/parser_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class ParserTests(unittest.TestCase): 5 | """There are no tests yet""" 6 | -------------------------------------------------------------------------------- /telethon_tests/tl_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class TLTests(unittest.TestCase): 5 | """There are no tests yet""" 6 | -------------------------------------------------------------------------------- /telethon_tests/utils_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from telethon.extensions import BinaryReader, BinaryWriter 4 | 5 | 6 | class UtilsTests(unittest.TestCase): 7 | @staticmethod 8 | def test_binary_writer_reader(): 9 | # Test that we can write and read properly 10 | with BinaryWriter() as writer: 11 | writer.write_byte(1) 12 | writer.write_int(5) 13 | writer.write_long(13) 14 | writer.write_float(17.0) 15 | writer.write_double(25.0) 16 | writer.write(bytes([26, 27, 28, 29, 30, 31, 32])) 17 | writer.write_large_int(2**127, 128, signed=False) 18 | 19 | data = writer.get_bytes() 20 | expected = b'\x01\x05\x00\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\x00\x00\x88A\x00\x00\x00\x00\x00\x00' \ 21 | b'9@\x1a\x1b\x1c\x1d\x1e\x1f \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80' 22 | 23 | assert data == expected, 'Retrieved data does not match the expected value' 24 | 25 | with BinaryReader(data) as reader: 26 | value = reader.read_byte() 27 | assert value == 1, 'Example byte should be 1 but is {}'.format( 28 | value) 29 | 30 | value = reader.read_int() 31 | assert value == 5, 'Example integer should be 5 but is {}'.format( 32 | value) 33 | 34 | value = reader.read_long() 35 | assert value == 13, 'Example long integer should be 13 but is {}'.format( 36 | value) 37 | 38 | value = reader.read_float() 39 | assert value == 17.0, 'Example float should be 17.0 but is {}'.format( 40 | value) 41 | 42 | value = reader.read_double() 43 | assert value == 25.0, 'Example double should be 25.0 but is {}'.format( 44 | value) 45 | 46 | value = reader.read(7) 47 | assert value == bytes([26, 27, 28, 29, 30, 31, 32]), 'Example bytes should be {} but is {}' \ 48 | .format(bytes([26, 27, 28, 29, 30, 31, 32]), value) 49 | 50 | value = reader.read_large_int(128, signed=False) 51 | assert value == 2**127, 'Example large integer should be {} but is {}'.format( 52 | 2**127, value) 53 | 54 | # Test Telegram that types are written right 55 | with BinaryWriter() as writer: 56 | writer.write_int(0x60469778) 57 | buffer = writer.get_bytes() 58 | valid = b'\x78\x97\x46\x60' # Tested written bytes using C#'s MemoryStream 59 | 60 | assert buffer == valid, 'Written type should be {} but is {}'.format( 61 | list(valid), list(buffer)) 62 | 63 | @staticmethod 64 | def test_binary_tgwriter_tgreader(): 65 | small_data = os.urandom(33) 66 | small_data_padded = os.urandom( 67 | 19) # +1 byte for length = 20 (evenly divisible by 4) 68 | 69 | large_data = os.urandom(999) 70 | large_data_padded = os.urandom(1024) 71 | 72 | data = (small_data, small_data_padded, large_data, large_data_padded) 73 | string = 'Testing Telegram strings, this should work properly!' 74 | 75 | with BinaryWriter() as writer: 76 | # First write the data 77 | for datum in data: 78 | writer.tgwrite_bytes(datum) 79 | writer.tgwrite_string(string) 80 | 81 | with BinaryReader(writer.get_bytes()) as reader: 82 | # And then try reading it without errors (it should be unharmed!) 83 | for datum in data: 84 | value = reader.tgread_bytes() 85 | assert value == datum, 'Example bytes should be {} but is {}'.format( 86 | datum, value) 87 | 88 | value = reader.tgread_string() 89 | assert value == string, 'Example string should be {} but is {}'.format( 90 | string, value) 91 | -------------------------------------------------------------------------------- /trader.py: -------------------------------------------------------------------------------- 1 | 2 | from telethon import TelegramClient 3 | from telethon.tl.types import UpdateNewMessage 4 | from telethon.tl.types import InputPeerChat 5 | from telethon.tl.types import Message 6 | from telethon.tl.types.input_peer_self import InputPeerSelf 7 | from telethon.tl.types.input_peer_chat import InputPeerChat 8 | from telethon.tl.types.input_peer_channel import InputPeerChannel 9 | from telethon.tl.functions.messages.forward_message import ForwardMessageRequest 10 | import multiprocessing, sys, time, json, requests, re, unicodedata, subprocess, os, sqlite3, threading 11 | from subprocess import PIPE,Popen,STDOUT 12 | from decimal import * 13 | 14 | global flag 15 | global variable 16 | pid = 'coin.run' 17 | print('CryptoAlert Auto-Trader, with live updates...') 18 | print('CryptoAlert Auto-Trader, with live updates...') 19 | print('CryptoAlert Auto-Trader, with live updates...') 20 | print('CTRL-C To exit') 21 | print('CTRL-C To exit') 22 | print('CTRL-C To exit') 23 | print('To test me, type a coin into the cryptoping telegram bot window on telegram such as #LTC and #DASH') 24 | print('The Telegram updates in a while loop, and creates a pid equialent... delete coin.run if exiting in the middle of a sell signal') 25 | threads = [] 26 | flag = "test" 27 | variable = "test" 28 | api_id = 189914 29 | api_hash = '75b1fbdede4c49f7b7ca4a8681d5dfdf' 30 | # 'session_id' can be 'your_name'. It'll be saved as your_name.session 31 | client = TelegramClient('session_id', api_id, api_hash) 32 | client.connect() 33 | 34 | # PUT YOUR PHONE NUMBER ASSICOATED WITH TELEGRAM BELOW google voice works... 35 | if not client.is_user_authorized(): 36 | client.send_code_request('+14698447320') 37 | client.sign_in('+14698447320', input('Enter code: ')) 38 | # Now you can use the connected client as you wish 39 | 40 | def generate_random_long(): 41 | import random 42 | return random.choice(range(0,10000000)) 43 | 44 | 45 | 46 | 47 | def update_handler(d): 48 | global flag 49 | global variable 50 | # On this example, we just show the update object itself 51 | d = str(d) 52 | #testChannel 53 | re1 = '( id: )(?:[0-9][0-9]+)(,)' 54 | 55 | rg = re.compile(re1,re.IGNORECASE|re.DOTALL) 56 | m = rg.search(d) 57 | if m: 58 | word1=m.group(0) 59 | word2=word1.replace(' id: ', '') 60 | word3=word2.replace(',', '') 61 | word4=word3 62 | idd = int(word4) 63 | peer1 = InputPeerSelf() 64 | #INPUT YOUR KEYWORDS BELOW 65 | word_list = ["#DCR", "#LTC", "#NAUT", "#NXT", "#XCP", "#GRC", "#REP", "#PPC", "#RIC", "#STRAT", "#GAME", "#BTM", "#CLAM", "#ARDR", "#BLK", "#OMNI", "#SJCX", "#FLDC", "#BCH", "#POT", "#VRC", "#ETH", "#PINK", "#NOTE", "#BTS", "#AMP", "#NAV", "#BELA", "#ETC", "#FLO", "#VIA", "#XBC", "#XPM", "#DASH", "#XVC", "#GNO", "#NMC", "#RADS", "#VTC", "#XEM", "#FCT", "#XRP", "#NXC", "#STEEM", "#SBD", "#BURST", "#XMR", "#DGB", "#LBC", "#BCY", "#PASC", "#LSK", "#EXP", "#MAID", "#BTCD", "#SYS", "#GNT", "#HUC", "#EMC2", "#NEOS", "#ZEC", "#STR"] 66 | regex_string = "(?<=\W)(%s)(?=\W)" % "|".join(word_list) 67 | finder = re.compile(regex_string) 68 | string_to_be_searched = d 69 | results = finder.findall(" %s " % string_to_be_searched) 70 | result_set = set(results) 71 | print(idd) 72 | for word in word_list: 73 | if word in result_set: 74 | try: 75 | var = word 76 | var1 = var.replace('#', '') 77 | btc = '-BTC' 78 | variable = var1 + btc 79 | if (os.path.isfile(pid)): 80 | print('Waiting on current process to finish... If you experience errors, delete process.run') 81 | else: 82 | sell = 'notready' 83 | m = multiprocessing.Process(target = runitt , args = ()) 84 | m.start() 85 | client(ForwardMessageRequest(peer=peer1, id=(idd), random_id=(generate_random_long()))) 86 | except Exception as e: 87 | print(e) 88 | 89 | def create_process(): 90 | return multiprocessing.Process(target = runitt , args = ()) 91 | 92 | def runitt(): 93 | open(pid, 'w').close() 94 | global variable 95 | variable=str(variable) 96 | variablestr=str(variable) 97 | print('Starting Trade of: ' + variablestr) 98 | process0='./zenbot.sh trade poloniex.' + variablestr 99 | subprocess.Popen(process0,shell=True) 100 | time.sleep(3600) 101 | print('Starting node kill process) 102 | process1='sudo killall node' 103 | subprocess.Popen(process1,shell=True) 104 | print('Starting final sell Of:' + variablestr + ' In Case of Error') 105 | process2='./zenbot.sh sell --order_adjust_time=1000000000 --sell_pct=100 --markup_pct=0 poloniex.' + variablestr 106 | proc2 = subprocess.Popen(process2,shell=True) 107 | proc2.communicate() 108 | print('Starting final sell Of:' + variablestr + ' In Case of Error') 109 | process3='./zenbot.sh sell --order_adjust_time=1000000000 --sell_pct=100 --markup_pct=0 poloniex.' + variablestr 110 | proc3 = subprocess.Popen(process3,shell=True) 111 | proc3.communicate() 112 | os.remove(pid) 113 | print('Done running loop, process file deleted. Waiting for another coin...') 114 | # From now on, any update received will be passed to 'update_handler' NOTE... Later, Zenbot will be modified to cancel on order adjust. 115 | while True: 116 | client.add_update_handler(update_handler) 117 | input('Press to exit...') 118 | client.disconnect() 119 | 120 | 121 | 122 | 123 | 124 | --------------------------------------------------------------------------------