Merge remote-tracking branch 'upstream/asyncio' into asyncio

This commit is contained in:
Andrey Egorov 2017-10-30 00:29:46 +03:00
commit 5e172053da
17 changed files with 550 additions and 352 deletions

View File

@ -89,7 +89,7 @@ def main():
for x in ('build', 'dist', 'Telethon.egg-info'):
rmtree(x, ignore_errors=True)
if len(argv) >= 2 and argv[1] == 'fetch_errors':
elif len(argv) >= 2 and argv[1] == 'fetch_errors':
from telethon_generator.error_generator import fetch_errors
fetch_errors(ERRORS_JSON)

View File

@ -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,12 +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('Resending request')
await self.send(request)
return
requests = self._pop_requests_of_container(msg_id)
if requests:
self._logger.debug('Resending container of requests')
await self.send(*requests)
async def _handle_pong(self, msg_id, sequence, reader):
@ -322,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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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):

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,113 +0,0 @@
#!/usr/bin/env python3
# disclaimer: you should not actually use this. it can be quite spammy.
from telethon import TelegramClient
from telethon.errors import SessionPasswordNeededError
from getpass import getpass
from telethon.tl.types import InputPeerUser,InputPeerChannel
from telethon.tl.types import Updates
from telethon.tl.types import UpdateNewChannelMessage,UpdateNewMessage
from telethon.tl.functions.messages import SendMessageRequest,EditMessageRequest
from telethon.tl.types import MessageService
from nltk.tokenize import word_tokenize
from os import environ
from time import sleep
CHANNELS = {}
CHANNELNAMES = {}
USERS = {}
EMACS_BLACKLIST = [1058260578, # si @linux_group
123456789]
REACTS = {'emacs':'Needs more vim.',
'chrome':'Needs more firefox.',
}
class NeedsMore(TelegramClient):
def __init__(self):
settings = {'api_id':int(environ['TG_API_ID']),
'api_hash':environ['TG_API_HASH'],
'user_phone':environ['TG_PHONE'],
'session_name':'needsmore'}
super().__init__(
settings.get('session_name','session1'),
settings['api_id'],
settings['api_hash'],
proxy=None,
process_updates=True)
user_phone = settings['user_phone']
print('INFO: Connecting to Telegram Servers...', end='', flush=True)
self.connect()
print('Done!')
if not self.is_user_authorized():
print('INFO: Unauthorized user')
self.send_code_request(user_phone)
code_ok = False
while not code_ok:
code = input('Enter the auth code: ')
try:
code_ok = self.sign_in(user_phone, code)
except SessionPasswordNeededError:
pw = getpass('Two step verification enabled. Please enter your password: ')
self.sign_in(password=pw)
print('INFO: Client initialized succesfully!')
def run(self):
# Listen for updates
while True:
update = self.updates.poll() # This will block until an update is available
triggers = []
if isinstance(update, Updates):
for x in update.updates:
if not isinstance(x,UpdateNewChannelMessage): continue
if isinstance(x.message,MessageService): continue
# We're only interested in messages to supergroups
words = word_tokenize(x.message.message.lower())
# Avoid matching 'emacs' in 'spacemacs' and similar
if 'emacs' in words and x.message.to_id.channel_id not in EMACS_BLACKLIST:
triggers.append(('emacs',x.message))
if 'chrome' in words:
triggers.append(('chrome',x.message))
if 'x files theme' == x.message.message.lower() and x.message.out:
# Automatically reply to yourself saying 'x files theme' with the audio
msg = x.message
chan = InputPeerChannel(msg.to_id.channel_id,CHANNELS[msg.to_id.channel_id])
self.send_voice_note(chan,'xfiles.m4a',reply_to=msg.id)
sleep(1)
if '.shrug' in x.message.message.lower() and x.message.out:
# Automatically replace '.shrug' in any message you
# send to a supergroup with the shrug emoticon
msg = x.message
chan = InputPeerChannel(msg.to_id.channel_id,CHANNELS[msg.to_id.channel_id])
self(EditMessageRequest(chan,msg.id,
message=msg.message.replace('.shrug','¯\_(ツ)_/¯')))
sleep(1)
for trigger in triggers:
msg = trigger[1]
chan = InputPeerChannel(msg.to_id.channel_id,CHANNELS[msg.to_id.channel_id])
log_chat = InputPeerUser(user_id=123456789,access_hash=987654321234567890)
self.send_message(log_chat,"{} said {} in {}. Sending react {}".format(
msg.from_id,msg.message,CHANNELNAMES[msg.to_id.channel_id],REACTS[trigger[0]][:20]))
react = '>{}\n{}'.format(trigger[0],REACTS[trigger[0]])
self.invoke(SendMessageRequest(chan,react,reply_to_msg_id=msg.id))
sleep(1)
if __name__ == "__main__":
#TODO: this block could be moved to __init__
# You can create these text files using https://github.com/LonamiWebs/Telethon/wiki/Retrieving-all-dialogs
with open('channels.txt','r') as f:
# Format: channel_id access_hash #Channel Name
lines = f.readlines()
chans = [l.split(' #',1)[0].split(' ') for l in lines]
CHANNELS = {int(c[0]):int(c[1]) for c in chans} # id:hash
CHANNELNAMES = {int(l.split()[0]):l.split('#',1)[1].strip() for l in lines} #id:name
with open('users','r') as f:
# Format: [user_id, access_hash, 'username', 'Firstname Lastname']
lines = f.readlines()
uss = [l.strip()[1:-1].split(',') for l in lines]
USERS = {int(user[0]):int(user[1]) for user in uss} # id:hash
needsmore = NeedsMore()
needsmore.run()

