Merge branch 'master' of git://github.com/LonamiWebs/Telethon

This commit is contained in:
Christian Stemmle 2017-06-19 17:40:29 +02:00
commit 8ada0e176d
14 changed files with 176 additions and 65 deletions

3
.gitignore vendored
View File

@ -8,6 +8,9 @@ telethon/tl/all_tlobjects.py
usermedia/ usermedia/
api/settings api/settings
# Quick tests should live in this file
example.py
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]

View File

@ -60,6 +60,7 @@ setup(
'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6'
], ],
# What does your project relate to? # What does your project relate to?

View File

@ -32,12 +32,12 @@ def rpc_message_to_error(code, message):
return cls(extra=extra) return cls(extra=extra)
elif code == 403: elif code == 403:
return ForbiddenError() return ForbiddenError(message)
elif code == 404: elif code == 404:
return NotFoundError() return NotFoundError(message)
elif code == 500: elif code == 500:
return ServerError() return ServerError(message)
return RPCError('{} (code {})'.format(message, code)) return RPCError('{} (code {})'.format(message, code))

View File

@ -38,6 +38,10 @@ class ForbiddenError(RPCError):
code = 403 code = 403
message = 'FORBIDDEN' message = 'FORBIDDEN'
def __init__(self, message):
super().__init__(self, message)
self.message = message
class NotFoundError(RPCError): class NotFoundError(RPCError):
""" """
@ -46,6 +50,10 @@ class NotFoundError(RPCError):
code = 404 code = 404
message = 'NOT_FOUND' message = 'NOT_FOUND'
def __init__(self, message):
super().__init__(self, message)
self.message = message
class FloodError(RPCError): class FloodError(RPCError):
""" """
@ -67,6 +75,10 @@ class ServerError(RPCError):
code = 500 code = 500
message = 'INTERNAL' message = 'INTERNAL'
def __init__(self, message):
super().__init__(self, message)
self.message = message
class BadMessageError(Exception): class BadMessageError(Exception):
"""Occurs when handling a bad_message_notification""" """Occurs when handling a bad_message_notification"""

View File

@ -148,7 +148,7 @@ def do_authentication(transport):
server_time = dh_inner_data_reader.read_int() server_time = dh_inner_data_reader.read_int()
time_offset = server_time - int(time.time()) time_offset = server_time - int(time.time())
b = get_int(os.urandom(2048), signed=False) b = get_int(os.urandom(256), signed=False)
gb = pow(g, b, dh_prime) gb = pow(g, b, dh_prime)
gab = pow(ga, b, dh_prime) gab = pow(ga, b, dh_prime)

View File

@ -113,11 +113,13 @@ class MtProtoSender:
self._logger.info('Request result received') self._logger.info('Request result received')
self._logger.debug('receive() released the lock') self._logger.debug('receive() released the lock')
def receive_update(self, timeout=timedelta(seconds=5)): def receive_updates(self, timeout=timedelta(seconds=5)):
"""Receives an update object and returns its result""" """Receives one or more update objects
and returns them as a list
"""
updates = [] updates = []
self.receive(timeout=timeout, updates=updates) self.receive(timeout=timeout, updates=updates)
return updates[0] return updates
def cancel_receive(self): def cancel_receive(self):
"""Cancels any pending receive operation """Cancels any pending receive operation
@ -131,13 +133,13 @@ class MtProtoSender:
def _send_packet(self, packet, request): def _send_packet(self, packet, request):
"""Sends the given packet bytes with the additional """Sends the given packet bytes with the additional
information of the original request. This does NOT lock the threads!""" information of the original request. This does NOT lock the threads!"""
request.msg_id = self.session.get_new_msg_id() request.request_msg_id = self.session.get_new_msg_id()
# First calculate plain_text to encrypt it # First calculate plain_text to encrypt it
with BinaryWriter() as plain_writer: with BinaryWriter() as plain_writer:
plain_writer.write_long(self.session.salt, signed=False) plain_writer.write_long(self.session.salt, signed=False)
plain_writer.write_long(self.session.id, signed=False) plain_writer.write_long(self.session.id, signed=False)
plain_writer.write_long(request.msg_id) plain_writer.write_long(request.request_msg_id)
plain_writer.write_int( plain_writer.write_int(
self.session.generate_sequence(request.confirmed)) self.session.generate_sequence(request.confirmed))
@ -221,7 +223,7 @@ class MtProtoSender:
if code == 0x62d6b459: if code == 0x62d6b459:
ack = reader.tgread_object() ack = reader.tgread_object()
for r in self._pending_receive: for r in self._pending_receive:
if r.msg_id in ack.msg_ids: if r.request_msg_id in ack.msg_ids:
self._logger.warning('Ack found for the a request') self._logger.warning('Ack found for the a request')
if self.logging_out: if self.logging_out:
@ -257,7 +259,7 @@ class MtProtoSender:
try: try:
request = next(r for r in self._pending_receive request = next(r for r in self._pending_receive
if r.msg_id == received_msg_id) if r.request_msg_id == received_msg_id)
self._logger.warning('Pong confirmed a request') self._logger.warning('Pong confirmed a request')
request.confirm_received = True request.confirm_received = True
@ -294,7 +296,7 @@ class MtProtoSender:
try: try:
request = next(r for r in self._pending_receive request = next(r for r in self._pending_receive
if r.msg_id == bad_msg_id) if r.request_msg_id == bad_msg_id)
self.send(request) self.send(request)
except StopIteration: pass except StopIteration: pass
@ -328,7 +330,7 @@ class MtProtoSender:
try: try:
request = next(r for r in self._pending_receive request = next(r for r in self._pending_receive
if r.msg_id == request_id) if r.request_msg_id == request_id)
request.confirm_received = True request.confirm_received = True
except StopIteration: except StopIteration:

View File

@ -47,7 +47,7 @@ class TelegramBareClient:
""" """
# Current TelegramClient version # Current TelegramClient version
__version__ = '0.10.1' __version__ = '0.11'
# region Initialization # region Initialization

View File

@ -11,7 +11,7 @@ from .errors import (RPCError, UnauthorizedError, InvalidParameterError,
ReadCancelledError, FileMigrateError, PhoneMigrateError, ReadCancelledError, FileMigrateError, PhoneMigrateError,
NetworkMigrateError, UserMigrateError, PhoneCodeEmptyError, NetworkMigrateError, UserMigrateError, PhoneCodeEmptyError,
PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeExpiredError, PhoneCodeHashEmptyError,
PhoneCodeInvalidError) PhoneCodeInvalidError, InvalidChecksumError)
# For sending and receiving requests # For sending and receiving requests
from .tl import MTProtoRequest, Session, JsonSession from .tl import MTProtoRequest, Session, JsonSession
@ -145,21 +145,13 @@ class TelegramClient(TelegramBareClient):
self._cached_clients.clear() self._cached_clients.clear()
def reconnect(self, new_dc=None):
"""Disconnects and connects again (effectively reconnecting).
If 'new_dc' is not None, the current authorization key is
removed, the DC used is switched, and a new connection is made.
*args will be ignored.
"""
super(TelegramClient, self).reconnect(new_dc=new_dc)
# endregion # endregion
# region Working with different Data Centers # region Working with different connections
def _get_exported_client(self, dc_id, init_connection=False): def _get_exported_client(self, dc_id,
init_connection=False,
bypass_cache=False):
"""Gets a cached exported TelegramBareClient for the desired DC. """Gets a cached exported TelegramBareClient for the desired DC.
If it's the first time retrieving the TelegramBareClient, the If it's the first time retrieving the TelegramBareClient, the
@ -168,12 +160,16 @@ class TelegramClient(TelegramBareClient):
If after using the sender a ConnectionResetError is raised, If after using the sender a ConnectionResetError is raised,
this method should be called again with init_connection=True this method should be called again with init_connection=True
in order to perform the reconnection.""" in order to perform the reconnection.
If bypass_cache is True, a new client will be exported and
it will not be cached.
"""
# Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt # Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt
# for clearly showing how to export the authorization! ^^ # for clearly showing how to export the authorization! ^^
client = self._cached_clients.get(dc_id) client = self._cached_clients.get(dc_id)
if client: if client and not bypass_cache:
if init_connection: if init_connection:
client.reconnect() client.reconnect()
return client return client
@ -185,16 +181,46 @@ class TelegramClient(TelegramBareClient):
# Create a temporary session for this IP address, which needs # Create a temporary session for this IP address, which needs
# to be different because each auth_key is unique per DC. # to be different because each auth_key is unique per DC.
session = JsonSession(None) #
# Construct this session with the connection parameters
# (system version, device model...) from the current one.
session = JsonSession(self.session)
session.server_address = dc.ip_address session.server_address = dc.ip_address
session.port = dc.port session.port = dc.port
client = TelegramBareClient(session, self.api_id, self.api_hash) client = TelegramBareClient(session, self.api_id, self.api_hash)
client.connect(exported_auth=export_auth) client.connect(exported_auth=export_auth)
# Don't go through this expensive process every time. if not bypass_cache:
self._cached_clients[dc_id] = client # Don't go through this expensive process every time.
self._cached_clients[dc_id] = client
return client return client
def create_new_connection(self, on_dc=None):
"""Creates a new connection which can be used in parallel
with the original TelegramClient. A TelegramBareClient
will be returned already connected, and the caller is
responsible to disconnect it.
If 'on_dc' is None, the new client will run on the same
data center as the current client (most common case).
If the client is meant to be used on a different data
center, the data center ID should be specified instead.
Note that TelegramBareClients will not handle automatic
reconnection (i.e. switching to another data center to
download media), and InvalidDCError will be raised in
such case.
"""
if on_dc is None:
client = TelegramBareClient(self.session, self.api_id, self.api_hash,
proxy=self.proxy)
client.connect()
else:
client = self._get_exported_client(on_dc, bypass_cache=True)
return client
# endregion # endregion
# region Telegram requests functions # region Telegram requests functions
@ -447,8 +473,10 @@ class TelegramClient(TelegramBareClient):
total_messages = getattr(result, 'count', len(result.messages)) total_messages = getattr(result, 'count', len(result.messages))
# Iterate over all the messages and find the sender User # Iterate over all the messages and find the sender User
entities = [find_user_or_chat(msg.from_id, result.users, result.chats) entities = [find_user_or_chat(m.from_id, result.users, result.chats)
for msg in result.messages] if m.from_id is not None else
find_user_or_chat(m.to_id, result.users, result.chats)
for m in result.messages]
return total_messages, result.messages, entities return total_messages, result.messages, entities
@ -474,6 +502,8 @@ class TelegramClient(TelegramBareClient):
# endregion # endregion
# region Uploading files
def send_photo_file(self, input_file, entity, caption=''): def send_photo_file(self, input_file, entity, caption=''):
"""Sends a previously uploaded input_file """Sends a previously uploaded input_file
(which should be a photo) to the given entity (or input peer)""" (which should be a photo) to the given entity (or input peer)"""
@ -771,14 +801,20 @@ class TelegramClient(TelegramBareClient):
self._logger.debug('Updates thread acquired the lock') self._logger.debug('Updates thread acquired the lock')
try: try:
self._updates_thread_receiving.set() self._updates_thread_receiving.set()
self._logger.debug('Trying to receive updates from the updates thread') self._logger.debug(
'Trying to receive updates from the updates thread'
)
result = self.sender.receive_update(timeout=timeout) updates = self.sender.receive_updates(timeout=timeout)
self._updates_thread_receiving.clear() self._updates_thread_receiving.clear()
self._logger.info('Received update from the updates thread') self._logger.info(
for handler in self._update_handlers: 'Received {} update(s) from the updates thread'
handler(result) .format(len(updates))
)
for update in updates:
for handler in self._update_handlers:
handler(update)
except ConnectionResetError: except ConnectionResetError:
self._logger.info('Server disconnected us. Reconnecting...') self._logger.info('Server disconnected us. Reconnecting...')
@ -790,6 +826,14 @@ class TelegramClient(TelegramBareClient):
except ReadCancelledError: except ReadCancelledError:
self._logger.info('Receiving updates cancelled') self._logger.info('Receiving updates cancelled')
except BrokenPipeError:
self._logger.info('Tcp session is broken. Reconnecting...')
self.reconnect()
except InvalidChecksumError:
self._logger.info('MTProto session is broken. Reconnecting...')
self.reconnect()
except OSError: except OSError:
self._logger.warning('OSError on updates thread, %s logging out', self._logger.warning('OSError on updates thread, %s logging out',
'was' if self.sender.logging_out else 'was not') 'was' if self.sender.logging_out else 'was not')

View File

@ -5,7 +5,7 @@ class MTProtoRequest:
def __init__(self): def __init__(self):
self.sent = False self.sent = False
self.msg_id = 0 # Long self.request_msg_id = 0 # Long
self.sequence = 0 self.sequence = 0
self.dirty = False self.dirty = False

View File

@ -99,14 +99,28 @@ class JsonSession:
through an official Telegram client to revoke the authorization. through an official Telegram client to revoke the authorization.
""" """
def __init__(self, session_user_id): def __init__(self, session_user_id):
"""session_user_id should either be a string or another Session.
Note that if another session is given, only parameters like
those required to init a connection will be copied.
"""
# These values will NOT be saved # These values will NOT be saved
self.session_user_id = session_user_id if isinstance(session_user_id, JsonSession):
self.session_user_id = None
# For connection purposes # For connection purposes
self.device_model = platform.node() session = session_user_id
self.system_version = platform.system() self.device_model = session.device_model
self.app_version = '0' self.system_version = session.system_version
self.lang_code = 'en' self.app_version = session.app_version
self.lang_code = session.lang_code
else: # str / None
self.session_user_id = session_user_id
self.device_model = platform.node()
self.system_version = platform.system()
self.app_version = '1.0' # note: '0' will provoke error
self.lang_code = 'en'
# Cross-thread safety # Cross-thread safety
self._lock = Lock() self._lock = Lock()

