diff --git a/run_tests.py b/run_tests.py
deleted file mode 100755
index d99cfb56..00000000
--- a/run_tests.py
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/usr/bin/env python3
-import unittest
-
-if __name__ == '__main__':
- from telethon_tests import \
- CryptoTests, ParserTests, TLTests, UtilsTests, NetworkTests
-
- test_classes = [CryptoTests, ParserTests, TLTests, UtilsTests]
-
- network = input('Run network tests (y/n)?: ').lower() == 'y'
- if network:
- test_classes.append(NetworkTests)
-
- loader = unittest.TestLoader()
-
- suites_list = []
- for test_class in test_classes:
- suite = loader.loadTestsFromTestCase(test_class)
- suites_list.append(suite)
-
- big_suite = unittest.TestSuite(suites_list)
-
- runner = unittest.TextTestRunner()
- results = runner.run(big_suite)
diff --git a/telethon/__init__.py b/telethon/__init__.py
index f0d2b0b8..fb945a13 100644
--- a/telethon/__init__.py
+++ b/telethon/__init__.py
@@ -2,7 +2,7 @@ import logging
from .client.telegramclient import TelegramClient
from .network import connection
from .tl import types, functions, custom
-from . import version, events, utils, errors
+from . import version, events, utils, errors, full_sync
__version__ = version.__version__
diff --git a/telethon/client/messageparse.py b/telethon/client/messageparse.py
index 1e71605b..0adea5d6 100644
--- a/telethon/client/messageparse.py
+++ b/telethon/client/messageparse.py
@@ -2,8 +2,8 @@ import itertools
import re
from .users import UserMethods
-from .. import utils
-from ..tl import types, custom
+from .. import default, utils
+from ..tl import types
class MessageParseMethods(UserMethods):
@@ -62,7 +62,7 @@ class MessageParseMethods(UserMethods):
"""
Returns a (parsed message, entities) tuple depending on ``parse_mode``.
"""
- if parse_mode == utils.Default:
+ if parse_mode == default:
parse_mode = self._parse_mode
else:
parse_mode = utils.sanitize_parse_mode(parse_mode)
diff --git a/telethon/client/messages.py b/telethon/client/messages.py
index 838c6b72..d9cb3d92 100644
--- a/telethon/client/messages.py
+++ b/telethon/client/messages.py
@@ -8,8 +8,8 @@ from async_generator import async_generator, yield_
from .messageparse import MessageParseMethods
from .uploads import UploadMethods
from .buttons import ButtonMethods
-from .. import utils, helpers
-from ..tl import types, functions, custom
+from .. import default, helpers, utils
+from ..tl import types, functions
__log__ = logging.getLogger(__name__)
@@ -360,7 +360,7 @@ class MessageMethods(UploadMethods, ButtonMethods, MessageParseMethods):
async def send_message(
self, entity, message='', *, reply_to=None,
- parse_mode=utils.Default, link_preview=True, file=None,
+ parse_mode=default, link_preview=True, file=None,
force_document=False, clear_draft=False, buttons=None,
silent=None):
"""
@@ -584,7 +584,7 @@ class MessageMethods(UploadMethods, ButtonMethods, MessageParseMethods):
async def edit_message(
self, entity, message=None, text=None,
- *, parse_mode=utils.Default, link_preview=True, file=None,
+ *, parse_mode=default, link_preview=True, file=None,
buttons=None):
"""
Edits the given message ID (to change its contents or disable preview).
diff --git a/telethon/client/telegrambaseclient.py b/telethon/client/telegrambaseclient.py
index 94d7a10d..98e14592 100644
--- a/telethon/client/telegrambaseclient.py
+++ b/telethon/client/telegrambaseclient.py
@@ -1,6 +1,5 @@
import abc
import asyncio
-import collections
import inspect
import logging
import platform
@@ -12,7 +11,6 @@ from .. import version
from ..crypto import rsa
from ..extensions import markdown
from ..network import MTProtoSender, ConnectionTcpFull
-from ..network.mtprotostate import MTProtoState
from ..sessions import Session, SQLiteSession, MemorySession
from ..tl import TLObject, functions, types
from ..tl.alltlobjects import LAYER
@@ -55,7 +53,7 @@ class TelegramBaseClient(abc.ABC):
connection (`telethon.network.connection.common.Connection`, optional):
The connection instance to be used when creating a new connection
- to the servers. If it's a type, the `proxy` argument will be used.
+ to the servers. It **must** be a type.
Defaults to `telethon.network.connection.tcpfull.ConnectionTcpFull`.
@@ -68,11 +66,11 @@ class TelegramBaseClient(abc.ABC):
A tuple consisting of ``(socks.SOCKS5, 'host', port)``.
See https://github.com/Anorov/PySocks#usage-1 for more.
- timeout (`int` | `float` | `timedelta`, optional):
- The timeout to be used when connecting, sending and receiving
- responses from the network. This is **not** the timeout to
- be used when ``await``'ing for invoked requests, and you
- should use ``asyncio.wait`` or ``asyncio.wait_for`` for that.
+ timeout (`int` | `float`, optional):
+ The timeout in seconds to be used when connecting.
+ This is **not** the timeout to be used when ``await``'ing for
+ invoked requests, and you should use ``asyncio.wait`` or
+ ``asyncio.wait_for`` for that.
request_retries (`int`, optional):
How many times a request should be retried. Request are retried
@@ -150,7 +148,7 @@ class TelegramBaseClient(abc.ABC):
connection=ConnectionTcpFull,
use_ipv6=False,
proxy=None,
- timeout=timedelta(seconds=10),
+ timeout=10,
request_retries=5,
connection_retries=5,
auto_reconnect=True,
@@ -205,11 +203,12 @@ class TelegramBaseClient(abc.ABC):
self._request_retries = request_retries or sys.maxsize
self._connection_retries = connection_retries or sys.maxsize
+ self._proxy = proxy
+ self._timeout = timeout
self._auto_reconnect = auto_reconnect
- if isinstance(connection, type):
- connection = connection(
- proxy=proxy, timeout=timeout, loop=self._loop)
+ assert isinstance(connection, type)
+ self._connection = connection
# Used on connection. Capture the variables in a lambda since
# exporting clients need to create this InvokeWithLayerRequest.
@@ -227,12 +226,12 @@ class TelegramBaseClient(abc.ABC):
)
)
- state = MTProtoState(self.session.auth_key)
self._connection = connection
self._sender = MTProtoSender(
- state, connection, self._loop,
+ self._loop,
retries=self._connection_retries,
auto_reconnect=self._auto_reconnect,
+ connect_timeout=self._timeout,
update_callback=self._handle_update,
auth_key_callback=self._auth_key_callback,
auto_reconnect_callback=self._handle_auto_reconnect
@@ -250,10 +249,6 @@ class TelegramBaseClient(abc.ABC):
# Save whether the user is authorized here (a.k.a. logged in)
self._authorized = None # None = We don't know yet
- # Default PingRequest delay
- self._last_ping = datetime.now()
- self._ping_delay = timedelta(minutes=1)
-
self._updates_handle = None
self._last_request = time.time()
self._channel_pts = {}
@@ -309,8 +304,10 @@ class TelegramBaseClient(abc.ABC):
"""
Connects to Telegram.
"""
- await self._sender.connect(
- self.session.server_address, self.session.port)
+ await self._sender.connect(self.session.auth_key, self._connection(
+ self.session.server_address, self.session.port,
+ loop=self._loop, proxy=self._proxy
+ ))
await self._sender.send(self._init_with(
functions.help.GetConfigRequest()))
@@ -373,7 +370,7 @@ class TelegramBaseClient(abc.ABC):
await self.session.set_dc(dc.id, dc.ip_address, dc.port)
# auth_key's are associated with a server, which has now changed
# so it's not valid anymore. Set to None to force recreating it.
- self.session.auth_key = self._sender.state.auth_key = None
+ self.session.auth_key = None
await self.session.save()
await self._disconnect()
return await self.connect()
@@ -416,13 +413,13 @@ class TelegramBaseClient(abc.ABC):
# Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt
# for clearly showing how to export the authorization
dc = await self._get_dc(dc_id)
- state = MTProtoState(None)
# Can't reuse self._sender._connection as it has its own seqno.
#
# If one were to do that, Telegram would reset the connection
# with no further clues.
- sender = MTProtoSender(state, self._connection.clone(), self._loop)
- await sender.connect(dc.ip_address, dc.port)
+ sender = MTProtoSender(self._loop)
+ await sender.connect(None, self._connection(
+ dc.ip_address, dc.port, loop=self._loop, proxy=self._proxy))
__log__.info('Exporting authorization for data center %s', dc)
auth = await self(functions.auth.ExportAuthorizationRequest(dc_id))
req = self._init_with(functions.auth.ImportAuthorizationRequest(
diff --git a/telethon/client/uploads.py b/telethon/client/uploads.py
index a43815a0..42707ff8 100644
--- a/telethon/client/uploads.py
+++ b/telethon/client/uploads.py
@@ -5,11 +5,11 @@ import os
import pathlib
import re
from io import BytesIO
-from mimetypes import guess_type
+from .buttons import ButtonMethods
from .messageparse import MessageParseMethods
from .users import UserMethods
-from .buttons import ButtonMethods
+from .. import default
from .. import utils, helpers
from ..tl import types, functions, custom
@@ -23,7 +23,7 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods):
async def send_file(
self, entity, file, *, caption='', force_document=False,
progress_callback=None, reply_to=None, attributes=None,
- thumb=None, allow_cache=True, parse_mode=utils.Default,
+ thumb=None, allow_cache=True, parse_mode=default,
voice_note=False, video_note=False, buttons=None, silent=None,
**kwargs):
"""
@@ -180,7 +180,7 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods):
async def _send_album(self, entity, files, caption='',
progress_callback=None, reply_to=None,
- parse_mode=utils.Default, silent=None):
+ parse_mode=default, silent=None):
"""Specialized version of .send_file for albums"""
# We don't care if the user wants to avoid cache, we will use it
# anyway. Why? The cached version will be exactly the same thing
diff --git a/telethon/default.py b/telethon/default.py
new file mode 100644
index 00000000..6ba4ad80
--- /dev/null
+++ b/telethon/default.py
@@ -0,0 +1,5 @@
+"""
+Sentinel module to signify that a parameter should use its default value.
+
+Useful when the default value or ``None`` are both valid options.
+"""
diff --git a/telethon/events/inlinequery.py b/telethon/events/inlinequery.py
index 75e991a6..4e3c6c1f 100644
--- a/telethon/events/inlinequery.py
+++ b/telethon/events/inlinequery.py
@@ -124,7 +124,7 @@ class InlineQuery(EventBuilder):
async def answer(
self, results=None, cache_time=0, *,
- gallery=False, private=False,
+ gallery=False, next_offset=None, private=False,
switch_pm=None, switch_pm_param=''):
"""
Answers the inline query with the given results.
@@ -147,6 +147,10 @@ class InlineQuery(EventBuilder):
gallery (`bool`, optional):
Whether the results should show as a gallery (grid) or not.
+
+ next_offset (`str`, optional):
+ The offset the client will send when the user scrolls the
+ results and it repeats the request.
private (`bool`, optional):
Whether the results should be cached by Telegram
@@ -163,11 +167,14 @@ class InlineQuery(EventBuilder):
if self._answered:
return
- results = [self._as_awaitable(x, self._client.loop)
- for x in results]
+ if results:
+ results = [self._as_awaitable(x, self._client.loop)
+ for x in results]
- done, _ = await asyncio.wait(results, loop=self._client.loop)
- results = [x.result() for x in done]
+ done, _ = await asyncio.wait(results, loop=self._client.loop)
+ results = [x.result() for x in done]
+ else:
+ results = []
if switch_pm:
switch_pm = types.InlineBotSwitchPM(switch_pm, switch_pm_param)
@@ -178,6 +185,7 @@ class InlineQuery(EventBuilder):
results=results,
cache_time=cache_time,
gallery=gallery,
+ next_offset=next_offset,
private=private,
switch_pm=switch_pm
)
diff --git a/telethon/extensions/__init__.py b/telethon/extensions/__init__.py
index 9e7f6686..903460b6 100644
--- a/telethon/extensions/__init__.py
+++ b/telethon/extensions/__init__.py
@@ -4,4 +4,3 @@ communication with support for cancelling the operation, and an utility class
to read arbitrary binary data in a more comfortable way, with int/strings/etc.
"""
from .binaryreader import BinaryReader
-from .tcpclient import TcpClient
diff --git a/telethon/extensions/html.py b/telethon/extensions/html.py
index d4233473..8a397336 100644
--- a/telethon/extensions/html.py
+++ b/telethon/extensions/html.py
@@ -9,8 +9,8 @@ from html.parser import HTMLParser
from ..tl.types import (
MessageEntityBold, MessageEntityItalic, MessageEntityCode,
MessageEntityPre, MessageEntityEmail, MessageEntityUrl,
- MessageEntityTextUrl
-)
+ MessageEntityTextUrl, MessageEntityMentionName
+ )
# Helpers from markdown.py
@@ -178,6 +178,9 @@ def unparse(text, entities):
elif entity_type == MessageEntityTextUrl:
html.append('{}'
.format(escape(entity.url), entity_text))
+ elif entity_type == MessageEntityMentionName:
+ html.append('{}'
+ .format(entity.user_id, entity_text))
else:
skip_entity = True
last_offset = entity.offset + (0 if skip_entity else entity.length)
diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py
index 5274dc85..be0ea507 100644
--- a/telethon/extensions/markdown.py
+++ b/telethon/extensions/markdown.py
@@ -9,8 +9,8 @@ from ..helpers import add_surrogate, del_surrogate
from ..tl import TLObject
from ..tl.types import (
MessageEntityBold, MessageEntityItalic, MessageEntityCode,
- MessageEntityPre, MessageEntityTextUrl
-)
+ MessageEntityPre, MessageEntityTextUrl, MessageEntityMentionName
+ )
DEFAULT_DELIMITERS = {
'**': MessageEntityBold,
@@ -161,11 +161,17 @@ def unparse(text, entities, delimiters=None, url_fmt=None):
delimiter = delimiters.get(type(entity), None)
if delimiter:
text = text[:s] + delimiter + text[s:e] + delimiter + text[e:]
- elif isinstance(entity, MessageEntityTextUrl) and url_fmt:
- text = (
- text[:s] +
- add_surrogate(url_fmt.format(text[s:e], entity.url)) +
- text[e:]
- )
+ elif url_fmt:
+ url = None
+ if isinstance(entity, MessageEntityTextUrl):
+ url = entity.url
+ elif isinstance(entity, MessageEntityMentionName):
+ url = 'tg://user?id={}'.format(entity.user_id)
+ if url:
+ text = (
+ text[:s] +
+ add_surrogate(url_fmt.format(text[s:e], url)) +
+ text[e:]
+ )
return del_surrogate(text)
diff --git a/telethon/extensions/tcpclient.py b/telethon/extensions/tcpclient.py
deleted file mode 100644
index 4baa46ca..00000000
--- a/telethon/extensions/tcpclient.py
+++ /dev/null
@@ -1,171 +0,0 @@
-"""
-This module holds a rough implementation of the C# TCP client.
-
-This class is **not** safe across several tasks since partial reads
-may be ``await``'ed before being able to return the exact byte count.
-
-This class is also not concerned about disconnections or retries of
-any sort, nor any other kind of errors such as connecting twice.
-"""
-import asyncio
-import errno
-import logging
-import socket
-import ssl
-
-CONN_RESET_ERRNOS = {
- errno.EBADF, errno.ENOTSOCK, errno.ENETUNREACH,
- errno.EINVAL, errno.ENOTCONN, errno.EHOSTUNREACH,
- errno.ECONNREFUSED, errno.ECONNRESET, errno.ECONNABORTED,
- errno.ENETDOWN, errno.ENETRESET, errno.ECONNABORTED,
- errno.EHOSTDOWN, errno.EPIPE, errno.ESHUTDOWN
-}
-# catched: EHOSTUNREACH, ECONNREFUSED, ECONNRESET, ENETUNREACH
-# ConnectionError: EPIPE, ESHUTDOWN, ECONNABORTED, ECONNREFUSED, ECONNRESET
-
-try:
- import socks
-except ImportError:
- socks = None
-
-SSL_PORT = 443
-__log__ = logging.getLogger(__name__)
-
-
-class TcpClient:
- """A simple TCP client to ease the work with sockets and proxies."""
-
- class SocketClosed(ConnectionError):
- pass
-
- def __init__(self, *, loop, timeout, ssl=None, proxy=None):
- """
- Initializes the TCP client.
-
- :param proxy: the proxy to be used, if any.
- :param timeout: the timeout for connect, read and write operations.
- :param ssl: ssl.wrap_socket keyword arguments to use when connecting
- if port == SSL_PORT, or do nothing if not present.
- """
- self._loop = loop
- self.proxy = proxy
- self.ssl = ssl
- self._socket = None
- self._reader = None
- self._writer = None
- self._closed = asyncio.Event(loop=self._loop)
- self._closed.set()
-
- if isinstance(timeout, (int, float)):
- self.timeout = float(timeout)
- elif hasattr(timeout, 'seconds'):
- self.timeout = float(timeout.seconds)
- else:
- raise TypeError('Invalid timeout type: {}'.format(type(timeout)))
-
- @staticmethod
- def _create_socket(mode, proxy):
- if proxy is None:
- s = socket.socket(mode, socket.SOCK_STREAM)
- else:
- __log__.info('Connection will be made through proxy %s', proxy)
- import socks
- s = socks.socksocket(mode, socket.SOCK_STREAM)
- if isinstance(proxy, dict):
- s.set_proxy(**proxy)
- else: # tuple, list, etc.
- s.set_proxy(*proxy)
- s.setblocking(False)
- return s
-
- async def connect(self, ip, port):
- """
- Tries connecting to IP:port unless an OSError is raised.
-
- :param ip: the IP to connect to.
- :param port: the port to connect to.
- """
- if ':' in ip: # IPv6
- ip = ip.replace('[', '').replace(']', '')
- mode, address = socket.AF_INET6, (ip, port, 0, 0)
- else:
- mode, address = socket.AF_INET, (ip, port)
-
- try:
- if self._socket is None:
- self._socket = self._create_socket(mode, self.proxy)
- wrap_ssl = self.ssl and port == SSL_PORT
- else:
- wrap_ssl = False
-
- await asyncio.wait_for(
- self._loop.sock_connect(self._socket, address),
- timeout=self.timeout,
- loop=self._loop
- )
- if wrap_ssl:
- # Temporarily set the socket to blocking
- # (timeout) until connection is established.
- self._socket.settimeout(self.timeout)
- self._socket = ssl.wrap_socket(
- self._socket, do_handshake_on_connect=True, **self.ssl)
- self._socket.setblocking(False)
-
- self._closed.clear()
- self._reader, self._writer =\
- await asyncio.open_connection(sock=self._socket)
- except OSError as e:
- if e.errno in CONN_RESET_ERRNOS:
- raise ConnectionResetError() from e
- else:
- raise
-
- @property
- def is_connected(self):
- """Determines whether the client is connected or not."""
- return not self._closed.is_set()
-
- def close(self):
- """Closes the connection."""
- fd = None
- try:
- if self._writer is not None:
- self._writer.close()
-
- if self._socket is not None:
- fd = self._socket.fileno()
- if self.is_connected:
- self._socket.shutdown(socket.SHUT_RDWR)
- self._socket.close()
- except OSError:
- pass # Ignore ENOTCONN, EBADF, and any other error when closing
- finally:
- self._socket = None
- self._reader = None
- self._writer = None
- self._closed.set()
- if fd and fd != -1:
- self._loop.remove_reader(fd)
-
- async def write(self, data):
- """
- Writes (sends) the specified bytes to the connected peer.
- :param data: the data to send.
- """
- if not self.is_connected:
- raise ConnectionResetError('Not connected')
-
- self._writer.write(data)
- await self._writer.drain()
-
- async def read(self, size):
- """
- Reads (receives) a whole block of size bytes from the connected peer.
-
- :param size: the size of the block to be read.
- :return: the read data with len(data) == size.
- """
- if not self.is_connected:
- raise ConnectionResetError('Not connected')
-
- return await self._reader.readexactly(size)
diff --git a/telethon/full_sync.py b/telethon/full_sync.py
new file mode 100644
index 00000000..5ca4a7e6
--- /dev/null
+++ b/telethon/full_sync.py
@@ -0,0 +1,161 @@
+"""
+This magical module will rewrite all public methods in the public interface of
+the library so they can delegate the call to an asyncio event loop in another
+thread and wait for the result. This rewrite may not be desirable if the end
+user always uses the methods they way they should be ran, but it's incredibly
+useful for quick scripts and legacy code.
+"""
+import asyncio
+import functools
+import inspect
+import threading
+from concurrent.futures import Future, ThreadPoolExecutor
+
+from async_generator import isasyncgenfunction
+
+from .client.telegramclient import TelegramClient
+from .tl.custom import (
+ Draft, Dialog, MessageButton, Forward, Message, InlineResult, Conversation
+)
+from .tl.custom.chatgetter import ChatGetter
+from .tl.custom.sendergetter import SenderGetter
+
+
+async def _proxy_future(af, cf):
+ try:
+ res = await af
+ cf.set_result(res)
+ except Exception as e:
+ cf.set_exception(e)
+
+
+def _sync_result(loop, x):
+ f = Future()
+ loop.call_soon_threadsafe(asyncio.ensure_future, _proxy_future(x, f))
+ return f.result()
+
+
+class _SyncGen:
+ def __init__(self, loop, gen):
+ self.loop = loop
+ self.gen = gen
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ try:
+ return _sync_result(self.loop, self.gen.__anext__())
+ except StopAsyncIteration:
+ raise StopIteration from None
+
+
+def _syncify_wrap(t, method_name, loop, thread_ident, syncifier=_sync_result):
+ method = getattr(t, method_name)
+
+ @functools.wraps(method)
+ def syncified(*args, **kwargs):
+ coro = method(*args, **kwargs)
+ return (
+ coro if threading.get_ident() == thread_ident
+ else syncifier(loop, coro)
+ )
+
+ setattr(t, method_name, syncified)
+
+
+def _syncify(*types, loop, thread_ident):
+ for t in types:
+ for method_name in dir(t):
+ if not method_name.startswith('_') or method_name == '__call__':
+ if inspect.iscoroutinefunction(getattr(t, method_name)):
+ _syncify_wrap(t, method_name, loop, thread_ident, _sync_result)
+ elif isasyncgenfunction(getattr(t, method_name)):
+ _syncify_wrap(t, method_name, loop, thread_ident, _SyncGen)
+
+
+__asyncthread = None
+
+
+def enable(*, loop=None, executor=None, max_workers=1):
+ """
+ Enables the fully synchronous mode. You should enable this at
+ the beginning of your script, right after importing, only once.
+
+ **Please** make sure to call `stop` at the end of your script.
+
+ You can define the event loop to use and executor, otherwise
+ the default loop and ``ThreadPoolExecutor`` will be used, in
+ which case `max_workers` will be passed to it. If you pass a
+ custom executor, `max_workers` will be ignored.
+ """
+ global __asyncthread
+ if __asyncthread is not None:
+ raise RuntimeError("full_sync can only be enabled once")
+
+ if not loop:
+ loop = asyncio.get_event_loop()
+ if not executor:
+ executor = ThreadPoolExecutor(max_workers=max_workers)
+
+ def start():
+ asyncio.set_event_loop(loop)
+ loop.run_forever()
+
+ __asyncthread = threading.Thread(
+ target=start, name="__telethon_async_thread__", daemon=True
+ )
+ __asyncthread.start()
+ __asyncthread.loop = loop
+ __asyncthread.executor = executor
+
+ TelegramClient.__init__ = functools.partialmethod(
+ TelegramClient.__init__, loop=loop
+ )
+
+ _syncify(TelegramClient, Draft, Dialog, MessageButton, ChatGetter,
+ SenderGetter, Forward, Message, InlineResult, Conversation,
+ loop=loop, thread_ident=__asyncthread.ident)
+ _syncify_wrap(TelegramClient, "start", loop, __asyncthread.ident)
+
+ old_add_event_handler = TelegramClient.add_event_handler
+ old_remove_event_handler = TelegramClient.remove_event_handler
+ proxied_event_handlers = {}
+
+ @functools.wraps(old_add_event_handler)
+ def add_proxied_event_handler(self, callback, *args, **kwargs):
+ async def _proxy(*pargs, **pkwargs):
+ await loop.run_in_executor(
+ executor, functools.partial(callback, *pargs, **pkwargs))
+
+ proxied_event_handlers[callback] = _proxy
+
+ args = (self, _proxy, *args)
+ return old_add_event_handler(*args, **kwargs)
+
+ @functools.wraps(old_remove_event_handler)
+ def remove_proxied_event_handler(self, callback, *args, **kwargs):
+ args = (self, proxied_event_handlers.get(callback, callback), *args)
+ return old_remove_event_handler(*args, **kwargs)
+
+ TelegramClient.add_event_handler = add_proxied_event_handler
+ TelegramClient.remove_event_handler = remove_proxied_event_handler
+
+ def run_until_disconnected(self):
+ return _sync_result(loop, self._run_until_disconnected())
+
+ TelegramClient.run_until_disconnected = run_until_disconnected
+
+ return __asyncthread
+
+
+def stop():
+ """
+ Stops the fully synchronous code. You
+ should call this before your script exits.
+ """
+ global __asyncthread
+ if not __asyncthread:
+ raise RuntimeError("Can't find asyncio thread")
+ __asyncthread.loop.call_soon_threadsafe(__asyncthread.loop.stop)
+ __asyncthread.executor.shutdown()
diff --git a/telethon/helpers.py b/telethon/helpers.py
index 1452aad2..5033adef 100644
--- a/telethon/helpers.py
+++ b/telethon/helpers.py
@@ -1,5 +1,5 @@
"""Various helpers not related to the Telegram API itself"""
-import collections
+import asyncio
import os
import struct
from hashlib import sha1, sha256
@@ -87,4 +87,46 @@ class TotalList(list):
return '[{}, total={}]'.format(
', '.join(repr(x) for x in self), self.total)
+
+class _ReadyQueue:
+ """
+ A queue list that supports an arbitrary cancellation token for `get`.
+ """
+ def __init__(self, loop):
+ self._list = []
+ self._loop = loop
+ self._ready = asyncio.Event(loop=loop)
+
+ def append(self, item):
+ self._list.append(item)
+ self._ready.set()
+
+ def extend(self, items):
+ self._list.extend(items)
+ self._ready.set()
+
+ async def get(self, cancellation):
+ """
+ Returns a list of all the items added to the queue until now and
+ clears the list from the queue itself. Returns ``None`` if cancelled.
+ """
+ ready = self._loop.create_task(self._ready.wait())
+ try:
+ done, pending = await asyncio.wait(
+ [ready, cancellation],
+ return_when=asyncio.FIRST_COMPLETED,
+ loop=self._loop
+ )
+ except asyncio.CancelledError:
+ done = [cancellation]
+
+ if cancellation in done:
+ ready.cancel()
+ return None
+
+ result = self._list
+ self._list = []
+ self._ready.clear()
+ return result
+
# endregion
diff --git a/telethon/network/connection/common.py b/telethon/network/connection/common.py
deleted file mode 100644
index a57c248e..00000000
--- a/telethon/network/connection/common.py
+++ /dev/null
@@ -1,74 +0,0 @@
-"""
-This module holds the abstract `Connection` class.
-
-The `Connection.send` and `Connection.recv` methods need **not** to be
-safe across several tasks and may use any amount of ``await`` keywords.
-
-The code using these `Connection`'s should be responsible for using
-an ``async with asyncio.Lock:`` block when calling said methods.
-
-Said subclasses need not to worry about reconnecting either, and
-should let the errors propagate instead.
-"""
-import abc
-
-
-class Connection(abc.ABC):
- """
- Represents an abstract connection for Telegram.
-
- Subclasses should implement the actual protocol
- being used when encoding/decoding messages.
- """
- def __init__(self, *, loop, timeout, proxy=None):
- """
- Initializes a new connection.
-
- :param loop: the event loop to be used.
- :param timeout: timeout to be used for all operations.
- :param proxy: whether to use a proxy or not.
- """
- self._loop = loop
- self._proxy = proxy
- self._timeout = timeout
-
- @abc.abstractmethod
- async def connect(self, ip, port):
- raise NotImplementedError
-
- @abc.abstractmethod
- def get_timeout(self):
- """Returns the timeout used by the connection."""
- raise NotImplementedError
-
- @abc.abstractmethod
- def is_connected(self):
- """
- Determines whether the connection is alive or not.
-
- :return: true if it's connected.
- """
- raise NotImplementedError
-
- @abc.abstractmethod
- async def close(self):
- """Closes the connection."""
- raise NotImplementedError
-
- def clone(self):
- """Creates a copy of this Connection."""
- return self.__class__(
- loop=self._loop,
- proxy=self._proxy,
- timeout=self._timeout
- )
-
- @abc.abstractmethod
- async def recv(self):
- """Receives and unpacks a message"""
- raise NotImplementedError
-
- @abc.abstractmethod
- async def send(self, message):
- """Encapsulates and sends the given message"""
- raise NotImplementedError
diff --git a/telethon/network/connection/connection.py b/telethon/network/connection/connection.py
new file mode 100644
index 00000000..e61f8720
--- /dev/null
+++ b/telethon/network/connection/connection.py
@@ -0,0 +1,180 @@
+import abc
+import asyncio
+import logging
+import socket
+import ssl as ssl_mod
+
+__log__ = logging.getLogger(__name__)
+
+
+class Connection(abc.ABC):
+ """
+ The `Connection` class is a wrapper around ``asyncio.open_connection``.
+
+ Subclasses are meant to communicate with this class through a queue.
+
+ This class provides a reliable interface that will stay connected
+ under any conditions for as long as the user doesn't disconnect or
+ the input parameters to auto-reconnect dictate otherwise.
+ """
+ def __init__(self, ip, port, *, loop, proxy=None):
+ self._ip = ip
+ self._port = port
+ self._loop = loop
+ self._proxy = proxy
+ self._reader = None
+ self._writer = None
+ self._disconnected = asyncio.Event(loop=loop)
+ self._disconnected.set()
+ self._disconnected_future = None
+ self._send_task = None
+ self._recv_task = None
+ self._send_queue = asyncio.Queue(1)
+ self._recv_queue = asyncio.Queue(1)
+
+ async def connect(self, timeout=None, ssl=None):
+ """
+ Establishes a connection with the server.
+ """
+ if not self._proxy:
+ self._reader, self._writer = await asyncio.wait_for(
+ asyncio.open_connection(
+ self._ip, self._port, loop=self._loop, ssl=ssl),
+ loop=self._loop, timeout=timeout
+ )
+ else:
+ import socks
+ if ':' in self._ip:
+ mode, address = socket.AF_INET6, (self._ip, self._port, 0, 0)
+ else:
+ mode, address = socket.AF_INET, (self._ip, self._port)
+
+ s = socks.socksocket(mode, socket.SOCK_STREAM)
+ if isinstance(self._proxy, dict):
+ s.set_proxy(**self._proxy)
+ else:
+ s.set_proxy(*self._proxy)
+
+ s.setblocking(False)
+ await asyncio.wait_for(
+ self._loop.sock_connect(s, address),
+ timeout=timeout,
+ loop=self._loop
+ )
+ if ssl:
+ self._socket.settimeout(timeout)
+ self._socket = ssl_mod.wrap_socket(
+ s,
+ do_handshake_on_connect=True,
+ ssl_version=ssl_mod.PROTOCOL_SSLv23,
+ ciphers='ADH-AES256-SHA'
+ )
+ self._socket.setblocking(False)
+
+ self._reader, self._writer = await asyncio.open_connection(
+ self._ip, self._port, loop=self._loop, sock=s
+ )
+
+ self._disconnected.clear()
+ self._disconnected_future = None
+ self._send_task = self._loop.create_task(self._send_loop())
+ self._recv_task = self._loop.create_task(self._recv_loop())
+
+ def disconnect(self):
+ """
+ Disconnects from the server.
+ """
+ self._disconnected.set()
+ if self._send_task:
+ self._send_task.cancel()
+
+ if self._recv_task:
+ self._recv_task.cancel()
+
+ if self._writer:
+ self._writer.close()
+
+ @property
+ def disconnected(self):
+ if not self._disconnected_future:
+ self._disconnected_future = \
+ self._loop.create_task(self._disconnected.wait())
+ return self._disconnected_future
+
+ def clone(self):
+ """
+ Creates a clone of the connection.
+ """
+ return self.__class__(self._ip, self._port, loop=self._loop)
+
+ def send(self, data):
+ """
+ Sends a packet of data through this connection mode.
+
+ This method returns a coroutine.
+ """
+ return self._send_queue.put(data)
+
+ async def recv(self):
+ """
+ Receives a packet of data through this connection mode.
+
+ This method returns a coroutine.
+ """
+ ok, result = await self._recv_queue.get()
+ if ok:
+ return result
+ else:
+ raise result from None
+
+ async def _send_loop(self):
+ """
+ This loop is constantly popping items off the queue to send them.
+ """
+ try:
+ while not self._disconnected.is_set():
+ self._send(await self._send_queue.get())
+ await self._writer.drain()
+ except asyncio.CancelledError:
+ pass
+ except Exception:
+ logging.exception('Unhandled exception in the sending loop')
+ self.disconnect()
+
+ async def _recv_loop(self):
+ """
+ This loop is constantly putting items on the queue as they're read.
+ """
+ try:
+ while not self._disconnected.is_set():
+ data = await self._recv()
+ await self._recv_queue.put((True, data))
+ except asyncio.CancelledError:
+ pass
+ except Exception as e:
+ await self._recv_queue.put((False, e))
+ self.disconnect()
+
+ @abc.abstractmethod
+ def _send(self, data):
+ """
+ This method should be implemented differently under each
+ connection mode and serialize the data into the packet
+ the way it should be sent through `self._writer`.
+ """
+ raise NotImplementedError
+
+ @abc.abstractmethod
+ async def _recv(self):
+ """
+ This method should be implemented differently under each
+ connection mode and deserialize the data from the packet
+ the way it should be read from `self._reader`.
+ """
+ raise NotImplementedError
+
+ def __str__(self):
+ return '{}:{}/{}'.format(
+ self._ip, self._port,
+ self.__class__.__name__.replace('Connection', '')
+ )
diff --git a/telethon/network/connection/http.py b/telethon/network/connection/http.py
index 955b9ab3..bfda941d 100644
--- a/telethon/network/connection/http.py
+++ b/telethon/network/connection/http.py
@@ -1,62 +1,34 @@
-import errno
-import ssl
+import asyncio
-from .common import Connection
-from ...extensions import TcpClient
+from .connection import Connection
+
+
+SSL_PORT = 443
class ConnectionHttp(Connection):
- def __init__(self, *, loop, timeout, proxy=None):
- super().__init__(loop=loop, timeout=timeout, proxy=proxy)
- self.conn = TcpClient(
- timeout=self._timeout, loop=self._loop, proxy=self._proxy,
- ssl=dict(ssl_version=ssl.PROTOCOL_SSLv23, ciphers='ADH-AES256-SHA')
- )
- self.read = self.conn.read
- self.write = self.conn.write
- self._host = None
+ async def connect(self, timeout=None, ssl=None):
+ await super().connect(timeout=timeout, ssl=self._port == SSL_PORT)
- async def connect(self, ip, port):
- self._host = '{}:{}'.format(ip, port)
- try:
- await self.conn.connect(ip, port)
- except OSError as e:
- if e.errno == errno.EISCONN:
- return # Already connected, no need to re-set everything up
- else:
- raise
-
- def get_timeout(self):
- return self.conn.timeout
-
- def is_connected(self):
- return self.conn.is_connected
-
- async def close(self):
- self.conn.close()
-
- async def recv(self):
- while True:
- line = await self._read_line()
- if line.lower().startswith(b'content-length: '):
- await self.read(2)
- length = int(line[16:-2])
- return await self.read(length)
-
- async def _read_line(self):
- newline = ord('\n')
- line = await self.read(1)
- while line[-1] != newline:
- line += await self.read(1)
- return line
-
- async def send(self, message):
- await self.write(
+ def _send(self, message):
+ self._writer.write(
'POST /api HTTP/1.1\r\n'
- 'Host: {}\r\n'
+ 'Host: {}:{}\r\n'
'Content-Type: application/x-www-form-urlencoded\r\n'
'Connection: keep-alive\r\n'
'Keep-Alive: timeout=100000, max=10000000\r\n'
- 'Content-Length: {}\r\n\r\n'.format(self._host, len(message))
+ 'Content-Length: {}\r\n\r\n'
+ .format(self._ip, self._port, len(message))
.encode('ascii') + message
)
+
+ async def _recv(self):
+ while True:
+ line = await self._reader.readline()
+ if not line or line[-1] != b'\n':
+ raise asyncio.IncompleteReadError(line, None)
+
+ if line.lower().startswith(b'content-length: '):
+ await self._reader.readexactly(2)
+ length = int(line[16:-2])
+ return await self._reader.readexactly(length)
diff --git a/telethon/network/connection/tcpabridged.py b/telethon/network/connection/tcpabridged.py
index d5943908..c9350da9 100644
--- a/telethon/network/connection/tcpabridged.py
+++ b/telethon/network/connection/tcpabridged.py
@@ -1,31 +1,44 @@
import struct
-from .tcpfull import ConnectionTcpFull
+from .connection import Connection
-class ConnectionTcpAbridged(ConnectionTcpFull):
+class ConnectionTcpAbridged(Connection):
"""
This is the mode with the lowest overhead, as it will
only require 1 byte if the packet length is less than
508 bytes (127 << 2, which is very common).
"""
- async def connect(self, ip, port):
- result = await super().connect(ip, port)
- await self.conn.write(b'\xef')
- return result
+ async def connect(self, timeout=None, ssl=None):
+ await super().connect(timeout=timeout, ssl=ssl)
+ self._writer.write(b'\xef')
+ await self._writer.drain()
- async def recv(self):
- length = struct.unpack('= 127:
- length = struct.unpack('> 2
+ def _send(self, data):
+ length = len(data) >> 2
if length < 127:
length = struct.pack('B', length)
else:
length = b'\x7f' + int.to_bytes(length, 3, 'little')
- await self.write(length + message)
+ self._write(length + data)
+
+ async def _recv(self):
+ length = struct.unpack('= 127:
+ length = struct.unpack(
+ ' MessageContainer.MAXIMUM_SIZE:
+ size -= MessageContainer.MAXIMUM_SIZE
+ if len(batch) > 1:
+ # Inlined code to pack several messages into a container
+ data = struct.pack(
+ ' MessageContainer.MAXIMUM_SIZE:
+ state.future.set_exception(
+ ValueError('Request payload is too big'))
+ return
+
+ # This is the only requirement to make this work.
+ state.msg_id = self._state.write_data_as_message(
+ buffer, state.data, isinstance(state.request, TLRequest),
+ after_id=after_id
+ )
+ __log__.debug('Assigned msg_id = %d to %s (%x)',
+ state.msg_id, state.request.__class__.__name__,
+ id(state.request))
+
+ # TODO Yield in the inner loop -> Telegram "Invalid container". Why?
+ for state in state_list:
+ if not isinstance(state, list):
+ yield write_state(state)
+ else:
+ after_id = None
+ for s in state:
+ yield write_state(s, after_id)
+ after_id = s.msg_id
+
+ yield write_state(None)
+
+ def __str__(self):
+ return str(self._connection)
diff --git a/telethon/network/mtprotoplainsender.py b/telethon/network/mtprotoplainsender.py
index 942c5e02..b53ac24d 100644
--- a/telethon/network/mtprotoplainsender.py
+++ b/telethon/network/mtprotoplainsender.py
@@ -30,7 +30,7 @@ class MTProtoPlainSender:
body = bytes(request)
msg_id = self._state._get_new_msg_id()
await self._connection.send(
- struct.pack(' MessageContainer.MAXIMUM_SIZE):
- self.put_nowait(item)
- break
- else:
- size += item.size()
- result.append(item)
-
- return result
diff --git a/telethon/network/mtprotostate.py b/telethon/network/mtprotostate.py
index 579d26cd..3495ae8a 100644
--- a/telethon/network/mtprotostate.py
+++ b/telethon/network/mtprotostate.py
@@ -8,7 +8,8 @@ from ..crypto import AES
from ..errors import SecurityError, BrokenAuthKeyError
from ..extensions import BinaryReader
from ..tl.core import TLMessage
-from ..tl.tlobject import TLRequest
+from ..tl.functions import InvokeAfterMsgRequest
+from ..tl.core.gzippacked import GzipPacked
__log__ = logging.getLogger(__name__)
@@ -27,6 +28,14 @@ class MTProtoState:
for all these is not a good idea as each need their own authkey, and
the concept of "copying" sessions with the unnecessary entities or
updates state for these connections doesn't make sense.
+
+ While it would be possible to have a `MTProtoPlainState` that does no
+ encryption so that it was usable through the `MTProtoLayer` and thus
+ avoid the need for a `MTProtoPlainSender`, the `MTProtoLayer` is more
+ focused to efficiency and this state is also more advanced (since it
+ supports gzipping and invoking after other message IDs). There are too
+ many methods that would be needed to make it convenient to use for the
+ authentication process, at which point the `MTProtoPlainSender` is better.
"""
def __init__(self, auth_key):
# Session IDs can be random on every connection
@@ -37,20 +46,6 @@ class MTProtoState:
self._sequence = 0
self._last_msg_id = 0
- def create_message(self, obj, *, loop, after=None):
- """
- Creates a new `telethon.tl.tl_message.TLMessage` from
- the given `telethon.tl.tlobject.TLObject` instance.
- """
- return TLMessage(
- msg_id=self._get_new_msg_id(),
- seq_no=self._get_seq_no(isinstance(obj, TLRequest)),
- obj=obj,
- after_id=after.msg_id if after else None,
- out=True, # Pre-convert the request into bytes
- loop=loop
- )
-
def update_message_id(self, message):
"""
Updates the message ID to a new one,
@@ -74,14 +69,31 @@ class MTProtoState:
return aes_key, aes_iv
- def pack_message(self, message):
+ def write_data_as_message(self, buffer, data, content_related,
+ *, after_id=None):
"""
- Packs the given `telethon.tl.tl_message.TLMessage` using the
- current authorization key following MTProto 2.0 guidelines.
+ Writes a message containing the given data into buffer.
- See https://core.telegram.org/mtproto/description.
+ Returns the message id.
"""
- data = struct.pack(' 512:
+ if content_related and len(data) > 512:
gzipped = bytes(GzipPacked(data))
return gzipped if len(gzipped) < len(data) else data
else:
diff --git a/telethon/tl/core/messagecontainer.py b/telethon/tl/core/messagecontainer.py
index de304424..800a31f0 100644
--- a/telethon/tl/core/messagecontainer.py
+++ b/telethon/tl/core/messagecontainer.py
@@ -27,11 +27,6 @@ class MessageContainer(TLObject):
],
}
- def __bytes__(self):
- return struct.pack(
- 'Z\n\t\xb9\xd2=\xbaF\xd1\x8e'"
-
- def test_sha1(self):
- string = 'Example string'
-
- hash_sum = sha1(string.encode('utf-8')).digest()
- expected = b'\nT\x92|\x8d\x06:)\x99\x04\x8e\xf8j?\xc4\x8e\xd3}m9'
-
- self.assertEqual(hash_sum, expected,
- msg='Invalid sha1 hash_sum representation (should be {}, but is {})'
- .format(expected, hash_sum))
-
- @unittest.skip("test_aes_encrypt needs fix")
- def test_aes_encrypt(self):
- value = AES.encrypt_ige(self.plain_text, self.key, self.iv)
- take = 16 # Don't take all the bytes, since latest involve are random padding
- self.assertEqual(value[:take], self.cipher_text[:take],
- msg='Ciphered text ("{}") does not equal expected ("{}")'
- .format(value[:take], self.cipher_text[:take]))
-
- value = AES.encrypt_ige(self.plain_text_padded, self.key, self.iv)
- self.assertEqual(value, self.cipher_text_padded,
- msg='Ciphered text ("{}") does not equal expected ("{}")'
- .format(value, self.cipher_text_padded))
-
- def test_aes_decrypt(self):
- # The ciphered text must always be padded
- value = AES.decrypt_ige(self.cipher_text_padded, self.key, self.iv)
- self.assertEqual(value, self.plain_text_padded,
- msg='Decrypted text ("{}") does not equal expected ("{}")'
- .format(value, self.plain_text_padded))
-
- @unittest.skip("test_calc_key needs fix")
- def test_calc_key(self):
- # TODO Upgrade test for MtProto 2.0
- shared_key = b'\xbc\xd2m\xb7\xcav\xf4][\x88\x83\' \xf3\x11\x8as\xd04\x941\xae' \
- b'*O\x03\x86\x9a/H#\x1a\x8c\xb5j\xe9$\xe0IvCm^\xe70\x1a5C\t\x16' \
- b'\x03\xd2\x9d\xa9\x89\xd6\xce\x08P\x0fdr\xa0\xb3\xeb\xfecv\x1a' \
- b'\xdfJ\x14\x96\x98\x16\xa3G\xab\x04\x14!\\\xeb\n\xbcn\xdf\xc4%' \
- b'\xc6\t\xb7\x16\x14\x9c\'\x81\x15=\xb0\xaf\x0e\x0bR\xaa\x0466s' \
- b'\xf0\xcf\xb7\xb8>,D\x94x\xd7\xf8\xe0\x84\xcb%\xd3\x05\xb2\xe8' \
- b'\x95Mr?\xa2\xe8In\xf9\x0b[E\x9b\xaa\x0cX\x7f\x0ei\xde\xeed\x1d' \
- b'x/J\xce\xea^}0;\xa83B\xbbR\xa1\xbfe\x04\xb9\x1e\xa1"f=\xa5M@' \
- b'\x9e\xdd\x81\x80\xc9\xa5\xfb\xfcg\xdd\x15\x03p!\x0ffD\x16\x892' \
- b'\xea\xca\xb1A\x99O\xa94P\xa9\xa2\xc6;\xb2C9\x1dC5\xd2\r\xecL' \
- b'\xd9\xabw-\x03\ry\xc2v\x17]\x02\x15\x0cBa\x97\xce\xa5\xb1\xe4]' \
- b'\x8e\xe0,\xcfC{o\xfa\x99f\xa4pM\x00'
-
- # Calculate key being the client
- msg_key = b'\xba\x1a\xcf\xda\xa8^Cbl\xfa\xb6\x0c:\x9b\xb0\xfc'
-
- key, iv = utils.calc_key(shared_key, msg_key, client=True)
- expected_key = b"\xaf\xe3\x84Qm\xe0!\x0c\xd91\xe4\x9a\xa0v_gc" \
- b"x\xa1\xb0\xc9\xbc\x16'v\xcf,\x9dM\xae\xc6\xa5"
-
- expected_iv = b'\xb8Q\xf3\xc5\xa3]\xc6\xdf\x9e\xe0Q\xbd"\x8d' \
- b'\x13\t\x0e\x9a\x9d^8\xa2\xf8\xe7\x00w\xd9\xc1' \
- b'\xa7\xa0\xf7\x0f'
-
- self.assertEqual(key, expected_key,
- msg='Invalid key (expected ("{}"), got ("{}"))'
- .format(expected_key, key))
- self.assertEqual(iv, expected_iv,
- msg='Invalid IV (expected ("{}"), got ("{}"))'
- .format(expected_iv, iv))
-
- # Calculate key being the server
- msg_key = b'\x86m\x92i\xcf\x8b\x93\xaa\x86K\x1fi\xd04\x83]'
-
- key, iv = utils.calc_key(shared_key, msg_key, client=False)
- expected_key = b'\xdd0X\xb6\x93\x8e\xc9y\xef\x83\xf8\x8cj' \
- b'\xa7h\x03\xe2\xc6\xb16\xc5\xbb\xfc\xe7' \
- b'\xdf\xd6\xb1g\xf7u\xcfk'
-
- expected_iv = b'\xdcL\xc2\x18\x01J"X\x86lb\xb6\xb547\xfd' \
- b'\xe2a4\xb6\xaf}FS\xd7[\xe0N\r\x19\xfb\xbc'
-
- self.assertEqual(key, expected_key,
- msg='Invalid key (expected ("{}"), got ("{}"))'
- .format(expected_key, key))
- self.assertEqual(iv, expected_iv,
- msg='Invalid IV (expected ("{}"), got ("{}"))'
- .format(expected_iv, iv))
-
- def test_generate_key_data_from_nonce(self):
- server_nonce = int.from_bytes(b'The 16-bit nonce', byteorder='little')
- new_nonce = int.from_bytes(b'The new, calculated 32-bit nonce', byteorder='little')
-
- key, iv = utils.generate_key_data_from_nonce(server_nonce, new_nonce)
- expected_key = b'/\xaa\x7f\xa1\xfcs\xef\xa0\x99zh\x03M\xa4\x8e\xb4\xab\x0eE]b\x95|\xfe\xc0\xf8\x1f\xd4\xa0\xd4\xec\x91'
- expected_iv = b'\xf7\xae\xe3\xc8+=\xc2\xb8\xd1\xe1\x1b\x0e\x10\x07\x9fn\x9e\xdc\x960\x05\xf9\xea\xee\x8b\xa1h The '
-
- self.assertEqual(key, expected_key,
- msg='Key ("{}") does not equal expected ("{}")'
- .format(key, expected_key))
- self.assertEqual(iv, expected_iv,
- msg='IV ("{}") does not equal expected ("{}")'
- .format(iv, expected_iv))
-
- # test_fringerprint_from_key can't be skipped due to ImportError
- # def test_fingerprint_from_key(self):
- # assert rsa._compute_fingerprint(PyCryptoRSA.importKey(
- # '-----BEGIN RSA PUBLIC KEY-----\n'
- # 'MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6\n'
- # 'lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS\n'
- # 'an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw\n'
- # 'Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+\n'
- # '8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n\n'
- # 'Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB\n'
- # '-----END RSA PUBLIC KEY-----'
- # )) == b'!k\xe8l\x02+\xb4\xc3', 'Wrong fingerprint calculated'
-
- def test_factorize(self):
- pq = 3118979781119966969
- p, q = Factorization.factorize(pq)
- if p > q:
- p, q = q, p
-
- self.assertEqual(p, 1719614201,
- msg='Factorized pair did not yield the correct result')
- self.assertEqual(q, 1813767169,
- msg='Factorized pair did not yield the correct result')
diff --git a/telethon_tests/test_higher_level.py b/telethon_tests/test_higher_level.py
deleted file mode 100644
index 67fac515..00000000
--- a/telethon_tests/test_higher_level.py
+++ /dev/null
@@ -1,49 +0,0 @@
-import unittest
-import os
-from io import BytesIO
-from random import randint
-from hashlib import sha256
-from telethon import TelegramClient
-
-# Fill in your api_id and api_hash when running the tests
-# and REMOVE THEM once you've finished testing them.
-api_id = None
-api_hash = None
-
-
-class HigherLevelTests(unittest.TestCase):
- def setUp(self):
- if not api_id or not api_hash:
- raise ValueError('Please fill in both your api_id and api_hash.')
-
- @unittest.skip("you can't seriously trash random mobile numbers like that :)")
- def test_cdn_download(self):
- client = TelegramClient(None, api_id, api_hash)
- client.session.set_dc(0, '149.154.167.40', 80)
- self.assertTrue(client.connect())
-
- try:
- phone = '+999662' + str(randint(0, 9999)).zfill(4)
- client.send_code_request(phone)
- client.sign_up('22222', 'Test', 'DC')
-
- me = client.get_me()
- data = os.urandom(2 ** 17)
- client.send_file(
- me, data,
- progress_callback=lambda c, t:
- print('test_cdn_download:uploading {:.2%}...'.format(c/t))
- )
- msg = client.get_messages(me)[1][0]
-
- out = BytesIO()
- client.download_media(msg, out)
- self.assertEqual(sha256(data).digest(), sha256(out.getvalue()).digest())
-
- out = BytesIO()
- client.download_media(msg, out) # Won't redirect
- self.assertEqual(sha256(data).digest(), sha256(out.getvalue()).digest())
-
- client.log_out()
- finally:
- client.disconnect()
diff --git a/telethon_tests/test_network.py b/telethon_tests/test_network.py
deleted file mode 100644
index 031ad99d..00000000
--- a/telethon_tests/test_network.py
+++ /dev/null
@@ -1,44 +0,0 @@
-import random
-import socket
-import threading
-import unittest
-
-import telethon.network.authenticator as authenticator
-from telethon.extensions import TcpClient
-from telethon.network import Connection
-
-
-def run_server_echo_thread(port):
- def server_thread():
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
- s.bind(('', port))
- s.listen(1)
- connection, address = s.accept()
- with connection:
- data = connection.recv(16)
- connection.send(data)
-
- server = threading.Thread(target=server_thread)
- server.start()
-
-
-class NetworkTests(unittest.TestCase):
-
- @unittest.skip("test_tcp_client needs fix")
- def test_tcp_client(self):
- port = random.randint(50000, 60000) # Arbitrary non-privileged port
- run_server_echo_thread(port)
-
- msg = b'Unit testing...'
- client = TcpClient()
- client.connect('localhost', port)
- client.write(msg)
- self.assertEqual(msg, client.read(15),
- msg='Read message does not equal sent message')
- client.close()
-
- @unittest.skip("Some parameters changed, so IP doesn't go there anymore.")
- def test_authenticator(self):
- transport = Connection('149.154.167.91', 443)
- self.assertTrue(authenticator.do_authentication(transport))
- transport.close()
diff --git a/telethon_tests/test_parser.py b/telethon_tests/test_parser.py
deleted file mode 100644
index c87686a6..00000000
--- a/telethon_tests/test_parser.py
+++ /dev/null
@@ -1,8 +0,0 @@
-import unittest
-
-
-class ParserTests(unittest.TestCase):
- """There are no tests yet"""
- @unittest.skip("there should be parser tests")
- def test_parser(self):
- self.assertTrue(True)
diff --git a/telethon_tests/test_tl.py b/telethon_tests/test_tl.py
deleted file mode 100644
index 189259f5..00000000
--- a/telethon_tests/test_tl.py
+++ /dev/null
@@ -1,8 +0,0 @@
-import unittest
-
-
-class TLTests(unittest.TestCase):
- """There are no tests yet"""
- @unittest.skip("there should be TL tests")
- def test_tl(self):
- self.assertTrue(True)
\ No newline at end of file
diff --git a/telethon_tests/test_utils.py b/telethon_tests/test_utils.py
deleted file mode 100644
index 4a550e3d..00000000
--- a/telethon_tests/test_utils.py
+++ /dev/null
@@ -1,66 +0,0 @@
-import os
-import unittest
-from telethon.tl import TLObject
-from telethon.extensions import BinaryReader
-
-
-class UtilsTests(unittest.TestCase):
- def test_binary_writer_reader(self):
- # Test that we can read properly
- data = b'\x01\x05\x00\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
- b'\x88A\x00\x00\x00\x00\x00\x009@\x1a\x1b\x1c\x1d\x1e\x1f ' \
- b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
- b'\x00\x80'
-
- with BinaryReader(data) as reader:
- value = reader.read_byte()
- self.assertEqual(value, 1,
- msg='Example byte should be 1 but is {}'.format(value))
-
- value = reader.read_int()
- self.assertEqual(value, 5,
- msg='Example integer should be 5 but is {}'.format(value))
-
- value = reader.read_long()
- self.assertEqual(value, 13,
- msg='Example long integer should be 13 but is {}'.format(value))
-
- value = reader.read_float()
- self.assertEqual(value, 17.0,
- msg='Example float should be 17.0 but is {}'.format(value))
-
- value = reader.read_double()
- self.assertEqual(value, 25.0,
- msg='Example double should be 25.0 but is {}'.format(value))
-
- value = reader.read(7)
- self.assertEqual(value, bytes([26, 27, 28, 29, 30, 31, 32]),
- msg='Example bytes should be {} but is {}'
- .format(bytes([26, 27, 28, 29, 30, 31, 32]), value))
-
- value = reader.read_large_int(128, signed=False)
- self.assertEqual(value, 2**127,
- msg='Example large integer should be {} but is {}'.format(2**127, value))
-
- def test_binary_tgwriter_tgreader(self):
- small_data = os.urandom(33)
- small_data_padded = os.urandom(19) # +1 byte for length = 20 (%4 = 0)
-
- large_data = os.urandom(999)
- large_data_padded = os.urandom(1024)
-
- data = (small_data, small_data_padded, large_data, large_data_padded)
- string = 'Testing Telegram strings, this should work properly!'
- serialized = b''.join(TLObject.serialize_bytes(d) for d in data) + \
- TLObject.serialize_bytes(string)
-
- with BinaryReader(serialized) as reader:
- # And then try reading it without errors (it should be unharmed!)
- for datum in data:
- value = reader.tgread_bytes()
- self.assertEqual(value, datum,
- msg='Example bytes should be {} but is {}'.format(datum, value))
-
- value = reader.tgread_string()
- self.assertEqual(value, string,
- msg='Example string should be {} but is {}'.format(string, value))