mirror of
				https://github.com/LonamiWebs/Telethon.git
				synced 2025-10-31 16:07:44 +03:00 
			
		
		
		
	Compare commits
	
		
			16 Commits
		
	
	
		
			v1
			...
			version2-s
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 334e6cc6a0 | ||
|  | f082a27ff8 | ||
|  | f9ca17c99f | ||
|  | 6d4c8ba8ff | ||
|  | f8137595c5 | ||
|  | b3f0c3d2ea | ||
|  | 3059ce2470 | ||
|  | c1a40630a3 | ||
|  | 0b69d7fd7b | ||
|  | 80e86e98ff | ||
|  | 9bafcdfe0f | ||
|  | 6226fa95ce | ||
|  | 78971fd2e5 | ||
|  | f6f7345a3a | ||
|  | ad37db1cd6 | ||
|  | ad7e62baf3 | 
|  | @ -1,4 +1,4 @@ | ||||||
| cryptg | cryptg | ||||||
| pysocks | pproxy | ||||||
| hachoir3 | hachoir3 | ||||||
| pillow | pillow | ||||||
|  |  | ||||||
|  | @ -107,7 +107,7 @@ Signing In behind a Proxy | ||||||
| ========================= | ========================= | ||||||
| 
 | 
 | ||||||
| If you need to use a proxy to access Telegram, | If you need to use a proxy to access Telegram, | ||||||
| you will need to  `install PySocks`__ and then change: | you will need to  `install aiosocks`__ and then change: | ||||||
| 
 | 
 | ||||||
| .. code-block:: python | .. code-block:: python | ||||||
| 
 | 
 | ||||||
|  | @ -117,15 +117,29 @@ with | ||||||
| 
 | 
 | ||||||
| .. code-block:: python | .. code-block:: python | ||||||
| 
 | 
 | ||||||
|     TelegramClient('anon', api_id, api_hash, proxy=(socks.SOCKS5, '127.0.0.1', 4444)) |     TelegramClient('anon', api_id, api_hash, proxy={ | ||||||
|  |         'host': '127.0.0.1', | ||||||
|  |         'port': 4444 | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| (of course, replacing the IP and port with the IP and port of the proxy). | (of course, replacing the IP and port with the IP and port of the proxy). | ||||||
| 
 | 
 | ||||||
| The ``proxy=`` argument should be a tuple, a list or a dict, | The ``proxy=`` argument should be a dictionary | ||||||
| consisting of parameters described `in PySocks usage`__. | where the following keys are allowed: | ||||||
| 
 | 
 | ||||||
| .. __: https://github.com/Anorov/PySocks#installation | .. code-block:: python | ||||||
| .. __: https://github.com/Anorov/PySocks#usage-1 | 
 | ||||||
|  |     proxy = { | ||||||
|  |         'host': 'localhost',    # (mandatory) proxy IP address | ||||||
|  |         'port': 42252,          # (mandatory) proxy port number | ||||||
|  |         'protocol': 'socks5',   # (optional) protocol to use, default socks5, allowed values: socks5, socks4 | ||||||
|  |         'username': 'foo',      # (optional) username if the proxy requires auth | ||||||
|  |         'password': 'bar',      # (optional) password if the proxy requires auth | ||||||
|  |         'remote_resolve': True  # (optional) whether to use remote or local resolve, default remote | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | .. __: https://github.com/nibrag/aiosocks | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| Using MTProto Proxies | Using MTProto Proxies | ||||||
|  |  | ||||||
|  | @ -45,7 +45,7 @@ Base | ||||||
| 
 | 
 | ||||||
|     connect |     connect | ||||||
|     disconnect |     disconnect | ||||||
|     is_connected |     connected | ||||||
|     disconnected |     disconnected | ||||||
|     loop |     loop | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -54,7 +54,7 @@ class _TakeoutClient: | ||||||
|                 self.__success)) |                 self.__success)) | ||||||
|             if not result: |             if not result: | ||||||
|                 raise ValueError("Failed to finish the takeout.") |                 raise ValueError("Failed to finish the takeout.") | ||||||
|             self.session.takeout_id = None |             self._session.takeout_id = None | ||||||
| 
 | 
 | ||||||
|     __enter__ = helpers._sync_enter |     __enter__ = helpers._sync_enter | ||||||
|     __exit__ = helpers._sync_exit |     __exit__ = helpers._sync_exit | ||||||
|  | @ -211,7 +211,7 @@ class AccountMethods(UserMethods): | ||||||
|         ) |         ) | ||||||
|         arg_specified = (arg is not None for arg in request_kwargs.values()) |         arg_specified = (arg is not None for arg in request_kwargs.values()) | ||||||
| 
 | 
 | ||||||
|         if self.session.takeout_id is None or any(arg_specified): |         if self._session.takeout_id is None or any(arg_specified): | ||||||
|             request = functions.account.InitTakeoutSessionRequest( |             request = functions.account.InitTakeoutSessionRequest( | ||||||
|                 **request_kwargs) |                 **request_kwargs) | ||||||
|         else: |         else: | ||||||
|  |  | ||||||
|  | @ -137,7 +137,7 @@ class AuthMethods(MessageParseMethods, UserMethods): | ||||||
|     async def _start( |     async def _start( | ||||||
|             self, phone, password, bot_token, force_sms, |             self, phone, password, bot_token, force_sms, | ||||||
|             code_callback, first_name, last_name, max_attempts): |             code_callback, first_name, last_name, max_attempts): | ||||||
|         if not self.is_connected(): |         if not self.connected: | ||||||
|             await self.connect() |             await self.connect() | ||||||
| 
 | 
 | ||||||
|         if await self.is_user_authorized(): |         if await self.is_user_authorized(): | ||||||
|  | @ -258,29 +258,22 @@ class AuthMethods(MessageParseMethods, UserMethods): | ||||||
| 
 | 
 | ||||||
|     async def sign_in( |     async def sign_in( | ||||||
|             self: 'TelegramClient', |             self: 'TelegramClient', | ||||||
|             phone: str = None, |  | ||||||
|             code: typing.Union[str, int] = None, |             code: typing.Union[str, int] = None, | ||||||
|             *, |             *, | ||||||
|             password: str = None, |             password: str = None, | ||||||
|             bot_token: str = None, |             bot_token: str = None, | ||||||
|  |             phone: str = None, | ||||||
|             phone_code_hash: str = None) -> 'types.User': |             phone_code_hash: str = None) -> 'types.User': | ||||||
|         """ |         """ | ||||||
|         Logs in to Telegram to an existing user or bot account. |         Logs in to Telegram to an existing user or bot account. | ||||||
| 
 | 
 | ||||||
|         You should only use this if you are not authorized yet. |         You should only use this if you are not authorized yet. | ||||||
| 
 | 
 | ||||||
|         This method will send the code if it's not provided. |  | ||||||
| 
 |  | ||||||
|         .. note:: |         .. note:: | ||||||
| 
 | 
 | ||||||
|             In most cases, you should simply use `start()` and not this method. |             In most cases, you should simply use `start()` and not this method. | ||||||
| 
 | 
 | ||||||
|         Arguments |         Arguments | ||||||
|             phone (`str` | `int`): |  | ||||||
|                 The phone to send the code to if no code was provided, |  | ||||||
|                 or to override the phone that was previously used with |  | ||||||
|                 these requests. |  | ||||||
| 
 |  | ||||||
|             code (`str` | `int`): |             code (`str` | `int`): | ||||||
|                 The code that Telegram sent. Note that if you have sent this |                 The code that Telegram sent. Note that if you have sent this | ||||||
|                 code through the application itself it will immediately |                 code through the application itself it will immediately | ||||||
|  | @ -296,30 +289,33 @@ class AuthMethods(MessageParseMethods, UserMethods): | ||||||
|                 This should be the hash the `@BotFather <https://t.me/BotFather>`_ |                 This should be the hash the `@BotFather <https://t.me/BotFather>`_ | ||||||
|                 gave you. |                 gave you. | ||||||
| 
 | 
 | ||||||
|  |             phone (`str` | `int`): | ||||||
|  |                 By default, the library remembers the phone passed to | ||||||
|  |                 `send_code_request`. If you are passing the code for | ||||||
|  |                 a different phone, you should set this parameter. | ||||||
|  | 
 | ||||||
|             phone_code_hash (`str`, optional): |             phone_code_hash (`str`, optional): | ||||||
|                 The hash returned by `send_code_request`. This can be left as |                 By default, the library remembers the hash that | ||||||
|                 ``None`` to use the last hash known for the phone to be used. |                 `send_code_request` returned. If you are passing the | ||||||
|  |                 code for a different phone, you should set this parameter. | ||||||
| 
 | 
 | ||||||
|         Returns |         Returns | ||||||
|             The signed in user, or the information about |             The signed in user. | ||||||
|             :meth:`send_code_request`. |  | ||||||
| 
 | 
 | ||||||
|         Example |         Example | ||||||
|             .. code-block:: python |             .. code-block:: python | ||||||
| 
 | 
 | ||||||
|                 phone = '+34 123 123 123' |                 phone = '+34 123 123 123' | ||||||
|                 client.sign_in(phone)  # send code |                 client.send_code_request(phone) | ||||||
| 
 | 
 | ||||||
|                 code = input('enter code: ') |                 code = input('enter code: ') | ||||||
|                 client.sign_in(phone, code) |                 client.sign_in(code) | ||||||
|         """ |         """ | ||||||
|         me = await self.get_me() |         me = await self.get_me() | ||||||
|         if me: |         if me: | ||||||
|             return me |             return me | ||||||
| 
 | 
 | ||||||
|         if phone and not code and not password: |         if code: | ||||||
|             return await self.send_code_request(phone) |  | ||||||
|         elif code: |  | ||||||
|             phone, phone_code_hash = \ |             phone, phone_code_hash = \ | ||||||
|                 self._parse_phone_and_hash(phone, phone_code_hash) |                 self._parse_phone_and_hash(phone, phone_code_hash) | ||||||
| 
 | 
 | ||||||
|  | @ -335,13 +331,10 @@ class AuthMethods(MessageParseMethods, UserMethods): | ||||||
|         elif bot_token: |         elif bot_token: | ||||||
|             result = await self(functions.auth.ImportBotAuthorizationRequest( |             result = await self(functions.auth.ImportBotAuthorizationRequest( | ||||||
|                 flags=0, bot_auth_token=bot_token, |                 flags=0, bot_auth_token=bot_token, | ||||||
|                 api_id=self.api_id, api_hash=self.api_hash |                 api_id=self._api_id, api_hash=self._api_hash | ||||||
|             )) |             )) | ||||||
|         else: |         else: | ||||||
|             raise ValueError( |             raise ValueError('You must provide a code, password or bot token') | ||||||
|                 'You must provide a phone and a code the first time, ' |  | ||||||
|                 'and a password only if an RPCError was raised before.' |  | ||||||
|             ) |  | ||||||
| 
 | 
 | ||||||
|         return self._on_login(result.user) |         return self._on_login(result.user) | ||||||
| 
 | 
 | ||||||
|  | @ -473,7 +466,7 @@ class AuthMethods(MessageParseMethods, UserMethods): | ||||||
|         if not phone_hash: |         if not phone_hash: | ||||||
|             try: |             try: | ||||||
|                 result = await self(functions.auth.SendCodeRequest( |                 result = await self(functions.auth.SendCodeRequest( | ||||||
|                     phone, self.api_id, self.api_hash, types.CodeSettings())) |                     phone, self._api_id, self._api_hash, types.CodeSettings())) | ||||||
|             except errors.AuthRestartError: |             except errors.AuthRestartError: | ||||||
|                 return await self.send_code_request(phone, force_sms=force_sms) |                 return await self.send_code_request(phone, force_sms=force_sms) | ||||||
| 
 | 
 | ||||||
|  | @ -516,7 +509,7 @@ class AuthMethods(MessageParseMethods, UserMethods): | ||||||
|         self._state_cache.reset() |         self._state_cache.reset() | ||||||
| 
 | 
 | ||||||
|         await self.disconnect() |         await self.disconnect() | ||||||
|         self.session.delete() |         self._session.delete() | ||||||
|         return True |         return True | ||||||
| 
 | 
 | ||||||
