2017-06-09 17:13:39 +03:00
|
|
|
"""Various helpers not related to the Telegram API itself"""
|
2019-03-21 14:21:00 +03:00
|
|
|
import asyncio
|
2019-12-23 15:52:07 +03:00
|
|
|
import enum
|
2016-11-30 00:29:42 +03:00
|
|
|
import os
|
2018-06-29 12:04:42 +03:00
|
|
|
import struct
|
2019-05-03 22:37:27 +03:00
|
|
|
from hashlib import sha1
|
2018-01-06 03:55:11 +03:00
|
|
|
|
2016-08-30 18:40:49 +03:00
|
|
|
|
2019-12-23 15:52:07 +03:00
|
|
|
class _EntityType(enum.Enum):
|
|
|
|
USER = 0
|
|
|
|
CHAT = 1
|
|
|
|
CHANNEL = 2
|
|
|
|
|
|
|
|
|
2016-09-08 17:11:37 +03:00
|
|
|
# region Multiple utilities
|
2016-08-26 13:58:53 +03:00
|
|
|
|
|
|
|
|
|
|
|
def generate_random_long(signed=True):
|
2016-08-28 14:43:00 +03:00
|
|
|
"""Generates a random long integer (8 bytes), which is optionally signed"""
|
2016-09-03 11:54:58 +03:00
|
|
|
return int.from_bytes(os.urandom(8), signed=signed, byteorder='little')
|
2016-08-26 13:58:53 +03:00
|
|
|
|
|
|
|
|
2016-09-12 20:32:16 +03:00
|
|
|
def ensure_parent_dir_exists(file_path):
|
|
|
|
"""Ensures that the parent directory exists"""
|
|
|
|
parent = os.path.dirname(file_path)
|
|
|
|
if parent:
|
|
|
|
os.makedirs(parent, exist_ok=True)
|
|
|
|
|
2018-06-29 12:04:42 +03:00
|
|
|
|
|
|
|
def add_surrogate(text):
|
|
|
|
return ''.join(
|
|
|
|
# SMP -> Surrogate Pairs (Telegram offsets are calculated with these).
|
|
|
|
# See https://en.wikipedia.org/wiki/Plane_(Unicode)#Overview for more.
|
|
|
|
''.join(chr(y) for y in struct.unpack('<HH', x.encode('utf-16le')))
|
|
|
|
if (0x10000 <= ord(x) <= 0x10FFFF) else x for x in text
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def del_surrogate(text):
|
|
|
|
return text.encode('utf-16', 'surrogatepass').decode('utf-16')
|
|
|
|
|
|
|
|
|
2020-02-20 12:53:28 +03:00
|
|
|
def within_surrogate(text, index, *, length=None):
|
|
|
|
"""
|
|
|
|
`True` if ``index`` is within a surrogate (before and after it, not at!).
|
|
|
|
"""
|
|
|
|
if length is None:
|
|
|
|
length = len(text)
|
|
|
|
|
|
|
|
return (
|
|
|
|
1 < index < len(text) and # in bounds
|
|
|
|
'\ud800' <= text[index - 1] <= '\udfff' and # previous is
|
|
|
|
'\ud800' <= text[index] <= '\udfff' # current is
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2018-11-19 12:15:56 +03:00
|
|
|
def strip_text(text, entities):
|
|
|
|
"""
|
|
|
|
Strips whitespace from the given text modifying the provided entities.
|
|
|
|
|
|
|
|
This assumes that there are no overlapping entities, that their length
|
|
|
|
is greater or equal to one, and that their length is not out of bounds.
|
|
|
|
"""
|
|
|
|
if not entities:
|
|
|
|
return text.strip()
|
|
|
|
|
|
|
|
while text and text[-1].isspace():
|
|
|
|
e = entities[-1]
|
|
|
|
if e.offset + e.length == len(text):
|
|
|
|
if e.length == 1:
|
|
|
|
del entities[-1]
|
|
|
|
if not entities:
|
|
|
|
return text.strip()
|
|
|
|
else:
|
|
|
|
e.length -= 1
|
|
|
|
text = text[:-1]
|
|
|
|
|
|
|
|
while text and text[0].isspace():
|
2018-12-06 14:33:15 +03:00
|
|
|
for i in reversed(range(len(entities))):
|
|
|
|
e = entities[i]
|
|
|
|
if e.offset != 0:
|
|
|
|
e.offset -= 1
|
|
|
|
continue
|
|
|
|
|
2018-11-19 12:15:56 +03:00
|
|
|
if e.length == 1:
|
|
|
|
del entities[0]
|
|
|
|
if not entities:
|
|
|
|
return text.lstrip()
|
|
|
|
else:
|
|
|
|
e.length -= 1
|
2018-12-06 14:33:15 +03:00
|
|
|
|
2018-11-19 12:15:56 +03:00
|
|
|
text = text[1:]
|
|
|
|
|
|
|
|
return text
|
|
|
|
|
|
|
|
|
2019-02-06 21:41:45 +03:00
|
|
|
def retry_range(retries):
|
|
|
|
"""
|
|
|
|
Generates an integer sequence starting from 1. If `retries` is
|
2019-02-06 23:55:34 +03:00
|
|
|
not a zero or a positive integer value, the sequence will be
|
|
|
|
infinite, otherwise it will end at `retries + 1`.
|
2019-02-06 21:41:45 +03:00
|
|
|
"""
|
|
|
|
yield 1
|
|
|
|
attempt = 0
|
|
|
|
while attempt != retries:
|
|
|
|
attempt += 1
|
|
|
|
yield 1 + attempt
|
|
|
|
|
|
|
|
|
2019-03-21 14:21:00 +03:00
|
|
|
async def _cancel(log, **tasks):
|
|
|
|
"""
|
|
|
|
Helper to cancel one or more tasks gracefully, logging exceptions.
|
|
|
|
"""
|
|
|
|
for name, task in tasks.items():
|
|
|
|
if not task:
|
|
|
|
continue
|
|
|
|
|
|
|
|
task.cancel()
|
|
|
|
try:
|
|
|
|
await task
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
pass
|
2019-12-27 12:46:01 +03:00
|
|
|
except RuntimeError:
|
|
|
|
# Probably: RuntimeError: await wasn't used with future
|
|
|
|
#
|
|
|
|
# Happens with _asyncio.Task instances (in "Task cancelling" state)
|
|
|
|
# trying to SIGINT the program right during initial connection, on
|
|
|
|
# _recv_loop coroutine (but we're creating its task explicitly with
|
|
|
|
# a loop, so how can it bug out like this?).
|
|
|
|
#
|
|
|
|
# Since we're aware of this error there's no point in logging it.
|
|
|
|
# *May* be https://bugs.python.org/issue37172
|
|
|
|
pass
|
2019-03-21 14:21:00 +03:00
|
|
|
except Exception:
|
2019-12-27 12:46:01 +03:00
|
|
|
log.exception('Unhandled exception from %s after cancelling '
|
|
|
|
'%s (%s)', name, type(task), task)
|
2019-03-21 14:21:00 +03:00
|
|
|
|
2019-04-13 11:53:33 +03:00
|
|
|
|
|
|
|
def _sync_enter(self):
|
|
|
|
"""
|
|
|
|
Helps to cut boilerplate on async context
|
|
|
|
managers that offer synchronous variants.
|
|
|
|
"""
|
2019-04-13 11:55:51 +03:00
|
|
|
if hasattr(self, 'loop'):
|
|
|
|
loop = self.loop
|
|
|
|
else:
|
|
|
|
loop = self._client.loop
|
|
|
|
|
|
|
|
if loop.is_running():
|
2019-04-13 11:53:33 +03:00
|
|
|
raise RuntimeError(
|
|
|
|
'You must use "async with" if the event loop '
|
|
|
|
'is running (i.e. you are inside an "async def")'
|
|
|
|
)
|
|
|
|
|
2019-04-13 11:55:51 +03:00
|
|
|
return loop.run_until_complete(self.__aenter__())
|
2019-04-13 11:53:33 +03:00
|
|
|
|
|
|
|
|
|
|
|
def _sync_exit(self, *args):
|
2019-04-13 11:55:51 +03:00
|
|
|
if hasattr(self, 'loop'):
|
|
|
|
loop = self.loop
|
|
|
|
else:
|
|
|
|
loop = self._client.loop
|
|
|
|
|
|
|
|
return loop.run_until_complete(self.__aexit__(*args))
|
2019-04-13 11:53:33 +03:00
|
|
|
|
|
|
|
|
2019-12-23 15:52:07 +03:00
|
|
|
def _entity_type(entity):
|
|
|
|
# This could be a `utils` method that just ran a few `isinstance` on
|
|
|
|
# `utils.get_peer(...)`'s result. However, there are *a lot* of auto
|
|
|
|
# casts going on, plenty of calls and temporary short-lived objects.
|
|
|
|
#
|
|
|
|
# So we just check if a string is in the class name.
|
|
|
|
# Still, assert that it's the right type to not return false results.
|
|
|
|
try:
|
|
|
|
if entity.SUBCLASS_OF_ID not in (
|
|
|
|
0x2d45687, # crc32(b'Peer')
|
|
|
|
0xc91c90b6, # crc32(b'InputPeer')
|
|
|
|
0xe669bf46, # crc32(b'InputUser')
|
|
|
|
0x40f202fd, # crc32(b'InputChannel')
|
|
|
|
0x2da17977, # crc32(b'User')
|
|
|
|
0xc5af5d94, # crc32(b'Chat')
|
|
|
|
0x1f4661b9, # crc32(b'UserFull')
|
|
|
|
0xd49a2697, # crc32(b'ChatFull')
|
|
|
|
):
|
|
|
|
raise TypeError('{} does not have any entity type'.format(entity))
|
|
|
|
except AttributeError:
|
|
|
|
raise TypeError('{} is not a TLObject, cannot determine entity type'.format(entity))
|
|
|
|
|
|
|
|
name = entity.__class__.__name__
|
|
|
|
if 'User' in name:
|
|
|
|
return _EntityType.USER
|
|
|
|
elif 'Chat' in name:
|
|
|
|
return _EntityType.CHAT
|
|
|
|
elif 'Channel' in name:
|
|
|
|
return _EntityType.CHANNEL
|
|
|
|
elif 'Self' in name:
|
|
|
|
return _EntityType.USER
|
|
|
|
|
|
|
|
# 'Empty' in name or not found, we don't care, not a valid entity.
|
|
|
|
raise TypeError('{} does not have any entity type'.format(entity))
|
|
|
|
|
2016-09-08 17:11:37 +03:00
|
|
|
# endregion
|
|
|
|
|
|
|
|
# region Cryptographic related utils
|
2016-08-30 18:40:49 +03:00
|
|
|
|
|
|
|
|
2017-05-21 14:59:16 +03:00
|
|
|
def generate_key_data_from_nonce(server_nonce, new_nonce):
|
|
|
|
"""Generates the key data corresponding to the given nonce"""
|
2017-09-28 12:36:51 +03:00
|
|
|
server_nonce = server_nonce.to_bytes(16, 'little', signed=True)
|
|
|
|
new_nonce = new_nonce.to_bytes(32, 'little', signed=True)
|
|
|
|
hash1 = sha1(new_nonce + server_nonce).digest()
|
|
|
|
hash2 = sha1(server_nonce + new_nonce).digest()
|
|
|
|
hash3 = sha1(new_nonce + new_nonce).digest()
|
2016-08-30 18:40:49 +03:00
|
|
|
|
2016-09-17 21:42:34 +03:00
|
|
|
key = hash1 + hash2[:12]
|
|
|
|
iv = hash2[12:20] + hash3 + new_nonce[:4]
|
|
|
|
return key, iv
|
2016-08-30 18:40:49 +03:00
|
|
|
|
|
|
|
|
2016-09-08 17:11:37 +03:00
|
|
|
# endregion
|
2018-08-03 00:00:10 +03:00
|
|
|
|
|
|
|
# region Custom Classes
|
|
|
|
|
2018-10-19 14:24:52 +03:00
|
|
|
|
2018-08-03 00:00:10 +03:00
|
|
|
class TotalList(list):
|
|
|
|
"""
|
|
|
|
A list with an extra `total` property, which may not match its `len`
|
|
|
|
since the total represents the total amount of items *available*
|
|
|
|
somewhere else, not the items *in this list*.
|
2019-05-06 12:38:26 +03:00
|
|
|
|
|
|
|
Examples:
|
|
|
|
|
|
|
|
.. code-block:: python
|
|
|
|
|
|
|
|
# Telethon returns these lists in some cases (for example,
|
|
|
|
# only when a chunk is returned, but the "total" count
|
|
|
|
# is available).
|
2019-08-14 00:33:39 +03:00
|
|
|
result = await client.get_messages(chat, limit=10)
|
2019-05-06 12:38:26 +03:00
|
|
|
|
|
|
|
print(result.total) # large number
|
|
|
|
print(len(result)) # 10
|
|
|
|
print(result[0]) # latest message
|
|
|
|
|
|
|
|
for x in result: # show the 10 messages
|
|
|
|
print(x.text)
|
|
|
|
|
2018-08-03 00:00:10 +03:00
|
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.total = 0
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return '[{}, total={}]'.format(
|
|
|
|
', '.join(str(x) for x in self), self.total)
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return '[{}, total={}]'.format(
|
|
|
|
', '.join(repr(x) for x in self), self.total)
|
|
|
|
|
2018-09-29 14:29:44 +03:00
|
|
|
|
2018-08-03 00:00:10 +03:00
|
|
|
# endregion
|