mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-10 10:49:39 +00:00
Merge branch 'master' into asyncio
This commit is contained in:
@@ -81,6 +81,9 @@ class MtProtoSender:
|
||||
message = messages[0]
|
||||
else:
|
||||
message = TLMessage(self.session, MessageContainer(messages))
|
||||
# On bad_msg_salt errors, Telegram will reply with the ID of
|
||||
# the container and not the requests it contains, so in case
|
||||
# this happens we need to know to which container they belong.
|
||||
for m in messages:
|
||||
m.container_msg_id = message.msg_id
|
||||
|
||||
@@ -261,7 +264,12 @@ class MtProtoSender:
|
||||
return self._pending_receive.pop(msg_id).request
|
||||
|
||||
def _pop_requests_of_container(self, container_msg_id):
|
||||
msgs = [msg for msg in self._pending_receive.values() if msg.container_msg_id == container_msg_id]
|
||||
"""Pops the pending requests (plural) from self._pending_receive if
|
||||
they were sent on a container that matches container_msg_id.
|
||||
"""
|
||||
msgs = [msg for msg in self._pending_receive.values()
|
||||
if msg.container_msg_id == container_msg_id]
|
||||
|
||||
requests = [msg.request for msg in msgs]
|
||||
for msg in msgs:
|
||||
self._pending_receive.pop(msg.msg_id, None)
|
||||
@@ -273,14 +281,17 @@ class MtProtoSender:
|
||||
self._pending_receive.clear()
|
||||
|
||||
async def _resend_request(self, msg_id):
|
||||
"""Re-sends the request that belongs to a certain msg_id. This may
|
||||
also be the msg_id of a container if they were sent in one.
|
||||
"""
|
||||
request = self._pop_request(msg_id)
|
||||
if request:
|
||||
self._logger.debug('requests is about to resend')
|
||||
self._logger.debug('Resending request')
|
||||
await self.send(request)
|
||||
return
|
||||
requests = self._pop_requests_of_container(msg_id)
|
||||
if requests:
|
||||
self._logger.debug('container of requests is about to resend')
|
||||
self._logger.debug('Resending container of requests')
|
||||
await self.send(*requests)
|
||||
|
||||
async def _handle_pong(self, msg_id, sequence, reader):
|
||||
@@ -324,6 +335,8 @@ class MtProtoSender:
|
||||
)[0]
|
||||
self.session.save()
|
||||
|
||||
# "the bad_server_salt response is received with the
|
||||
# correct salt, and the message is to be re-sent with it"
|
||||
await self._resend_request(bad_salt.bad_msg_id)
|
||||
|
||||
return True
|
||||
|
@@ -30,6 +30,7 @@ from .tl.functions.upload import (
|
||||
GetFileRequest, SaveBigFilePartRequest, SaveFilePartRequest
|
||||
)
|
||||
from .tl.types import InputFile, InputFileBig
|
||||
from .tl.types.auth import ExportedAuthorization
|
||||
from .tl.types.upload import FileCdnRedirect
|
||||
from .update_state import UpdateState
|
||||
from .utils import get_appropriated_part_size
|
||||
@@ -59,7 +60,7 @@ class TelegramBareClient:
|
||||
__version__ = '0.15.3'
|
||||
|
||||
# TODO Make this thread-safe, all connections share the same DC
|
||||
_dc_options = None
|
||||
_config = None # Server configuration (with .dc_options)
|
||||
|
||||
# region Initialization
|
||||
|
||||
@@ -148,7 +149,7 @@ class TelegramBareClient:
|
||||
|
||||
# region Connecting
|
||||
|
||||
async def connect(self, _exported_auth=None, _sync_updates=True, _cdn=False):
|
||||
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
|
||||
@@ -156,60 +157,21 @@ class TelegramBareClient:
|
||||
|
||||
Note that the optional parameters are meant for internal use.
|
||||
|
||||
If '_exported_auth' is not None, it will be used instead to
|
||||
determine the authorization key for the current session.
|
||||
|
||||
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.
|
||||
|
||||
If '_cdn' is False, methods that are not allowed on such data
|
||||
centers won't be invoked.
|
||||
"""
|
||||
try:
|
||||
await self._sender.connect()
|
||||
if not self.session.auth_key:
|
||||
# New key, we need to tell the server we're going to use
|
||||
# the latest layer
|
||||
try:
|
||||
self.session.auth_key, self.session.time_offset = \
|
||||
await authenticator.do_authentication(self._sender.connection)
|
||||
except BrokenAuthKeyError:
|
||||
self._user_connected = False
|
||||
return False
|
||||
|
||||
self.session.layer = LAYER
|
||||
self.session.save()
|
||||
init_connection = True
|
||||
else:
|
||||
init_connection = self.session.layer != LAYER
|
||||
|
||||
if init_connection:
|
||||
if _exported_auth is not None:
|
||||
await self._init_connection(ImportAuthorizationRequest(
|
||||
_exported_auth.id, _exported_auth.bytes
|
||||
))
|
||||
elif not _cdn:
|
||||
TelegramBareClient._dc_options = \
|
||||
(await self._init_connection(GetConfigRequest())).dc_options
|
||||
|
||||
elif _exported_auth is not None:
|
||||
await self(ImportAuthorizationRequest(
|
||||
_exported_auth.id, _exported_auth.bytes
|
||||
))
|
||||
|
||||
if TelegramBareClient._dc_options is None and not _cdn:
|
||||
TelegramBareClient._dc_options = \
|
||||
(await self(GetConfigRequest())).dc_options
|
||||
|
||||
# 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 and not _cdn:
|
||||
if self._authorized is None and _sync_updates:
|
||||
try:
|
||||
await self.sync_updates()
|
||||
self._set_connected_and_authorized()
|
||||
@@ -224,11 +186,7 @@ class TelegramBareClient:
|
||||
# This is fine, probably layer migration
|
||||
self._logger.debug('Found invalid item, probably migrating', e)
|
||||
self.disconnect()
|
||||
return await self.connect(
|
||||
_exported_auth=_exported_auth,
|
||||
_sync_updates=_sync_updates,
|
||||
_cdn=_cdn
|
||||
)
|
||||
return await self.connect(_sync_updates=_sync_updates)
|
||||
|
||||
except (RPCError, ConnectionError) as error:
|
||||
# Probably errors from the previous session, ignore them
|
||||
@@ -241,8 +199,9 @@ class TelegramBareClient:
|
||||
def is_connected(self):
|
||||
return self._sender.is_connected()
|
||||
|
||||
async def _init_connection(self, query=None):
|
||||
result = await self(InvokeWithLayerRequest(LAYER, InitConnectionRequest(
|
||||
def _wrap_init_connection(self, query):
|
||||
"""Wraps query around InvokeWithLayerRequest(InitConnectionRequest())"""
|
||||
return InvokeWithLayerRequest(LAYER, InitConnectionRequest(
|
||||
api_id=self.api_id,
|
||||
device_model=self.session.device_model,
|
||||
system_version=self.session.system_version,
|
||||
@@ -251,10 +210,7 @@ class TelegramBareClient:
|
||||
system_lang_code=self.session.system_lang_code,
|
||||
lang_pack='', # "langPacks are for official apps only"
|
||||
query=query
|
||||
)))
|
||||
self.session.layer = LAYER
|
||||
self.session.save()
|
||||
return result
|
||||
))
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnects from the Telegram server"""
|
||||
@@ -286,13 +242,18 @@ class TelegramBareClient:
|
||||
finally:
|
||||
self._reconnect_lock.release()
|
||||
else:
|
||||
self.disconnect()
|
||||
self.session.auth_key = None # Force creating new auth_key
|
||||
# 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 = await self._get_dc(new_dc)
|
||||
ip = dc.ip_address
|
||||
self.session.server_address = ip
|
||||
|
||||
self.session.server_address = dc.ip_address
|
||||
self.session.port = 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 await self.connect()
|
||||
|
||||
# endregion
|
||||
@@ -301,11 +262,8 @@ class TelegramBareClient:
|
||||
|
||||
async def _get_dc(self, dc_id, ipv6=False, cdn=False):
|
||||
"""Gets the Data Center (DC) associated to 'dc_id'"""
|
||||
if TelegramBareClient._dc_options is None:
|
||||
raise ConnectionError(
|
||||
'Cannot determine the required data center IP address. '
|
||||
'Stabilise a successful initial connection first.'
|
||||
)
|
||||
if not TelegramBareClient._config:
|
||||
TelegramBareClient._config = await self(GetConfigRequest())
|
||||
|
||||
try:
|
||||
if cdn:
|
||||
@@ -314,15 +272,15 @@ class TelegramBareClient:
|
||||
rsa.add_key(pk.public_key)
|
||||
|
||||
return next(
|
||||
dc for dc in TelegramBareClient._dc_options if dc.id == dc_id
|
||||
and bool(dc.ipv6) == ipv6 and bool(dc.cdn) == cdn
|
||||
dc for dc in TelegramBareClient._config.dc_options
|
||||
if dc.id == dc_id and bool(dc.ipv6) == ipv6 and bool(dc.cdn) == cdn
|
||||
)
|
||||
except StopIteration:
|
||||
if not cdn:
|
||||
raise
|
||||
|
||||
# New configuration, perhaps a new CDN was added?
|
||||
TelegramBareClient._dc_options = await (self(GetConfigRequest())).dc_options
|
||||
TelegramBareClient._config = await self(GetConfigRequest())
|
||||
return await self._get_dc(dc_id, ipv6=ipv6, cdn=cdn)
|
||||
|
||||
async def _get_exported_client(self, dc_id):
|
||||
@@ -363,7 +321,14 @@ class TelegramBareClient:
|
||||
timeout=self._sender.connection.get_timeout(),
|
||||
loop=self._loop
|
||||
)
|
||||
await client.connect(_exported_auth=export_auth, _sync_updates=False)
|
||||
await client.connect(_sync_updates=False)
|
||||
if isinstance(export_auth, ExportedAuthorization):
|
||||
await client(ImportAuthorizationRequest(
|
||||
id=export_auth.id, bytes=export_auth.bytes
|
||||
))
|
||||
elif export_auth is not None:
|
||||
self._logger.warning('Unknown return export_auth type', export_auth)
|
||||
|
||||
client._authorized = True # We exported the auth, so we got auth
|
||||
return client
|
||||
|
||||
@@ -386,9 +351,10 @@ class TelegramBareClient:
|
||||
|
||||
# This will make use of the new RSA keys for this specific CDN.
|
||||
#
|
||||
# This relies on the fact that TelegramBareClient._dc_options is
|
||||
# static and it won't be called from this DC (it would fail).
|
||||
await client.connect(_cdn=True) # Avoid invoking non-CDN methods
|
||||
# We won't be calling GetConfigRequest because it's only called
|
||||
# when needed by ._get_dc, and also it's static so it's likely
|
||||
# set already. Avoid invoking non-CDN methods by not syncing updates.
|
||||
await client.connect(_sync_updates=False)
|
||||
client._authorized = self._authorized
|
||||
return client
|
||||
|
||||
@@ -423,11 +389,33 @@ class TelegramBareClient:
|
||||
invoke = __call__
|
||||
|
||||
async def _invoke(self, call_receive, retry, *requests):
|
||||
# We need to specify the new layer (by initializing a new
|
||||
# connection) if it has changed from the latest known one.
|
||||
init_connection = self.session.layer != LAYER
|
||||
|
||||
try:
|
||||
# Ensure that we start with no previous errors (i.e. resending)
|
||||
for x in requests:
|
||||
x.rpc_error = None
|
||||
|
||||
if not self.session.auth_key:
|
||||
# New key, we need to tell the server we're going to use
|
||||
# the latest layer and initialize the connection doing so.
|
||||
self.session.auth_key, self.session.time_offset = \
|
||||
await authenticator.do_authentication(self._sender.connection)
|
||||
init_connection = True
|
||||
|
||||
if init_connection:
|
||||
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 = await self(
|
||||
self._wrap_init_connection(GetConfigRequest())
|
||||
)
|
||||
|
||||
await self._sender.send(*requests)
|
||||
|
||||
if not call_receive:
|
||||
@@ -440,6 +428,13 @@ class TelegramBareClient:
|
||||
while not all(x.confirm_received.is_set() for x in requests):
|
||||
await self._sender.receive(update_state=self.updates)
|
||||
|
||||
except BrokenAuthKeyError:
|
||||
self._logger.error('Broken auth key, a new one will be generated')
|
||||
self.session.auth_key = None
|
||||
|
||||
except TimeoutError:
|
||||
pass # We will just retry
|
||||
|
||||
except ConnectionResetError:
|
||||
if not self._user_connected or self._reconnect_lock.locked():
|
||||
# Only attempt reconnecting if the user called connect and not
|
||||
@@ -453,6 +448,12 @@ class TelegramBareClient:
|
||||
await asyncio.sleep(retry + 1, loop=self._loop)
|
||||
return None
|
||||
|
||||
if init_connection:
|
||||
# We initialized the connection successfully, even if
|
||||
# a request had an RPC error we have invoked it fine.
|
||||
self.session.layer = LAYER
|
||||
self.session.save()
|
||||
|
||||
try:
|
||||
raise next(x.rpc_error for x in requests if x.rpc_error)
|
||||
except StopIteration:
|
||||
@@ -728,7 +729,8 @@ class TelegramBareClient:
|
||||
if need_reconnect:
|
||||
need_reconnect = False
|
||||
while self._user_connected and not await self._reconnect():
|
||||
await asyncio.sleep(0.1, loop=self._loop) # Retry forever, this is instant messaging
|
||||
# Retry forever, this is instant messaging
|
||||
await asyncio.sleep(0.1, loop=self._loop)
|
||||
|
||||
await self._sender.receive(update_state=self.updates)
|
||||
except TimeoutError:
|
||||
@@ -748,7 +750,8 @@ class TelegramBareClient:
|
||||
try:
|
||||
import socks
|
||||
if isinstance(error, (
|
||||
socks.GeneralProxyError, socks.ProxyConnectionError
|
||||
socks.GeneralProxyError,
|
||||
socks.ProxyConnectionError
|
||||
)):
|
||||
# This is a known error, and it's not related to
|
||||
# Telegram but rather to the proxy. Disconnect and
|
||||
@@ -764,6 +767,7 @@ class TelegramBareClient:
|
||||
# add a little sleep to avoid the CPU usage going mad.
|
||||
await asyncio.sleep(0.1, loop=self._loop)
|
||||
break
|
||||
|
||||
self._recv_loop = None
|
||||
|
||||
# endregion
|
||||
|
@@ -51,7 +51,6 @@ from .tl.types import (
|
||||
PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel)
|
||||
from .tl.types.messages import DialogsSlice
|
||||
|
||||
|
||||
class TelegramClient(TelegramBareClient):
|
||||
"""Full featured TelegramClient meant to extend the basic functionality"""
|
||||
|
||||
@@ -103,7 +102,11 @@ class TelegramClient(TelegramBareClient):
|
||||
# region Authorization requests
|
||||
|
||||
async def send_code_request(self, phone):
|
||||
"""Sends a code request to the specified phone number"""
|
||||
"""Sends a code request to the specified phone number.
|
||||
|
||||
:param str | int phone: The phone to which the code will be sent.
|
||||
:return auth.SentCode: Information about the result of the request.
|
||||
"""
|
||||
phone = EntityDatabase.parse_phone(phone) or self._phone
|
||||
result = await self(SendCodeRequest(phone, self.api_id, self.api_hash))
|
||||
self._phone = phone
|
||||
@@ -112,26 +115,27 @@ class TelegramClient(TelegramBareClient):
|
||||
|
||||
async def sign_in(self, phone=None, code=None,
|
||||
password=None, bot_token=None, phone_code_hash=None):
|
||||
"""Completes the sign in process with the phone number + code pair.
|
||||
"""
|
||||
Starts or completes the sign in process with the given phone number
|
||||
or code that Telegram sent.
|
||||
|
||||
If no phone or code is provided, then the sole password will be used.
|
||||
The password should be used after a normal authorization attempt
|
||||
has happened and an SessionPasswordNeededError was raised.
|
||||
:param str | int phone:
|
||||
The phone to send the code to if no code was provided, or to
|
||||
override the phone that was previously used with these requests.
|
||||
:param str | int code:
|
||||
The code that Telegram sent.
|
||||
:param str password:
|
||||
2FA password, should be used if a previous call raised
|
||||
SessionPasswordNeededError.
|
||||
:param str bot_token:
|
||||
Used to sign in as a bot. Not all requests will be available.
|
||||
This should be the hash the @BotFather gave you.
|
||||
:param str phone_code_hash:
|
||||
The hash returned by .send_code_request. This can be set to None
|
||||
to use the last hash known.
|
||||
|
||||
If you're calling .sign_in() on two completely different clients
|
||||
(for example, through an API that creates a new client per phone),
|
||||
you must first call .sign_in(phone) to receive the code, and then
|
||||
with the result such method results, call
|
||||
.sign_in(phone, code, phone_code_hash=result.phone_code_hash).
|
||||
|
||||
If this is done on the same client, the client will fill said values
|
||||
for you.
|
||||
|
||||
To login as a bot, only `bot_token` should be provided.
|
||||
This should equal to the bot access hash provided by
|
||||
https://t.me/BotFather during your bot creation.
|
||||
|
||||
If the login succeeds, the logged in user is returned.
|
||||
:return auth.SentCode | User:
|
||||
The signed in user, or the information about .send_code_request().
|
||||
"""
|
||||
|
||||
if phone and not code:
|
||||
@@ -175,7 +179,15 @@ class TelegramClient(TelegramBareClient):
|
||||
return result.user
|
||||
|
||||
async def sign_up(self, code, first_name, last_name=''):
|
||||
"""Signs up to Telegram. Make sure you sent a code request first!"""
|
||||
"""
|
||||
Signs up to Telegram if you don't have an account yet.
|
||||
You must call .send_code_request(phone) first.
|
||||
|
||||
:param str | int code: The code sent by Telegram
|
||||
:param str first_name: The first name to be used by the new account.
|
||||
:param str last_name: Optional last name.
|
||||
:return User: The new created user.
|
||||
"""
|
||||
result = await self(SignUpRequest(
|
||||
phone_number=self._phone,
|
||||
phone_code_hash=self._phone_code_hash,
|
||||
@@ -188,8 +200,10 @@ class TelegramClient(TelegramBareClient):
|
||||
return result.user
|
||||
|
||||
async def log_out(self):
|
||||
"""Logs out and deletes the current session.
|
||||
Returns True if everything went okay."""
|
||||
"""Logs out Telegram and deletes the current *.session file.
|
||||
|
||||
:return bool: True if the operation was successful.
|
||||
"""
|
||||
try:
|
||||
await self(LogOutRequest())
|
||||
except RPCError:
|
||||
@@ -201,8 +215,12 @@ class TelegramClient(TelegramBareClient):
|
||||
return True
|
||||
|
||||
async def get_me(self):
|
||||
"""Gets "me" (the self user) which is currently authenticated,
|
||||
or None if the request fails (hence, not authenticated)."""
|
||||
"""
|
||||
Gets "me" (the self user) which is currently authenticated,
|
||||
or None if the request fails (hence, not authenticated).
|
||||
|
||||
:return User: Your own user.
|
||||
"""
|
||||
try:
|
||||
return (await self(GetUsersRequest([InputUserSelf()])))[0]
|
||||
except UnauthorizedError:
|
||||
@@ -217,15 +235,21 @@ class TelegramClient(TelegramBareClient):
|
||||
offset_date=None,
|
||||
offset_id=0,
|
||||
offset_peer=InputPeerEmpty()):
|
||||
"""Returns a tuple of lists ([dialogs], [entities])
|
||||
with at least 'limit' items each unless all dialogs were consumed.
|
||||
"""
|
||||
Gets N "dialogs" (open "chats" or conversations with other people).
|
||||
|
||||
If `limit` is None, all dialogs will be retrieved (from the given
|
||||
offset) will be retrieved.
|
||||
|
||||
The `entities` represent the user, chat or channel
|
||||
corresponding to that dialog. If it's an integer, not
|
||||
all dialogs may be retrieved at once.
|
||||
:param limit:
|
||||
How many dialogs to be retrieved as maximum. Can be set to None
|
||||
to retrieve all dialogs. Note that this may take whole minutes
|
||||
if you have hundreds of dialogs, as Telegram will tell the library
|
||||
to slow down through a FloodWaitError.
|
||||
:param offset_date:
|
||||
The offset date to be used.
|
||||
:param offset_id:
|
||||
The message ID to be used as an offset.
|
||||
:param offset_peer:
|
||||
The peer to be used as an offset.
|
||||
:return: A tuple of lists ([dialogs], [entities]).
|
||||
"""
|
||||
if limit is None:
|
||||
limit = float('inf')
|
||||
@@ -284,8 +308,9 @@ class TelegramClient(TelegramBareClient):
|
||||
"""
|
||||
Gets all open draft messages.
|
||||
|
||||
Returns a list of custom `Draft` objects that are easy to work with: You can call
|
||||
`draft.set_message('text')` to change the message, or delete it through `draft.delete()`.
|
||||
Returns a list of custom `Draft` objects that are easy to work with:
|
||||
You can call `draft.set_message('text')` to change the message,
|
||||
or delete it through `draft.delete()`.
|
||||
|
||||
:return List[telethon.tl.custom.Draft]: A list of open drafts
|
||||
"""
|
||||
@@ -300,11 +325,14 @@ class TelegramClient(TelegramBareClient):
|
||||
message,
|
||||
reply_to=None,
|
||||
link_preview=True):
|
||||
"""Sends a message to the given entity (or input peer)
|
||||
and returns the sent message as a Telegram object.
|
||||
"""
|
||||
Sends the given message to the specified entity (user/chat/channel).
|
||||
|
||||
If 'reply_to' is set to either a message or a message ID,
|
||||
the sent message will be replying to such message.
|
||||
:param str | int | User | Chat | Channel entity: To who will it be sent.
|
||||
:param str message: The message to be sent.
|
||||
:param int | Message reply_to: Whether to reply to a message or not.
|
||||
:param link_preview: Should the link preview be shown?
|
||||
:return Message: the sent message
|
||||
"""
|
||||
entity = await self.get_input_entity(entity)
|
||||
request = SendMessageRequest(
|
||||
@@ -348,11 +376,11 @@ class TelegramClient(TelegramBareClient):
|
||||
Deletes a message from a chat, optionally "for everyone" with argument
|
||||
`revoke` set to `True`.
|
||||
|
||||
The `revoke` argument has no effect for Channels and Supergroups,
|
||||
The `revoke` argument has no effect for Channels and Megagroups,
|
||||
where it inherently behaves as being `True`.
|
||||
|
||||
Note: The `entity` argument can be `None` for normal chats, but it's
|
||||
mandatory to delete messages from Channels and Supergroups. It is also
|
||||
mandatory to delete messages from Channels and Megagroups. It is also
|
||||
possible to supply a chat_id which will be automatically resolved to
|
||||
the right type of InputPeer.
|
||||
|
||||
@@ -397,9 +425,6 @@ class TelegramClient(TelegramBareClient):
|
||||
|
||||
:return: A tuple containing total message count and two more lists ([messages], [senders]).
|
||||
Note that the sender can be null if it was not found!
|
||||
|
||||
The entity may be a phone or an username at the expense of
|
||||
some performance loss.
|
||||
"""
|
||||
result = await self(GetHistoryRequest(
|
||||
peer=await self.get_input_entity(entity),
|
||||
@@ -429,16 +454,15 @@ class TelegramClient(TelegramBareClient):
|
||||
return total_messages, result.messages, entities
|
||||
|
||||
async def send_read_acknowledge(self, entity, messages=None, max_id=None):
|
||||
"""Sends a "read acknowledge" (i.e., notifying the given peer that we've
|
||||
read their messages, also known as the "double check").
|
||||
"""
|
||||
Sends a "read acknowledge" (i.e., notifying the given peer that we've
|
||||
read their messages, also known as the "double check").
|
||||
|
||||
Either a list of messages (or a single message) can be given,
|
||||
or the maximum message ID (until which message we want to send the read acknowledge).
|
||||
|
||||
Returns an AffectedMessages TLObject
|
||||
|
||||
The entity may be a phone or an username at the expense of
|
||||
some performance loss.
|
||||
:param entity: The chat where these messages are located.
|
||||
:param messages: Either a list of messages or a single message.
|
||||
:param max_id: Overrides messages, until which message should the
|
||||
acknowledge should be sent.
|
||||
:return:
|
||||
"""
|
||||
if max_id is None:
|
||||
if not messages:
|
||||
@@ -480,36 +504,36 @@ class TelegramClient(TelegramBareClient):
|
||||
reply_to=None,
|
||||
attributes=None,
|
||||
**kwargs):
|
||||
"""Sends a file to the specified entity.
|
||||
The file may either be a path, a byte array, or a stream.
|
||||
Note that if a byte array or a stream is given, a filename
|
||||
or its type won't be inferred, and it will be sent as an
|
||||
"unnamed application/octet-stream".
|
||||
"""
|
||||
Sends a file to the specified entity.
|
||||
|
||||
An optional caption can also be specified for said file.
|
||||
|
||||
If "force_document" is False, the file will be sent as a photo
|
||||
if it's recognised to have a common image format (e.g. .png, .jpg).
|
||||
|
||||
Otherwise, the file will always be sent as an uncompressed document.
|
||||
|
||||
Subsequent calls with the very same file will result in
|
||||
immediate uploads, unless .clear_file_cache() is called.
|
||||
|
||||
If "progress_callback" is not None, it should be a function that
|
||||
takes two parameters, (bytes_uploaded, total_bytes).
|
||||
|
||||
The "reply_to" parameter works exactly as the one on .send_message.
|
||||
|
||||
If "attributes" is set to be a list of DocumentAttribute's, these
|
||||
will override the automatically inferred ones (so that you can
|
||||
modify the file name of the file sent for instance).
|
||||
:param entity:
|
||||
Who will receive the file.
|
||||
:param file:
|
||||
The path of the file, byte array, or stream that will be sent.
|
||||
Note that if a byte array or a stream is given, a filename
|
||||
or its type won't be inferred, and it will be sent as an
|
||||
"unnamed application/octet-stream".
|
||||
|
||||
Subsequent calls with the very same file will result in
|
||||
immediate uploads, unless .clear_file_cache() is called.
|
||||
:param caption:
|
||||
Optional caption for the sent media message.
|
||||
:param force_document:
|
||||
If left to False and the file is a path that ends with .png, .jpg
|
||||
and such, the file will be sent as a photo. Otherwise always as
|
||||
a document.
|
||||
:param progress_callback:
|
||||
A callback function accepting two parameters: (sent bytes, total)
|
||||
:param reply_to:
|
||||
Same as reply_to from .send_message().
|
||||
:param attributes:
|
||||
Optional attributes that override the inferred ones, like
|
||||
DocumentAttributeFilename and so on.
|
||||
:param kwargs:
|
||||
If "is_voice_note" in kwargs, despite its value, and the file is
|
||||
sent as a document, it will be sent as a voice note.
|
||||
|
||||
The entity may be a phone or an username at the expense of
|
||||
some performance loss.
|
||||
:return:
|
||||
"""
|
||||
as_photo = False
|
||||
if isinstance(file, str):
|
||||
@@ -600,21 +624,19 @@ class TelegramClient(TelegramBareClient):
|
||||
# region Downloading media requests
|
||||
|
||||
async def download_profile_photo(self, entity, file=None, download_big=True):
|
||||
"""Downloads the profile photo for an user or a chat (channels too).
|
||||
Returns None if no photo was provided, or if it was Empty.
|
||||
"""
|
||||
Downloads the profile photo of the given entity (user/chat/channel).
|
||||
|
||||
If an entity itself (an user, chat or channel) is given, the photo
|
||||
to be downloaded will be downloaded automatically.
|
||||
|
||||
On success, the file path is returned since it may differ from
|
||||
the one provided.
|
||||
|
||||
The specified output file can either be a file path, a directory,
|
||||
or a stream-like object. If the path exists and is a file, it will
|
||||
be overwritten.
|
||||
|
||||
The entity may be a phone or an username at the expense of
|
||||
some performance loss.
|
||||
:param entity:
|
||||
From who the photo will be downloaded.
|
||||
:param file:
|
||||
The output file path, directory, or stream-like object.
|
||||
If the path exists and is a file, it will be overwritten.
|
||||
:param download_big:
|
||||
Whether to use the big version of the available photos.
|
||||
:return:
|
||||
None if no photo was provided, or if it was Empty. On success
|
||||
the file path is returned since it may differ from the one given.
|
||||
"""
|
||||
possible_names = []
|
||||
if not isinstance(entity, TLObject) or type(entity).SUBCLASS_OF_ID in (
|
||||
@@ -669,21 +691,16 @@ class TelegramClient(TelegramBareClient):
|
||||
return file
|
||||
|
||||
async def download_media(self, message, file=None, progress_callback=None):
|
||||
"""Downloads the media from a specified Message (it can also be
|
||||
the message.media) into the desired file (a stream or str),
|
||||
optionally finding its extension automatically.
|
||||
|
||||
The specified output file can either be a file path, a directory,
|
||||
or a stream-like object. If the path exists and is a file, it will
|
||||
be overwritten.
|
||||
|
||||
If the operation succeeds, the path will be returned (since
|
||||
the extension may have been added automatically). Otherwise,
|
||||
None is returned.
|
||||
|
||||
The progress_callback should be a callback function which takes
|
||||
two parameters, uploaded size and total file size (both in bytes).
|
||||
This will be called every time a part is downloaded
|
||||
"""
|
||||
Downloads the given media, or the media from a specified Message.
|
||||
:param message:
|
||||
The media or message containing the media that will be downloaded.
|
||||
:param file:
|
||||
The output file path, directory, or stream-like object.
|
||||
If the path exists and is a file, it will be overwritten.
|
||||
:param progress_callback:
|
||||
A callback function accepting two parameters: (recv bytes, total)
|
||||
:return:
|
||||
"""
|
||||
# TODO This won't work for messageService
|
||||
if isinstance(message, Message):
|
||||
@@ -781,14 +798,14 @@ class TelegramClient(TelegramBareClient):
|
||||
f = file
|
||||
|
||||
try:
|
||||
# Remove these pesky characters
|
||||
first_name = first_name.replace(';', '')
|
||||
last_name = (last_name or '').replace(';', '')
|
||||
f.write('BEGIN:VCARD\n')
|
||||
f.write('VERSION:4.0\n')
|
||||
f.write('N:{};{};;;\n'.format(
|
||||
first_name, last_name if last_name else '')
|
||||
)
|
||||
f.write('FN:{}\n'.format(' '.join((first_name, last_name))))
|
||||
f.write('TEL;TYPE=cell;VALUE=uri:tel:+{}\n'.format(
|
||||
phone_number))
|
||||
f.write('N:{};{};;;\n'.format(first_name, last_name))
|
||||
f.write('FN:{} {}\n'.format(first_name, last_name))
|
||||
f.write('TEL;TYPE=cell;VALUE=uri:tel:+{}\n'.format(phone_number))
|
||||
f.write('END:VCARD\n')
|
||||
finally:
|
||||
# Only close the stream if we opened it
|
||||
@@ -862,20 +879,24 @@ class TelegramClient(TelegramBareClient):
|
||||
# region Small utilities to make users' life easier
|
||||
|
||||
async def get_entity(self, entity):
|
||||
"""Turns an entity into a valid Telegram user or chat.
|
||||
If "entity" is a string which can be converted to an integer,
|
||||
or if it starts with '+' it will be resolved as if it
|
||||
were a phone number.
|
||||
"""
|
||||
Turns the given entity into a valid Telegram user or chat.
|
||||
|
||||
If "entity" is a string and doesn't start with '+', or
|
||||
it starts with '@', it will be resolved from the username.
|
||||
If no exact match is returned, an error will be raised.
|
||||
:param entity:
|
||||
The entity to be transformed.
|
||||
If it's a string which can be converted to an integer or starts
|
||||
with '+' it will be resolved as if it were a phone number.
|
||||
|
||||
If "entity" is an integer or a "Peer", its information will
|
||||
be returned through a call to self.get_input_peer(entity).
|
||||
If it doesn't start with '+' or starts with a '@' it will be
|
||||
be resolved from the username. If no exact match is returned,
|
||||
an error will be raised.
|
||||
|
||||
If the entity is neither, and it's not a TLObject, an
|
||||
error will be raised.
|
||||
If the entity is an integer or a Peer, its information will be
|
||||
returned through a call to self.get_input_peer(entity).
|
||||
|
||||
If the entity is neither, and it's not a TLObject, an
|
||||
error will be raised.
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
return self.session.entities[entity]
|
||||
@@ -929,14 +950,23 @@ class TelegramClient(TelegramBareClient):
|
||||
)
|
||||
|
||||
async def get_input_entity(self, peer):
|
||||
"""Gets the input entity given its PeerUser, PeerChat, PeerChannel.
|
||||
If no Peer class is used, peer is assumed to be the integer ID
|
||||
of an User.
|
||||
"""
|
||||
Turns the given peer into its input entity version. Most requests
|
||||
use this kind of InputUser, InputChat and so on, so this is the
|
||||
most suitable call to make for those cases.
|
||||
|
||||
If this Peer hasn't been seen before by the library, all dialogs
|
||||
will loaded, and their entities saved to the session file.
|
||||
:param peer:
|
||||
The integer ID of an user or otherwise either of a
|
||||
PeerUser, PeerChat or PeerChannel, for which to get its
|
||||
Input* version.
|
||||
|
||||
If even after it's not found, a ValueError is raised.
|
||||
If this Peer hasn't been seen before by the library, the top
|
||||
dialogs will be loaded and their entities saved to the session
|
||||
file (unless this feature was disabled explicitly).
|
||||
|
||||
If in the end the access hash required for the peer was not found,
|
||||
a ValueError will be raised.
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
# First try to get the entity from cache, otherwise figure it out
|
||||
@@ -987,4 +1017,4 @@ class TelegramClient(TelegramBareClient):
|
||||
'Make sure you have encountered this peer before.'.format(peer)
|
||||
)
|
||||
|
||||
# endregion
|
||||
# endregion
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import re
|
||||
|
||||
from .. import utils
|
||||
from ..tl import TLObject
|
||||
from ..tl.types import (
|
||||
User, Chat, Channel, PeerUser, PeerChat, PeerChannel,
|
||||
InputPeerUser, InputPeerChat, InputPeerChannel
|
||||
)
|
||||
from .. import utils # Keep this line the last to maybe fix #357
|
||||
|
||||
|
||||
class EntityDatabase:
|
||||
|
@@ -13,7 +13,6 @@ class TLMessage(TLObject):
|
||||
self.seq_no = session.generate_sequence(request.content_related)
|
||||
self.request = request
|
||||
self.container_msg_id = None
|
||||
logging.getLogger(__name__).debug(self)
|
||||
|
||||
def to_dict(self, recursive=True):
|
||||
return {
|
||||
|
@@ -91,8 +91,11 @@ class TLObject:
|
||||
@staticmethod
|
||||
def serialize_bytes(data):
|
||||
"""Write bytes by using Telegram guidelines"""
|
||||
if isinstance(data, str):
|
||||
data = data.encode('utf-8')
|
||||
if not isinstance(data, bytes):
|
||||
if isinstance(data, str):
|
||||
data = data.encode('utf-8')
|
||||
else:
|
||||
raise ValueError('bytes or str expected, not', type(data))
|
||||
|
||||
r = []
|
||||
if len(data) < 254:
|
||||
|
@@ -142,7 +142,10 @@ def get_input_user(entity):
|
||||
else:
|
||||
return InputUser(entity.id, entity.access_hash)
|
||||
|
||||
if isinstance(entity, UserEmpty):
|
||||
if isinstance(entity, InputPeerSelf):
|
||||
return InputUserSelf()
|
||||
|
||||
if isinstance(entity, (UserEmpty, InputPeerEmpty)):
|
||||
return InputUserEmpty()
|
||||
|
||||
if isinstance(entity, UserFull):
|
||||
|
Reference in New Issue
Block a user