mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-06-22 13:06:37 +00:00
Clean-up TelegramBareClient - unnecessary?
This commit is contained in:
parent
a63580c350
commit
cc5753137c
@ -1,25 +1,16 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import platform
|
import platform
|
||||||
import threading
|
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
from signal import signal, SIGINT, SIGTERM, SIGABRT
|
|
||||||
from threading import Lock
|
|
||||||
from time import sleep
|
|
||||||
from . import version, utils
|
from . import version, utils
|
||||||
from .crypto import rsa
|
from .crypto import rsa
|
||||||
from .errors import (
|
from .extensions import markdown
|
||||||
RPCError, BrokenAuthKeyError, ServerError, FloodWaitError,
|
from .network import MTProtoSender, ConnectionTcpFull
|
||||||
FloodTestPhoneWaitError, TypeNotFoundError, UnauthorizedError,
|
|
||||||
PhoneMigrateError, NetworkMigrateError, UserMigrateError, AuthKeyError,
|
|
||||||
RpcCallFailError
|
|
||||||
)
|
|
||||||
from .network import authenticator, MTProtoSender, ConnectionTcpFull
|
|
||||||
from .sessions import Session, SQLiteSession
|
from .sessions import Session, SQLiteSession
|
||||||
from .tl import TLObject
|
from .tl import TLObject
|
||||||
from .tl.all_tlobjects import LAYER
|
from .tl.all_tlobjects import LAYER
|
||||||
from .tl.functions import (
|
from .tl.functions import (
|
||||||
InitConnectionRequest, InvokeWithLayerRequest, PingRequest
|
InitConnectionRequest, InvokeWithLayerRequest
|
||||||
)
|
)
|
||||||
from .tl.functions.auth import (
|
from .tl.functions.auth import (
|
||||||
ImportAuthorizationRequest, ExportAuthorizationRequest
|
ImportAuthorizationRequest, ExportAuthorizationRequest
|
||||||
@ -27,7 +18,6 @@ from .tl.functions.auth import (
|
|||||||
from .tl.functions.help import (
|
from .tl.functions.help import (
|
||||||
GetCdnConfigRequest, GetConfigRequest
|
GetCdnConfigRequest, GetConfigRequest
|
||||||
)
|
)
|
||||||
from .tl.functions.updates import GetStateRequest
|
|
||||||
from .tl.types.auth import ExportedAuthorization
|
from .tl.types.auth import ExportedAuthorization
|
||||||
from .update_state import UpdateState
|
from .update_state import UpdateState
|
||||||
|
|
||||||
@ -39,31 +29,95 @@ DEFAULT_PORT = 443
|
|||||||
__log__ = logging.getLogger(__name__)
|
__log__ = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO Do we need this class?
|
||||||
class TelegramBareClient:
|
class TelegramBareClient:
|
||||||
"""Bare Telegram Client with just the minimum -
|
"""
|
||||||
|
A bare Telegram client that somewhat eases the usage of the
|
||||||
|
``MTProtoSender``.
|
||||||
|
|
||||||
The reason to distinguish between a MtProtoSender and a
|
Args:
|
||||||
TelegramClient itself is because the sender is just that,
|
session (`str` | `telethon.sessions.abstract.Session`, `None`):
|
||||||
a sender, which should know nothing about Telegram but
|
The file name of the session file to be used if a string is
|
||||||
rather how to handle this specific connection.
|
given (it may be a full path), or the Session instance to be
|
||||||
|
used otherwise. If it's ``None``, the session will not be saved,
|
||||||
|
and you should call :meth:`.log_out()` when you're done.
|
||||||
|
|
||||||
The TelegramClient itself should know how to initialize
|
Note that if you pass a string it will be a file in the current
|
||||||
a proper connection to the servers, as well as other basic
|
working directory, although you can also pass absolute paths.
|
||||||
methods such as disconnection and reconnection.
|
|
||||||
|
|
||||||
This distinction between a bare client and a full client
|
The session file contains enough information for you to login
|
||||||
makes it possible to create clones of the bare version
|
without re-sending the code, so if you have to enter the code
|
||||||
(by using the same session, IP address and port) to be
|
more than once, maybe you're changing the working directory,
|
||||||
able to execute queries on either, without the additional
|
renaming or removing the file, or using random names.
|
||||||
cost that would involve having the methods for signing in,
|
|
||||||
logging out, and such.
|
api_id (`int` | `str`):
|
||||||
|
The API ID you obtained from https://my.telegram.org.
|
||||||
|
|
||||||
|
api_hash (`str`):
|
||||||
|
The API ID you obtained from https://my.telegram.org.
|
||||||
|
|
||||||
|
connection (`telethon.network.connection.common.Connection`, optional):
|
||||||
|
The connection instance to be used when creating a new connection
|
||||||
|
to the servers. If it's a type, the `proxy` argument will be used.
|
||||||
|
|
||||||
|
Defaults to `telethon.network.connection.tcpfull.ConnectionTcpFull`.
|
||||||
|
|
||||||
|
use_ipv6 (`bool`, optional):
|
||||||
|
Whether to connect to the servers through IPv6 or not.
|
||||||
|
By default this is ``False`` as IPv6 support is not
|
||||||
|
too widespread yet.
|
||||||
|
|
||||||
|
proxy (`tuple` | `dict`, optional):
|
||||||
|
A tuple consisting of ``(socks.SOCKS5, 'host', port)``.
|
||||||
|
See https://github.com/Anorov/PySocks#usage-1 for more.
|
||||||
|
|
||||||
|
update_workers (`int`, optional):
|
||||||
|
If specified, represents how many extra threads should
|
||||||
|
be spawned to handle incoming updates, and updates will
|
||||||
|
be kept in memory until they are processed. Note that
|
||||||
|
you must set this to at least ``0`` if you want to be
|
||||||
|
able to process updates through :meth:`updates.poll()`.
|
||||||
|
|
||||||
|
timeout (`int` | `float` | `timedelta`, optional):
|
||||||
|
The timeout to be used when receiving responses from
|
||||||
|
the network. Defaults to 5 seconds.
|
||||||
|
|
||||||
|
spawn_read_thread (`bool`, optional):
|
||||||
|
Whether to use an extra background thread or not. Defaults
|
||||||
|
to ``True`` so receiving items from the network happens
|
||||||
|
instantly, as soon as they arrive. Can still be disabled
|
||||||
|
if you want to run the library without any additional thread.
|
||||||
|
|
||||||
|
report_errors (`bool`, optional):
|
||||||
|
Whether to report RPC errors or not. Defaults to ``True``,
|
||||||
|
see :ref:`api-status` for more information.
|
||||||
|
|
||||||
|
device_model (`str`, optional):
|
||||||
|
"Device model" to be sent when creating the initial connection.
|
||||||
|
Defaults to ``platform.node()``.
|
||||||
|
|
||||||
|
system_version (`str`, optional):
|
||||||
|
"System version" to be sent when creating the initial connection.
|
||||||
|
Defaults to ``platform.system()``.
|
||||||
|
|
||||||
|
app_version (`str`, optional):
|
||||||
|
"App version" to be sent when creating the initial connection.
|
||||||
|
Defaults to `telethon.version.__version__`.
|
||||||
|
|
||||||
|
lang_code (`str`, optional):
|
||||||
|
"Language code" to be sent when creating the initial connection.
|
||||||
|
Defaults to ``'en'``.
|
||||||
|
|
||||||
|
system_lang_code (`str`, optional):
|
||||||
|
"System lang code" to be sent when creating the initial connection.
|
||||||
|
Defaults to `lang_code`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Current TelegramClient version
|
# Current TelegramClient version
|
||||||
__version__ = version.__version__
|
__version__ = version.__version__
|
||||||
|
|
||||||
# TODO Make this thread-safe, all connections share the same DC
|
# Server configuration (with .dc_options)
|
||||||
_config = None # Server configuration (with .dc_options)
|
_config = None
|
||||||
|
|
||||||
# region Initialization
|
# region Initialization
|
||||||
|
|
||||||
@ -72,8 +126,6 @@ class TelegramBareClient:
|
|||||||
connection=ConnectionTcpFull,
|
connection=ConnectionTcpFull,
|
||||||
use_ipv6=False,
|
use_ipv6=False,
|
||||||
proxy=None,
|
proxy=None,
|
||||||
update_workers=None,
|
|
||||||
spawn_read_thread=False,
|
|
||||||
timeout=timedelta(seconds=5),
|
timeout=timedelta(seconds=5),
|
||||||
report_errors=True,
|
report_errors=True,
|
||||||
device_model=None,
|
device_model=None,
|
||||||
@ -118,11 +170,7 @@ class TelegramBareClient:
|
|||||||
if isinstance(connection, type):
|
if isinstance(connection, type):
|
||||||
connection = connection(proxy=proxy, timeout=timeout)
|
connection = connection(proxy=proxy, timeout=timeout)
|
||||||
|
|
||||||
self._sender = MtProtoSender(self.session, connection)
|
self._sender = MTProtoSender(self.session, connection)
|
||||||
|
|
||||||
# Two threads may be calling reconnect() when the connection is lost,
|
|
||||||
# we only want one to actually perform the reconnection.
|
|
||||||
self._reconnect_lock = Lock()
|
|
||||||
|
|
||||||
# Cache "exported" sessions as 'dc_id: Session' not to recreate
|
# Cache "exported" sessions as 'dc_id: Session' not to recreate
|
||||||
# them all the time since generating a new key is a relatively
|
# them all the time since generating a new key is a relatively
|
||||||
@ -131,7 +179,8 @@ class TelegramBareClient:
|
|||||||
|
|
||||||
# This member will process updates if enabled.
|
# This member will process updates if enabled.
|
||||||
# One may change self.updates.enabled at any later point.
|
# One may change self.updates.enabled at any later point.
|
||||||
self.updates = UpdateState(workers=update_workers)
|
# TODO Stop using that 1
|
||||||
|
self.updates = UpdateState(1)
|
||||||
|
|
||||||
# Used on connection - the user may modify these and reconnect
|
# Used on connection - the user may modify these and reconnect
|
||||||
system = platform.uname()
|
system = platform.uname()
|
||||||
@ -141,15 +190,6 @@ class TelegramBareClient:
|
|||||||
self.lang_code = lang_code
|
self.lang_code = lang_code
|
||||||
self.system_lang_code = system_lang_code
|
self.system_lang_code = system_lang_code
|
||||||
|
|
||||||
# Despite the state of the real connection, keep track of whether
|
|
||||||
# the user has explicitly called .connect() or .disconnect() here.
|
|
||||||
# This information is required by the read thread, who will be the
|
|
||||||
# one attempting to reconnect on the background *while* the user
|
|
||||||
# doesn't explicitly call .disconnect(), thus telling it to stop
|
|
||||||
# retrying. The main thread, knowing there is a background thread
|
|
||||||
# attempting reconnection as soon as it happens, will just sleep.
|
|
||||||
self._user_connected = False
|
|
||||||
|
|
||||||
# Save whether the user is authorized here (a.k.a. logged in)
|
# Save whether the user is authorized here (a.k.a. logged in)
|
||||||
self._authorized = None # None = We don't know yet
|
self._authorized = None # None = We don't know yet
|
||||||
|
|
||||||
@ -157,12 +197,6 @@ class TelegramBareClient:
|
|||||||
# See https://core.telegram.org/api/invoking#saving-client-info.
|
# See https://core.telegram.org/api/invoking#saving-client-info.
|
||||||
self._first_request = True
|
self._first_request = True
|
||||||
|
|
||||||
# Constantly read for results and updates from within the main client,
|
|
||||||
# if the user has left enabled such option.
|
|
||||||
self._spawn_read_thread = spawn_read_thread
|
|
||||||
self._recv_thread = None
|
|
||||||
self._idling = threading.Event()
|
|
||||||
|
|
||||||
# Default PingRequest delay
|
# Default PingRequest delay
|
||||||
self._last_ping = datetime.now()
|
self._last_ping = datetime.now()
|
||||||
self._ping_delay = timedelta(minutes=1)
|
self._ping_delay = timedelta(minutes=1)
|
||||||
@ -175,81 +209,48 @@ class TelegramBareClient:
|
|||||||
self._last_state = datetime.now()
|
self._last_state = datetime.now()
|
||||||
self._state_delay = timedelta(hours=1)
|
self._state_delay = timedelta(hours=1)
|
||||||
|
|
||||||
# Some errors are known but there's nothing we can do from the
|
# Some further state for subclasses
|
||||||
# background thread. If any of these happens, call .disconnect(),
|
self._event_builders = []
|
||||||
# and raise them next time .invoke() is tried to be called.
|
self._events_pending_resolve = []
|
||||||
self._background_error = None
|
|
||||||
|
# Default parse mode
|
||||||
|
self._parse_mode = markdown
|
||||||
|
|
||||||
|
# Some fields to easy signing in. Let {phone: hash} be
|
||||||
|
# a dictionary because the user may change their mind.
|
||||||
|
self._phone_code_hash = {}
|
||||||
|
self._phone = None
|
||||||
|
self._tos = None
|
||||||
|
|
||||||
|
# Sometimes we need to know who we are, cache the self peer
|
||||||
|
self._self_input_peer = None
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region Connecting
|
# region Connecting
|
||||||
|
|
||||||
def connect(self, _sync_updates=True):
|
async def connect(self, _sync_updates=True):
|
||||||
"""Connects to the Telegram servers, executing authentication if
|
|
||||||
required. Note that authenticating to the Telegram servers is
|
|
||||||
not the same as authenticating the desired user itself, which
|
|
||||||
may require a call (or several) to 'sign_in' for the first time.
|
|
||||||
|
|
||||||
Note that the optional parameters are meant for internal use.
|
|
||||||
|
|
||||||
If '_sync_updates', sync_updates() will be called and a
|
|
||||||
second thread will be started if necessary. Note that this
|
|
||||||
will FAIL if the client is not connected to the user's
|
|
||||||
native data center, raising a "UserMigrateError", and
|
|
||||||
calling .disconnect() in the process.
|
|
||||||
"""
|
"""
|
||||||
__log__.info('Connecting to %s:%d...',
|
Connects to Telegram.
|
||||||
self.session.server_address, self.session.port)
|
"""
|
||||||
|
# TODO Maybe we should rethink what the session does if the sender
|
||||||
self._background_error = None # Clear previous errors
|
# needs a session but it might connect to arbitrary IPs?
|
||||||
|
#
|
||||||
try:
|
# TODO sync updates/connected and authorized if no UnauthorizedError?
|
||||||
self._sender.connect()
|
await self._sender.connect(
|
||||||
__log__.info('Connection success!')
|
self.session.server_address, self.session.port)
|
||||||
|
|
||||||
# Connection was successful! Try syncing the update state
|
|
||||||
# UNLESS '_sync_updates' is False (we probably are in
|
|
||||||
# another data center and this would raise UserMigrateError)
|
|
||||||
# to also assert whether the user is logged in or not.
|
|
||||||
self._user_connected = True
|
|
||||||
if self._authorized is None and _sync_updates:
|
|
||||||
try:
|
|
||||||
self.sync_updates()
|
|
||||||
self._set_connected_and_authorized()
|
|
||||||
except UnauthorizedError:
|
|
||||||
self._authorized = False
|
|
||||||
elif self._authorized:
|
|
||||||
self._set_connected_and_authorized()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
except TypeNotFoundError as e:
|
|
||||||
# This is fine, probably layer migration
|
|
||||||
__log__.warning('Connection failed, got unexpected type with ID '
|
|
||||||
'%s. Migrating?', hex(e.invalid_constructor_id))
|
|
||||||
self.disconnect()
|
|
||||||
return self.connect(_sync_updates=_sync_updates)
|
|
||||||
|
|
||||||
except AuthKeyError as e:
|
|
||||||
# As of late March 2018 there were two AUTH_KEY_DUPLICATED
|
|
||||||
# reports. Retrying with a clean auth_key should fix this.
|
|
||||||
__log__.warning('Auth key error %s. Clearing it and retrying.', e)
|
|
||||||
self.disconnect()
|
|
||||||
self.session.auth_key = None
|
|
||||||
self.session.save()
|
|
||||||
return self.connect(_sync_updates=_sync_updates)
|
|
||||||
|
|
||||||
except (RPCError, ConnectionError) as e:
|
|
||||||
# Probably errors from the previous session, ignore them
|
|
||||||
__log__.error('Connection failed due to %s', e)
|
|
||||||
self.disconnect()
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_connected(self):
|
def is_connected(self):
|
||||||
|
"""
|
||||||
|
Returns ``True`` if the user has connected.
|
||||||
|
"""
|
||||||
return self._sender.is_connected()
|
return self._sender.is_connected()
|
||||||
|
|
||||||
def _wrap_init_connection(self, query):
|
def _wrap_init_connection(self, query):
|
||||||
"""Wraps query around InvokeWithLayerRequest(InitConnectionRequest())"""
|
"""
|
||||||
|
Wraps `query` around
|
||||||
|
``InvokeWithLayerRequest(InitConnectionRequest(...))``.
|
||||||
|
"""
|
||||||
return InvokeWithLayerRequest(LAYER, InitConnectionRequest(
|
return InvokeWithLayerRequest(LAYER, InitConnectionRequest(
|
||||||
api_id=self.api_id,
|
api_id=self.api_id,
|
||||||
device_model=self.device_model,
|
device_model=self.device_model,
|
||||||
@ -261,75 +262,46 @@ class TelegramBareClient:
|
|||||||
query=query
|
query=query
|
||||||
))
|
))
|
||||||
|
|
||||||
def disconnect(self):
|
async def disconnect(self):
|
||||||
"""Disconnects from the Telegram server
|
"""
|
||||||
and stops all the spawned threads"""
|
Disconnects from Telegram.
|
||||||
__log__.info('Disconnecting...')
|
"""
|
||||||
self._user_connected = False # This will stop recv_thread's loop
|
await self._sender.disconnect()
|
||||||
|
# TODO What to do with the update state? Does it belong here?
|
||||||
__log__.debug('Stopping all workers...')
|
# self.session.set_update_state(0, self.updates.get_update_state(0))
|
||||||
self.updates.stop_workers()
|
|
||||||
|
|
||||||
# This will trigger a "ConnectionResetError" on the recv_thread,
|
|
||||||
# which won't attempt reconnecting as ._user_connected is False.
|
|
||||||
__log__.debug('Disconnecting the socket...')
|
|
||||||
self._sender.disconnect()
|
|
||||||
|
|
||||||
# TODO Shall we clear the _exported_sessions, or may be reused?
|
|
||||||
self._first_request = True # On reconnect it will be first again
|
|
||||||
self.session.set_update_state(0, self.updates.get_update_state(0))
|
|
||||||
self.session.close()
|
self.session.close()
|
||||||
|
|
||||||
def _reconnect(self, new_dc=None):
|
def _switch_dc(self, new_dc):
|
||||||
"""If 'new_dc' is not set, only a call to .connect() will be made
|
|
||||||
since it's assumed that the connection has been lost and the
|
|
||||||
library is reconnecting.
|
|
||||||
|
|
||||||
If 'new_dc' is set, the client is first disconnected from the
|
|
||||||
current data center, clears the auth key for the old DC, and
|
|
||||||
connects to the new data center.
|
|
||||||
"""
|
"""
|
||||||
if new_dc is None:
|
Switches the current connection to the new data center.
|
||||||
if self.is_connected():
|
"""
|
||||||
__log__.info('Reconnection aborted: already connected')
|
# TODO Implement
|
||||||
return True
|
raise NotImplementedError
|
||||||
|
dc = self._get_dc(new_dc)
|
||||||
|
__log__.info('Reconnecting to new data center %s', dc)
|
||||||
|
|
||||||
try:
|
self.session.set_dc(dc.id, dc.ip_address, dc.port)
|
||||||
__log__.info('Attempting reconnection...')
|
# auth_key's are associated with a server, which has now changed
|
||||||
return self.connect()
|
# so it's not valid anymore. Set to None to force recreating it.
|
||||||
except ConnectionResetError as e:
|
self.session.auth_key = None
|
||||||
__log__.warning('Reconnection failed due to %s', e)
|
self.session.save()
|
||||||
return False
|
self.disconnect()
|
||||||
else:
|
return self.connect()
|
||||||
# Since we're reconnecting possibly due to a UserMigrateError,
|
|
||||||
# we need to first know the Data Centers we can connect to. Do
|
|
||||||
# that before disconnecting.
|
|
||||||
dc = self._get_dc(new_dc)
|
|
||||||
__log__.info('Reconnecting to new data center %s', dc)
|
|
||||||
|
|
||||||
self.session.set_dc(dc.id, dc.ip_address, dc.port)
|
|
||||||
# auth_key's are associated with a server, which has now changed
|
|
||||||
# so it's not valid anymore. Set to None to force recreating it.
|
|
||||||
self.session.auth_key = None
|
|
||||||
self.session.save()
|
|
||||||
self.disconnect()
|
|
||||||
return self.connect()
|
|
||||||
|
|
||||||
def set_proxy(self, proxy):
|
def set_proxy(self, proxy):
|
||||||
"""Change the proxy used by the connections.
|
"""Change the proxy used by the connections.
|
||||||
"""
|
"""
|
||||||
if self.is_connected():
|
if self.is_connected():
|
||||||
raise RuntimeError("You can't change the proxy while connected.")
|
raise RuntimeError("You can't change the proxy while connected.")
|
||||||
self._sender.connection.conn.proxy = proxy
|
|
||||||
|
# TODO Should we tell the user to create a new client?
|
||||||
|
# Can this be done more cleanly? Similar to `switch_dc`
|
||||||
|
self._sender._connection.conn.proxy = proxy
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region Working with different connections/Data Centers
|
# region Working with different connections/Data Centers
|
||||||
|
|
||||||
def _on_read_thread(self):
|
|
||||||
return self._recv_thread is not None and \
|
|
||||||
threading.get_ident() == self._recv_thread.ident
|
|
||||||
|
|
||||||
def _get_dc(self, dc_id, cdn=False):
|
def _get_dc(self, dc_id, cdn=False):
|
||||||
"""Gets the Data Center (DC) associated to 'dc_id'"""
|
"""Gets the Data Center (DC) associated to 'dc_id'"""
|
||||||
if not TelegramBareClient._config:
|
if not TelegramBareClient._config:
|
||||||
@ -431,7 +403,7 @@ class TelegramBareClient:
|
|||||||
|
|
||||||
# region Invoking Telegram requests
|
# region Invoking Telegram requests
|
||||||
|
|
||||||
def __call__(self, request, retries=5, ordered=False):
|
async def __call__(self, request, ordered=False):
|
||||||
"""
|
"""
|
||||||
Invokes (sends) one or more MTProtoRequests and returns (receives)
|
Invokes (sends) one or more MTProtoRequests and returns (receives)
|
||||||
their result.
|
their result.
|
||||||
@ -456,302 +428,30 @@ class TelegramBareClient:
|
|||||||
The result of the request (often a `TLObject`) or a list of
|
The result of the request (often a `TLObject`) or a list of
|
||||||
results if more than one request was given.
|
results if more than one request was given.
|
||||||
"""
|
"""
|
||||||
single = not utils.is_list_like(request)
|
requests = (request,) if not utils.is_list_like(request) else request
|
||||||
if single:
|
|
||||||
request = (request,)
|
|
||||||
|
|
||||||
if not all(isinstance(x, TLObject) and
|
if not all(isinstance(x, TLObject) and
|
||||||
x.content_related for x in request):
|
x.content_related for x in requests):
|
||||||
raise TypeError('You can only invoke requests, not types!')
|
raise TypeError('You can only invoke requests, not types!')
|
||||||
|
|
||||||
if self._background_error:
|
# TODO Resolve requests, should be done by TelegramClient
|
||||||
raise self._background_error
|
# for r in requests:
|
||||||
|
# await r.resolve(self, utils)
|
||||||
|
|
||||||
for r in request:
|
# TODO InvokeWithLayer if no authkey, maybe done in MTProtoSender?
|
||||||
r.resolve(self, utils)
|
# TODO Handle PhoneMigrateError, NetworkMigrateError, UserMigrateError
|
||||||
|
# ^ by switching DC
|
||||||
# For logging purposes
|
# TODO Retry on ServerError, RpcCallFailError
|
||||||
if single:
|
# TODO Auto-sleep on some FloodWaitError, FloodTestPhoneWaitError
|
||||||
which = type(request[0]).__name__
|
future = await self._sender.send(request, ordered=ordered)
|
||||||
|
if isinstance(future, list):
|
||||||
|
results = []
|
||||||
|
for f in future:
|
||||||
|
results.append(await future)
|
||||||
|
return results
|
||||||
else:
|
else:
|
||||||
which = '{} requests ({})'.format(
|
return await future
|
||||||
len(request), [type(x).__name__ for x in request])
|
|
||||||
|
|
||||||
# Determine the sender to be used (main or a new connection)
|
|
||||||
__log__.debug('Invoking %s', which)
|
|
||||||
call_receive = \
|
|
||||||
not self._idling.is_set() or self._reconnect_lock.locked()
|
|
||||||
|
|
||||||
for retry in range(retries):
|
|
||||||
result = self._invoke(call_receive, request, ordered=ordered)
|
|
||||||
if result is not None:
|
|
||||||
return result[0] if single else result
|
|
||||||
|
|
||||||
log = __log__.info if retry == 0 else __log__.warning
|
|
||||||
log('Invoking %s failed %d times, connecting again and retrying',
|
|
||||||
which, retry + 1)
|
|
||||||
|
|
||||||
sleep(1)
|
|
||||||
# The ReadThread has priority when attempting reconnection,
|
|
||||||
# since this thread is constantly running while __call__ is
|
|
||||||
# only done sometimes. Here try connecting only once/retry.
|
|
||||||
if not self._reconnect_lock.locked():
|
|
||||||
with self._reconnect_lock:
|
|
||||||
self._reconnect()
|
|
||||||
|
|
||||||
raise RuntimeError('Number of retries reached 0 for {}.'.format(
|
|
||||||
which
|
|
||||||
))
|
|
||||||
|
|
||||||
# Let people use client.invoke(SomeRequest()) instead client(...)
|
# Let people use client.invoke(SomeRequest()) instead client(...)
|
||||||
invoke = __call__
|
invoke = __call__
|
||||||
|
|
||||||
def _invoke(self, call_receive, requests, ordered=False):
|
|
||||||
try:
|
|
||||||
# Ensure that we start with no previous errors (i.e. resending)
|
|
||||||
for x in requests:
|
|
||||||
x.confirm_received.clear()
|
|
||||||
x.rpc_error = None
|
|
||||||
|
|
||||||
if not self.session.auth_key:
|
|
||||||
__log__.info('Need to generate new auth key before invoking')
|
|
||||||
self._first_request = True
|
|
||||||
self.session.auth_key, self.session.time_offset = \
|
|
||||||
authenticator.do_authentication(self._sender.connection)
|
|
||||||
|
|
||||||
if self._first_request:
|
|
||||||
__log__.info('Initializing a new connection while invoking')
|
|
||||||
if len(requests) == 1:
|
|
||||||
requests = [self._wrap_init_connection(requests[0])]
|
|
||||||
else:
|
|
||||||
# We need a SINGLE request (like GetConfig) to init conn.
|
|
||||||
# Once that's done, the N original requests will be
|
|
||||||
# invoked.
|
|
||||||
TelegramBareClient._config = self(
|
|
||||||
self._wrap_init_connection(GetConfigRequest())
|
|
||||||
)
|
|
||||||
|
|
||||||
self._sender.send(requests, ordered=ordered)
|
|
||||||
|
|
||||||
if not call_receive:
|
|
||||||
# TODO This will be slightly troublesome if we allow
|
|
||||||
# switching between constant read or not on the fly.
|
|
||||||
# Must also watch out for calling .read() from two places,
|
|
||||||
# in which case a Lock would be required for .receive().
|
|
||||||
for x in requests:
|
|
||||||
x.confirm_received.wait(
|
|
||||||
self._sender.connection.get_timeout()
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
while not all(x.confirm_received.is_set() for x in requests):
|
|
||||||
self._sender.receive(update_state=self.updates)
|
|
||||||
|
|
||||||
except BrokenAuthKeyError:
|
|
||||||
__log__.error('Authorization key seems broken and was invalid!')
|
|
||||||
self.session.auth_key = None
|
|
||||||
|
|
||||||
except TypeNotFoundError as e:
|
|
||||||
# Only occurs when we call receive. May happen when
|
|
||||||
# we need to reconnect to another DC on login and
|
|
||||||
# Telegram somehow sends old objects (like configOld)
|
|
||||||
self._first_request = True
|
|
||||||
__log__.warning('Read unknown TLObject code ({}). '
|
|
||||||
'Setting again first_request flag.'
|
|
||||||
.format(hex(e.invalid_constructor_id)))
|
|
||||||
|
|
||||||
except TimeoutError:
|
|
||||||
__log__.warning('Invoking timed out') # We will just retry
|
|
||||||
|
|
||||||
except ConnectionResetError as e:
|
|
||||||
__log__.warning('Connection was reset while invoking')
|
|
||||||
if self._user_connected:
|
|
||||||
# Server disconnected us, __call__ will try reconnecting.
|
|
||||||
try:
|
|
||||||
self._sender.disconnect()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
# User never called .connect(), so raise this error.
|
|
||||||
raise RuntimeError('Tried to invoke without .connect()') from e
|
|
||||||
|
|
||||||
# Clear the flag if we got this far
|
|
||||||
self._first_request = False
|
|
||||||
|
|
||||||
try:
|
|
||||||
raise next(x.rpc_error for x in requests if x.rpc_error)
|
|
||||||
except StopIteration:
|
|
||||||
if any(x.result is None for x in requests):
|
|
||||||
# "A container may only be accepted or
|
|
||||||
# rejected by the other party as a whole."
|
|
||||||
return None
|
|
||||||
|
|
||||||
return [x.result for x in requests]
|
|
||||||
|
|
||||||
except (PhoneMigrateError, NetworkMigrateError,
|
|
||||||
UserMigrateError) as e:
|
|
||||||
|
|
||||||
# TODO What happens with the background thread here?
|
|
||||||
# For normal use cases, this won't happen, because this will only
|
|
||||||
# be on the very first connection (not authorized, not running),
|
|
||||||
# but may be an issue for people who actually travel?
|
|
||||||
self._reconnect(new_dc=e.new_dc)
|
|
||||||
return self._invoke(call_receive, requests)
|
|
||||||
|
|
||||||
except (ServerError, RpcCallFailError) as e:
|
|
||||||
# Telegram is having some issues, just retry
|
|
||||||
__log__.warning('Telegram is having internal issues: %s', e)
|
|
||||||
|
|
||||||
except (FloodWaitError, FloodTestPhoneWaitError) as e:
|
|
||||||
__log__.warning('Request invoked too often, wait %ds', e.seconds)
|
|
||||||
if e.seconds > self.session.flood_sleep_threshold | 0:
|
|
||||||
raise
|
|
||||||
|
|
||||||
sleep(e.seconds)
|
|
||||||
|
|
||||||
# Some really basic functionality
|
|
||||||
|
|
||||||
def is_user_authorized(self):
|
|
||||||
"""Has the user been authorized yet
|
|
||||||
(code request sent and confirmed)?"""
|
|
||||||
return self._authorized
|
|
||||||
|
|
||||||
def get_input_entity(self, peer):
|
|
||||||
"""
|
|
||||||
Stub method, no functionality so that calling
|
|
||||||
``.get_input_entity()`` from ``.resolve()`` doesn't fail.
|
|
||||||
"""
|
|
||||||
return peer
|
|
||||||
|
|
||||||
# endregion
|
|
||||||
|
|
||||||
# region Updates handling
|
|
||||||
|
|
||||||
def sync_updates(self):
|
|
||||||
"""Synchronizes self.updates to their initial state. Will be
|
|
||||||
called automatically on connection if self.updates.enabled = True,
|
|
||||||
otherwise it should be called manually after enabling updates.
|
|
||||||
"""
|
|
||||||
self.updates.process(self(GetStateRequest()))
|
|
||||||
self._last_state = datetime.now()
|
|
||||||
|
|
||||||
# endregion
|
|
||||||
|
|
||||||
# region Constant read
|
|
||||||
|
|
||||||
def _set_connected_and_authorized(self):
|
|
||||||
self._authorized = True
|
|
||||||
self.updates.setup_workers()
|
|
||||||
if self._spawn_read_thread and self._recv_thread is None:
|
|
||||||
self._recv_thread = threading.Thread(
|
|
||||||
name='ReadThread', daemon=True,
|
|
||||||
target=self._recv_thread_impl
|
|
||||||
)
|
|
||||||
self._recv_thread.start()
|
|
||||||
|
|
||||||
def _signal_handler(self, signum, frame):
|
|
||||||
if self._user_connected:
|
|
||||||
self.disconnect()
|
|
||||||
else:
|
|
||||||
os._exit(1)
|
|
||||||
|
|
||||||
def idle(self, stop_signals=(SIGINT, SIGTERM, SIGABRT)):
|
|
||||||
"""
|
|
||||||
Idles the program by looping forever and listening for updates
|
|
||||||
until one of the signals are received, which breaks the loop.
|
|
||||||
|
|
||||||
:param stop_signals:
|
|
||||||
Iterable containing signals from the signal module that will
|
|
||||||
be subscribed to TelegramClient.disconnect() (effectively
|
|
||||||
stopping the idle loop), which will be called on receiving one
|
|
||||||
of those signals.
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
if self._spawn_read_thread and not self._on_read_thread():
|
|
||||||
raise RuntimeError('Can only idle if spawn_read_thread=False')
|
|
||||||
|
|
||||||
self._idling.set()
|
|
||||||
for sig in stop_signals:
|
|
||||||
signal(sig, self._signal_handler)
|
|
||||||
|
|
||||||
if self._on_read_thread():
|
|
||||||
__log__.info('Starting to wait for items from the network')
|
|
||||||
else:
|
|
||||||
__log__.info('Idling to receive items from the network')
|
|
||||||
|
|
||||||
while self._user_connected:
|
|
||||||
try:
|
|
||||||
if datetime.now() > self._last_ping + self._ping_delay:
|
|
||||||
self._sender.send(PingRequest(
|
|
||||||
int.from_bytes(os.urandom(8), 'big', signed=True)
|
|
||||||
))
|
|
||||||
self._last_ping = datetime.now()
|
|
||||||
|
|
||||||
if datetime.now() > self._last_state + self._state_delay:
|
|
||||||
self._sender.send(GetStateRequest())
|
|
||||||
self._last_state = datetime.now()
|
|
||||||
|
|
||||||
__log__.debug('Receiving items from the network...')
|
|
||||||
self._sender.receive(update_state=self.updates)
|
|
||||||
except TimeoutError:
|
|
||||||
# No problem
|
|
||||||
__log__.debug('Receiving items from the network timed out')
|
|
||||||
except ConnectionResetError:
|
|
||||||
if self._user_connected:
|
|
||||||
__log__.error('Connection was reset while receiving '
|
|
||||||
'items. Reconnecting')
|
|
||||||
with self._reconnect_lock:
|
|
||||||
while self._user_connected and not self._reconnect():
|
|
||||||
sleep(0.1) # Retry forever, this is instant messaging
|
|
||||||
|
|
||||||
if self.is_connected():
|
|
||||||
# Telegram seems to kick us every 1024 items received
|
|
||||||
# from the network not considering things like bad salt.
|
|
||||||
# We must execute some *high level* request (that's not
|
|
||||||
# a ping) if we want to receive updates again.
|
|
||||||
# TODO Test if getDifference works too (better alternative)
|
|
||||||
self._sender.send(GetStateRequest())
|
|
||||||
except:
|
|
||||||
self._idling.clear()
|
|
||||||
raise
|
|
||||||
|
|
||||||
self._idling.clear()
|
|
||||||
__log__.info('Connection closed by the user, not reading anymore')
|
|
||||||
|
|
||||||
# By using this approach, another thread will be
|
|
||||||
# created and started upon connection to constantly read
|
|
||||||
# from the other end. Otherwise, manual calls to .receive()
|
|
||||||
# must be performed. The MtProtoSender cannot be connected,
|
|
||||||
# or an error will be thrown.
|
|
||||||
#
|
|
||||||
# This way, sending and receiving will be completely independent.
|
|
||||||
def _recv_thread_impl(self):
|
|
||||||
# This thread is "idle" (only listening for updates), but also
|
|
||||||
# excepts everything unlike the manual idle because it should
|
|
||||||
# not crash.
|
|
||||||
while self._user_connected:
|
|
||||||
try:
|
|
||||||
self.idle(stop_signals=tuple())
|
|
||||||
except Exception as error:
|
|
||||||
__log__.exception('Unknown exception in the read thread! '
|
|
||||||
'Disconnecting and leaving it to main thread')
|
|
||||||
# Unknown exception, pass it to the main thread
|
|
||||||
|
|
||||||
try:
|
|
||||||
import socks
|
|
||||||
if isinstance(error, (
|
|
||||||
socks.GeneralProxyError, socks.ProxyConnectionError
|
|
||||||
)):
|
|
||||||
# This is a known error, and it's not related to
|
|
||||||
# Telegram but rather to the proxy. Disconnect and
|
|
||||||
# hand it over to the main thread.
|
|
||||||
self._background_error = error
|
|
||||||
self.disconnect()
|
|
||||||
break
|
|
||||||
except ImportError:
|
|
||||||
"Not using PySocks, so it can't be a proxy error"
|
|
||||||
|
|
||||||
self._recv_thread = None
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
Loading…
Reference in New Issue
Block a user