mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-03-10 06:05:47 +03:00
Merge remote-tracking branch 'upstream/asyncio' into asyncio
This commit is contained in:
commit
5e172053da
2
setup.py
2
setup.py
|
@ -89,7 +89,7 @@ def main():
|
||||||
for x in ('build', 'dist', 'Telethon.egg-info'):
|
for x in ('build', 'dist', 'Telethon.egg-info'):
|
||||||
rmtree(x, ignore_errors=True)
|
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
|
from telethon_generator.error_generator import fetch_errors
|
||||||
fetch_errors(ERRORS_JSON)
|
fetch_errors(ERRORS_JSON)
|
||||||
|
|
||||||
|
|
|
@ -81,6 +81,9 @@ class MtProtoSender:
|
||||||
message = messages[0]
|
message = messages[0]
|
||||||
else:
|
else:
|
||||||
message = TLMessage(self.session, MessageContainer(messages))
|
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:
|
for m in messages:
|
||||||
m.container_msg_id = message.msg_id
|
m.container_msg_id = message.msg_id
|
||||||
|
|
||||||
|
@ -261,7 +264,12 @@ class MtProtoSender:
|
||||||
return self._pending_receive.pop(msg_id).request
|
return self._pending_receive.pop(msg_id).request
|
||||||
|
|
||||||
def _pop_requests_of_container(self, container_msg_id):
|
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]
|
requests = [msg.request for msg in msgs]
|
||||||
for msg in msgs:
|
for msg in msgs:
|
||||||
self._pending_receive.pop(msg.msg_id, None)
|
self._pending_receive.pop(msg.msg_id, None)
|
||||||
|
@ -273,12 +281,17 @@ class MtProtoSender:
|
||||||
self._pending_receive.clear()
|
self._pending_receive.clear()
|
||||||
|
|
||||||
async def _resend_request(self, msg_id):
|
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)
|
request = self._pop_request(msg_id)
|
||||||
if request:
|
if request:
|
||||||
|
self._logger.debug('Resending request')
|
||||||
await self.send(request)
|
await self.send(request)
|
||||||
return
|
return
|
||||||
requests = self._pop_requests_of_container(msg_id)
|
requests = self._pop_requests_of_container(msg_id)
|
||||||
if requests:
|
if requests:
|
||||||
|
self._logger.debug('Resending container of requests')
|
||||||
await self.send(*requests)
|
await self.send(*requests)
|
||||||
|
|
||||||
async def _handle_pong(self, msg_id, sequence, reader):
|
async def _handle_pong(self, msg_id, sequence, reader):
|
||||||
|
@ -322,6 +335,8 @@ class MtProtoSender:
|
||||||
)[0]
|
)[0]
|
||||||
self.session.save()
|
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)
|
await self._resend_request(bad_salt.bad_msg_id)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -30,6 +30,7 @@ from .tl.functions.upload import (
|
||||||
GetFileRequest, SaveBigFilePartRequest, SaveFilePartRequest
|
GetFileRequest, SaveBigFilePartRequest, SaveFilePartRequest
|
||||||
)
|
)
|
||||||
from .tl.types import InputFile, InputFileBig
|
from .tl.types import InputFile, InputFileBig
|
||||||
|
from .tl.types.auth import ExportedAuthorization
|
||||||
from .tl.types.upload import FileCdnRedirect
|
from .tl.types.upload import FileCdnRedirect
|
||||||
from .update_state import UpdateState
|
from .update_state import UpdateState
|
||||||
from .utils import get_appropriated_part_size
|
from .utils import get_appropriated_part_size
|
||||||
|
@ -59,7 +60,7 @@ class TelegramBareClient:
|
||||||
__version__ = '0.15.3'
|
__version__ = '0.15.3'
|
||||||
|
|
||||||
# TODO Make this thread-safe, all connections share the same DC
|
# TODO Make this thread-safe, all connections share the same DC
|
||||||
_dc_options = None
|
_config = None # Server configuration (with .dc_options)
|
||||||
|
|
||||||
# region Initialization
|
# region Initialization
|
||||||
|
|
||||||
|
@ -148,7 +149,7 @@ class TelegramBareClient:
|
||||||
|
|
||||||
# region Connecting
|
# 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
|
"""Connects to the Telegram servers, executing authentication if
|
||||||
required. Note that authenticating to the Telegram servers is
|
required. Note that authenticating to the Telegram servers is
|
||||||
not the same as authenticating the desired user itself, which
|
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.
|
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
|
If '_sync_updates', sync_updates() will be called and a
|
||||||
second thread will be started if necessary. Note that this
|
second thread will be started if necessary. Note that this
|
||||||
will FAIL if the client is not connected to the user's
|
will FAIL if the client is not connected to the user's
|
||||||
native data center, raising a "UserMigrateError", and
|
native data center, raising a "UserMigrateError", and
|
||||||
calling .disconnect() in the process.
|
calling .disconnect() in the process.
|
||||||
|
|
||||||
If '_cdn' is False, methods that are not allowed on such data
|
|
||||||
centers won't be invoked.
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
await self._sender.connect()
|
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
|
# Connection was successful! Try syncing the update state
|
||||||
# UNLESS '_sync_updates' is False (we probably are in
|
# UNLESS '_sync_updates' is False (we probably are in
|
||||||
# another data center and this would raise UserMigrateError)
|
# another data center and this would raise UserMigrateError)
|
||||||
# to also assert whether the user is logged in or not.
|
# to also assert whether the user is logged in or not.
|
||||||
self._user_connected = True
|
self._user_connected = True
|
||||||
if self._authorized is None and _sync_updates and not _cdn:
|
if self._authorized is None and _sync_updates:
|
||||||
try:
|
try:
|
||||||
await self.sync_updates()
|
await self.sync_updates()
|
||||||
self._set_connected_and_authorized()
|
self._set_connected_and_authorized()
|
||||||
|
@ -224,11 +186,7 @@ class TelegramBareClient:
|
||||||
# This is fine, probably layer migration
|
# This is fine, probably layer migration
|
||||||
self._logger.debug('Found invalid item, probably migrating', e)
|
self._logger.debug('Found invalid item, probably migrating', e)
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
return await self.connect(
|
return await self.connect(_sync_updates=_sync_updates)
|
||||||
_exported_auth=_exported_auth,
|
|
||||||
_sync_updates=_sync_updates,
|
|
||||||
_cdn=_cdn
|
|
||||||
)
|
|
||||||
|
|
||||||
except (RPCError, ConnectionError) as error:
|
except (RPCError, ConnectionError) as error:
|
||||||
# Probably errors from the previous session, ignore them
|
# Probably errors from the previous session, ignore them
|
||||||
|
@ -241,8 +199,9 @@ class TelegramBareClient:
|
||||||
def is_connected(self):
|
def is_connected(self):
|
||||||
return self._sender.is_connected()
|
return self._sender.is_connected()
|
||||||
|
|
||||||
async def _init_connection(self, query=None):
|
def _wrap_init_connection(self, query):
|
||||||
result = await self(InvokeWithLayerRequest(LAYER, InitConnectionRequest(
|
"""Wraps query around InvokeWithLayerRequest(InitConnectionRequest())"""
|
||||||
|
return InvokeWithLayerRequest(LAYER, InitConnectionRequest(
|
||||||
api_id=self.api_id,
|
api_id=self.api_id,
|
||||||
device_model=self.session.device_model,
|
device_model=self.session.device_model,
|
||||||
system_version=self.session.system_version,
|
system_version=self.session.system_version,
|
||||||
|
@ -251,10 +210,7 @@ class TelegramBareClient:
|
||||||
system_lang_code=self.session.system_lang_code,
|
system_lang_code=self.session.system_lang_code,
|
||||||
lang_pack='', # "langPacks are for official apps only"
|
lang_pack='', # "langPacks are for official apps only"
|
||||||
query=query
|
query=query
|
||||||
)))
|
))
|
||||||
self.session.layer = LAYER
|
|
||||||
self.session.save()
|
|
||||||
return result
|
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
"""Disconnects from the Telegram server"""
|
"""Disconnects from the Telegram server"""
|
||||||
|
@ -286,13 +242,18 @@ class TelegramBareClient:
|
||||||
finally:
|
finally:
|
||||||
self._reconnect_lock.release()
|
self._reconnect_lock.release()
|
||||||
else:
|
else:
|
||||||
self.disconnect()
|
# Since we're reconnecting possibly due to a UserMigrateError,
|
||||||
self.session.auth_key = None # Force creating new auth_key
|
# we need to first know the Data Centers we can connect to. Do
|
||||||
|
# that before disconnecting.
|
||||||
dc = await self._get_dc(new_dc)
|
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
|
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.session.save()
|
||||||
|
self.disconnect()
|
||||||
return await self.connect()
|
return await self.connect()
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
@ -301,11 +262,8 @@ class TelegramBareClient:
|
||||||
|
|
||||||
async def _get_dc(self, dc_id, ipv6=False, cdn=False):
|
async def _get_dc(self, dc_id, ipv6=False, cdn=False):
|
||||||
"""Gets the Data Center (DC) associated to 'dc_id'"""
|
"""Gets the Data Center (DC) associated to 'dc_id'"""
|
||||||
if TelegramBareClient._dc_options is None:
|
if not TelegramBareClient._config:
|
||||||
raise ConnectionError(
|
TelegramBareClient._config = await self(GetConfigRequest())
|
||||||
'Cannot determine the required data center IP address. '
|
|
||||||
'Stabilise a successful initial connection first.'
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if cdn:
|
if cdn:
|
||||||
|
@ -314,15 +272,15 @@ class TelegramBareClient:
|
||||||
rsa.add_key(pk.public_key)
|
rsa.add_key(pk.public_key)
|
||||||
|
|
||||||
return next(
|
return next(
|
||||||
dc for dc in TelegramBareClient._dc_options if dc.id == dc_id
|
dc for dc in TelegramBareClient._config.dc_options
|
||||||
and bool(dc.ipv6) == ipv6 and bool(dc.cdn) == cdn
|
if dc.id == dc_id and bool(dc.ipv6) == ipv6 and bool(dc.cdn) == cdn
|
||||||
)
|
)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
if not cdn:
|
if not cdn:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# New configuration, perhaps a new CDN was added?
|
# 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)
|
return await self._get_dc(dc_id, ipv6=ipv6, cdn=cdn)
|
||||||
|
|
||||||
async def _get_exported_client(self, dc_id):
|
async def _get_exported_client(self, dc_id):
|
||||||
|
@ -363,7 +321,14 @@ class TelegramBareClient:
|
||||||
timeout=self._sender.connection.get_timeout(),
|
timeout=self._sender.connection.get_timeout(),
|
||||||
loop=self._loop
|
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
|
client._authorized = True # We exported the auth, so we got auth
|
||||||
return client
|
return client
|
||||||
|
|
||||||
|
@ -386,9 +351,10 @@ class TelegramBareClient:
|
||||||
|
|
||||||
# This will make use of the new RSA keys for this specific CDN.
|
# This will make use of the new RSA keys for this specific CDN.
|
||||||
#
|
#
|
||||||
# This relies on the fact that TelegramBareClient._dc_options is
|
# We won't be calling GetConfigRequest because it's only called
|
||||||
# static and it won't be called from this DC (it would fail).
|
# when needed by ._get_dc, and also it's static so it's likely
|
||||||
await client.connect(_cdn=True) # Avoid invoking non-CDN methods
|
# set already. Avoid invoking non-CDN methods by not syncing updates.
|
||||||
|
await client.connect(_sync_updates=False)
|
||||||
client._authorized = self._authorized
|
client._authorized = self._authorized
|
||||||
return client
|
return client
|
||||||
|
|
||||||
|
@ -423,11 +389,33 @@ class TelegramBareClient:
|
||||||
invoke = __call__
|
invoke = __call__
|
||||||
|
|
||||||
async def _invoke(self, call_receive, retry, *requests):
|
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:
|
try:
|
||||||
# Ensure that we start with no previous errors (i.e. resending)
|
# Ensure that we start with no previous errors (i.e. resending)
|
||||||
for x in requests:
|
for x in requests:
|
||||||
x.rpc_error = None
|
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)
|
await self._sender.send(*requests)
|
||||||
|
|
||||||
if not call_receive:
|
if not call_receive:
|
||||||
|
@ -440,6 +428,13 @@ class TelegramBareClient:
|
||||||
while not all(x.confirm_received.is_set() for x in requests):
|
while not all(x.confirm_received.is_set() for x in requests):
|
||||||
await self._sender.receive(update_state=self.updates)
|
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:
|
except ConnectionResetError:
|
||||||
if not self._user_connected or self._reconnect_lock.locked():
|
if not self._user_connected or self._reconnect_lock.locked():
|
||||||
# Only attempt reconnecting if the user called connect and not
|
# Only attempt reconnecting if the user called connect and not
|
||||||
|
@ -453,6 +448,12 @@ class TelegramBareClient:
|
||||||
await asyncio.sleep(retry + 1, loop=self._loop)
|
await asyncio.sleep(retry + 1, loop=self._loop)
|
||||||
return None
|
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:
|
try:
|
||||||
raise next(x.rpc_error for x in requests if x.rpc_error)
|
raise next(x.rpc_error for x in requests if x.rpc_error)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
|
@ -728,7 +729,8 @@ class TelegramBareClient:
|
||||||
if need_reconnect:
|
if need_reconnect:
|
||||||
need_reconnect = False
|
need_reconnect = False
|
||||||
while self._user_connected and not await self._reconnect():
|
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)
|
await self._sender.receive(update_state=self.updates)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
|
@ -748,7 +750,8 @@ class TelegramBareClient:
|
||||||
try:
|
try:
|
||||||
import socks
|
import socks
|
||||||
if isinstance(error, (
|
if isinstance(error, (
|
||||||
socks.GeneralProxyError, socks.ProxyConnectionError
|
socks.GeneralProxyError,
|
||||||
|
socks.ProxyConnectionError
|
||||||
)):
|
)):
|
||||||
# This is a known error, and it's not related to
|
# This is a known error, and it's not related to
|
||||||
# Telegram but rather to the proxy. Disconnect and
|
# 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.
|
# add a little sleep to avoid the CPU usage going mad.
|
||||||
await asyncio.sleep(0.1, loop=self._loop)
|
await asyncio.sleep(0.1, loop=self._loop)
|
||||||
break
|
break
|
||||||
|
|
||||||
self._recv_loop = None
|
self._recv_loop = None
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
|
@ -51,7 +51,6 @@ from .tl.types import (
|
||||||
PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel)
|
PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel)
|
||||||
from .tl.types.messages import DialogsSlice
|
from .tl.types.messages import DialogsSlice
|
||||||
|
|
||||||
|
|
||||||
class TelegramClient(TelegramBareClient):
|
class TelegramClient(TelegramBareClient):
|
||||||
"""Full featured TelegramClient meant to extend the basic functionality"""
|
"""Full featured TelegramClient meant to extend the basic functionality"""
|
||||||
|
|
||||||
|
@ -103,7 +102,11 @@ class TelegramClient(TelegramBareClient):
|
||||||
# region Authorization requests
|
# region Authorization requests
|
||||||
|
|
||||||
async def send_code_request(self, phone):
|
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
|
phone = EntityDatabase.parse_phone(phone) or self._phone
|
||||||
result = await self(SendCodeRequest(phone, self.api_id, self.api_hash))
|
result = await self(SendCodeRequest(phone, self.api_id, self.api_hash))
|
||||||
self._phone = phone
|
self._phone = phone
|
||||||
|
@ -112,26 +115,27 @@ class TelegramClient(TelegramBareClient):
|
||||||
|
|
||||||
async def sign_in(self, phone=None, code=None,
|
async def sign_in(self, phone=None, code=None,
|
||||||
password=None, bot_token=None, phone_code_hash=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.
|
:param str | int phone:
|
||||||
The password should be used after a normal authorization attempt
|
The phone to send the code to if no code was provided, or to
|
||||||
has happened and an SessionPasswordNeededError was raised.
|
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
|
:return auth.SentCode | User:
|
||||||
(for example, through an API that creates a new client per phone),
|
The signed in user, or the information about .send_code_request().
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if phone and not code:
|
if phone and not code:
|
||||||
|
@ -175,7 +179,15 @@ class TelegramClient(TelegramBareClient):
|
||||||
return result.user
|
return result.user
|
||||||
|
|
||||||
async def sign_up(self, code, first_name, last_name=''):
|
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(
|
result = await self(SignUpRequest(
|
||||||
phone_number=self._phone,
|
phone_number=self._phone,
|
||||||
phone_code_hash=self._phone_code_hash,
|
phone_code_hash=self._phone_code_hash,
|
||||||
|
@ -188,8 +200,10 @@ class TelegramClient(TelegramBareClient):
|
||||||
return result.user
|
return result.user
|
||||||
|
|
||||||
async def log_out(self):
|
async def log_out(self):
|
||||||
"""Logs out and deletes the current session.
|
"""Logs out Telegram and deletes the current *.session file.
|
||||||
Returns True if everything went okay."""
|
|
||||||
|
:return bool: True if the operation was successful.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
await self(LogOutRequest())
|
await self(LogOutRequest())
|
||||||
except RPCError:
|
except RPCError:
|
||||||
|
@ -201,8 +215,12 @@ class TelegramClient(TelegramBareClient):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def get_me(self):
|
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:
|
try:
|
||||||
return (await self(GetUsersRequest([InputUserSelf()])))[0]
|
return (await self(GetUsersRequest([InputUserSelf()])))[0]
|
||||||
except UnauthorizedError:
|
except UnauthorizedError:
|
||||||
|
@ -217,15 +235,21 @@ class TelegramClient(TelegramBareClient):
|
||||||
offset_date=None,
|
offset_date=None,
|
||||||
offset_id=0,
|
offset_id=0,
|
||||||
offset_peer=InputPeerEmpty()):
|
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
|
:param limit:
|
||||||
offset) will be retrieved.
|
How many dialogs to be retrieved as maximum. Can be set to None
|
||||||
|
to retrieve all dialogs. Note that this may take whole minutes
|
||||||
The `entities` represent the user, chat or channel
|
if you have hundreds of dialogs, as Telegram will tell the library
|
||||||
corresponding to that dialog. If it's an integer, not
|
to slow down through a FloodWaitError.
|
||||||
all dialogs may be retrieved at once.
|
: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:
|
if limit is None:
|
||||||
limit = float('inf')
|
limit = float('inf')
|
||||||
|
@ -284,8 +308,9 @@ class TelegramClient(TelegramBareClient):
|
||||||
"""
|
"""
|
||||||
Gets all open draft messages.
|
Gets all open draft messages.
|
||||||
|
|
||||||
Returns a list of custom `Draft` objects that are easy to work with: You can call
|
Returns a list of custom `Draft` objects that are easy to work with:
|
||||||
`draft.set_message('text')` to change the message, or delete it through `draft.delete()`.
|
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
|
:return List[telethon.tl.custom.Draft]: A list of open drafts
|
||||||
"""
|
"""
|
||||||
|
@ -300,11 +325,14 @@ class TelegramClient(TelegramBareClient):
|
||||||
message,
|
message,
|
||||||
reply_to=None,
|
reply_to=None,
|
||||||
link_preview=True):
|
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,
|
:param str | int | User | Chat | Channel entity: To who will it be sent.
|
||||||
the sent message will be replying to such message.
|
: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)
|
entity = await self.get_input_entity(entity)
|
||||||
request = SendMessageRequest(
|
request = SendMessageRequest(
|
||||||
|
@ -348,11 +376,11 @@ class TelegramClient(TelegramBareClient):
|
||||||
Deletes a message from a chat, optionally "for everyone" with argument
|
Deletes a message from a chat, optionally "for everyone" with argument
|
||||||
`revoke` set to `True`.
|
`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`.
|
where it inherently behaves as being `True`.
|
||||||
|
|
||||||
Note: The `entity` argument can be `None` for normal chats, but it's
|
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
|
possible to supply a chat_id which will be automatically resolved to
|
||||||
the right type of InputPeer.
|
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]).
|
: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!
|
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(
|
result = await self(GetHistoryRequest(
|
||||||
peer=await self.get_input_entity(entity),
|
peer=await self.get_input_entity(entity),
|
||||||
|
@ -429,16 +454,15 @@ class TelegramClient(TelegramBareClient):
|
||||||
return total_messages, result.messages, entities
|
return total_messages, result.messages, entities
|
||||||
|
|
||||||
async def send_read_acknowledge(self, entity, messages=None, max_id=None):
|
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,
|
:param entity: The chat where these messages are located.
|
||||||
or the maximum message ID (until which message we want to send the read acknowledge).
|
:param messages: Either a list of messages or a single message.
|
||||||
|
:param max_id: Overrides messages, until which message should the
|
||||||
Returns an AffectedMessages TLObject
|
acknowledge should be sent.
|
||||||
|
:return:
|
||||||
The entity may be a phone or an username at the expense of
|
|
||||||
some performance loss.
|
|
||||||
"""
|
"""
|
||||||
if max_id is None:
|
if max_id is None:
|
||||||
if not messages:
|
if not messages:
|
||||||
|
@ -480,36 +504,36 @@ class TelegramClient(TelegramBareClient):
|
||||||
reply_to=None,
|
reply_to=None,
|
||||||
attributes=None,
|
attributes=None,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
"""Sends a file to the specified entity.
|
"""
|
||||||
The file may either be a path, a byte array, or a stream.
|
Sends a file to the specified entity.
|
||||||
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".
|
|
||||||
|
|
||||||
An optional caption can also be specified for said file.
|
:param entity:
|
||||||
|
Who will receive the file.
|
||||||
If "force_document" is False, the file will be sent as a photo
|
:param file:
|
||||||
if it's recognised to have a common image format (e.g. .png, .jpg).
|
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
|
||||||
Otherwise, the file will always be sent as an uncompressed document.
|
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.
|
|
||||||
|
|
||||||
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).
|
|
||||||
|
|
||||||
|
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
|
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.
|
sent as a document, it will be sent as a voice note.
|
||||||
|
:return:
|
||||||
The entity may be a phone or an username at the expense of
|
|
||||||
some performance loss.
|
|
||||||
"""
|
"""
|
||||||
as_photo = False
|
as_photo = False
|
||||||
if isinstance(file, str):
|
if isinstance(file, str):
|
||||||
|
@ -600,21 +624,19 @@ class TelegramClient(TelegramBareClient):
|
||||||
# region Downloading media requests
|
# region Downloading media requests
|
||||||
|
|
||||||
async def download_profile_photo(self, entity, file=None, download_big=True):
|
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
|
:param entity:
|
||||||
to be downloaded will be downloaded automatically.
|
From who the photo will be downloaded.
|
||||||
|
:param file:
|
||||||
On success, the file path is returned since it may differ from
|
The output file path, directory, or stream-like object.
|
||||||
the one provided.
|
If the path exists and is a file, it will be overwritten.
|
||||||
|
:param download_big:
|
||||||
The specified output file can either be a file path, a directory,
|
Whether to use the big version of the available photos.
|
||||||
or a stream-like object. If the path exists and is a file, it will
|
:return:
|
||||||
be overwritten.
|
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.
|
||||||
The entity may be a phone or an username at the expense of
|
|
||||||
some performance loss.
|
|
||||||
"""
|
"""
|
||||||
possible_names = []
|
possible_names = []
|
||||||
if not isinstance(entity, TLObject) or type(entity).SUBCLASS_OF_ID in (
|
if not isinstance(entity, TLObject) or type(entity).SUBCLASS_OF_ID in (
|
||||||
|
@ -669,21 +691,16 @@ class TelegramClient(TelegramBareClient):
|
||||||
return file
|
return file
|
||||||
|
|
||||||
async def download_media(self, message, file=None, progress_callback=None):
|
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),
|
Downloads the given media, or the media from a specified Message.
|
||||||
optionally finding its extension automatically.
|
:param message:
|
||||||
|
The media or message containing the media that will be downloaded.
|
||||||
The specified output file can either be a file path, a directory,
|
:param file:
|
||||||
or a stream-like object. If the path exists and is a file, it will
|
The output file path, directory, or stream-like object.
|
||||||
be overwritten.
|
If the path exists and is a file, it will be overwritten.
|
||||||
|
:param progress_callback:
|
||||||
If the operation succeeds, the path will be returned (since
|
A callback function accepting two parameters: (recv bytes, total)
|
||||||
the extension may have been added automatically). Otherwise,
|
:return:
|
||||||
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
|
|
||||||
"""
|
"""
|
||||||
# TODO This won't work for messageService
|
# TODO This won't work for messageService
|
||||||
if isinstance(message, Message):
|
if isinstance(message, Message):
|
||||||
|
@ -781,14 +798,14 @@ class TelegramClient(TelegramBareClient):
|
||||||
f = file
|
f = file
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Remove these pesky characters
|
||||||
|
first_name = first_name.replace(';', '')
|
||||||
|
last_name = (last_name or '').replace(';', '')
|
||||||
f.write('BEGIN:VCARD\n')
|
f.write('BEGIN:VCARD\n')
|
||||||
f.write('VERSION:4.0\n')
|
f.write('VERSION:4.0\n')
|
||||||
f.write('N:{};{};;;\n'.format(
|
f.write('N:{};{};;;\n'.format(first_name, last_name))
|
||||||
first_name, last_name if last_name else '')
|
f.write('FN:{} {}\n'.format(first_name, last_name))
|
||||||
)
|
f.write('TEL;TYPE=cell;VALUE=uri:tel:+{}\n'.format(phone_number))
|
||||||
f.write('FN:{}\n'.format(' '.join((first_name, last_name))))
|
|
||||||
f.write('TEL;TYPE=cell;VALUE=uri:tel:+{}\n'.format(
|
|
||||||
phone_number))
|
|
||||||
f.write('END:VCARD\n')
|
f.write('END:VCARD\n')
|
||||||
finally:
|
finally:
|
||||||
# Only close the stream if we opened it
|
# Only close the stream if we opened it
|
||||||
|
@ -862,20 +879,24 @@ class TelegramClient(TelegramBareClient):
|
||||||
# region Small utilities to make users' life easier
|
# region Small utilities to make users' life easier
|
||||||
|
|
||||||
async def get_entity(self, entity):
|
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,
|
Turns the given entity into a valid Telegram user or chat.
|
||||||
or if it starts with '+' it will be resolved as if it
|
|
||||||
were a phone number.
|
|
||||||
|
|
||||||
If "entity" is a string and doesn't start with '+', or
|
:param entity:
|
||||||
it starts with '@', it will be resolved from the username.
|
The entity to be transformed.
|
||||||
If no exact match is returned, an error will be raised.
|
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
|
If it doesn't start with '+' or starts with a '@' it will be
|
||||||
be returned through a call to self.get_input_peer(entity).
|
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
|
If the entity is an integer or a Peer, its information will be
|
||||||
error will be raised.
|
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:
|
try:
|
||||||
return self.session.entities[entity]
|
return self.session.entities[entity]
|
||||||
|
@ -929,14 +950,23 @@ class TelegramClient(TelegramBareClient):
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_input_entity(self, peer):
|
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
|
Turns the given peer into its input entity version. Most requests
|
||||||
of an User.
|
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
|
:param peer:
|
||||||
will loaded, and their entities saved to the session file.
|
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:
|
try:
|
||||||
# First try to get the entity from cache, otherwise figure it out
|
# 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)
|
'Make sure you have encountered this peer before.'.format(peer)
|
||||||
)
|
)
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .. import utils
|
|
||||||
from ..tl import TLObject
|
from ..tl import TLObject
|
||||||
from ..tl.types import (
|
from ..tl.types import (
|
||||||
User, Chat, Channel, PeerUser, PeerChat, PeerChannel,
|
User, Chat, Channel, PeerUser, PeerChat, PeerChannel,
|
||||||
InputPeerUser, InputPeerChat, InputPeerChannel
|
InputPeerUser, InputPeerChat, InputPeerChannel
|
||||||
)
|
)
|
||||||
|
from .. import utils # Keep this line the last to maybe fix #357
|
||||||
|
|
||||||
|
|
||||||
class EntityDatabase:
|
class EntityDatabase:
|
||||||
|
|
|
@ -91,8 +91,11 @@ class TLObject:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def serialize_bytes(data):
|
def serialize_bytes(data):
|
||||||
"""Write bytes by using Telegram guidelines"""
|
"""Write bytes by using Telegram guidelines"""
|
||||||
if isinstance(data, str):
|
if not isinstance(data, bytes):
|
||||||
data = data.encode('utf-8')
|
if isinstance(data, str):
|
||||||
|
data = data.encode('utf-8')
|
||||||
|
else:
|
||||||
|
raise ValueError('bytes or str expected, not', type(data))
|
||||||
|
|
||||||
r = []
|
r = []
|
||||||
if len(data) < 254:
|
if len(data) < 254:
|
||||||
|
|
|
@ -142,7 +142,10 @@ def get_input_user(entity):
|
||||||
else:
|
else:
|
||||||
return InputUser(entity.id, entity.access_hash)
|
return InputUser(entity.id, entity.access_hash)
|
||||||
|
|
||||||
if isinstance(entity, UserEmpty):
|
if isinstance(entity, InputPeerSelf):
|
||||||
|
return InputUserSelf()
|
||||||
|
|
||||||
|
if isinstance(entity, (UserEmpty, InputPeerEmpty)):
|
||||||
return InputUserEmpty()
|
return InputUserEmpty()
|
||||||
|
|
||||||
if isinstance(entity, UserFull):
|
if isinstance(entity, UserFull):
|
||||||
|
|
BIN
telethon_examples/anytime.png
Normal file
BIN
telethon_examples/anytime.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
|
@ -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()
|
|
|
@ -20,11 +20,11 @@ def sprint(string, *args, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
def print_title(title):
|
def print_title(title):
|
||||||
# Clear previous window
|
"""Helper function to print titles to the console more nicely"""
|
||||||
print('\n')
|
sprint('\n')
|
||||||
print('=={}=='.format('=' * len(title)))
|
sprint('=={}=='.format('=' * len(title)))
|
||||||
sprint('= {} ='.format(title))
|
sprint('= {} ='.format(title))
|
||||||
print('=={}=='.format('=' * len(title)))
|
sprint('=={}=='.format('=' * len(title)))
|
||||||
|
|
||||||
|
|
||||||
def bytes_to_string(byte_count):
|
def bytes_to_string(byte_count):
|
||||||
|
@ -34,8 +34,9 @@ def bytes_to_string(byte_count):
|
||||||
byte_count /= 1024
|
byte_count /= 1024
|
||||||
suffix_index += 1
|
suffix_index += 1
|
||||||
|
|
||||||
return '{:.2f}{}'.format(byte_count,
|
return '{:.2f}{}'.format(
|
||||||
[' bytes', 'KB', 'MB', 'GB', 'TB'][suffix_index])
|
byte_count, [' bytes', 'KB', 'MB', 'GB', 'TB'][suffix_index]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InteractiveTelegramClient(TelegramClient):
|
class InteractiveTelegramClient(TelegramClient):
|
||||||
|
@ -48,13 +49,38 @@ class InteractiveTelegramClient(TelegramClient):
|
||||||
"""
|
"""
|
||||||
def __init__(self, session_user_id, user_phone, api_id, api_hash,
|
def __init__(self, session_user_id, user_phone, api_id, api_hash,
|
||||||
proxy=None):
|
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_title('Initialization')
|
||||||
|
|
||||||
print('Initializing interactive example...')
|
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__(
|
super().__init__(
|
||||||
|
# These parameters should be passed always, session name and API
|
||||||
session_user_id, api_id, api_hash,
|
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,
|
connection_mode=ConnectionMode.TCP_ABRIDGED,
|
||||||
|
|
||||||
|
# If you're using a proxy, set it here.
|
||||||
proxy=proxy,
|
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
|
update_workers=1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -62,6 +88,8 @@ class InteractiveTelegramClient(TelegramClient):
|
||||||
# so it can be downloaded if the user wants
|
# so it can be downloaded if the user wants
|
||||||
self.found_media = set()
|
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...')
|
print('Connecting to Telegram servers...')
|
||||||
if not self.connect():
|
if not self.connect():
|
||||||
print('Initial connection failed. Retrying...')
|
print('Initial connection failed. Retrying...')
|
||||||
|
@ -69,18 +97,24 @@ class InteractiveTelegramClient(TelegramClient):
|
||||||
print('Could not connect to Telegram servers.')
|
print('Could not connect to Telegram servers.')
|
||||||
return
|
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():
|
if not self.is_user_authorized():
|
||||||
print('First run. Sending code request...')
|
print('First run. Sending code request...')
|
||||||
self.send_code_request(user_phone)
|
self.sign_in(user_phone)
|
||||||
|
|
||||||
self_user = None
|
self_user = None
|
||||||
while self_user is None:
|
while self_user is None:
|
||||||
code = input('Enter the code you just received: ')
|
code = input('Enter the code you just received: ')
|
||||||
try:
|
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:
|
except SessionPasswordNeededError:
|
||||||
pw = getpass('Two step verification is enabled. '
|
pw = getpass('Two step verification is enabled. '
|
||||||
'Please enter your password: ')
|
'Please enter your password: ')
|
||||||
|
@ -88,16 +122,22 @@ class InteractiveTelegramClient(TelegramClient):
|
||||||
self_user = self.sign_in(password=pw)
|
self_user = self.sign_in(password=pw)
|
||||||
|
|
||||||
def run(self):
|
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)
|
self.add_update_handler(self.update_handler)
|
||||||
|
|
||||||
# Enter a while loop to chat as long as the user wants
|
# Enter a while loop to chat as long as the user wants
|
||||||
while True:
|
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
|
dialog_count = 15
|
||||||
|
|
||||||
# Entities represent the user, chat or channel
|
# 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)
|
dialogs, entities = self.get_dialogs(limit=dialog_count)
|
||||||
|
|
||||||
i = None
|
i = None
|
||||||
|
@ -119,6 +159,12 @@ class InteractiveTelegramClient(TelegramClient):
|
||||||
if i == '!q':
|
if i == '!q':
|
||||||
return
|
return
|
||||||
if i == '!l':
|
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()
|
self.log_out()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -158,8 +204,8 @@ class InteractiveTelegramClient(TelegramClient):
|
||||||
# History
|
# History
|
||||||
elif msg == '!h':
|
elif msg == '!h':
|
||||||
# First retrieve the messages and some information
|
# First retrieve the messages and some information
|
||||||
total_count, messages, senders = self.get_message_history(
|
total_count, messages, senders = \
|
||||||
entity, limit=10)
|
self.get_message_history(entity, limit=10)
|
||||||
|
|
||||||
# Iterate over all (in reverse order so the latest appear
|
# Iterate over all (in reverse order so the latest appear
|
||||||
# the last in the console) and print them with format:
|
# the last in the console) and print them with format:
|
||||||
|
@ -237,6 +283,7 @@ class InteractiveTelegramClient(TelegramClient):
|
||||||
entity, msg, link_preview=False)
|
entity, msg, link_preview=False)
|
||||||
|
|
||||||
def send_photo(self, path, entity):
|
def send_photo(self, path, entity):
|
||||||
|
"""Sends the file located at path to the desired entity as a photo"""
|
||||||
self.send_file(
|
self.send_file(
|
||||||
entity, path,
|
entity, path,
|
||||||
progress_callback=self.upload_progress_callback
|
progress_callback=self.upload_progress_callback
|
||||||
|
@ -244,6 +291,7 @@ class InteractiveTelegramClient(TelegramClient):
|
||||||
print('Photo sent!')
|
print('Photo sent!')
|
||||||
|
|
||||||
def send_document(self, path, entity):
|
def send_document(self, path, entity):
|
||||||
|
"""Sends the file located at path to the desired entity as a document"""
|
||||||
self.send_file(
|
self.send_file(
|
||||||
entity, path,
|
entity, path,
|
||||||
force_document=True,
|
force_document=True,
|
||||||
|
@ -252,6 +300,9 @@ class InteractiveTelegramClient(TelegramClient):
|
||||||
print('Document sent!')
|
print('Document sent!')
|
||||||
|
|
||||||
def download_media_by_id(self, media_id):
|
def download_media_by_id(self, media_id):
|
||||||
|
"""Given a message ID, finds the media this message contained and
|
||||||
|
downloads it.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
# The user may have entered a non-integer string!
|
# The user may have entered a non-integer string!
|
||||||
msg_media_id = int(media_id)
|
msg_media_id = int(media_id)
|
||||||
|
@ -291,6 +342,11 @@ class InteractiveTelegramClient(TelegramClient):
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_handler(self, update):
|
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):
|
if isinstance(update, UpdateShortMessage):
|
||||||
who = self.get_entity(update.user_id)
|
who = self.get_entity(update.user_id)
|
||||||
if update.out:
|
if update.out:
|
||||||
|
|
46
telethon_examples/print_updates.py
Executable file
46
telethon_examples/print_updates.py
Executable 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
137
telethon_examples/replier.py
Executable 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()
|
1
telethon_generator/__init__.py
Normal file
1
telethon_generator/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -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
|
PHONE_NUMBER_UNOCCUPIED=The phone number is not yet being used
|
||||||
PHOTO_INVALID_DIMENSIONS=The photo dimensions are invalid
|
PHOTO_INVALID_DIMENSIONS=The photo dimensions are invalid
|
||||||
TYPE_CONSTRUCTOR_INVALID=The type constructor is 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_MODIFIED=The username is not different from the current username
|
||||||
USERNAME_NOT_OCCUPIED=The username is not in use by anyone else yet
|
USERNAME_NOT_OCCUPIED=The username is not in use by anyone else yet
|
||||||
USERNAME_OCCUPIED=The username is already taken
|
USERNAME_OCCUPIED=The username is already taken
|
||||||
|
|
|
@ -154,11 +154,10 @@ def generate_code(output, json_file, errors_desc):
|
||||||
patterns.append((pattern, name))
|
patterns.append((pattern, name))
|
||||||
capture = capture_names.get(name, 'x') if has_captures else None
|
capture = capture_names.get(name, 'x') if has_captures else None
|
||||||
# TODO Some errors have the same name but different code,
|
# 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)
|
write_error(f, error_code, name, description, capture)
|
||||||
|
|
||||||
f.write('\n\nrpc_errors_all = {\n')
|
f.write('\n\nrpc_errors_all = {\n')
|
||||||
for pattern, name in patterns:
|
for pattern, name in patterns:
|
||||||
f.write(' {}: {},\n'.format(repr(pattern), name))
|
f.write(' {}: {},\n'.format(repr(pattern), name))
|
||||||
f.write('}\n')
|
f.write('}\n')
|
||||||
|
|
||||||
|
|
|
@ -104,8 +104,7 @@ class TLObject:
|
||||||
def class_name_for(typename, is_function=False):
|
def class_name_for(typename, is_function=False):
|
||||||
"""Gets the class name following the Python style guidelines"""
|
"""Gets the class name following the Python style guidelines"""
|
||||||
# Courtesy of http://stackoverflow.com/a/31531797/4759433
|
# Courtesy of http://stackoverflow.com/a/31531797/4759433
|
||||||
result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(),
|
result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), typename)
|
||||||
typename)
|
|
||||||
result = result[:1].upper() + result[1:].replace('_', '')
|
result = result[:1].upper() + result[1:].replace('_', '')
|
||||||
# If it's a function, let it end with "Request" to identify them
|
# If it's a function, let it end with "Request" to identify them
|
||||||
if is_function:
|
if is_function:
|
||||||
|
|
|
@ -129,9 +129,6 @@ class TLGenerator:
|
||||||
builder.writeln(
|
builder.writeln(
|
||||||
'from {}.tl.tlobject import TLObject'.format('.' * depth)
|
'from {}.tl.tlobject import TLObject'.format('.' * depth)
|
||||||
)
|
)
|
||||||
builder.writeln(
|
|
||||||
'from {}.tl import types'.format('.' * depth)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add the relative imports to the namespaces,
|
# Add the relative imports to the namespaces,
|
||||||
# unless we already are in a namespace.
|
# unless we already are in a namespace.
|
||||||
|
@ -494,11 +491,15 @@ class TLGenerator:
|
||||||
|
|
||||||
elif arg.flag_indicator:
|
elif arg.flag_indicator:
|
||||||
# Calculate the flags with those items which are not None
|
# Calculate the flags with those items which are not None
|
||||||
builder.write("struct.pack('<I', {})".format(
|
if not any(f.is_flag for f in args):
|
||||||
' | '.join('({} if {} else 0)'.format(
|
# There's a flag indicator, but no flag arguments so it's 0
|
||||||
1 << flag.flag_index, 'self.{}'.format(flag.name)
|
builder.write(r"b'\0\0\0\0'")
|
||||||
) for flag in args if flag.is_flag)
|
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:
|
elif 'int' == arg.type:
|
||||||
# struct.pack is around 4 times faster than int.to_bytes
|
# struct.pack is around 4 times faster than int.to_bytes
|
||||||
|
@ -644,8 +645,25 @@ class TLGenerator:
|
||||||
if not arg.skip_constructor_id:
|
if not arg.skip_constructor_id:
|
||||||
builder.writeln('{} = reader.tgread_object()'.format(name))
|
builder.writeln('{} = reader.tgread_object()'.format(name))
|
||||||
else:
|
else:
|
||||||
builder.writeln('{} = types.{}.from_reader(reader)'.format(
|
# Import the correct type inline to avoid cyclic imports.
|
||||||
name, TLObject.class_name_for(arg.type)))
|
# 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)
|
# End vector and flag blocks if required (if we opened them before)
|
||||||
if arg.is_vector:
|
if arg.is_vector:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user