View File

@ -5,9 +5,10 @@ to convert between an entity like an User, Chat, etc. into its Input version)
from mimetypes import add_type, guess_extension from mimetypes import add_type, guess_extension
from .tl.types import ( from .tl.types import (
Channel, Chat, ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull,
MessageMediaDocument, MessageMediaPhoto, PeerChannel, PeerChat, PeerUser, ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, InputPeerEmpty,
User, UserProfilePhoto) InputPeerSelf, MessageMediaDocument, MessageMediaPhoto, PeerChannel,
PeerChat, PeerUser, User, UserFull, UserProfilePhoto)
def get_display_name(entity): def get_display_name(entity):
@ -51,18 +52,27 @@ def get_extension(media):
def get_input_peer(entity): def get_input_peer(entity):
"""Gets the input peer for the given "entity" (user, chat or channel). """Gets the input peer for the given "entity" (user, chat or channel).
A ValueError is raised if the given entity isn't a supported type.""" A ValueError is raised if the given entity isn't a supported type."""
if (isinstance(entity, InputPeerUser) or if type(entity).subclass_of_id == 0xc91c90b6: # crc32('InputUser')
isinstance(entity, InputPeerChat) or
isinstance(entity, InputPeerChannel)):
return entity return entity
if isinstance(entity, User): if isinstance(entity, User):
return InputPeerUser(entity.id, entity.access_hash) return InputPeerUser(entity.id, entity.access_hash)
if isinstance(entity, Chat):
if any(isinstance(entity, c) for c in (
Chat, ChatEmpty, ChatForbidden)):
return InputPeerChat(entity.id) return InputPeerChat(entity.id)
if isinstance(entity, Channel):
if any(isinstance(entity, c) for c in (
Channel, ChannelForbidden)):
return InputPeerChannel(entity.id, entity.access_hash) return InputPeerChannel(entity.id, entity.access_hash)
# Less common cases
if isinstance(entity, UserFull):
return InputPeerUser(entity.user.id, entity.user.access_hash)
if isinstance(entity, ChatFull):
return InputPeerChat(entity.id)
raise ValueError('Cannot cast {} to any kind of InputPeer.' raise ValueError('Cannot cast {} to any kind of InputPeer.'
.format(type(entity).__name__)) .format(type(entity).__name__))

View File

@ -1,4 +1,3 @@
import shutil
from getpass import getpass from getpass import getpass
from telethon import TelegramClient from telethon import TelegramClient
@ -6,9 +5,6 @@ from telethon.errors import SessionPasswordNeededError
from telethon.tl.types import UpdateShortChatMessage, UpdateShortMessage from telethon.tl.types import UpdateShortChatMessage, UpdateShortMessage
from telethon.utils import get_display_name from telethon.utils import get_display_name
# Get the (current) number of lines in the terminal
cols, rows = shutil.get_terminal_size()
def sprint(string, *args, **kwargs): def sprint(string, *args, **kwargs):
"""Safe Print (handle UnicodeEncodeErrors on some terminals)""" """Safe Print (handle UnicodeEncodeErrors on some terminals)"""
@ -47,7 +43,8 @@ class InteractiveTelegramClient(TelegramClient):
Telegram through Telethon, such as listing dialogs (open chats), Telegram through Telethon, such as listing dialogs (open chats),
talking to people, downloading media, and receiving updates. talking to people, downloading media, and receiving updates.
""" """
def __init__(self, session_user_id, user_phone, api_id, api_hash, proxy=None): def __init__(self, session_user_id, user_phone, api_id, api_hash,
proxy=None):
print_title('Initialization') print_title('Initialization')
print('Initializing interactive example...') print('Initializing interactive example...')
@ -76,7 +73,7 @@ class InteractiveTelegramClient(TelegramClient):
self_user = self.sign_in(user_phone, code) self_user = self.sign_in(user_phone, code)
# Two-step verification may be enabled # Two-step verification may be enabled
except SessionPasswordNeededError as e: except SessionPasswordNeededError:
pw = getpass('Two step verification is enabled. ' pw = getpass('Two step verification is enabled. '
'Please enter your password: ') 'Please enter your password: ')
@ -100,8 +97,7 @@ class InteractiveTelegramClient(TelegramClient):
print_title('Dialogs window') print_title('Dialogs window')
# Display them so the user can choose # Display them so the user can choose
for i, entity in enumerate(entities): for i, entity in enumerate(entities, start=1):
i += 1 # 1-based index
sprint('{}. {}'.format(i, get_display_name(entity))) sprint('{}. {}'.format(i, get_display_name(entity)))
# Let the user decide who they want to talk to # Let the user decide who they want to talk to
@ -120,7 +116,7 @@ class InteractiveTelegramClient(TelegramClient):
try: try:
i = int(i if i else 0) - 1 i = int(i if i else 0) - 1
# Ensure it is inside the bounds, otherwise set to None and retry # Ensure it is inside the bounds, otherwise retry
if not 0 <= i < dialog_count: if not 0 <= i < dialog_count:
i = None i = None
except ValueError: except ValueError:
@ -162,7 +158,14 @@ class InteractiveTelegramClient(TelegramClient):
for msg, sender in zip( for msg, sender in zip(
reversed(messages), reversed(senders)): reversed(messages), reversed(senders)):
# Get the name of the sender if any # Get the name of the sender if any
name = sender.first_name if sender else '???' if sender:
name = getattr(sender, 'first_name', None)
if not name:
name = getattr(sender, 'title')
if not name:
name = '???'
else:
name = '???'
# Format the message content # Format the message content
if getattr(msg, 'media', None): if getattr(msg, 'media', None):

View File

@ -2,6 +2,7 @@
import os import os
import re import re
import shutil import shutil
from zlib import crc32
from collections import defaultdict from collections import defaultdict
try: try:
@ -186,6 +187,13 @@ class TLGenerator:
builder.writeln('from {}.tl.mtproto_request import MTProtoRequest' builder.writeln('from {}.tl.mtproto_request import MTProtoRequest'
.format('.' * depth)) .format('.' * depth))
if tlobject.is_function and \
any(a for a in tlobject.args if a.type == 'InputPeer'):
# We can automatically convert a normal peer to an InputPeer,
# it will make invoking a lot of requests a lot simpler.
builder.writeln('from {}.utils import get_input_peer'
.format('.' * depth))
if any(a for a in tlobject.args if a.can_be_inferred): if any(a for a in tlobject.args if a.can_be_inferred):
# Currently only 'random_id' needs 'os' to be imported # Currently only 'random_id' needs 'os' to be imported
builder.writeln('import os') builder.writeln('import os')
@ -207,6 +215,9 @@ class TLGenerator:
# Class-level variable to store its constructor ID # Class-level variable to store its constructor ID
builder.writeln("# Telegram's constructor (U)ID for this class") builder.writeln("# Telegram's constructor (U)ID for this class")
builder.writeln('constructor_id = {}'.format(hex(tlobject.id))) builder.writeln('constructor_id = {}'.format(hex(tlobject.id)))
builder.writeln("# Also the ID of its resulting type for fast checks")
builder.writeln('subclass_of_id = {}'.format(
hex(crc32(tlobject.result.encode('ascii')))))
builder.writeln() builder.writeln()
# Flag arguments must go last # Flag arguments must go last
@ -306,6 +317,10 @@ class TLGenerator:
) )
else: else:
raise ValueError('Cannot infer a value for ', arg) raise ValueError('Cannot infer a value for ', arg)
elif arg.type == 'InputPeer' and tlobject.is_function:
# Well-known case, auto-cast it to the right type
builder.writeln(
'self.{0} = get_input_peer({0})'.format(arg.name))
else: else:
builder.writeln('self.{0} = {0}'.format(arg.name)) builder.writeln('self.{0} = {0}'.format(arg.name))

View File

@ -24,11 +24,18 @@ def load_settings(path='api/settings'):
if __name__ == '__main__': if __name__ == '__main__':
# Load the settings and initialize the client # Load the settings and initialize the client
settings = load_settings() settings = load_settings()
kwargs = {}
if settings.get('socks_proxy'):
import socks # $ pip install pysocks
host, port = settings['socks_proxy'].split(':')
kwargs = dict(proxy=(socks.SOCKS5, host, int(port)))
client = InteractiveTelegramClient( client = InteractiveTelegramClient(
session_user_id=str(settings.get('session_name', 'anonymous')), session_user_id=str(settings.get('session_name', 'anonymous')),
user_phone=str(settings['user_phone']), user_phone=str(settings['user_phone']),
api_id=settings['api_id'], api_id=settings['api_id'],
api_hash=str(settings['api_hash'])) api_hash=str(settings['api_hash']),
**kwargs)
print('Initialization done!') print('Initialization done!')