View File

@ -20,11 +20,11 @@ def sprint(string, *args, **kwargs):
def print_title(title):
# Clear previous window
print('\n')
print('=={}=='.format('=' * len(title)))
"""Helper function to print titles to the console more nicely"""
sprint('\n')
sprint('=={}=='.format('=' * len(title)))
sprint('= {} ='.format(title))
print('=={}=='.format('=' * len(title)))
sprint('=={}=='.format('=' * len(title)))
def bytes_to_string(byte_count):
@ -34,8 +34,9 @@ def bytes_to_string(byte_count):
byte_count /= 1024
suffix_index += 1
return '{:.2f}{}'.format(byte_count,
[' bytes', 'KB', 'MB', 'GB', 'TB'][suffix_index])
return '{:.2f}{}'.format(
byte_count, [' bytes', 'KB', 'MB', 'GB', 'TB'][suffix_index]
)
class InteractiveTelegramClient(TelegramClient):
@ -48,13 +49,38 @@ class InteractiveTelegramClient(TelegramClient):
"""
def __init__(self, session_user_id, user_phone, api_id, api_hash,
proxy=None):
"""
Initializes the InteractiveTelegramClient.
:param session_user_id: Name of the *.session file.
:param user_phone: The phone of the user that will login.
:param api_id: Telegram's api_id acquired through my.telegram.org.
:param api_hash: Telegram's api_hash.
:param proxy: Optional proxy tuple/dictionary.
"""
print_title('Initialization')
print('Initializing interactive example...')
# The first step is to initialize the TelegramClient, as we are
# subclassing it, we need to call super().__init__(). On a more
# normal case you would want 'client = TelegramClient(...)'
super().__init__(
# These parameters should be passed always, session name and API
session_user_id, api_id, api_hash,
# You can optionally change the connection mode by using this enum.
# This changes how much data will be sent over the network with
# every request, and how it will be formatted. Default is
# ConnectionMode.TCP_FULL, and smallest is TCP_TCP_ABRIDGED.
connection_mode=ConnectionMode.TCP_ABRIDGED,
# If you're using a proxy, set it here.
proxy=proxy,
# If you want to receive updates, you need to start one or more
# "update workers" which are background threads that will allow
# you to run things when your update handlers (callbacks) are
# called with an Update object.
update_workers=1
)
@ -62,6 +88,8 @@ class InteractiveTelegramClient(TelegramClient):
# so it can be downloaded if the user wants
self.found_media = set()
# Calling .connect() may return False, so you need to assert it's
# True before continuing. Otherwise you may want to retry as done here.
print('Connecting to Telegram servers...')
if not self.connect():
print('Initial connection failed. Retrying...')
@ -69,18 +97,24 @@ class InteractiveTelegramClient(TelegramClient):
print('Could not connect to Telegram servers.')
return
# Then, ensure we're authorized and have access
# If the user hasn't called .sign_in() or .sign_up() yet, they won't
# be authorized. The first thing you must do is authorize. Calling
# .sign_in() should only be done once as the information is saved on
# the *.session file so you don't need to enter the code every time.
if not self.is_user_authorized():
print('First run. Sending code request...')
self.send_code_request(user_phone)
self.sign_in(user_phone)
self_user = None
while self_user is None:
code = input('Enter the code you just received: ')
try:
self_user = self.sign_in(user_phone, code)
self_user = self.sign_in(code=code)
# Two-step verification may be enabled
# Two-step verification may be enabled, and .sign_in will
# raise this error. If that's the case ask for the password.
# Note that getpass() may not work on PyCharm due to a bug,
# if that's the case simply change it for input().
except SessionPasswordNeededError:
pw = getpass('Two step verification is enabled. '
'Please enter your password: ')
@ -88,16 +122,22 @@ class InteractiveTelegramClient(TelegramClient):
self_user = self.sign_in(password=pw)
def run(self):
# Listen for updates
"""Main loop of the TelegramClient, will wait for user action"""
# Once everything is ready, we can add an update handler. Every
# update object will be passed to the self.update_handler method,
# where we can process it as we need.
self.add_update_handler(self.update_handler)
# Enter a while loop to chat as long as the user wants
while True:
# Retrieve the top dialogs
# Retrieve the top dialogs. You can set the limit to None to
# retrieve all of them if you wish, but beware that may take
# a long time if you have hundreds of them.
dialog_count = 15
# Entities represent the user, chat or channel
# corresponding to the dialog on the same index
# corresponding to the dialog on the same index.
dialogs, entities = self.get_dialogs(limit=dialog_count)
i = None
@ -119,6 +159,12 @@ class InteractiveTelegramClient(TelegramClient):
if i == '!q':
return
if i == '!l':
# Logging out will cause the user to need to reenter the
# code next time they want to use the library, and will
# also delete the *.session file off the filesystem.
#
# This is not the same as simply calling .disconnect(),
# which simply shuts down everything gracefully.
self.log_out()
return
@ -158,8 +204,8 @@ class InteractiveTelegramClient(TelegramClient):
# History
elif msg == '!h':
# First retrieve the messages and some information
total_count, messages, senders = self.get_message_history(
entity, limit=10)
total_count, messages, senders = \
self.get_message_history(entity, limit=10)
# Iterate over all (in reverse order so the latest appear
# the last in the console) and print them with format:
@ -237,6 +283,7 @@ class InteractiveTelegramClient(TelegramClient):
entity, msg, link_preview=False)
def send_photo(self, path, entity):
"""Sends the file located at path to the desired entity as a photo"""
self.send_file(
entity, path,
progress_callback=self.upload_progress_callback
@ -244,6 +291,7 @@ class InteractiveTelegramClient(TelegramClient):
print('Photo sent!')
def send_document(self, path, entity):
"""Sends the file located at path to the desired entity as a document"""
self.send_file(
entity, path,
force_document=True,
@ -252,6 +300,9 @@ class InteractiveTelegramClient(TelegramClient):
print('Document sent!')
def download_media_by_id(self, media_id):
"""Given a message ID, finds the media this message contained and
downloads it.
"""
try:
# The user may have entered a non-integer string!
msg_media_id = int(media_id)
@ -291,6 +342,11 @@ class InteractiveTelegramClient(TelegramClient):
)
def update_handler(self, update):
"""Callback method for received Updates"""
# We have full control over what we want to do with the updates.
# In our case we only want to react to chat messages, so we use
# isinstance() to behave accordingly on these cases.
if isinstance(update, UpdateShortMessage):
who = self.get_entity(update.user_id)
if update.out:

View File

@ -0,0 +1,46 @@
#!/usr/bin/env python3
# A simple script to print all updates received
from getpass import getpass
from os import environ
# environ is used to get API information from environment variables
# You could also use a config file, pass them as arguments,
# or even hardcode them (not recommended)
from telethon import TelegramClient
from telethon.errors import SessionPasswordNeededError
def main():
session_name = environ.get('TG_SESSION', 'session')
user_phone = environ['TG_PHONE']
client = TelegramClient(session_name,
int(environ['TG_API_ID']),
environ['TG_API_HASH'],
proxy=None,
update_workers=4)
print('INFO: Connecting to Telegram Servers...', end='', flush=True)
client.connect()
print('Done!')
if not client.is_user_authorized():
print('INFO: Unauthorized user')
client.send_code_request(user_phone)
code_ok = False
while not code_ok:
code = input('Enter the auth code: ')
try:
code_ok = client.sign_in(user_phone, code)
except SessionPasswordNeededError:
password = getpass('Two step verification enabled. Please enter your password: ')
code_ok = client.sign_in(password=password)
print('INFO: Client initialized succesfully!')
client.add_update_handler(update_handler)
input('Press Enter to stop this!\n')
def update_handler(update):
print(update)
print('Press Enter to stop this!')
if __name__ == '__main__':
main()

137
telethon_examples/replier.py Executable file
View File

@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""
A example script to automatically send messages based on certain triggers.
The script makes uses of environment variables to determine the API ID,
hash, phone and such to be used. You may want to add these to your .bashrc
file, including TG_API_ID, TG_API_HASH, TG_PHONE and optionally TG_SESSION.
This script assumes that you have certain files on the working directory,
such as "xfiles.m4a" or "anytime.png" for some of the automated replies.
"""
from getpass import getpass
from collections import defaultdict
from datetime import datetime, timedelta
from os import environ
import re
from telethon import TelegramClient
from telethon.errors import SessionPasswordNeededError
from telethon.tl.types import UpdateNewChannelMessage, UpdateShortMessage, MessageService
from telethon.tl.functions.messages import EditMessageRequest
"""Uncomment this for debugging
import logging
logging.basicConfig(level=logging.DEBUG)
logging.debug('dbg')
logging.info('info')
"""
REACTS = {'emacs': 'Needs more vim',
'chrome': 'Needs more Firefox'}
# A list of dates of reactions we've sent, so we can keep track of floods
recent_reacts = defaultdict(list)
def update_handler(update):
global recent_reacts
try:
msg = update.message
except AttributeError:
# print(update, 'did not have update.message')
return
if isinstance(msg, MessageService):
print(msg, 'was service msg')
return
# React to messages in supergroups and PMs
if isinstance(update, UpdateNewChannelMessage):
words = re.split('\W+', msg.message)
for trigger, response in REACTS.items():
if len(recent_reacts[msg.to_id.channel_id]) > 3:
# Silently ignore triggers if we've recently sent 3 reactions
break
if trigger in words:
# Remove recent replies older than 10 minutes
recent_reacts[msg.to_id.channel_id] = [
a for a in recent_reacts[msg.to_id.channel_id] if
datetime.now() - a < timedelta(minutes=10)
]
# Send a reaction
client.send_message(msg.to_id, response, reply_to=msg.id)
# Add this reaction to the list of recent actions
recent_reacts[msg.to_id.channel_id].append(datetime.now())
if isinstance(update, UpdateShortMessage):
words = re.split('\W+', msg)
for trigger, response in REACTS.items():
if len(recent_reacts[update.user_id]) > 3:
# Silently ignore triggers if we've recently sent 3 reactions
break
if trigger in words:
# Send a reaction
client.send_message(update.user_id, response, reply_to=update.id)
# Add this reaction to the list of recent reactions
recent_reacts[update.user_id].append(datetime.now())
# Automatically send relevant media when we say certain things
# When invoking requests, get_input_entity needs to be called manually
if isinstance(update, UpdateNewChannelMessage) and msg.out:
if msg.message.lower() == 'x files theme':
client.send_voice_note(msg.to_id, 'xfiles.m4a', reply_to=msg.id)
if msg.message.lower() == 'anytime':
client.send_file(msg.to_id, 'anytime.png', reply_to=msg.id)
if '.shrug' in msg.message:
client(EditMessageRequest(
client.get_input_entity(msg.to_id), msg.id,
message=msg.message.replace('.shrug', r'¯\_(ツ)_/¯')
))
if isinstance(update, UpdateShortMessage) and update.out:
if msg.lower() == 'x files theme':
client.send_voice_note(update.user_id, 'xfiles.m4a', reply_to=update.id)
if msg.lower() == 'anytime':
client.send_file(update.user_id, 'anytime.png', reply_to=update.id)
if '.shrug' in msg:
client(EditMessageRequest(
client.get_input_entity(update.user_id), update.id,
message=msg.replace('.shrug', r'¯\_(ツ)_/¯')
))
if __name__ == '__main__':
session_name = environ.get('TG_SESSION', 'session')
user_phone = environ['TG_PHONE']
client = TelegramClient(
session_name, int(environ['TG_API_ID']), environ['TG_API_HASH'],
proxy=None, update_workers=4
)
try:
print('INFO: Connecting to Telegram Servers...', end='', flush=True)
client.connect()
print('Done!')
if not client.is_user_authorized():
print('INFO: Unauthorized user')
client.send_code_request(user_phone)
code_ok = False
while not code_ok:
code = input('Enter the auth code: ')
try:
code_ok = client.sign_in(user_phone, code)
except SessionPasswordNeededError:
password = getpass('Two step verification enabled. '
'Please enter your password: ')
code_ok = client.sign_in(password=password)
print('INFO: Client initialized successfully!')
client.add_update_handler(update_handler)
input('Press Enter to stop this!\n')
except KeyboardInterrupt:
pass
finally:
client.disconnect()

