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