├── 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 |
--------------------------------------------------------------------------------