View File

@ -0,0 +1 @@

View File

@ -45,7 +45,7 @@ PHONE_NUMBER_OCCUPIED=The phone number is already in use
PHONE_NUMBER_UNOCCUPIED=The phone number is not yet being used
PHOTO_INVALID_DIMENSIONS=The photo dimensions are invalid
TYPE_CONSTRUCTOR_INVALID=The type constructor is invalid
USERNAME_INVALID=Unacceptable username. Must match r"[a-zA-Z][\w\d]{4,31}"
USERNAME_INVALID=Unacceptable username. Must match r"[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]"
USERNAME_NOT_MODIFIED=The username is not different from the current username
USERNAME_NOT_OCCUPIED=The username is not in use by anyone else yet
USERNAME_OCCUPIED=The username is already taken

View File

@ -154,11 +154,10 @@ def generate_code(output, json_file, errors_desc):
patterns.append((pattern, name))
capture = capture_names.get(name, 'x') if has_captures else None
# TODO Some errors have the same name but different code,
# split this accross different files?
# split this across different files?
write_error(f, error_code, name, description, capture)
f.write('\n\nrpc_errors_all = {\n')
for pattern, name in patterns:
f.write(' {}: {},\n'.format(repr(pattern), name))
f.write('}\n')

View File

@ -104,8 +104,7 @@ class TLObject:
def class_name_for(typename, is_function=False):
"""Gets the class name following the Python style guidelines"""
# Courtesy of http://stackoverflow.com/a/31531797/4759433
result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(),
typename)
result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), typename)
result = result[:1].upper() + result[1:].replace('_', '')
# If it's a function, let it end with "Request" to identify them
if is_function:

View File

@ -129,9 +129,6 @@ class TLGenerator:
builder.writeln(
'from {}.tl.tlobject import TLObject'.format('.' * depth)
)
builder.writeln(
'from {}.tl import types'.format('.' * depth)
)
# Add the relative imports to the namespaces,
# unless we already are in a namespace.
@ -494,11 +491,15 @@ class TLGenerator:
elif arg.flag_indicator:
# Calculate the flags with those items which are not None
builder.write("struct.pack('<I', {})".format(
' | '.join('({} if {} else 0)'.format(
1 << flag.flag_index, 'self.{}'.format(flag.name)
) for flag in args if flag.is_flag)
))
if not any(f.is_flag for f in args):
# There's a flag indicator, but no flag arguments so it's 0
builder.write(r"b'\0\0\0\0'")
else:
builder.write("struct.pack('<I', {})".format(
' | '.join('({} if {} else 0)'.format(
1 << flag.flag_index, 'self.{}'.format(flag.name)
) for flag in args if flag.is_flag)
))
elif 'int' == arg.type:
# struct.pack is around 4 times faster than int.to_bytes
@ -644,8 +645,25 @@ class TLGenerator:
if not arg.skip_constructor_id:
builder.writeln('{} = reader.tgread_object()'.format(name))
else:
builder.writeln('{} = types.{}.from_reader(reader)'.format(
name, TLObject.class_name_for(arg.type)))
# Import the correct type inline to avoid cyclic imports.
# There may be better solutions so that we can just access
# all the types before the files have been parsed, but I
# don't know of any.
sep_index = arg.type.find('.')
if sep_index == -1:
ns, t = '.', arg.type
else:
ns, t = '.' + arg.type[:sep_index], arg.type[sep_index+1:]
class_name = TLObject.class_name_for(t)
# There would be no need to import the type if we're in the
# file with the same namespace, but since it does no harm
# and we don't have information about such thing in the
# method we just ignore that case.
builder.writeln('from {} import {}'.format(ns, class_name))
builder.writeln('{} = {}.from_reader(reader)'.format(
name, class_name
))
# End vector and flag blocks if required (if we opened them before)
if arg.is_vector: