mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-05 04:30:22 +03:00
Merge branch 'master' of git://github.com/LonamiWebs/Telethon
This commit is contained in:
commit
8ada0e176d
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -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]
|
||||
|
|
1
setup.py
1
setup.py
|
@ -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?
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -47,7 +47,7 @@ class TelegramBareClient:
|
|||
"""
|
||||
|
||||
# Current TelegramClient version
|
||||
__version__ = '0.10.1'
|
||||
__version__ = '0.11'
|
||||
|
||||
# region Initialization
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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__))
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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!')
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user