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/
api/settings
# Quick tests should live in this file
example.py
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

View File

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

View File

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

View File

@ -38,6 +38,10 @@ class ForbiddenError(RPCError):
code = 403
message = 'FORBIDDEN'
def __init__(self, message):
super().__init__(self, message)
self.message = message
class NotFoundError(RPCError):
"""
@ -46,6 +50,10 @@ class NotFoundError(RPCError):
code = 404
message = 'NOT_FOUND'
def __init__(self, message):
super().__init__(self, message)
self.message = message
class FloodError(RPCError):
"""
@ -67,6 +75,10 @@ class ServerError(RPCError):
code = 500
message = 'INTERNAL'
def __init__(self, message):
super().__init__(self, message)
self.message = message
class BadMessageError(Exception):
"""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()
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)
gab = pow(ga, b, dh_prime)

View File

@ -113,11 +113,13 @@ class MtProtoSender:
self._logger.info('Request result received')
self._logger.debug('receive() released the lock')
def receive_update(self, timeout=timedelta(seconds=5)):
"""Receives an update object and returns its result"""
def receive_updates(self, timeout=timedelta(seconds=5)):
"""Receives one or more update objects
and returns them as a list
"""
updates = []
self.receive(timeout=timeout, updates=updates)
return updates[0]
return updates
def cancel_receive(self):
"""Cancels any pending receive operation
@ -131,13 +133,13 @@ class MtProtoSender:
def _send_packet(self, packet, request):
"""Sends the given packet bytes with the additional
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
with BinaryWriter() as plain_writer:
plain_writer.write_long(self.session.salt, 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(
self.session.generate_sequence(request.confirmed))
@ -221,7 +223,7 @@ class MtProtoSender:
if code == 0x62d6b459:
ack = reader.tgread_object()
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')
if self.logging_out:
@ -257,7 +259,7 @@ class MtProtoSender:
try:
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')
request.confirm_received = True
@ -294,7 +296,7 @@ class MtProtoSender:
try:
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)
except StopIteration: pass
@ -328,7 +330,7 @@ class MtProtoSender:
try:
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
except StopIteration:

View File

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

View File

@ -11,7 +11,7 @@ from .errors import (RPCError, UnauthorizedError, InvalidParameterError,
ReadCancelledError, FileMigrateError, PhoneMigrateError,
NetworkMigrateError, UserMigrateError, PhoneCodeEmptyError,
PhoneCodeExpiredError, PhoneCodeHashEmptyError,
PhoneCodeInvalidError)
PhoneCodeInvalidError, InvalidChecksumError)
# For sending and receiving requests
from .tl import MTProtoRequest, Session, JsonSession
@ -145,21 +145,13 @@ class TelegramClient(TelegramBareClient):
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
# 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.
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,
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
# for clearly showing how to export the authorization! ^^
client = self._cached_clients.get(dc_id)
if client:
if client and not bypass_cache:
if init_connection:
client.reconnect()
return client
@ -185,16 +181,46 @@ class TelegramClient(TelegramBareClient):
# Create a temporary session for this IP address, which needs
# 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.port = dc.port
client = TelegramBareClient(session, self.api_id, self.api_hash)
client.connect(exported_auth=export_auth)
# Don't go through this expensive process every time.
self._cached_clients[dc_id] = client
if not bypass_cache:
# Don't go through this expensive process every time.
self._cached_clients[dc_id] = 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
# region Telegram requests functions
@ -447,8 +473,10 @@ class TelegramClient(TelegramBareClient):
total_messages = getattr(result, 'count', len(result.messages))
# Iterate over all the messages and find the sender User
entities = [find_user_or_chat(msg.from_id, result.users, result.chats)
for msg in result.messages]
entities = [find_user_or_chat(m.from_id, result.users, result.chats)
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
@ -474,6 +502,8 @@ class TelegramClient(TelegramBareClient):
# endregion
# region Uploading files
def send_photo_file(self, input_file, entity, caption=''):
"""Sends a previously uploaded input_file
(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')
try:
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._logger.info('Received update from the updates thread')
for handler in self._update_handlers:
handler(result)
self._logger.info(
'Received {} update(s) from the updates thread'
.format(len(updates))
)
for update in updates:
for handler in self._update_handlers:
handler(update)
except ConnectionResetError:
self._logger.info('Server disconnected us. Reconnecting...')
@ -790,6 +826,14 @@ class TelegramClient(TelegramBareClient):
except ReadCancelledError:
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:
self._logger.warning('OSError on updates thread, %s logging out',
'was' if self.sender.logging_out else 'was not')

View File

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

View File

@ -99,14 +99,28 @@ class JsonSession:
through an official Telegram client to revoke the authorization.
"""
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
self.session_user_id = session_user_id
if isinstance(session_user_id, JsonSession):
self.session_user_id = None
# For connection purposes
self.device_model = platform.node()
self.system_version = platform.system()
self.app_version = '0'
self.lang_code = 'en'
# For connection purposes
session = session_user_id
self.device_model = session.device_model
self.system_version = session.system_version
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
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 .tl.types import (
Channel, Chat, ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser,
MessageMediaDocument, MessageMediaPhoto, PeerChannel, PeerChat, PeerUser,
User, UserProfilePhoto)
Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull,
ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, InputPeerEmpty,
InputPeerSelf, MessageMediaDocument, MessageMediaPhoto, PeerChannel,
PeerChat, PeerUser, User, UserFull, UserProfilePhoto)
def get_display_name(entity):
@ -51,18 +52,27 @@ def get_extension(media):
def get_input_peer(entity):
"""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."""
if (isinstance(entity, InputPeerUser) or
isinstance(entity, InputPeerChat) or
isinstance(entity, InputPeerChannel)):
if type(entity).subclass_of_id == 0xc91c90b6: # crc32('InputUser')
return entity
if isinstance(entity, User):
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)
if isinstance(entity, Channel):
if any(isinstance(entity, c) for c in (
Channel, ChannelForbidden)):
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.'
.format(type(entity).__name__))

View File

@ -1,4 +1,3 @@
import shutil
from getpass import getpass
from telethon import TelegramClient
@ -6,9 +5,6 @@ from telethon.errors import SessionPasswordNeededError
from telethon.tl.types import UpdateShortChatMessage, UpdateShortMessage
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):
"""Safe Print (handle UnicodeEncodeErrors on some terminals)"""
@ -47,7 +43,8 @@ class InteractiveTelegramClient(TelegramClient):
Telegram through Telethon, such as listing dialogs (open chats),
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('Initializing interactive example...')
@ -76,7 +73,7 @@ class InteractiveTelegramClient(TelegramClient):
self_user = self.sign_in(user_phone, code)
# Two-step verification may be enabled
except SessionPasswordNeededError as e:
except SessionPasswordNeededError:
pw = getpass('Two step verification is enabled. '
'Please enter your password: ')
@ -100,8 +97,7 @@ class InteractiveTelegramClient(TelegramClient):
print_title('Dialogs window')
# Display them so the user can choose
for i, entity in enumerate(entities):
i += 1 # 1-based index
for i, entity in enumerate(entities, start=1):
sprint('{}. {}'.format(i, get_display_name(entity)))
# Let the user decide who they want to talk to
@ -120,7 +116,7 @@ class InteractiveTelegramClient(TelegramClient):
try:
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:
i = None
except ValueError:
@ -162,7 +158,14 @@ class InteractiveTelegramClient(TelegramClient):
for msg, sender in zip(
reversed(messages), reversed(senders)):
# 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
if getattr(msg, 'media', None):

View File

@ -2,6 +2,7 @@
import os
import re
import shutil
from zlib import crc32
from collections import defaultdict
try:
@ -186,6 +187,13 @@ class TLGenerator:
builder.writeln('from {}.tl.mtproto_request import MTProtoRequest'
.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):
# Currently only 'random_id' needs 'os' to be imported
builder.writeln('import os')
@ -207,6 +215,9 @@ class TLGenerator:
# Class-level variable to store its constructor ID
builder.writeln("# Telegram's constructor (U)ID for this class")
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()
# Flag arguments must go last
@ -306,6 +317,10 @@ class TLGenerator:
)
else:
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:
builder.writeln('self.{0} = {0}'.format(arg.name))

View File

@ -24,11 +24,18 @@ def load_settings(path='api/settings'):
if __name__ == '__main__':
# Load the settings and initialize the client
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(
session_user_id=str(settings.get('session_name', 'anonymous')),
user_phone=str(settings['user_phone']),
api_id=settings['api_id'],
api_hash=str(settings['api_hash']))
api_hash=str(settings['api_hash']),
**kwargs)
print('Initialization done!')