|     async def edit_2fa( |     async def edit_2fa( | ||||||
|  |  | ||||||
|  | @ -53,7 +53,7 @@ class ButtonMethods(UpdateMethods): | ||||||
|         if not utils.is_list_like(buttons): |         if not utils.is_list_like(buttons): | ||||||
|             buttons = [[buttons]] |             buttons = [[buttons]] | ||||||
|         elif not utils.is_list_like(buttons[0]): |         elif not utils.is_list_like(buttons[0]): | ||||||
|             buttons = [buttons] |             buttons = [[b] for b in buttons] | ||||||
| 
 | 
 | ||||||
|         is_inline = False |         is_inline = False | ||||||
|         is_normal = False |         is_normal = False | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import itertools | import itertools | ||||||
| import typing | import typing | ||||||
|  | import warnings | ||||||
| 
 | 
 | ||||||
| from .buttons import ButtonMethods | from .buttons import ButtonMethods | ||||||
| from .messageparse import MessageParseMethods | from .messageparse import MessageParseMethods | ||||||
|  | @ -636,6 +637,14 @@ class MessageMethods(UploadMethods, ButtonMethods, MessageParseMethods): | ||||||
|                 client.send_message(chat, 'A single button, with "clk1" as data', |                 client.send_message(chat, 'A single button, with "clk1" as data', | ||||||
|                                     buttons=Button.inline('Click me', b'clk1')) |                                     buttons=Button.inline('Click me', b'clk1')) | ||||||
| 
 | 
 | ||||||
|  |                 # Row of inline buttons (just a list) | ||||||
|  |                 client.send_message(chat, 'Look at this row', | ||||||
|  |                                     [Button.inline('Row 1'), Button.inline('Row 2')]) | ||||||
|  | 
 | ||||||
|  |                 # Columns of inline buttons (a list of lists) | ||||||
|  |                 client.send_message(chat, 'Look at this row', | ||||||
|  |                                     [[Button.inline('Col 1'), Button.inline('Col 2')]]) | ||||||
|  | 
 | ||||||
|                 # Matrix of inline buttons |                 # Matrix of inline buttons | ||||||
|                 client.send_message(chat, 'Pick one from this grid', buttons=[ |                 client.send_message(chat, 'Pick one from this grid', buttons=[ | ||||||
|                     [Button.inline('Left'), Button.inline('Right')], |                     [Button.inline('Left'), Button.inline('Right')], | ||||||
|  | @ -1061,65 +1070,71 @@ class MessageMethods(UploadMethods, ButtonMethods, MessageParseMethods): | ||||||
|             max_id: int = None, |             max_id: int = None, | ||||||
|             clear_mentions: bool = False) -> bool: |             clear_mentions: bool = False) -> bool: | ||||||
|         """ |         """ | ||||||
|         Marks messages as read and optionally clears mentions. |         Deprecated, use `mark_read` instead. | ||||||
|  |         """ | ||||||
|  |         warnings.warn('client.send_read_acknowledge is deprecated, use client.mark_read instead') | ||||||
|  |         if max_id: | ||||||
|  |             message = max_id | ||||||
| 
 | 
 | ||||||
|         This effectively marks a message as read (or more than one) in the |         return await self.mark_read(entity, message, clear_mentions=clear_mentions) | ||||||
|         given conversation. |  | ||||||
| 
 | 
 | ||||||
|         If neither message nor maximum ID are provided, all messages will be |     async def mark_read( | ||||||
|         marked as read by assuming that ``max_id = 0``. |             self: 'TelegramClient', | ||||||
|  |             entity: 'hints.EntityLike', | ||||||
|  |             message: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]' = (), | ||||||
|  |             *, | ||||||
|  |             clear_mentions: bool = False) -> bool: | ||||||
|  |         """ | ||||||
|  |         Marks a chat as read, and optionally clears mentions. | ||||||
|  | 
 | ||||||
|  |         By default, all messages will be marked as read, and mentions won't | ||||||
|  |         be cleared. You can also specify up to which message the client has | ||||||
|  |         read, and optionally clear mentions. | ||||||
| 
 | 
 | ||||||
|         Arguments |         Arguments | ||||||
|             entity (`entity`): |             entity (`entity`): | ||||||
|                 The chat where these messages are located. |                 The chat where these messages are located. | ||||||
| 
 | 
 | ||||||
|             message (`list` | `Message <telethon.tl.custom.message.Message>`): |             message (`int` | `list` | `Message <telethon.tl.custom.message.Message>`): | ||||||
|                 Either a list of messages or a single message. |                 Either a list of messages, a single message or an ID. | ||||||
| 
 |                 The chat will be marked as read up to the highest ID. | ||||||
|             max_id (`int`): |  | ||||||
|                 Overrides messages, until which message should the |  | ||||||
|                 acknowledge should be sent. |  | ||||||
| 
 | 
 | ||||||
|             clear_mentions (`bool`): |             clear_mentions (`bool`): | ||||||
|                 Whether the mention badge should be cleared (so that |                 Whether the mention badge should be cleared (so that | ||||||
|                 there are no more mentions) or not for the given entity. |                 there are no more mentions) or not for the given entity. | ||||||
| 
 | 
 | ||||||
|                 If no message is provided, this will be the only action |                 If no message is provided, this will be the only action | ||||||
|                 taken. |                 taken. If you want to mark as read *and* clear mentions, | ||||||
|  |                 pass ``0`` as the message and set this to ``True``. | ||||||
| 
 | 
 | ||||||
|         Example |         Example | ||||||
|             .. code-block:: python |             .. code-block:: python | ||||||
| 
 | 
 | ||||||
|                 client.send_read_acknowledge(last_message) |                 client.mark_read(chat) | ||||||
|                 # or |                 # or | ||||||
|                 client.send_read_acknowledge(last_message_id) |                 client.mark_read(chat, some_message) | ||||||
|                 # or |                 # or | ||||||
|                 client.send_read_acknowledge(messages) |                 client.mark_read(chat, clear_mentions=True) | ||||||
|         """ |         """ | ||||||
|         if max_id is None: |  | ||||||
|             if not message: |  | ||||||
|                 max_id = 0 |  | ||||||
|             else: |  | ||||||
|                 if utils.is_list_like(message): |  | ||||||
|                     max_id = max(msg.id for msg in message) |  | ||||||
|                 else: |  | ||||||
|                     max_id = message.id |  | ||||||
| 
 |  | ||||||
|         entity = await self.get_input_entity(entity) |         entity = await self.get_input_entity(entity) | ||||||
|         if clear_mentions: |         if clear_mentions: | ||||||
|             await self(functions.messages.ReadMentionsRequest(entity)) |             await self(functions.messages.ReadMentionsRequest(entity)) | ||||||
|             if max_id is None: |             if message == (): | ||||||
|                 return True |                 return True | ||||||
| 
 | 
 | ||||||
|         if max_id is not None: |         if not message: | ||||||
|  |             message = 0 | ||||||
|  |         elif utils.is_list_like(message): | ||||||
|  |             message = max(map(utils.get_message_id, message)) | ||||||
|  |         else: | ||||||
|  |             message = utils.get_message_id(message) | ||||||
|  | 
 | ||||||
|         if isinstance(entity, types.InputPeerChannel): |         if isinstance(entity, types.InputPeerChannel): | ||||||
|             return await self(functions.channels.ReadHistoryRequest( |             return await self(functions.channels.ReadHistoryRequest( | ||||||
|                     utils.get_input_channel(entity), max_id=max_id)) |                 utils.get_input_channel(entity), max_id=message)) | ||||||
|         else: |         else: | ||||||
|             return await self(functions.messages.ReadHistoryRequest( |             return await self(functions.messages.ReadHistoryRequest( | ||||||
|                     entity, max_id=max_id)) |                 entity, max_id=message)) | ||||||
| 
 |  | ||||||
|         return False |  | ||||||
| 
 | 
 | ||||||
|     async def pin_message( |     async def pin_message( | ||||||
|             self: 'TelegramClient', |             self: 'TelegramClient', | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ from .. import version, helpers, __name__ as __base_name__ | ||||||
| from ..crypto import rsa | from ..crypto import rsa | ||||||
| from ..entitycache import EntityCache | from ..entitycache import EntityCache | ||||||
| from ..extensions import markdown | from ..extensions import markdown | ||||||
| from ..network import MTProtoSender, Connection, ConnectionTcpFull, TcpMTProxy | from ..network import MTProtoSender, AsyncioConnection, BaseCodec, FullCodec | ||||||
| from ..sessions import Session, SQLiteSession, MemorySession | from ..sessions import Session, SQLiteSession, MemorySession | ||||||
| from ..statecache import StateCache | from ..statecache import StateCache | ||||||
| from ..tl import TLObject, functions, types | from ..tl import TLObject, functions, types | ||||||
|  | @ -67,12 +67,10 @@ class TelegramBaseClient(abc.ABC): | ||||||
|             By default this is ``False`` as IPv6 support is not |             By default this is ``False`` as IPv6 support is not | ||||||
|             too widespread yet. |             too widespread yet. | ||||||
| 
 | 
 | ||||||
|         proxy (`tuple` | `list` | `dict`, optional): |         proxy (`dict`, optional): | ||||||
|             An iterable consisting of the proxy info. If `connection` is |             A dictionary with information about the proxy to connect to. | ||||||
|             one of `MTProxy`, then it should contain MTProxy credentials: | 
 | ||||||
|             ``('hostname', port, 'secret')``. Otherwise, it's meant to store |             See :ref:`signing-in` for details. | ||||||
|             function parameters for PySocks, like ``(type, 'hostname', port)``. |  | ||||||
|             See https://github.com/Anorov/PySocks#usage-1 for more. |  | ||||||
| 
 | 
 | ||||||
|         timeout (`int` | `float`, optional): |         timeout (`int` | `float`, optional): | ||||||
|             The timeout in seconds to be used when connecting. |             The timeout in seconds to be used when connecting. | ||||||
|  | @ -169,9 +167,9 @@ class TelegramBaseClient(abc.ABC): | ||||||
|             api_id: int, |             api_id: int, | ||||||
|             api_hash: str, |             api_hash: str, | ||||||
|             *, |             *, | ||||||
|             connection: 'typing.Type[Connection]' = ConnectionTcpFull, |             connection: 'typing.Type[BaseCodec]' = FullCodec,  # TODO rename | ||||||
|             use_ipv6: bool = False, |             use_ipv6: bool = False, | ||||||
|             proxy: typing.Union[tuple, dict] = None, |             proxy: typing.Union[str, dict] = None, | ||||||
|             timeout: int = 10, |             timeout: int = 10, | ||||||
|             request_retries: int = 5, |             request_retries: int = 5, | ||||||
|             connection_retries: int =5, |             connection_retries: int =5, | ||||||
|  | @ -246,10 +244,10 @@ class TelegramBaseClient(abc.ABC): | ||||||
|         # them to disk, and to save additional useful information. |         # them to disk, and to save additional useful information. | ||||||
|         # TODO Session should probably return all cached |         # TODO Session should probably return all cached | ||||||
|         #      info of entities, not just the input versions |         #      info of entities, not just the input versions | ||||||
|         self.session = session |         self._session = session | ||||||
|         self._entity_cache = EntityCache() |         self._entity_cache = EntityCache() | ||||||
|         self.api_id = int(api_id) |         self._api_id = int(api_id) | ||||||
|         self.api_hash = api_hash |         self._api_hash = api_hash | ||||||
| 
 | 
 | ||||||
|         self._request_retries = request_retries |         self._request_retries = request_retries | ||||||
|         self._connection_retries = connection_retries |         self._connection_retries = connection_retries | ||||||
|  | @ -259,16 +257,17 @@ class TelegramBaseClient(abc.ABC): | ||||||
|         self._auto_reconnect = auto_reconnect |         self._auto_reconnect = auto_reconnect | ||||||
| 
 | 
 | ||||||
|         assert isinstance(connection, type) |         assert isinstance(connection, type) | ||||||
|         self._connection = connection |         self._codec = connection | ||||||
|         init_proxy = None if not issubclass(connection, TcpMTProxy) else \ | 
 | ||||||
|             types.InputClientProxy(*connection.address_info(proxy)) |         # TODO set types.InputClientProxy if appropriated | ||||||
|  |         init_proxy = None | ||||||
| 
 | 
 | ||||||
|         # Used on connection. Capture the variables in a lambda since |         # Used on connection. Capture the variables in a lambda since | ||||||
|         # exporting clients need to create this InvokeWithLayerRequest. |         # exporting clients need to create this InvokeWithLayerRequest. | ||||||
|         system = platform.uname() |         system = platform.uname() | ||||||
|         self._init_with = lambda x: functions.InvokeWithLayerRequest( |         self._init_with = lambda x: functions.InvokeWithLayerRequest( | ||||||
|             LAYER, functions.InitConnectionRequest( |             LAYER, functions.InitConnectionRequest( | ||||||
|                 api_id=self.api_id, |                 api_id=self._api_id, | ||||||
|                 device_model=device_model or system.system or 'Unknown', |                 device_model=device_model or system.system or 'Unknown', | ||||||
|                 system_version=system_version or system.release or '1.0', |                 system_version=system_version or system.release or '1.0', | ||||||
|                 app_version=app_version or self.__version__, |                 app_version=app_version or self.__version__, | ||||||
|  | @ -281,7 +280,7 @@ class TelegramBaseClient(abc.ABC): | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         self._sender = MTProtoSender( |         self._sender = MTProtoSender( | ||||||
|             self.session.auth_key, self._loop, |             self._session.auth_key, self._loop, | ||||||
|             loggers=self._log, |             loggers=self._log, | ||||||
|             retries=self._connection_retries, |             retries=self._connection_retries, | ||||||
|             delay=self._retry_delay, |             delay=self._retry_delay, | ||||||
|  | @ -319,7 +318,7 @@ class TelegramBaseClient(abc.ABC): | ||||||
|         # Update state (for catching up after a disconnection) |         # Update state (for catching up after a disconnection) | ||||||
|         # TODO Get state from channels too |         # TODO Get state from channels too | ||||||
|         self._state_cache = StateCache( |         self._state_cache = StateCache( | ||||||
|             self.session.get_update_state(0), self._log) |             self._session.get_update_state(0), self._log) | ||||||
| 
 | 
 | ||||||
|         # Some further state for subclasses |         # Some further state for subclasses | ||||||
|         self._event_builders = [] |         self._event_builders = [] | ||||||
|  | @ -363,6 +362,18 @@ class TelegramBaseClient(abc.ABC): | ||||||
|         """ |         """ | ||||||
|         return self._loop |         return self._loop | ||||||
| 
 | 
 | ||||||
|  |     @property | ||||||
|  |     def session(self) -> Session: | ||||||
|  |         """ | ||||||
|  |         The ``Session`` instance used by the client. | ||||||
|  | 
 | ||||||
|  |         Example | ||||||
|  |             .. code-block:: python | ||||||
|  | 
 | ||||||
|  |                 client.session.set_dc(dc_id, ip, port) | ||||||
|  |         """ | ||||||
|  |         return self._session | ||||||
|  | 
 | ||||||
|     @property |     @property | ||||||
|     def disconnected(self: 'TelegramClient') -> asyncio.Future: |     def disconnected(self: 'TelegramClient') -> asyncio.Future: | ||||||
|         """ |         """ | ||||||
|  | @ -405,36 +416,36 @@ class TelegramBaseClient(abc.ABC): | ||||||
|                 except OSError: |                 except OSError: | ||||||
|                     print('Failed to connect') |                     print('Failed to connect') | ||||||
|         """ |         """ | ||||||
|         await self._sender.connect(self._connection( |         await self._sender.connect(AsyncioConnection( | ||||||
|             self.session.server_address, |             self._session.server_address, | ||||||
|             self.session.port, |             self._session.port, | ||||||
|             self.session.dc_id, |             self._session.dc_id, | ||||||
|  |             codec=self._codec(), | ||||||
|             loop=self._loop, |             loop=self._loop, | ||||||
|             loggers=self._log, |             loggers=self._log, | ||||||
|             proxy=self._proxy |             proxy=self._proxy | ||||||
|         )) |         )) | ||||||
|         self.session.auth_key = self._sender.auth_key |         self._session.auth_key = self._sender.auth_key | ||||||
|         self.session.save() |         self._session.save() | ||||||
| 
 | 
 | ||||||
|         await self._sender.send(self._init_with( |         await self._sender.send(self._init_with( | ||||||
|             functions.help.GetConfigRequest())) |             functions.help.GetConfigRequest())) | ||||||
| 
 | 
 | ||||||
|         self._updates_handle = self._loop.create_task(self._update_loop()) |         self._updates_handle = self._loop.create_task(self._update_loop()) | ||||||
| 
 | 
 | ||||||
|     def is_connected(self: 'TelegramClient') -> bool: |     @property | ||||||
|  |     def connected(self: 'TelegramClient') -> bool: | ||||||
|         """ |         """ | ||||||
|         Returns ``True`` if the user has connected. |         Property which is ``True`` if the user has connected. | ||||||
| 
 |  | ||||||
|         This method is **not** asynchronous (don't use ``await`` on it). |  | ||||||
| 
 | 
 | ||||||
|         Example |         Example | ||||||
|             .. code-block:: python |             .. code-block:: python | ||||||
| 
 | 
 | ||||||
|                 while client.is_connected(): |                 while client.connected: | ||||||
|                     await asyncio.sleep(1) |                     await asyncio.sleep(1) | ||||||
|         """ |         """ | ||||||
|         sender = getattr(self, '_sender', None) |         sender = getattr(self, '_sender', None) | ||||||
|         return sender and sender.is_connected() |         return sender and sender.connected | ||||||
| 
 | 
 | ||||||
|     def disconnect(self: 'TelegramClient'): |     def disconnect(self: 'TelegramClient'): | ||||||
|         """ |         """ | ||||||
|  | @ -477,7 +488,7 @@ class TelegramBaseClient(abc.ABC): | ||||||
| 
 | 
 | ||||||
|         pts, date = self._state_cache[None] |         pts, date = self._state_cache[None] | ||||||
|         if pts and date: |         if pts and date: | ||||||
|             self.session.set_update_state(0, types.updates.State( |             self._session.set_update_state(0, types.updates.State( | ||||||
|                 pts=pts, |                 pts=pts, | ||||||
|                 qts=0, |                 qts=0, | ||||||
|                 date=date, |                 date=date, | ||||||
|  | @ -485,7 +496,7 @@ class TelegramBaseClient(abc.ABC): | ||||||
|                 unread_count=0 |                 unread_count=0 | ||||||
|             )) |             )) | ||||||
| 
 | 
 | ||||||
|         self.session.close() |         self._session.close() | ||||||
| 
 | 
 | ||||||
|     async def _disconnect(self: 'TelegramClient'): |     async def _disconnect(self: 'TelegramClient'): | ||||||
|         """ |         """ | ||||||
|  | @ -505,12 +516,12 @@ class TelegramBaseClient(abc.ABC): | ||||||
|         self._log[__name__].info('Reconnecting to new data center %s', new_dc) |         self._log[__name__].info('Reconnecting to new data center %s', new_dc) | ||||||
|         dc = await self._get_dc(new_dc) |         dc = await self._get_dc(new_dc) | ||||||
| 
 | 
 | ||||||
|         self.session.set_dc(dc.id, dc.ip_address, dc.port) |         self._session.set_dc(dc.id, dc.ip_address, dc.port) | ||||||
|         # auth_key's are associated with a server, which has now changed |         # 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. |         # so it's not valid anymore. Set to None to force recreating it. | ||||||
|         self._sender.auth_key.key = None |         self._sender.auth_key.key = None | ||||||
|         self.session.auth_key = None |         self._session.auth_key = None | ||||||
|         self.session.save() |         self._session.save() | ||||||
|         await self._disconnect() |         await self._disconnect() | ||||||
|         return await self.connect() |         return await self.connect() | ||||||
| 
 | 
 | ||||||
|  | @ -519,8 +530,8 @@ class TelegramBaseClient(abc.ABC): | ||||||
|         Callback from the sender whenever it needed to generate a |         Callback from the sender whenever it needed to generate a | ||||||
|         new authorization key. This means we are not authorized. |         new authorization key. This means we are not authorized. | ||||||
|         """ |         """ | ||||||
|         self.session.auth_key = auth_key |         self._session.auth_key = auth_key | ||||||
|         self.session.save() |         self._session.save() | ||||||
| 
 | 
 | ||||||
|     # endregion |     # endregion | ||||||
| 
 | 
 | ||||||
|  | @ -623,13 +634,13 @@ class TelegramBaseClient(abc.ABC): | ||||||
|         session = self._exported_sessions.get(cdn_redirect.dc_id) |         session = self._exported_sessions.get(cdn_redirect.dc_id) | ||||||
|         if not session: |         if not session: | ||||||
|             dc = await self._get_dc(cdn_redirect.dc_id, cdn=True) |             dc = await self._get_dc(cdn_redirect.dc_id, cdn=True) | ||||||
|             session = self.session.clone() |             session = self._session.clone() | ||||||
|             await session.set_dc(dc.id, dc.ip_address, dc.port) |             await session.set_dc(dc.id, dc.ip_address, dc.port) | ||||||
|             self._exported_sessions[cdn_redirect.dc_id] = session |             self._exported_sessions[cdn_redirect.dc_id] = session | ||||||
| 
 | 
 | ||||||
|         self._log[__name__].info('Creating new CDN client') |         self._log[__name__].info('Creating new CDN client') | ||||||
|         client = TelegramBareClient( |         client = TelegramBareClient( | ||||||
|             session, self.api_id, self.api_hash, |             session, self._api_id, self._api_hash, | ||||||
|             proxy=self._sender.connection.conn.proxy, |             proxy=self._sender.connection.conn.proxy, | ||||||
|             timeout=self._sender.connection.get_timeout() |             timeout=self._sender.connection.get_timeout() | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  | @ -225,7 +225,7 @@ class UpdateMethods(UserMethods): | ||||||
|         if not pts: |         if not pts: | ||||||
|             return |             return | ||||||
| 
 | 
 | ||||||
|         self.session.catching_up = True |         self._session.catching_up = True | ||||||
|         try: |         try: | ||||||
|             while True: |             while True: | ||||||
|                 d = await self(functions.updates.GetDifferenceRequest( |                 d = await self(functions.updates.GetDifferenceRequest( | ||||||
|  | @ -275,7 +275,7 @@ class UpdateMethods(UserMethods): | ||||||
|         finally: |         finally: | ||||||
|             # TODO Save new pts to session |             # TODO Save new pts to session | ||||||
|             self._state_cache._pts_date = (pts, date) |             self._state_cache._pts_date = (pts, date) | ||||||
|             self.session.catching_up = False |             self._session.catching_up = False | ||||||
| 
 | 
 | ||||||
|     # endregion |     # endregion | ||||||
| 
 | 
 | ||||||
|  | @ -285,7 +285,7 @@ class UpdateMethods(UserMethods): | ||||||
|     # the order that the updates arrive in to update the pts and date to |     # the order that the updates arrive in to update the pts and date to | ||||||
|     # be always-increasing. There is also no need to make this async. |     # be always-increasing. There is also no need to make this async. | ||||||
|     def _handle_update(self: 'TelegramClient', update): |     def _handle_update(self: 'TelegramClient', update): | ||||||
|         self.session.process_entities(update) |         self._session.process_entities(update) | ||||||
|         self._entity_cache.add(update) |         self._entity_cache.add(update) | ||||||
| 
 | 
 | ||||||
|         if isinstance(update, (types.Updates, types.UpdatesCombined)): |         if isinstance(update, (types.Updates, types.UpdatesCombined)): | ||||||
|  | @ -323,7 +323,7 @@ class UpdateMethods(UserMethods): | ||||||
|     async def _update_loop(self: 'TelegramClient'): |     async def _update_loop(self: 'TelegramClient'): | ||||||
|         # Pings' ID don't really need to be secure, just "random" |         # Pings' ID don't really need to be secure, just "random" | ||||||
|         rnd = lambda: random.randrange(-2**63, 2**63) |         rnd = lambda: random.randrange(-2**63, 2**63) | ||||||
|         while self.is_connected(): |         while self.connected: | ||||||
|             try: |             try: | ||||||
|                 await asyncio.wait_for( |                 await asyncio.wait_for( | ||||||
|                     self.disconnected, timeout=60, loop=self._loop |                     self.disconnected, timeout=60, loop=self._loop | ||||||
|  | @ -347,7 +347,7 @@ class UpdateMethods(UserMethods): | ||||||
|             # inserted because this is a rather expensive operation |             # inserted because this is a rather expensive operation | ||||||
|             # (default's sqlite3 takes ~0.1s to commit changes). Do |             # (default's sqlite3 takes ~0.1s to commit changes). Do | ||||||
|             # it every minute instead. No-op if there's nothing new. |             # it every minute instead. No-op if there's nothing new. | ||||||
|             self.session.save() |             self._session.save() | ||||||
| 
 | 
 | ||||||
|             # We need to send some content-related request at least hourly |             # We need to send some content-related request at least hourly | ||||||
|             # for Telegram to keep delivering updates, otherwise they will |             # for Telegram to keep delivering updates, otherwise they will | ||||||
|  | @ -425,7 +425,7 @@ class UpdateMethods(UserMethods): | ||||||
|                 ) |                 ) | ||||||
|                 break |                 break | ||||||
|             except Exception as e: |             except Exception as e: | ||||||
|                 if not isinstance(e, asyncio.CancelledError) or self.is_connected(): |                 if not isinstance(e, asyncio.CancelledError) or self.connected: | ||||||
|                     name = getattr(callback, '__name__', repr(callback)) |                     name = getattr(callback, '__name__', repr(callback)) | ||||||
|                     self._log[__name__].exception('Unhandled exception on %s', |                     self._log[__name__].exception('Unhandled exception on %s', | ||||||
|                                                   name) |                                                   name) | ||||||
|  |  | ||||||
|  | @ -24,18 +24,6 @@ if typing.TYPE_CHECKING: | ||||||
|     from .telegramclient import TelegramClient |     from .telegramclient import TelegramClient | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class _CacheType: |  | ||||||
|     """Like functools.partial but pretends to be the wrapped class.""" |  | ||||||
|     def __init__(self, cls): |  | ||||||
|         self._cls = cls |  | ||||||
| 
 |  | ||||||
|     def __call__(self, *args, **kwargs): |  | ||||||
|         return self._cls(*args, file_reference=b'', **kwargs) |  | ||||||
| 
 |  | ||||||
|     def __eq__(self, other): |  | ||||||
|         return self._cls == other |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def _resize_photo_if_needed( | def _resize_photo_if_needed( | ||||||
|         file, is_image, width=1280, height=1280, background=(255, 255, 255)): |         file, is_image, width=1280, height=1280, background=(255, 255, 255)): | ||||||
| 
 | 
 | ||||||
|  | @ -99,7 +87,6 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | ||||||
|             reply_to: 'hints.MessageIDLike' = None, |             reply_to: 'hints.MessageIDLike' = None, | ||||||
|             attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, |             attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, | ||||||
|             thumb: 'hints.FileLike' = None, |             thumb: 'hints.FileLike' = None, | ||||||
|             allow_cache: bool = True, |  | ||||||
|             parse_mode: str = (), |             parse_mode: str = (), | ||||||
|             voice_note: bool = False, |             voice_note: bool = False, | ||||||
|             video_note: bool = False, |             video_note: bool = False, | ||||||
|  | @ -156,8 +143,10 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | ||||||
|                 To send an album, you should provide a list in this parameter. |                 To send an album, you should provide a list in this parameter. | ||||||
| 
 | 
 | ||||||
|                 If a list or similar is provided, the files in it will be |                 If a list or similar is provided, the files in it will be | ||||||
|                 sent as an album in the order in which they appear, sliced |                 sent as an album in the order in which they appear. Currently, | ||||||
|                 in chunks of 10 if more than 10 are given. |                 only up to 10 files are allowed, and you're responsible for | ||||||
|  |                 making sure that they are all allowed inside albums (e.g. | ||||||
|  |                 only photos or only videos, no other documents in between). | ||||||
| 
 | 
 | ||||||
|             caption (`str`, optional): |             caption (`str`, optional): | ||||||
|                 Optional caption for the sent media message. When sending an |                 Optional caption for the sent media message. When sending an | ||||||
|  | @ -188,12 +177,6 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | ||||||
|                 Successful thumbnails were files below 20kb and 200x200px. |                 Successful thumbnails were files below 20kb and 200x200px. | ||||||
|                 Width/height and dimensions/size ratios may be important. |                 Width/height and dimensions/size ratios may be important. | ||||||
| 
 | 
 | ||||||
|             allow_cache (`bool`, optional): |  | ||||||
|                 Whether to allow using the cached version stored in the |  | ||||||
|                 database or not. Defaults to ``True`` to avoid re-uploads. |  | ||||||
|                 Must be ``False`` if you wish to use different attributes |  | ||||||
|                 or thumb than those that were used when the file was cached. |  | ||||||
| 
 |  | ||||||
|             parse_mode (`object`, optional): |             parse_mode (`object`, optional): | ||||||
|                 See the `TelegramClient.parse_mode |                 See the `TelegramClient.parse_mode | ||||||
|                 <telethon.client.messageparse.MessageParseMethods.parse_mode>` |                 <telethon.client.messageparse.MessageParseMethods.parse_mode>` | ||||||
|  | @ -203,16 +186,10 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | ||||||
|             voice_note (`bool`, optional): |             voice_note (`bool`, optional): | ||||||
|                 If ``True`` the audio will be sent as a voice note. |                 If ``True`` the audio will be sent as a voice note. | ||||||
| 
 | 
 | ||||||
|                 Set `allow_cache` to ``False`` if you sent the same file |  | ||||||
|                 without this setting before for it to work. |  | ||||||
| 
 |  | ||||||
|             video_note (`bool`, optional): |             video_note (`bool`, optional): | ||||||
|                 If ``True`` the video will be sent as a video note, |                 If ``True`` the video will be sent as a video note, | ||||||
|                 also known as a round video message. |                 also known as a round video message. | ||||||
| 
 | 
 | ||||||
|                 Set `allow_cache` to ``False`` if you sent the same file |  | ||||||
|                 without this setting before for it to work. |  | ||||||
| 
 |  | ||||||
|             buttons (`list`, `custom.Button <telethon.tl.custom.button.Button>`, :tl:`KeyboardButton`): |             buttons (`list`, `custom.Button <telethon.tl.custom.button.Button>`, :tl:`KeyboardButton`): | ||||||
|                 The matrix (list of lists), row list or button to be shown |                 The matrix (list of lists), row list or button to be shown | ||||||
|                 after sending the message. This parameter will only work if |                 after sending the message. This parameter will only work if | ||||||
|  | @ -267,52 +244,14 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | ||||||
|         if not caption: |         if not caption: | ||||||
|             caption = '' |             caption = '' | ||||||
| 
 | 
 | ||||||
|         # First check if the user passed an iterable, in which case |         # First check if the user passed an iterable -> send as album | ||||||
|         # we may want to send as an album if all are photo files. |  | ||||||
|         if utils.is_list_like(file): |         if utils.is_list_like(file): | ||||||
|             image_captions = [] |  | ||||||
|             document_captions = [] |  | ||||||
|             if utils.is_list_like(caption): |  | ||||||
|                 captions = caption |  | ||||||
|             else: |  | ||||||
|                 captions = [caption] |  | ||||||
| 
 |  | ||||||
|             # TODO Fix progress_callback |             # TODO Fix progress_callback | ||||||
|             images = [] |             return await self._send_album( | ||||||
|             if force_document: |                 entity, file, caption=file, | ||||||
|                 documents = file |  | ||||||
|             else: |  | ||||||
|                 documents = [] |  | ||||||
|                 for doc, cap in itertools.zip_longest(file, captions): |  | ||||||
|                     if utils.is_image(doc): |  | ||||||
|                         images.append(doc) |  | ||||||
|                         image_captions.append(cap) |  | ||||||
|                     else: |  | ||||||
|                         documents.append(doc) |  | ||||||
|                         document_captions.append(cap) |  | ||||||
| 
 |  | ||||||
|             result = [] |  | ||||||
|             while images: |  | ||||||
|                 result += await self._send_album( |  | ||||||
|                     entity, images[:10], caption=image_captions[:10], |  | ||||||
|                 progress_callback=progress_callback, reply_to=reply_to, |                 progress_callback=progress_callback, reply_to=reply_to, | ||||||
|                 parse_mode=parse_mode, silent=silent |                 parse_mode=parse_mode, silent=silent | ||||||
|             ) |             ) | ||||||
|                 images = images[10:] |  | ||||||
|                 image_captions = image_captions[10:] |  | ||||||
| 
 |  | ||||||
|             for doc, cap in zip(documents, captions): |  | ||||||
|                 result.append(await self.send_file( |  | ||||||
|                     entity, doc, allow_cache=allow_cache, |  | ||||||
|                     caption=cap, force_document=force_document, |  | ||||||
|                     progress_callback=progress_callback, reply_to=reply_to, |  | ||||||
|                     attributes=attributes, thumb=thumb, voice_note=voice_note, |  | ||||||
|                     video_note=video_note, buttons=buttons, silent=silent, |  | ||||||
|                     supports_streaming=supports_streaming, |  | ||||||
|                     **kwargs |  | ||||||
|                 )) |  | ||||||
| 
 |  | ||||||
|             return result |  | ||||||
| 
 | 
 | ||||||
|         entity = await self.get_input_entity(entity) |         entity = await self.get_input_entity(entity) | ||||||
|         reply_to = utils.get_message_id(reply_to) |         reply_to = utils.get_message_id(reply_to) | ||||||
|  | @ -328,7 +267,7 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | ||||||
|         file_handle, media, image = await self._file_to_media( |         file_handle, media, image = await self._file_to_media( | ||||||
|             file, force_document=force_document, |             file, force_document=force_document, | ||||||
|             progress_callback=progress_callback, |             progress_callback=progress_callback, | ||||||
|             attributes=attributes,  allow_cache=allow_cache, thumb=thumb, |             attributes=attributes, thumb=thumb, | ||||||
|             voice_note=voice_note, video_note=video_note, |             voice_note=voice_note, video_note=video_note, | ||||||
|             supports_streaming=supports_streaming |             supports_streaming=supports_streaming | ||||||
|         ) |         ) | ||||||
|  | @ -343,7 +282,6 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | ||||||
|             entities=msg_entities, reply_markup=markup, silent=silent |             entities=msg_entities, reply_markup=markup, silent=silent | ||||||
|         ) |         ) | ||||||
|         msg = self._get_response_message(request, await self(request), entity) |         msg = self._get_response_message(request, await self(request), entity) | ||||||
|         await self._cache_media(msg, file, file_handle, image=image) |  | ||||||
| 
 | 
 | ||||||
|         return msg |         return msg | ||||||
| 
 | 
 | ||||||
|  | @ -351,15 +289,6 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | ||||||
|                           progress_callback=None, reply_to=None, |                           progress_callback=None, reply_to=None, | ||||||
|                           parse_mode=(), silent=None): |                           parse_mode=(), silent=None): | ||||||
|         """Specialized version of .send_file for albums""" |         """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 |  | ||||||
|         # we need to produce right now to send albums (uploadMedia), and |  | ||||||
|         # cache only makes a difference for documents where the user may |  | ||||||
|         # want the attributes used on them to change. |  | ||||||
|         # |  | ||||||
|         # In theory documents can be sent inside the albums but they appear |  | ||||||
|         # as different messages (not inside the album), and the logic to set |  | ||||||
|         # the attributes/avoid cache is already written in .send_file(). |  | ||||||
|         entity = await self.get_input_entity(entity) |         entity = await self.get_input_entity(entity) | ||||||
|         if not utils.is_list_like(caption): |         if not utils.is_list_like(caption): | ||||||
|             caption = (caption,) |             caption = (caption,) | ||||||
|  | @ -370,7 +299,6 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | ||||||
| 
 | 
 | ||||||
|         reply_to = utils.get_message_id(reply_to) |         reply_to = utils.get_message_id(reply_to) | ||||||
| 
 | 
 | ||||||
|         # Need to upload the media first, but only if they're not cached yet |  | ||||||
|         media = [] |         media = [] | ||||||
|         for file in files: |         for file in files: | ||||||
|             # Albums want :tl:`InputMedia` which, in theory, includes |             # Albums want :tl:`InputMedia` which, in theory, includes | ||||||
|  | @ -382,10 +310,12 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | ||||||
|                 r = await self(functions.messages.UploadMediaRequest( |                 r = await self(functions.messages.UploadMediaRequest( | ||||||
|                     entity, media=fm |                     entity, media=fm | ||||||
|                 )) |                 )) | ||||||
|                 self.session.cache_file( |  | ||||||
|                     fh.md5, fh.size, utils.get_input_photo(r.photo)) |  | ||||||
| 
 |  | ||||||
|                 fm = utils.get_input_media(r.photo) |                 fm = utils.get_input_media(r.photo) | ||||||
|  |             elif isinstance(fm, types.InputMediaUploadedDocument): | ||||||
|  |                 r = await self(functions.messages.UploadMediaRequest( | ||||||
|  |                     entity, media=fm | ||||||
|  |                 )) | ||||||
|  |                 fm = utils.get_input_media(r.document) | ||||||
| 
 | 
 | ||||||
|             if captions: |             if captions: | ||||||
|                 caption, msg_entities = captions.pop() |                 caption, msg_entities = captions.pop() | ||||||
|  | @ -414,13 +344,14 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | ||||||
|         # Sent photo IDs -> messages |         # Sent photo IDs -> messages | ||||||
|         return [messages[m.media.id.id] for m in media] |         return [messages[m.media.id.id] for m in media] | ||||||
| 
 | 
 | ||||||
|  |     # TODO Offer a way to easily save media for later use, to replace old caching system | ||||||
|  | 
 | ||||||
|     async def upload_file( |     async def upload_file( | ||||||
|             self: 'TelegramClient', |             self: 'TelegramClient', | ||||||
|             file: 'hints.FileLike', |             file: 'hints.FileLike', | ||||||
|             *, |             *, | ||||||
|             part_size_kb: float = None, |             part_size_kb: float = None, | ||||||
|             file_name: str = None, |             file_name: str = None, | ||||||
|             use_cache: type = None, |  | ||||||
|             progress_callback: 'hints.ProgressCallback' = None) -> 'types.TypeInputFile': |             progress_callback: 'hints.ProgressCallback' = None) -> 'types.TypeInputFile': | ||||||
|         """ |         """ | ||||||
|         Uploads a file to Telegram's servers, without sending it. |         Uploads a file to Telegram's servers, without sending it. | ||||||
|  | @ -449,13 +380,6 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | ||||||
|                 If not specified, the name will be taken from the ``file`` |                 If not specified, the name will be taken from the ``file`` | ||||||
|                 and if this is not a ``str``, it will be ``"unnamed"``. |                 and if this is not a ``str``, it will be ``"unnamed"``. | ||||||
| 
 | 
 | ||||||
|             use_cache (`type`, optional): |  | ||||||
|                 The type of cache to use (currently either :tl:`InputDocument` |  | ||||||
|                 or :tl:`InputPhoto`). If present and the file is small enough |  | ||||||
|                 to need the MD5, it will be checked against the database, |  | ||||||
|                 and if a match is found, the upload won't be made. Instead, |  | ||||||
|                 an instance of type ``use_cache`` will be returned. |  | ||||||
| 
 |  | ||||||
|             progress_callback (`callable`, optional): |             progress_callback (`callable`, optional): | ||||||
|                 A callback function accepting two parameters: |                 A callback function accepting two parameters: | ||||||
|                 ``(sent bytes, total)``. |                 ``(sent bytes, total)``. | ||||||
|  | @ -537,19 +461,11 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | ||||||
|         hash_md5 = hashlib.md5() |         hash_md5 = hashlib.md5() | ||||||
|         if not is_large: |         if not is_large: | ||||||
|             # Calculate the MD5 hash before anything else. |             # Calculate the MD5 hash before anything else. | ||||||
|             # As this needs to be done always for small files, |             # This needs to be done always for small files. | ||||||
|             # might as well do it before anything else and |  | ||||||
|             # check the cache. |  | ||||||
|             if isinstance(file, str): |             if isinstance(file, str): | ||||||
|                 with open(file, 'rb') as stream: |                 with open(file, 'rb') as stream: | ||||||
|                     file = stream.read() |                     file = stream.read() | ||||||
|             hash_md5.update(file) |             hash_md5.update(file) | ||||||
|             if use_cache: |  | ||||||
|                 cached = self.session.get_file( |  | ||||||
|                     hash_md5.digest(), file_size, cls=_CacheType(use_cache) |  | ||||||
|                 ) |  | ||||||
|                 if cached: |  | ||||||
|                     return cached |  | ||||||
| 
 | 
 | ||||||
|         part_count = (file_size + part_size - 1) // part_size |         part_count = (file_size + part_size - 1) // part_size | ||||||
|         self._log[__name__].info('Uploading file of %d bytes in %d chunks of %d', |         self._log[__name__].info('Uploading file of %d bytes in %d chunks of %d', | ||||||
|  | @ -592,7 +508,7 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | ||||||
|     async def _file_to_media( |     async def _file_to_media( | ||||||
|             self, file, force_document=False, |             self, file, force_document=False, | ||||||
|             progress_callback=None, attributes=None, thumb=None, |             progress_callback=None, attributes=None, thumb=None, | ||||||
|             allow_cache=True, voice_note=False, video_note=False, |             voice_note=False, video_note=False, | ||||||
|             supports_streaming=False, mime_type=None, as_image=None): |             supports_streaming=False, mime_type=None, as_image=None): | ||||||
|         if not file: |         if not file: | ||||||
|             return None, None, None |             return None, None, None | ||||||
|  | @ -626,12 +542,10 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | ||||||
| 
 | 
 | ||||||
|         media = None |         media = None | ||||||
|         file_handle = None |         file_handle = None | ||||||
|         use_cache = types.InputPhoto if as_image else types.InputDocument |  | ||||||
|         if not isinstance(file, str) or os.path.isfile(file): |         if not isinstance(file, str) or os.path.isfile(file): | ||||||
|             file_handle = await self.upload_file( |             file_handle = await self.upload_file( | ||||||
|                 _resize_photo_if_needed(file, as_image), |                 _resize_photo_if_needed(file, as_image), | ||||||
|                 progress_callback=progress_callback, |                 progress_callback=progress_callback | ||||||
|                 use_cache=use_cache if allow_cache else None |  | ||||||
|             ) |             ) | ||||||
|         elif re.match('https?://', file): |         elif re.match('https?://', file): | ||||||
|             if as_image: |             if as_image: | ||||||
|  | @ -652,12 +566,6 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | ||||||
|                 'Failed to convert {} to media. Not an existing file, ' |                 'Failed to convert {} to media. Not an existing file, ' | ||||||
|                 'an HTTP URL or a valid bot-API-like file ID'.format(file) |                 'an HTTP URL or a valid bot-API-like file ID'.format(file) | ||||||
|             ) |             ) | ||||||
|         elif isinstance(file_handle, use_cache): |  | ||||||
|             # File was cached, so an instance of use_cache was returned |  | ||||||
|             if as_image: |  | ||||||
|                 media = types.InputMediaPhoto(file_handle) |  | ||||||
|             else: |  | ||||||
|                 media = types.InputMediaDocument(file_handle) |  | ||||||
|         elif as_image: |         elif as_image: | ||||||
|             media = types.InputMediaUploadedPhoto(file_handle) |             media = types.InputMediaUploadedPhoto(file_handle) | ||||||
|         else: |         else: | ||||||
|  | @ -685,16 +593,4 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | ||||||
|             ) |             ) | ||||||
|         return file_handle, media, as_image |         return file_handle, media, as_image | ||||||
| 
 | 
 | ||||||
|     async def _cache_media(self: 'TelegramClient', msg, file, file_handle, image): |  | ||||||
|         if file and msg and isinstance(file_handle, |  | ||||||
|                                        custom.InputSizedFile): |  | ||||||
|             # There was a response message and we didn't use cached |  | ||||||
|             # version, so cache whatever we just sent to the database. |  | ||||||
|             md5, size = file_handle.md5, file_handle.size |  | ||||||
|             if image: |  | ||||||
|                 to_cache = utils.get_input_photo(msg.media.photo) |  | ||||||
|             else: |  | ||||||
|                 to_cache = utils.get_input_document(msg.media.document) |  | ||||||
|             self.session.cache_file(md5, size, to_cache) |  | ||||||
| 
 |  | ||||||
|     # endregion |     # endregion | ||||||
|  |  | ||||||
|  | @ -52,7 +52,7 @@ class UserMethods(TelegramBaseClient): | ||||||
|                             exceptions.append(e) |                             exceptions.append(e) | ||||||
|                             results.append(None) |                             results.append(None) | ||||||
|                             continue |                             continue | ||||||
|                         self.session.process_entities(result) |                         self._session.process_entities(result) | ||||||
|                         self._entity_cache.add(result) |                         self._entity_cache.add(result) | ||||||
|                         exceptions.append(None) |                         exceptions.append(None) | ||||||
|                         results.append(result) |                         results.append(result) | ||||||
|  | @ -63,7 +63,7 @@ class UserMethods(TelegramBaseClient): | ||||||
|                         return results |                         return results | ||||||
|                 else: |                 else: | ||||||
|                     result = await future |                     result = await future | ||||||
|                     self.session.process_entities(result) |                     self._session.process_entities(result) | ||||||
|                     self._entity_cache.add(result) |                     self._entity_cache.add(result) | ||||||
|                     return result |                     return result | ||||||
|             except (errors.ServerError, errors.RpcCallFailError, |             except (errors.ServerError, errors.RpcCallFailError, | ||||||
|  | @ -377,7 +377,7 @@ class UserMethods(TelegramBaseClient): | ||||||
| 
 | 
 | ||||||
|         # No InputPeer, cached peer, or known string. Fetch from disk cache |         # No InputPeer, cached peer, or known string. Fetch from disk cache | ||||||
|         try: |         try: | ||||||
|             return self.session.get_input_entity(peer) |             return self._session.get_input_entity(peer) | ||||||
|         except ValueError: |         except ValueError: | ||||||
|             pass |             pass | ||||||
| 
 | 
 | ||||||
|  | @ -513,7 +513,7 @@ class UserMethods(TelegramBaseClient): | ||||||
|             try: |             try: | ||||||
|                 # Nobody with this username, maybe it's an exact name/title |                 # Nobody with this username, maybe it's an exact name/title | ||||||
|                 return await self.get_entity( |                 return await self.get_entity( | ||||||
|                     self.session.get_input_entity(string)) |                     self._session.get_input_entity(string)) | ||||||
|             except ValueError: |             except ValueError: | ||||||
|                 pass |                 pass | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,10 +5,5 @@ with Telegram's servers and the protocol used (TCP full, abridged, etc.). | ||||||
| from .mtprotoplainsender import MTProtoPlainSender | from .mtprotoplainsender import MTProtoPlainSender | ||||||
| from .authenticator import do_authentication | from .authenticator import do_authentication | ||||||
| from .mtprotosender import MTProtoSender | from .mtprotosender import MTProtoSender | ||||||
| from .connection import ( | from .codec import BaseCodec, FullCodec, IntermediateCodec, AbridgedCodec | ||||||
|     Connection, | from .connection import BaseConnection, AsyncioConnection | ||||||
|     ConnectionTcpFull, ConnectionTcpIntermediate, ConnectionTcpAbridged, |  | ||||||
|     ConnectionTcpObfuscated, ConnectionTcpMTProxyAbridged, |  | ||||||
|     ConnectionTcpMTProxyIntermediate, |  | ||||||
|     ConnectionTcpMTProxyRandomizedIntermediate, ConnectionHttp, TcpMTProxy |  | ||||||
| ) |  | ||||||
|  |  | ||||||
							
								
								
									
										5
									
								
								telethon/network/codec/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								telethon/network/codec/__init__.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | from .basecodec import BaseCodec | ||||||
|  | from .fullcodec import FullCodec | ||||||
|  | from .intermediatecodec import IntermediateCodec | ||||||
|  | from .abridgedcodec import AbridgedCodec | ||||||
|  | from .httpcodec import HttpCodec | ||||||
							
								
								
									
										37
									
								
								telethon/network/codec/abridgedcodec.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								telethon/network/codec/abridgedcodec.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | ||||||
|  | import struct | ||||||
|  | 
 | ||||||
|  | from .basecodec import BaseCodec | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class AbridgedCodec(BaseCodec): | ||||||
|  |     """ | ||||||
|  |     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). | ||||||
|  |     """ | ||||||
|  |     @staticmethod | ||||||
|  |     def header_length(): | ||||||
|  |         return 1 | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def tag(): | ||||||
|  |         return b'\xef'  # note: obfuscated tag is this 4 times | ||||||
|  | 
 | ||||||
|  |     def encode_packet(self, data, ip, port): | ||||||
|  |         length = len(data) >> 2 | ||||||
|  |         if length < 127: | ||||||
|  |             length = struct.pack('B', length) | ||||||
|  |         else: | ||||||
|  |             length = b'\x7f' + int.to_bytes(length, 3, 'little') | ||||||
|  | 
 | ||||||
|  |         return length + data | ||||||
|  | 
 | ||||||
|  |     def decode_header(self, header): | ||||||
|  |         if len(header) == 4: | ||||||
|  |             length = struct.unpack('<i', header[1:] + b'\0')[0] | ||||||
|  |         else: | ||||||
|  |             length = struct.unpack('<B', header)[0] | ||||||
|  |             if length >= 127: | ||||||
|  |                 return -3  # needs 3 more bytes | ||||||
|  | 
 | ||||||
|  |         return length << 2 | ||||||
							
								
								
									
										53
									
								
								telethon/network/codec/basecodec.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								telethon/network/codec/basecodec.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,53 @@ | ||||||
|  | import abc | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class BaseCodec(abc.ABC): | ||||||
|  |     @staticmethod | ||||||
|  |     @abc.abstractmethod | ||||||
|  |     def header_length(): | ||||||
|  |         """ | ||||||
|  |         Returns the initial length of the header. | ||||||
|  |         """ | ||||||
|  |         raise NotImplementedError | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     @abc.abstractmethod | ||||||
|  |     def tag(): | ||||||
|  |         """ | ||||||
|  |         The bytes tag that identifies the codec. | ||||||
|  | 
 | ||||||
|  |         It may be ``None`` if there is no tag to send. | ||||||
|  | 
 | ||||||
|  |         The tag will be sent upon successful connections to the | ||||||
|  |         server so that it knows which codec we will be using next. | ||||||
|  |         """ | ||||||
|  |         raise NotImplementedError | ||||||
|  | 
 | ||||||
|  |     @abc.abstractmethod | ||||||
|  |     def encode_packet(self, data, ip, port): | ||||||
|  |         """ | ||||||
|  |         Encodes the given data with the current codec instance. | ||||||
|  | 
 | ||||||
|  |         Should return header + body. | ||||||
|  |         """ | ||||||
|  |         raise NotImplementedError | ||||||
|  | 
 | ||||||
|  |     @abc.abstractmethod | ||||||
|  |     def decode_header(self, header): | ||||||
|  |         """ | ||||||
|  |         Decodes the header. | ||||||
|  | 
 | ||||||
|  |         Should return the length of the body as a positive number. | ||||||
|  | 
 | ||||||
|  |         If more data is needed, a ``-length`` should be returned, where | ||||||
|  |         ``length`` is how much more data is needed for the full header. | ||||||
|  |         """ | ||||||
|  |         raise NotImplementedError | ||||||
|  | 
 | ||||||
|  |     def decode_body(self, header, body): | ||||||
|  |         """ | ||||||
|  |         Decodes the body. | ||||||
|  | 
 | ||||||
|  |         The default implementation returns ``body``. | ||||||
|  |         """ | ||||||
|  |         return body | ||||||
|  | @ -1,43 +1,45 @@ | ||||||
| import struct | import struct | ||||||
| from zlib import crc32 | import zlib | ||||||
| 
 | 
 | ||||||
| from .connection import Connection, PacketCodec | from .basecodec import BaseCodec | ||||||
| from ...errors import InvalidChecksumError | from ...errors import InvalidChecksumError | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class FullPacketCodec(PacketCodec): | class FullCodec(BaseCodec): | ||||||
|     tag = None |     """ | ||||||
| 
 |     Default Telegram codec. Sends 12 additional bytes and | ||||||
|     def __init__(self, connection): |     needs to calculate the CRC value of the packet itself. | ||||||
|         super().__init__(connection) |     """ | ||||||
|  |     def __init__(self): | ||||||
|         self._send_counter = 0  # Important or Telegram won't reply |         self._send_counter = 0  # Important or Telegram won't reply | ||||||
| 
 | 
 | ||||||
|     def encode_packet(self, data): |     @staticmethod | ||||||
|  |     def header_length(): | ||||||
|  |         return 8 | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def tag(): | ||||||
|  |         return None | ||||||
|  | 
 | ||||||
|  |     def encode_packet(self, data, ip, port): | ||||||
|         # https://core.telegram.org/mtproto#tcp-transport |         # https://core.telegram.org/mtproto#tcp-transport | ||||||
|         # total length, sequence number, packet and checksum (CRC32) |         # total length, sequence number, packet and checksum (CRC32) | ||||||
|         length = len(data) + 12 |         length = len(data) + 12 | ||||||
|         data = struct.pack('<ii', length, self._send_counter) + data |         data = struct.pack('<ii', length, self._send_counter) + data | ||||||
|         crc = struct.pack('<I', crc32(data)) |         crc = struct.pack('<I', zlib.crc32(data)) | ||||||
|         self._send_counter += 1 |         self._send_counter += 1 | ||||||
|         return data + crc |         return data + crc | ||||||
| 
 | 
 | ||||||
|     async def read_packet(self, reader): |     def decode_header(self, header): | ||||||
|         packet_len_seq = await reader.readexactly(8)  # 4 and 4 |         length, seq = struct.unpack('<ii', header) | ||||||
|         packet_len, seq = struct.unpack('<ii', packet_len_seq) |         return length - 8 | ||||||
|         body = await reader.readexactly(packet_len - 8) | 
 | ||||||
|  |     def decode_body(self, header, body): | ||||||
|         checksum = struct.unpack('<I', body[-4:])[0] |         checksum = struct.unpack('<I', body[-4:])[0] | ||||||
|         body = body[:-4] |         body = body[:-4] | ||||||
| 
 | 
 | ||||||
|         valid_checksum = crc32(packet_len_seq + body) |         valid_checksum = zlib.crc32(header + body) | ||||||
|         if checksum != valid_checksum: |         if checksum != valid_checksum: | ||||||
|             raise InvalidChecksumError(checksum, valid_checksum) |             raise InvalidChecksumError(checksum, valid_checksum) | ||||||
| 
 | 
 | ||||||
|         return body |         return body | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class ConnectionTcpFull(Connection): |  | ||||||
|     """ |  | ||||||
|     Default Telegram mode. Sends 12 additional bytes and |  | ||||||
|     needs to calculate the CRC value of the packet itself. |  | ||||||
|     """ |  | ||||||
|     packet_codec = FullPacketCodec |  | ||||||
							
								
								
									
										33
									
								
								telethon/network/codec/httpcodec.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								telethon/network/codec/httpcodec.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | ||||||
|  | from .basecodec import BaseCodec | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | SSL_PORT = 443 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class HttpCodec(BaseCodec): | ||||||
|  |     @staticmethod | ||||||
|  |     def header_length(): | ||||||
|  |         return 4 | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def tag(): | ||||||
|  |         return None | ||||||
|  | 
 | ||||||
|  |     def encode_packet(self, data, ip, port): | ||||||
|  |         return ('POST /api HTTP/1.1\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(ip, port, len(data)) | ||||||
|  |                 .encode('ascii') + data) | ||||||
|  | 
 | ||||||
|  |     def decode_header(self, header): | ||||||
|  |         if not header.endswith(b'\r\n\r\n'): | ||||||
|  |             return -1 | ||||||
|  | 
 | ||||||
|  |         header = header.lower() | ||||||
|  |         start = header.index(b'content-length: ') + 16 | ||||||
|  |         print(header) | ||||||
|  |         return int(header[start:header.index(b'\r', start)]) | ||||||
							
								
								
									
										47
									
								
								telethon/network/codec/intermediatecodec.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								telethon/network/codec/intermediatecodec.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,47 @@ | ||||||
|  | import struct | ||||||
|  | import random | ||||||
|  | import os | ||||||
|  | 
 | ||||||
|  | from .basecodec import BaseCodec | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class IntermediateCodec(BaseCodec): | ||||||
|  |     """ | ||||||
|  |     Intermediate mode between `FullCodec` and `AbridgedCodec`. | ||||||
|  |     Always sends 4 extra bytes for the packet length. | ||||||
|  |     """ | ||||||
|  |     @staticmethod | ||||||
|  |     def header_length(): | ||||||
|  |         return 4 | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def tag(): | ||||||
|  |         return b'\xee\xee\xee\xee'  # same as obfuscate tag | ||||||
|  | 
 | ||||||
|  |     def encode_packet(self, data, ip, port): | ||||||
|  |         return struct.pack('<i', len(data)) + data | ||||||
|  | 
 | ||||||
|  |     def decode_header(self, header): | ||||||
|  |         return struct.unpack('<i', header)[0] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class RandomizedIntermediateCodec(IntermediateCodec): | ||||||
|  |     """ | ||||||
|  |     Data packets are aligned to 4 bytes. This codec adds random | ||||||
|  |     bytes of size from 0 to 3 bytes, which are ignored by decoder. | ||||||
|  |     """ | ||||||
|  |     tag = None | ||||||
|  |     obfuscate_tag = b'\xdd\xdd\xdd\xdd' | ||||||
|  | 
 | ||||||
|  |     def encode_packet(self, data, ip, port): | ||||||
|  |         pad_size = random.randint(0, 3) | ||||||
|  |         padding = os.urandom(pad_size) | ||||||
|  |         return super().encode_packet(data + padding) | ||||||
|  | 
 | ||||||
|  |     async def read_packet(self, reader): | ||||||
|  |         raise NotImplementedError(':)') | ||||||
|  |         packet_with_padding = await super().read_packet(reader) | ||||||
|  |         pad_size = len(packet_with_padding) % 4 | ||||||
|  |         if pad_size > 0: | ||||||
|  |             return packet_with_padding[:-pad_size] | ||||||
|  |         return packet_with_padding | ||||||
|  | @ -1,12 +1,2 @@ | ||||||
| from .connection import Connection | from .baseconnection import BaseConnection | ||||||
| from .tcpfull import ConnectionTcpFull | from .asyncioconnection import AsyncioConnection | ||||||
| from .tcpintermediate import ConnectionTcpIntermediate |  | ||||||
| from .tcpabridged import ConnectionTcpAbridged |  | ||||||
| from .tcpobfuscated import ConnectionTcpObfuscated |  | ||||||
| from .tcpmtproxy import ( |  | ||||||
|     TcpMTProxy, |  | ||||||
|     ConnectionTcpMTProxyAbridged, |  | ||||||
|     ConnectionTcpMTProxyIntermediate, |  | ||||||
|     ConnectionTcpMTProxyRandomizedIntermediate |  | ||||||
| ) |  | ||||||
| from .http import ConnectionHttp |  | ||||||
|  |  | ||||||
							
								
								
									
										169
									
								
								telethon/network/connection/asyncioconnection.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								telethon/network/connection/asyncioconnection.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,169 @@ | ||||||
|  | import abc | ||||||
|  | import asyncio | ||||||
|  | import socket | ||||||
|  | import ssl as ssl_mod | ||||||
|  | import sys | ||||||
|  | 
 | ||||||
|  | from ...errors import InvalidChecksumError | ||||||
|  | from ... import helpers | ||||||
|  | from .baseconnection import BaseConnection | ||||||
|  | from ..codec import HttpCodec | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class AsyncioConnection(BaseConnection): | ||||||
|  |     """ | ||||||
|  |     The `AsyncioConnection` class is a wrapper around ``asyncio.open_connection``. | ||||||
|  | 
 | ||||||
|  |     Subclasses will implement different transport modes as atomic operations, | ||||||
|  |     which this class eases doing since the exposed interface simply puts and | ||||||
|  |     gets complete data payloads to and from queues. | ||||||
|  | 
 | ||||||
|  |     The only error that will raise from send and receive methods is | ||||||
|  |     ``ConnectionError``, which will raise when attempting to send if | ||||||
|  |     the client is disconnected (includes remote disconnections). | ||||||
|  |     """ | ||||||
|  |     # this static attribute should be redefined by `Connection` subclasses and | ||||||
|  |     # should be one of `PacketCodec` implementations | ||||||
|  |     packet_codec = None | ||||||
|  | 
 | ||||||
|  |     def __init__(self, ip, port, dc_id, *, loop, codec, loggers, proxy=None): | ||||||
|  |         super().__init__(ip, port, loop=loop, codec=codec) | ||||||
|  |         self._dc_id = dc_id  # only for MTProxy, it's an abstraction leak | ||||||
|  |         self._log = loggers[__name__] | ||||||
|  |         self._proxy = proxy | ||||||
|  |         self._reader = None | ||||||
|  |         self._writer = None | ||||||
|  |         self._connected = False | ||||||
|  |         self._obfuscation = None  # TcpObfuscated and MTProxy | ||||||
|  | 
 | ||||||
|  |     async def _connect(self, timeout=None): | ||||||
|  |         if not self._proxy: | ||||||
|  |             connect_coroutine = asyncio.open_connection( | ||||||
|  |                 self._ip, self._port, loop=self._loop) | ||||||
|  |         else: | ||||||
|  |             import aiosocks | ||||||
|  | 
 | ||||||
|  |             auth = None | ||||||
|  |             proto = self._proxy.get('protocol', 'socks5').lower() | ||||||
|  |             if proto == 'socks5': | ||||||
|  |                 proxy = aiosocks.Socks5Addr(self._proxy['host'], self._proxy['port']) | ||||||
|  |                 if 'username' in self._proxy: | ||||||
|  |                     auth = aiosocks.Socks5Auth(self._proxy['username'], self._proxy['password']) | ||||||
|  | 
 | ||||||
|  |             elif proto == 'socks4': | ||||||
|  |                 proxy = aiosocks.Socks4Addr(self._proxy['host'], self._proxy['port']) | ||||||
|  |                 if 'username' in self._proxy: | ||||||
|  |                     auth = aiosocks.Socks4Auth(self._proxy['username']) | ||||||
|  | 
 | ||||||
|  |             else: | ||||||
|  |                 raise ValueError('Unsupported proxy protocol {}'.format(self._proxy['protocol'])) | ||||||
|  | 
 | ||||||
|  |             connect_coroutine = aiosocks.open_connection( | ||||||
|  |                 proxy=proxy, | ||||||
|  |                 proxy_auth=auth, | ||||||
|  |                 dst=(self._ip, self._port), | ||||||
|  |                 remote_resolve=self._proxy.get('remote_resolve', True), | ||||||
|  |                 loop=self._loop | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |         self._reader, self._writer = await asyncio.wait_for( | ||||||
|  |             connect_coroutine, | ||||||
|  |             loop=self._loop, timeout=timeout | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         self._codec.__init__()  # reset the codec | ||||||
|  |         if self._codec.tag(): | ||||||
|  |             await self._send(self._codec.tag()) | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def connected(self): | ||||||
|  |         return self._connected | ||||||
|  | 
 | ||||||
|  |     async def connect(self, timeout=None): | ||||||
|  |         """ | ||||||
|  |         Establishes a connection with the server. | ||||||
|  |         """ | ||||||
|  |         await self._connect(timeout=timeout) | ||||||
|  |         self._connected = True | ||||||
|  | 
 | ||||||
|  |     async def disconnect(self): | ||||||
|  |         """ | ||||||
|  |         Disconnects from the server, and clears | ||||||
|  |         pending outgoing and incoming messages. | ||||||
|  |         """ | ||||||
|  |         self._connected = False | ||||||
|  | 
 | ||||||
|  |         if self._writer: | ||||||
|  |             self._writer.close() | ||||||
|  |             if sys.version_info >= (3, 7): | ||||||
|  |                 try: | ||||||
|  |                     await self._writer.wait_closed() | ||||||
|  |                 except Exception as e: | ||||||
|  |                     # Seen OSError: No route to host | ||||||
|  |                     # Disconnecting should never raise | ||||||
|  |                     self._log.warning('Unhandled %s on disconnect: %s', type(e), e) | ||||||
|  | 
 | ||||||
|  |     async def _send(self, data): | ||||||
|  |         self._writer.write(data) | ||||||
|  |         await self._writer.drain() | ||||||
|  | 
 | ||||||
|  |     async def _recv(self, length): | ||||||
|  |         return await self._reader.readexactly(length) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Connection(abc.ABC): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ObfuscatedConnection(Connection): | ||||||
|  |     """ | ||||||
|  |     Base class for "obfuscated" connections ("obfuscated2", "mtproto proxy") | ||||||
|  |     """ | ||||||
|  |     """ | ||||||
|  |     This attribute should be redefined by subclasses | ||||||
|  |     """ | ||||||
|  |     obfuscated_io = None | ||||||
|  | 
 | ||||||
|  |     def _init_conn(self): | ||||||
|  |         self._obfuscation = self.obfuscated_io(self) | ||||||
|  |         self._writer.write(self._obfuscation.header) | ||||||
|  | 
 | ||||||
|  |     def _send(self, data): | ||||||
|  |         self._obfuscation.write(self._codec.encode_packet(data)) | ||||||
|  | 
 | ||||||
|  |     async def _recv(self): | ||||||
|  |         return await self._codec.read_packet(self._obfuscation) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PacketCodec(abc.ABC): | ||||||
|  |     """ | ||||||
|  |     Base class for packet codecs | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     """ | ||||||
|  |     This attribute should be re-defined by subclass to define if some | ||||||
|  |     "magic bytes" should be sent to server right after conection is made to | ||||||
|  |     signal which protocol will be used | ||||||
|  |     """ | ||||||
|  |     tag = None | ||||||
|  | 
 | ||||||
|  |     def __init__(self, connection): | ||||||
|  |         """ | ||||||
|  |         Codec is created when connection is just made. | ||||||
|  |         """ | ||||||
|  |         self._conn = connection | ||||||
|  | 
 | ||||||
|  |     @abc.abstractmethod | ||||||
|  |     def encode_packet(self, data): | ||||||
|  |         """ | ||||||
|  |         Encodes single packet and returns encoded bytes. | ||||||
|  |         """ | ||||||
|  |         raise NotImplementedError | ||||||
|  | 
 | ||||||
|  |     @abc.abstractmethod | ||||||
|  |     async def read_packet(self, reader): | ||||||
|  |         """ | ||||||
|  |         Reads single packet from `reader` object that should have | ||||||
|  |         `readexactly(n)` method. | ||||||
|  |         """ | ||||||
|  |         raise NotImplementedError | ||||||
							
								
								
									
										81
									
								
								telethon/network/connection/baseconnection.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								telethon/network/connection/baseconnection.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,81 @@ | ||||||
|  | import abc | ||||||
|  | import asyncio | ||||||
|  | 
 | ||||||
|  | from ..codec import BaseCodec | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class BaseConnection(abc.ABC): | ||||||
|  |     """ | ||||||
|  |     The base connection class. | ||||||
|  | 
 | ||||||
|  |     It offers atomic send and receive methods. | ||||||
|  | 
 | ||||||
|  |     Subclasses are only responsible of sending and receiving data, | ||||||
|  |     since this base class already makes use of the given codec for | ||||||
|  |     correctly adapting the data. | ||||||
|  |     """ | ||||||
|  |     def __init__(self, ip: str, port: int, *, loop: asyncio.AbstractEventLoop, codec: BaseCodec): | ||||||
|  |         self._ip = ip | ||||||
|  |         self._port = port | ||||||
|  |         self._loop = loop | ||||||
|  |         self._codec = codec | ||||||
|  |         self._send_lock = asyncio.Lock(loop=loop) | ||||||
|  |         self._recv_lock = asyncio.Lock(loop=loop) | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     @abc.abstractmethod | ||||||
|  |     def connected(self): | ||||||
|  |         raise NotImplementedError | ||||||
|  | 
 | ||||||
|  |     @abc.abstractmethod | ||||||
|  |     async def connect(self): | ||||||
|  |         raise NotImplementedError | ||||||
|  | 
 | ||||||
|  |     @abc.abstractmethod | ||||||
|  |     async def disconnect(self): | ||||||
|  |         raise NotImplementedError | ||||||
|  | 
 | ||||||
|  |     @abc.abstractmethod | ||||||
|  |     async def _send(self, data): | ||||||
|  |         raise NotImplementedError | ||||||
|  | 
 | ||||||
|  |     @abc.abstractmethod | ||||||
|  |     async def _recv(self, length): | ||||||
|  |         raise NotImplementedError | ||||||
|  | 
 | ||||||
|  |     async def send(self, data): | ||||||
|  |         if not self.connected: | ||||||
|  |             raise ConnectionError('Not connected') | ||||||
|  | 
 | ||||||
|  |         # TODO Handle asyncio.CancelledError, IOError, Exception | ||||||
|  |         data = self._codec.encode_packet(data, self._ip, self._port) | ||||||
|  |         async with self._send_lock: | ||||||
|  |             return await self._send(data) | ||||||
|  | 
 | ||||||
|  |     async def recv(self): | ||||||
|  |         if not self.connected: | ||||||
|  |             raise ConnectionError('Not connected') | ||||||
|  | 
 | ||||||
|  |         # TODO Handle asyncio.CancelledError, asyncio.IncompleteReadError, | ||||||
|  |         #      IOError, InvalidChecksumError, Exception properly | ||||||
|  |         await self._recv_lock.acquire() | ||||||
|  |         try: | ||||||
|  |             header = await self._recv(self._codec.header_length()) | ||||||
|  | 
 | ||||||
|  |             length = self._codec.decode_header(header) | ||||||
|  |             while length < 0: | ||||||
|  |                 header += await self._recv(-length) | ||||||
|  |                 length = self._codec.decode_header(header) | ||||||
|  | 
 | ||||||
|  |             body = await self._recv(length) | ||||||
|  |             return self._codec.decode_body(header, body) | ||||||
|  |         except Exception: | ||||||
|  |             raise ConnectionError | ||||||
|  |         finally: | ||||||
|  |             self._recv_lock.release() | ||||||
|  | 
 | ||||||
|  |     def __str__(self): | ||||||
|  |         return '{}:{}/{}'.format( | ||||||
|  |             self._ip, self._port, | ||||||
|  |             self.__class__.__name__.replace('Connection', '') | ||||||
|  |         ) | ||||||
|  | @ -1,271 +0,0 @@ | ||||||
| import abc |  | ||||||
| import asyncio |  | ||||||
| import socket |  | ||||||
| import ssl as ssl_mod |  | ||||||
| import sys |  | ||||||
| 
 |  | ||||||
| from ...errors import InvalidChecksumError |  | ||||||
| from ... import helpers |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class Connection(abc.ABC): |  | ||||||
|     """ |  | ||||||
|     The `Connection` class is a wrapper around ``asyncio.open_connection``. |  | ||||||
| 
 |  | ||||||
|     Subclasses will implement different transport modes as atomic operations, |  | ||||||
|     which this class eases doing since the exposed interface simply puts and |  | ||||||
|     gets complete data payloads to and from queues. |  | ||||||
| 
 |  | ||||||
|     The only error that will raise from send and receive methods is |  | ||||||
|     ``ConnectionError``, which will raise when attempting to send if |  | ||||||
|     the client is disconnected (includes remote disconnections). |  | ||||||
|     """ |  | ||||||
|     # this static attribute should be redefined by `Connection` subclasses and |  | ||||||
|     # should be one of `PacketCodec` implementations |  | ||||||
|     packet_codec = None |  | ||||||
| 
 |  | ||||||
|     def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None): |  | ||||||
|         self._ip = ip |  | ||||||
|         self._port = port |  | ||||||
|         self._dc_id = dc_id  # only for MTProxy, it's an abstraction leak |  | ||||||
|         self._loop = loop |  | ||||||
|         self._log = loggers[__name__] |  | ||||||
|         self._proxy = proxy |  | ||||||
|         self._reader = None |  | ||||||
|         self._writer = None |  | ||||||
|         self._connected = False |  | ||||||
|         self._send_task = None |  | ||||||
|         self._recv_task = None |  | ||||||
|         self._codec = None |  | ||||||
|         self._obfuscation = None  # TcpObfuscated and MTProxy |  | ||||||
|         self._send_queue = asyncio.Queue(1) |  | ||||||
|         self._recv_queue = asyncio.Queue(1) |  | ||||||
| 
 |  | ||||||
|     async def _connect(self, timeout=None, ssl=None): |  | ||||||
|         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: |  | ||||||
|                 s.settimeout(timeout) |  | ||||||
|                 s = ssl_mod.wrap_socket( |  | ||||||
|                     s, |  | ||||||
|                     do_handshake_on_connect=True, |  | ||||||
|                     ssl_version=ssl_mod.PROTOCOL_SSLv23, |  | ||||||
|                     ciphers='ADH-AES256-SHA' |  | ||||||
|                 ) |  | ||||||
|                 s.setblocking(False) |  | ||||||
| 
 |  | ||||||
|             self._reader, self._writer = \ |  | ||||||
|                 await asyncio.open_connection(sock=s, loop=self._loop) |  | ||||||
| 
 |  | ||||||
|         self._codec = self.packet_codec(self) |  | ||||||
|         self._init_conn() |  | ||||||
|         await self._writer.drain() |  | ||||||
| 
 |  | ||||||
|     async def connect(self, timeout=None, ssl=None): |  | ||||||
|         """ |  | ||||||
|         Establishes a connection with the server. |  | ||||||
|         """ |  | ||||||
|         await self._connect(timeout=timeout, ssl=ssl) |  | ||||||
|         self._connected = True |  | ||||||
| 
 |  | ||||||
|         self._send_task = self._loop.create_task(self._send_loop()) |  | ||||||
|         self._recv_task = self._loop.create_task(self._recv_loop()) |  | ||||||
| 
 |  | ||||||
|     async def disconnect(self): |  | ||||||
|         """ |  | ||||||
|         Disconnects from the server, and clears |  | ||||||
|         pending outgoing and incoming messages. |  | ||||||
|         """ |  | ||||||
|         self._connected = False |  | ||||||
| 
 |  | ||||||
|         await helpers._cancel( |  | ||||||
|             self._log, |  | ||||||
|             send_task=self._send_task, |  | ||||||
|             recv_task=self._recv_task |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|         if self._writer: |  | ||||||
|             self._writer.close() |  | ||||||
|             if sys.version_info >= (3, 7): |  | ||||||
|                 try: |  | ||||||
|                     await self._writer.wait_closed() |  | ||||||
|                 except Exception as e: |  | ||||||
|                     # Seen OSError: No route to host |  | ||||||
|                     # Disconnecting should never raise |  | ||||||
|                     self._log.warning('Unhandled %s on disconnect: %s', type(e), e) |  | ||||||
| 
 |  | ||||||
|     def send(self, data): |  | ||||||
|         """ |  | ||||||
|         Sends a packet of data through this connection mode. |  | ||||||
| 
 |  | ||||||
|         This method returns a coroutine. |  | ||||||
|         """ |  | ||||||
|         if not self._connected: |  | ||||||
|             raise ConnectionError('Not connected') |  | ||||||
| 
 |  | ||||||
|         return self._send_queue.put(data) |  | ||||||
| 
 |  | ||||||
|     async def recv(self): |  | ||||||
|         """ |  | ||||||
|         Receives a packet of data through this connection mode. |  | ||||||
| 
 |  | ||||||
|         This method returns a coroutine. |  | ||||||
|         """ |  | ||||||
|         while self._connected: |  | ||||||
|             result = await self._recv_queue.get() |  | ||||||
|             if result:  # None = sentinel value = keep trying |  | ||||||
|                 return result |  | ||||||
| 
 |  | ||||||
|         raise ConnectionError('Not connected') |  | ||||||
| 
 |  | ||||||
|     async def _send_loop(self): |  | ||||||
|         """ |  | ||||||
|         This loop is constantly popping items off the queue to send them. |  | ||||||
|         """ |  | ||||||
|         try: |  | ||||||
|             while self._connected: |  | ||||||
|                 self._send(await self._send_queue.get()) |  | ||||||
|                 await self._writer.drain() |  | ||||||
|         except asyncio.CancelledError: |  | ||||||
|             pass |  | ||||||
|         except Exception as e: |  | ||||||
|             if isinstance(e, IOError): |  | ||||||
|                 self._log.info('The server closed the connection while sending') |  | ||||||
|             else: |  | ||||||
|                 self._log.exception('Unexpected exception in the send loop') |  | ||||||
| 
 |  | ||||||
|             await self.disconnect() |  | ||||||
| 
 |  | ||||||
|     async def _recv_loop(self): |  | ||||||
|         """ |  | ||||||
|         This loop is constantly putting items on the queue as they're read. |  | ||||||
|         """ |  | ||||||
|         while self._connected: |  | ||||||
|             try: |  | ||||||
|                 data = await self._recv() |  | ||||||
|             except asyncio.CancelledError: |  | ||||||
|                 break |  | ||||||
|             except Exception as e: |  | ||||||
|                 if isinstance(e, (IOError, asyncio.IncompleteReadError)): |  | ||||||
|                     msg = 'The server closed the connection' |  | ||||||
|                     self._log.info(msg) |  | ||||||
|                 elif isinstance(e, InvalidChecksumError): |  | ||||||
|                     msg = 'The server response had an invalid checksum' |  | ||||||
|                     self._log.info(msg) |  | ||||||
|                 else: |  | ||||||
|                     msg = 'Unexpected exception in the receive loop' |  | ||||||
|                     self._log.exception(msg) |  | ||||||
| 
 |  | ||||||
|                 await self.disconnect() |  | ||||||
| 
 |  | ||||||
|                 # Add a sentinel value to unstuck recv |  | ||||||
|                 if self._recv_queue.empty(): |  | ||||||
|                     self._recv_queue.put_nowait(None) |  | ||||||
| 
 |  | ||||||
|                 break |  | ||||||
| 
 |  | ||||||
|             try: |  | ||||||
|                 await self._recv_queue.put(data) |  | ||||||
|             except asyncio.CancelledError: |  | ||||||
|                 break |  | ||||||
| 
 |  | ||||||
|     def _init_conn(self): |  | ||||||
|         """ |  | ||||||
|         This method will be called after `connect` is called. |  | ||||||
|         After this method finishes, the writer will be drained. |  | ||||||
| 
 |  | ||||||
|         Subclasses should make use of this if they need to send |  | ||||||
|         data to Telegram to indicate which connection mode will |  | ||||||
|         be used. |  | ||||||
|         """ |  | ||||||
|         if self._codec.tag: |  | ||||||
|             self._writer.write(self._codec.tag) |  | ||||||
| 
 |  | ||||||
|     def _send(self, data): |  | ||||||
|         self._writer.write(self._codec.encode_packet(data)) |  | ||||||
| 
 |  | ||||||
|     async def _recv(self): |  | ||||||
|         return await self._codec.read_packet(self._reader) |  | ||||||
| 
 |  | ||||||
|     def __str__(self): |  | ||||||
|         return '{}:{}/{}'.format( |  | ||||||
|             self._ip, self._port, |  | ||||||
|             self.__class__.__name__.replace('Connection', '') |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class ObfuscatedConnection(Connection): |  | ||||||
|     """ |  | ||||||
|     Base class for "obfuscated" connections ("obfuscated2", "mtproto proxy") |  | ||||||
|     """ |  | ||||||
|     """ |  | ||||||
|     This attribute should be redefined by subclasses |  | ||||||
|     """ |  | ||||||
|     obfuscated_io = None |  | ||||||
| 
 |  | ||||||
|     def _init_conn(self): |  | ||||||
|         self._obfuscation = self.obfuscated_io(self) |  | ||||||
|         self._writer.write(self._obfuscation.header) |  | ||||||
| 
 |  | ||||||
|     def _send(self, data): |  | ||||||
|         self._obfuscation.write(self._codec.encode_packet(data)) |  | ||||||
| 
 |  | ||||||
|     async def _recv(self): |  | ||||||
|         return await self._codec.read_packet(self._obfuscation) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class PacketCodec(abc.ABC): |  | ||||||
|     """ |  | ||||||
|     Base class for packet codecs |  | ||||||
|     """ |  | ||||||
| 
 |  | ||||||
|     """ |  | ||||||
|     This attribute should be re-defined by subclass to define if some |  | ||||||
|     "magic bytes" should be sent to server right after conection is made to |  | ||||||
|     signal which protocol will be used |  | ||||||
|     """ |  | ||||||
|     tag = None |  | ||||||
| 
 |  | ||||||
|     def __init__(self, connection): |  | ||||||
|         """ |  | ||||||
|         Codec is created when connection is just made. |  | ||||||
|         """ |  | ||||||
|         self._conn = connection |  | ||||||
| 
 |  | ||||||
|     @abc.abstractmethod |  | ||||||
|     def encode_packet(self, data): |  | ||||||
|         """ |  | ||||||
|         Encodes single packet and returns encoded bytes. |  | ||||||
|         """ |  | ||||||
|         raise NotImplementedError |  | ||||||
| 
 |  | ||||||
|     @abc.abstractmethod |  | ||||||
|     async def read_packet(self, reader): |  | ||||||
|         """ |  | ||||||
|         Reads single packet from `reader` object that should have |  | ||||||
|         `readexactly(n)` method. |  | ||||||
|         """ |  | ||||||
|         raise NotImplementedError |  | ||||||
|  | @ -1,39 +0,0 @@ | ||||||
| import asyncio |  | ||||||
| 
 |  | ||||||
| from .connection import Connection, PacketCodec |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| SSL_PORT = 443 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class HttpPacketCodec(PacketCodec): |  | ||||||
|     tag = None |  | ||||||
|     obfuscate_tag = None |  | ||||||
| 
 |  | ||||||
|     def encode_packet(self, data): |  | ||||||
|         return ('POST /api HTTP/1.1\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._conn._ip, self._conn._port, len(data)) |  | ||||||
|                 .encode('ascii') + data) |  | ||||||
| 
 |  | ||||||
|     async def read_packet(self, reader): |  | ||||||
|         while True: |  | ||||||
|             line = await reader.readline() |  | ||||||
|             if not line or line[-1] != b'\n': |  | ||||||
|                 raise asyncio.IncompleteReadError(line, None) |  | ||||||
| 
 |  | ||||||
|             if line.lower().startswith(b'content-length: '): |  | ||||||
|                 await reader.readexactly(2) |  | ||||||
|                 length = int(line[16:-2]) |  | ||||||
|                 return await reader.readexactly(length) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class ConnectionHttp(Connection): |  | ||||||
|     packet_codec = HttpPacketCodec |  | ||||||
| 
 |  | ||||||
|     async def connect(self, timeout=None, ssl=None): |  | ||||||
|         await super().connect(timeout=timeout, ssl=self._port == SSL_PORT) |  | ||||||
|  | @ -1,33 +0,0 @@ | ||||||
| import struct |  | ||||||
| 
 |  | ||||||
| from .connection import Connection, PacketCodec |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class AbridgedPacketCodec(PacketCodec): |  | ||||||
|     tag = b'\xef' |  | ||||||
|     obfuscate_tag = b'\xef\xef\xef\xef' |  | ||||||
| 
 |  | ||||||
|     def encode_packet(self, data): |  | ||||||
|         length = len(data) >> 2 |  | ||||||
|         if length < 127: |  | ||||||
|             length = struct.pack('B', length) |  | ||||||
|         else: |  | ||||||
|             length = b'\x7f' + int.to_bytes(length, 3, 'little') |  | ||||||
|         return length + data |  | ||||||
| 
 |  | ||||||
|     async def read_packet(self, reader): |  | ||||||
|         length = struct.unpack('<B', await reader.readexactly(1))[0] |  | ||||||
|         if length >= 127: |  | ||||||
|             length = struct.unpack( |  | ||||||
|                 '<i', await reader.readexactly(3) + b'\0')[0] |  | ||||||
| 
 |  | ||||||
|         return await reader.readexactly(length << 2) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 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). |  | ||||||
|     """ |  | ||||||
|     packet_codec = AbridgedPacketCodec |  | ||||||
|  | @ -1,46 +0,0 @@ | ||||||
| import struct |  | ||||||
| import random |  | ||||||
| import os |  | ||||||
| 
 |  | ||||||
| from .connection import Connection, PacketCodec |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class IntermediatePacketCodec(PacketCodec): |  | ||||||
|     tag = b'\xee\xee\xee\xee' |  | ||||||
|     obfuscate_tag = tag |  | ||||||
| 
 |  | ||||||
|     def encode_packet(self, data): |  | ||||||
|         return struct.pack('<i', len(data)) + data |  | ||||||
| 
 |  | ||||||
|     async def read_packet(self, reader): |  | ||||||
|         length = struct.unpack('<i', await reader.readexactly(4))[0] |  | ||||||
|         return await reader.readexactly(length) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class RandomizedIntermediatePacketCodec(IntermediatePacketCodec): |  | ||||||
|     """ |  | ||||||
|     Data packets are aligned to 4bytes. This codec adds random bytes of size |  | ||||||
|     from 0 to 3 bytes, which are ignored by decoder. |  | ||||||
|     """ |  | ||||||
|     tag = None |  | ||||||
|     obfuscate_tag = b'\xdd\xdd\xdd\xdd' |  | ||||||
| 
 |  | ||||||
|     def encode_packet(self, data): |  | ||||||
|         pad_size = random.randint(0, 3) |  | ||||||
|         padding = os.urandom(pad_size) |  | ||||||
|         return super().encode_packet(data + padding) |  | ||||||
| 
 |  | ||||||
|     async def read_packet(self, reader): |  | ||||||
|         packet_with_padding = await super().read_packet(reader) |  | ||||||
|         pad_size = len(packet_with_padding) % 4 |  | ||||||
|         if pad_size > 0: |  | ||||||
|             return packet_with_padding[:-pad_size] |  | ||||||
|         return packet_with_padding |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class ConnectionTcpIntermediate(Connection): |  | ||||||
|     """ |  | ||||||
|     Intermediate mode between `ConnectionTcpFull` and `ConnectionTcpAbridged`. |  | ||||||
|     Always sends 4 extra bytes for the packet length. |  | ||||||
|     """ |  | ||||||
|     packet_codec = IntermediatePacketCodec |  | ||||||
|  | @ -126,6 +126,7 @@ class TcpMTProxy(ObfuscatedConnection): | ||||||
| 
 | 
 | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def address_info(proxy_info): |     def address_info(proxy_info): | ||||||
|  |         raise NotImplementedError('New proxy format is not implemented') | ||||||
|         if proxy_info is None: |         if proxy_info is None: | ||||||
|             raise ValueError("No proxy info specified for MTProxy connection") |             raise ValueError("No proxy info specified for MTProxy connection") | ||||||
|         return proxy_info[:2] |         return proxy_info[:2] | ||||||
|  |  | ||||||
|  | @ -122,7 +122,8 @@ class MTProtoSender: | ||||||
|         await self._connect() |         await self._connect() | ||||||
|         self._user_connected = True |         self._user_connected = True | ||||||
| 
 | 
 | ||||||
|     def is_connected(self): |     @property | ||||||
|  |     def connected(self): | ||||||
|         return self._user_connected |         return self._user_connected | ||||||
| 
 | 
 | ||||||
|     async def disconnect(self): |     async def disconnect(self): | ||||||
|  |  | ||||||
|  | @ -144,24 +144,3 @@ class Session(ABC): | ||||||
|         to use a cached username to avoid extra RPC). |         to use a cached username to avoid extra RPC). | ||||||
|         """ |         """ | ||||||
|         raise NotImplementedError |         raise NotImplementedError | ||||||
| 
 |  | ||||||
|     @abstractmethod |  | ||||||
|     def cache_file(self, md5_digest, file_size, instance): |  | ||||||
|         """ |  | ||||||
|         Caches the given file information persistently, so that it |  | ||||||
|         doesn't need to be re-uploaded in case the file is used again. |  | ||||||
| 
 |  | ||||||
|         The ``instance`` will be either an ``InputPhoto`` or ``InputDocument``, |  | ||||||
|         both with an ``.id`` and ``.access_hash`` attributes. |  | ||||||
|         """ |  | ||||||
|         raise NotImplementedError |  | ||||||
| 
 |  | ||||||
|     @abstractmethod |  | ||||||
|     def get_file(self, md5_digest, file_size, cls): |  | ||||||
|         """ |  | ||||||
|         Returns an instance of ``cls`` if the ``md5_digest`` and ``file_size`` |  | ||||||
|         match an existing saved record. The class will either be an |  | ||||||
|         ``InputPhoto`` or ``InputDocument``, both with two parameters |  | ||||||
|         ``id`` and ``access_hash`` in that order. |  | ||||||
|         """ |  | ||||||
|         raise NotImplementedError |  | ||||||
|  |  | ||||||
|  | @ -1,29 +1,12 @@ | ||||||
| from enum import Enum |  | ||||||
| 
 |  | ||||||
| from .abstract import Session | from .abstract import Session | ||||||
| from .. import utils | from .. import utils | ||||||
| from ..tl import TLObject | from ..tl import TLObject | ||||||
| from ..tl.types import ( | from ..tl.types import ( | ||||||
|     PeerUser, PeerChat, PeerChannel, |     PeerUser, PeerChat, PeerChannel, | ||||||
|     InputPeerUser, InputPeerChat, InputPeerChannel, |     InputPeerUser, InputPeerChat, InputPeerChannel | ||||||
|     InputPhoto, InputDocument |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class _SentFileType(Enum): |  | ||||||
|     DOCUMENT = 0 |  | ||||||
|     PHOTO = 1 |  | ||||||
| 
 |  | ||||||
|     @staticmethod |  | ||||||
|     def from_type(cls): |  | ||||||
|         if cls == InputDocument: |  | ||||||
|             return _SentFileType.DOCUMENT |  | ||||||
|         elif cls == InputPhoto: |  | ||||||
|             return _SentFileType.PHOTO |  | ||||||
|         else: |  | ||||||
|             raise ValueError('The cls must be either InputDocument/InputPhoto') |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class MemorySession(Session): | class MemorySession(Session): | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         super().__init__() |         super().__init__() | ||||||
|  | @ -34,7 +17,6 @@ class MemorySession(Session): | ||||||
|         self._auth_key = None |         self._auth_key = None | ||||||
|         self._takeout_id = None |         self._takeout_id = None | ||||||
| 
 | 
 | ||||||
|         self._files = {} |  | ||||||
|         self._entities = set() |         self._entities = set() | ||||||
|         self._update_states = {} |         self._update_states = {} | ||||||
| 
 | 
 | ||||||
|  | @ -228,17 +210,3 @@ class MemorySession(Session): | ||||||
|                 return InputPeerChannel(entity_id, entity_hash) |                 return InputPeerChannel(entity_id, entity_hash) | ||||||
|         else: |         else: | ||||||
|             raise ValueError('Could not find input entity with key ', key) |             raise ValueError('Could not find input entity with key ', key) | ||||||
| 
 |  | ||||||
|     def cache_file(self, md5_digest, file_size, instance): |  | ||||||
|         if not isinstance(instance, (InputDocument, InputPhoto)): |  | ||||||
|             raise TypeError('Cannot cache %s instance' % type(instance)) |  | ||||||
|         key = (md5_digest, file_size, _SentFileType.from_type(type(instance))) |  | ||||||
|         value = (instance.id, instance.access_hash) |  | ||||||
|         self._files[key] = value |  | ||||||
| 
 |  | ||||||
|     def get_file(self, md5_digest, file_size, cls): |  | ||||||
|         key = (md5_digest, file_size, _SentFileType.from_type(cls)) |  | ||||||
|         try: |  | ||||||
|             return cls(*self._files[key]) |  | ||||||
|         except KeyError: |  | ||||||
|             return None |  | ||||||
|  |  | ||||||
|  | @ -2,11 +2,11 @@ import datetime | ||||||
| import os | import os | ||||||
| 
 | 
 | ||||||
| from telethon.tl import types | from telethon.tl import types | ||||||
| from .memory import MemorySession, _SentFileType | from .memory import MemorySession | ||||||
| from .. import utils | from .. import utils | ||||||
| from ..crypto import AuthKey | from ..crypto import AuthKey | ||||||
| from ..tl.types import ( | from ..tl.types import ( | ||||||
|     InputPhoto, InputDocument, PeerUser, PeerChat, PeerChannel |     PeerUser, PeerChat, PeerChannel | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| try: | try: | ||||||
|  | @ -17,7 +17,7 @@ except ImportError as e: | ||||||
|     sqlite3_err = type(e) |     sqlite3_err = type(e) | ||||||
| 
 | 
 | ||||||
| EXTENSION = '.session' | EXTENSION = '.session' | ||||||
| CURRENT_VERSION = 5  # database version | CURRENT_VERSION = 6  # database version | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class SQLiteSession(MemorySession): | class SQLiteSession(MemorySession): | ||||||
|  | @ -87,15 +87,6 @@ class SQLiteSession(MemorySession): | ||||||
|                     name text |                     name text | ||||||
|                 )""" |                 )""" | ||||||
|                 , |                 , | ||||||
|                 """sent_files ( |  | ||||||
|                     md5_digest blob, |  | ||||||
|                     file_size integer, |  | ||||||
|                     type integer, |  | ||||||
|                     id integer, |  | ||||||
|                     hash integer, |  | ||||||
|                     primary key(md5_digest, file_size, type) |  | ||||||
|                 )""" |  | ||||||
|                 , |  | ||||||
|                 """update_state ( |                 """update_state ( | ||||||
|                     id integer primary key, |                     id integer primary key, | ||||||
|                     pts integer, |                     pts integer, | ||||||
|  | @ -143,6 +134,9 @@ class SQLiteSession(MemorySession): | ||||||
|         if old == 4: |         if old == 4: | ||||||
|             old += 1 |             old += 1 | ||||||
|             c.execute("alter table sessions add column takeout_id integer") |             c.execute("alter table sessions add column takeout_id integer") | ||||||
|  |         if old == 5: | ||||||
|  |             old += 1 | ||||||
|  |             c.execute('drop table sent_files') | ||||||
|         c.close() |         c.close() | ||||||
| 
 | 
 | ||||||
|     @staticmethod |     @staticmethod | ||||||
|  | @ -300,26 +294,3 @@ class SQLiteSession(MemorySession): | ||||||
|                 utils.get_peer_id(PeerChat(id)), |                 utils.get_peer_id(PeerChat(id)), | ||||||
|                 utils.get_peer_id(PeerChannel(id)) |                 utils.get_peer_id(PeerChannel(id)) | ||||||
|             ) |             ) | ||||||
| 
 |  | ||||||
|     # File processing |  | ||||||
| 
 |  | ||||||
|     def get_file(self, md5_digest, file_size, cls): |  | ||||||
|         row = self._execute( |  | ||||||
|             'select id, hash from sent_files ' |  | ||||||
|             'where md5_digest = ? and file_size = ? and type = ?', |  | ||||||
|             md5_digest, file_size, _SentFileType.from_type(cls).value |  | ||||||
|         ) |  | ||||||
|         if row: |  | ||||||
|             # Both allowed classes have (id, access_hash) as parameters |  | ||||||
|             return cls(row[0], row[1]) |  | ||||||
| 
 |  | ||||||
|     def cache_file(self, md5_digest, file_size, instance): |  | ||||||
|         if not isinstance(instance, (InputDocument, InputPhoto)): |  | ||||||
|             raise TypeError('Cannot cache %s instance' % type(instance)) |  | ||||||
| 
 |  | ||||||
|         self._execute( |  | ||||||
|             'insert or replace into sent_files values (?,?,?,?,?)', |  | ||||||
|             md5_digest, file_size, |  | ||||||
|             _SentFileType.from_type(type(instance)).value, |  | ||||||
|             instance.id, instance.access_hash |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|  | @ -228,7 +228,7 @@ class App(tkinter.Tk): | ||||||
|         """ |         """ | ||||||
|         Sends a message. Does nothing if the client is not connected. |         Sends a message. Does nothing if the client is not connected. | ||||||
|         """ |         """ | ||||||
|         if not self.cl.is_connected(): |         if not self.cl.connected: | ||||||
|             return |             return | ||||||
| 
 | 
 | ||||||
|         # The user needs to configure a chat where the message should be sent. |         # The user needs to configure a chat where the message should be sent. | ||||||
|  |  | ||||||
|  | @ -116,9 +116,8 @@ class InteractiveTelegramClient(TelegramClient): | ||||||
|         try: |         try: | ||||||
|             loop.run_until_complete(self.connect()) |             loop.run_until_complete(self.connect()) | ||||||
|         except IOError: |         except IOError: | ||||||
|             # We handle IOError and not ConnectionError because |             # To avoid issues in the future, we except the most | ||||||
|             # PySocks' errors do not subclass ConnectionError |             # generic IOError as possible (instead of ConnectionError) | ||||||
|             # (so this will work with and without proxies). |  | ||||||
|             print('Initial connection failed. Retrying...') |             print('Initial connection failed. Retrying...') | ||||||
|             loop.run_until_complete(self.connect()) |             loop.run_until_complete(self.connect()) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -22,7 +22,7 @@ def get_env(name, message, cast=str): | ||||||
| session = os.environ.get('TG_SESSION', 'printer') | session = os.environ.get('TG_SESSION', 'printer') | ||||||
| api_id = get_env('TG_API_ID', 'Enter your API ID: ', int) | api_id = get_env('TG_API_ID', 'Enter your API ID: ', int) | ||||||
| api_hash = get_env('TG_API_HASH', 'Enter your API hash: ') | api_hash = get_env('TG_API_HASH', 'Enter your API hash: ') | ||||||
| proxy = None  # https://github.com/Anorov/PySocks | proxy = None  # https://docs.telethon.dev/en/latest/basic/signing-in.html#signing-in-behind-a-proxy | ||||||
| 
 | 
 | ||||||
| # Create and start the client so we can make requests (we don't here) | # Create and start the client so we can make requests (we don't here) | ||||||
| client = TelegramClient(session, api_id, api_hash, proxy=proxy).start() | client = TelegramClient(session, api_id, api_hash, proxy=proxy).start() | ||||||
|  |  | ||||||
|  | @ -27,7 +27,7 @@ def get_env(name, message, cast=str): | ||||||
| session = os.environ.get('TG_SESSION', 'printer') | session = os.environ.get('TG_SESSION', 'printer') | ||||||
| api_id = get_env('TG_API_ID', 'Enter your API ID: ', int) | api_id = get_env('TG_API_ID', 'Enter your API ID: ', int) | ||||||
| api_hash = get_env('TG_API_HASH', 'Enter your API hash: ') | api_hash = get_env('TG_API_HASH', 'Enter your API hash: ') | ||||||
| proxy = None  # https://github.com/Anorov/PySocks | proxy = None  # https://docs.telethon.dev/en/latest/basic/signing-in.html#signing-in-behind-a-proxy | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # This is our update handler. It is called when a new update arrives. | # This is our update handler. It is called when a new update arrives. | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user