mirror of
				https://github.com/LonamiWebs/Telethon.git
				synced 2025-10-31 07:57:38 +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 | ||||
| pysocks | ||||
| pproxy | ||||
| hachoir3 | ||||
| pillow | ||||
|  |  | |||
|  | @ -107,7 +107,7 @@ Signing In behind a Proxy | |||
| ========================= | ||||
| 
 | ||||
| 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 | ||||
| 
 | ||||
|  | @ -117,15 +117,29 @@ with | |||
| 
 | ||||
| .. 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). | ||||
| 
 | ||||
| The ``proxy=`` argument should be a tuple, a list or a dict, | ||||
| consisting of parameters described `in PySocks usage`__. | ||||
| The ``proxy=`` argument should be a dictionary | ||||
| where the following keys are allowed: | ||||
| 
 | ||||
| .. __: https://github.com/Anorov/PySocks#installation | ||||
| .. __: https://github.com/Anorov/PySocks#usage-1 | ||||
| .. code-block:: python | ||||
| 
 | ||||
|     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 | ||||
|  |  | |||
|  | @ -45,7 +45,7 @@ Base | |||
| 
 | ||||
|     connect | ||||
|     disconnect | ||||
|     is_connected | ||||
|     connected | ||||
|     disconnected | ||||
|     loop | ||||
| 
 | ||||
|  |  | |||
|  | @ -54,7 +54,7 @@ class _TakeoutClient: | |||
|                 self.__success)) | ||||
|             if not result: | ||||
|                 raise ValueError("Failed to finish the takeout.") | ||||
|             self.session.takeout_id = None | ||||
|             self._session.takeout_id = None | ||||
| 
 | ||||
|     __enter__ = helpers._sync_enter | ||||
|     __exit__ = helpers._sync_exit | ||||
|  | @ -211,7 +211,7 @@ class AccountMethods(UserMethods): | |||
|         ) | ||||
|         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_kwargs) | ||||
|         else: | ||||
|  |  | |||
|  | @ -137,7 +137,7 @@ class AuthMethods(MessageParseMethods, UserMethods): | |||
|     async def _start( | ||||
|             self, phone, password, bot_token, force_sms, | ||||
|             code_callback, first_name, last_name, max_attempts): | ||||
|         if not self.is_connected(): | ||||
|         if not self.connected: | ||||
|             await self.connect() | ||||
| 
 | ||||
|         if await self.is_user_authorized(): | ||||
|  | @ -258,29 +258,22 @@ class AuthMethods(MessageParseMethods, UserMethods): | |||
| 
 | ||||
|     async def sign_in( | ||||
|             self: 'TelegramClient', | ||||
|             phone: str = None, | ||||
|             code: typing.Union[str, int] = None, | ||||
|             *, | ||||
|             password: str = None, | ||||
|             bot_token: str = None, | ||||
|             phone: str = None, | ||||
|             phone_code_hash: str = None) -> 'types.User': | ||||
|         """ | ||||
|         Logs in to Telegram to an existing user or bot account. | ||||
| 
 | ||||
|         You should only use this if you are not authorized yet. | ||||
| 
 | ||||
|         This method will send the code if it's not provided. | ||||
| 
 | ||||
|         .. note:: | ||||
| 
 | ||||
|             In most cases, you should simply use `start()` and not this method. | ||||
| 
 | ||||
|         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`): | ||||
|                 The code that Telegram sent. Note that if you have sent this | ||||
|                 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>`_ | ||||
|                 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): | ||||
|                 The hash returned by `send_code_request`. This can be left as | ||||
|                 ``None`` to use the last hash known for the phone to be used. | ||||
|                 By default, the library remembers the hash that | ||||
|                 `send_code_request` returned. If you are passing the | ||||
|                 code for a different phone, you should set this parameter. | ||||
| 
 | ||||
|         Returns | ||||
|             The signed in user, or the information about | ||||
|             :meth:`send_code_request`. | ||||
|             The signed in user. | ||||
| 
 | ||||
|         Example | ||||
|             .. code-block:: python | ||||
| 
 | ||||
|                 phone = '+34 123 123 123' | ||||
|                 client.sign_in(phone)  # send code | ||||
|                 client.send_code_request(phone) | ||||
| 
 | ||||
|                 code = input('enter code: ') | ||||
|                 client.sign_in(phone, code) | ||||
|                 client.sign_in(code) | ||||
|         """ | ||||
|         me = await self.get_me() | ||||
|         if me: | ||||
|             return me | ||||
| 
 | ||||
|         if phone and not code and not password: | ||||
|             return await self.send_code_request(phone) | ||||
|         elif code: | ||||
|         if code: | ||||
|             phone, phone_code_hash = \ | ||||
|                 self._parse_phone_and_hash(phone, phone_code_hash) | ||||
| 
 | ||||
|  | @ -335,13 +331,10 @@ class AuthMethods(MessageParseMethods, UserMethods): | |||
|         elif bot_token: | ||||
|             result = await self(functions.auth.ImportBotAuthorizationRequest( | ||||
|                 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: | ||||
|             raise ValueError( | ||||
|                 'You must provide a phone and a code the first time, ' | ||||
|                 'and a password only if an RPCError was raised before.' | ||||
|             ) | ||||
|             raise ValueError('You must provide a code, password or bot token') | ||||
| 
 | ||||
|         return self._on_login(result.user) | ||||
| 
 | ||||
|  | @ -473,7 +466,7 @@ class AuthMethods(MessageParseMethods, UserMethods): | |||
|         if not phone_hash: | ||||
|             try: | ||||
|                 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: | ||||
|                 return await self.send_code_request(phone, force_sms=force_sms) | ||||
| 
 | ||||
|  | @ -516,7 +509,7 @@ class AuthMethods(MessageParseMethods, UserMethods): | |||
|         self._state_cache.reset() | ||||
| 
 | ||||
|         await self.disconnect() | ||||
|         self.session.delete() | ||||
|         self._session.delete() | ||||
|         return True | ||||
| 
 | ||||
|     async def edit_2fa( | ||||
|  |  | |||
|  | @ -53,7 +53,7 @@ class ButtonMethods(UpdateMethods): | |||
|         if not utils.is_list_like(buttons): | ||||
|             buttons = [[buttons]] | ||||
|         elif not utils.is_list_like(buttons[0]): | ||||
|             buttons = [buttons] | ||||
|             buttons = [[b] for b in buttons] | ||||
| 
 | ||||
|         is_inline = False | ||||
|         is_normal = False | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| import itertools | ||||
| import typing | ||||
| import warnings | ||||
| 
 | ||||
| from .buttons import ButtonMethods | ||||
| from .messageparse import MessageParseMethods | ||||
|  | @ -636,6 +637,14 @@ class MessageMethods(UploadMethods, ButtonMethods, MessageParseMethods): | |||
|                 client.send_message(chat, 'A single button, with "clk1" as data', | ||||
|                                     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 | ||||
|                 client.send_message(chat, 'Pick one from this grid', buttons=[ | ||||
|                     [Button.inline('Left'), Button.inline('Right')], | ||||
|  | @ -1061,65 +1070,71 @@ class MessageMethods(UploadMethods, ButtonMethods, MessageParseMethods): | |||
|             max_id: int = None, | ||||
|             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 | ||||
|         given conversation. | ||||
|         return await self.mark_read(entity, message, clear_mentions=clear_mentions) | ||||
| 
 | ||||
|         If neither message nor maximum ID are provided, all messages will be | ||||
|         marked as read by assuming that ``max_id = 0``. | ||||
|     async def mark_read( | ||||
|             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 | ||||
|             entity (`entity`): | ||||
|                 The chat where these messages are located. | ||||
| 
 | ||||
|             message (`list` | `Message <telethon.tl.custom.message.Message>`): | ||||
|                 Either a list of messages or a single message. | ||||
| 
 | ||||
|             max_id (`int`): | ||||
|                 Overrides messages, until which message should the | ||||
|                 acknowledge should be sent. | ||||
|             message (`int` | `list` | `Message <telethon.tl.custom.message.Message>`): | ||||
|                 Either a list of messages, a single message or an ID. | ||||
|                 The chat will be marked as read up to the highest ID. | ||||
| 
 | ||||
|             clear_mentions (`bool`): | ||||
|                 Whether the mention badge should be cleared (so that | ||||
|                 there are no more mentions) or not for the given entity. | ||||
| 
 | ||||
|                 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 | ||||
|             .. code-block:: python | ||||
| 
 | ||||
|                 client.send_read_acknowledge(last_message) | ||||
|                 client.mark_read(chat) | ||||
|                 # or | ||||
|                 client.send_read_acknowledge(last_message_id) | ||||
|                 client.mark_read(chat, some_message) | ||||
|                 # 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) | ||||
|         if clear_mentions: | ||||
|             await self(functions.messages.ReadMentionsRequest(entity)) | ||||
|             if max_id is None: | ||||
|             if message == (): | ||||
|                 return True | ||||
| 
 | ||||
|         if max_id is not None: | ||||
|             if isinstance(entity, types.InputPeerChannel): | ||||
|                 return await self(functions.channels.ReadHistoryRequest( | ||||
|                     utils.get_input_channel(entity), max_id=max_id)) | ||||
|             else: | ||||
|                 return await self(functions.messages.ReadHistoryRequest( | ||||
|                     entity, max_id=max_id)) | ||||
|         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) | ||||
| 
 | ||||
|         return False | ||||
|         if isinstance(entity, types.InputPeerChannel): | ||||
|             return await self(functions.channels.ReadHistoryRequest( | ||||
|                 utils.get_input_channel(entity), max_id=message)) | ||||
|         else: | ||||
|             return await self(functions.messages.ReadHistoryRequest( | ||||
|                 entity, max_id=message)) | ||||
| 
 | ||||
|     async def pin_message( | ||||
|             self: 'TelegramClient', | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ from .. import version, helpers, __name__ as __base_name__ | |||
| from ..crypto import rsa | ||||
| from ..entitycache import EntityCache | ||||
| 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 ..statecache import StateCache | ||||
| from ..tl import TLObject, functions, types | ||||
|  | @ -67,12 +67,10 @@ class TelegramBaseClient(abc.ABC): | |||
|             By default this is ``False`` as IPv6 support is not | ||||
|             too widespread yet. | ||||
| 
 | ||||
|         proxy (`tuple` | `list` | `dict`, optional): | ||||
|             An iterable consisting of the proxy info. If `connection` is | ||||
|             one of `MTProxy`, then it should contain MTProxy credentials: | ||||
|             ``('hostname', port, 'secret')``. Otherwise, it's meant to store | ||||
|             function parameters for PySocks, like ``(type, 'hostname', port)``. | ||||
|             See https://github.com/Anorov/PySocks#usage-1 for more. | ||||
|         proxy (`dict`, optional): | ||||
|             A dictionary with information about the proxy to connect to. | ||||
| 
 | ||||
|             See :ref:`signing-in` for details. | ||||
| 
 | ||||
|         timeout (`int` | `float`, optional): | ||||
|             The timeout in seconds to be used when connecting. | ||||
|  | @ -169,9 +167,9 @@ class TelegramBaseClient(abc.ABC): | |||
|             api_id: int, | ||||
|             api_hash: str, | ||||
|             *, | ||||
|             connection: 'typing.Type[Connection]' = ConnectionTcpFull, | ||||
|             connection: 'typing.Type[BaseCodec]' = FullCodec,  # TODO rename | ||||
|             use_ipv6: bool = False, | ||||
|             proxy: typing.Union[tuple, dict] = None, | ||||
|             proxy: typing.Union[str, dict] = None, | ||||
|             timeout: int = 10, | ||||
|             request_retries: int = 5, | ||||
|             connection_retries: int =5, | ||||
|  | @ -246,10 +244,10 @@ class TelegramBaseClient(abc.ABC): | |||
|         # them to disk, and to save additional useful information. | ||||
|         # TODO Session should probably return all cached | ||||
|         #      info of entities, not just the input versions | ||||
|         self.session = session | ||||
|         self._session = session | ||||
|         self._entity_cache = EntityCache() | ||||
|         self.api_id = int(api_id) | ||||
|         self.api_hash = api_hash | ||||
|         self._api_id = int(api_id) | ||||
|         self._api_hash = api_hash | ||||
| 
 | ||||
|         self._request_retries = request_retries | ||||
|         self._connection_retries = connection_retries | ||||
|  | @ -259,16 +257,17 @@ class TelegramBaseClient(abc.ABC): | |||
|         self._auto_reconnect = auto_reconnect | ||||
| 
 | ||||
|         assert isinstance(connection, type) | ||||
|         self._connection = connection | ||||
|         init_proxy = None if not issubclass(connection, TcpMTProxy) else \ | ||||
|             types.InputClientProxy(*connection.address_info(proxy)) | ||||
|         self._codec = connection | ||||
| 
 | ||||
|         # TODO set types.InputClientProxy if appropriated | ||||
|         init_proxy = None | ||||
| 
 | ||||
|         # Used on connection. Capture the variables in a lambda since | ||||
|         # exporting clients need to create this InvokeWithLayerRequest. | ||||
|         system = platform.uname() | ||||
|         self._init_with = lambda x: functions.InvokeWithLayerRequest( | ||||
|             LAYER, functions.InitConnectionRequest( | ||||
|                 api_id=self.api_id, | ||||
|                 api_id=self._api_id, | ||||
|                 device_model=device_model or system.system or 'Unknown', | ||||
|                 system_version=system_version or system.release or '1.0', | ||||
|                 app_version=app_version or self.__version__, | ||||
|  | @ -281,7 +280,7 @@ class TelegramBaseClient(abc.ABC): | |||
|         ) | ||||
| 
 | ||||
|         self._sender = MTProtoSender( | ||||
|             self.session.auth_key, self._loop, | ||||
|             self._session.auth_key, self._loop, | ||||
|             loggers=self._log, | ||||
|             retries=self._connection_retries, | ||||
|             delay=self._retry_delay, | ||||
|  | @ -319,7 +318,7 @@ class TelegramBaseClient(abc.ABC): | |||
|         # Update state (for catching up after a disconnection) | ||||
|         # TODO Get state from channels too | ||||
|         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 | ||||
|         self._event_builders = [] | ||||
|  | @ -363,6 +362,18 @@ class TelegramBaseClient(abc.ABC): | |||
|         """ | ||||
|         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 | ||||
|     def disconnected(self: 'TelegramClient') -> asyncio.Future: | ||||
|         """ | ||||
|  | @ -405,36 +416,36 @@ class TelegramBaseClient(abc.ABC): | |||
|                 except OSError: | ||||
|                     print('Failed to connect') | ||||
|         """ | ||||
|         await self._sender.connect(self._connection( | ||||
|             self.session.server_address, | ||||
|             self.session.port, | ||||
|             self.session.dc_id, | ||||
|         await self._sender.connect(AsyncioConnection( | ||||
|             self._session.server_address, | ||||
|             self._session.port, | ||||
|             self._session.dc_id, | ||||
|             codec=self._codec(), | ||||
|             loop=self._loop, | ||||
|             loggers=self._log, | ||||
|             proxy=self._proxy | ||||
|         )) | ||||
|         self.session.auth_key = self._sender.auth_key | ||||
|         self.session.save() | ||||
|         self._session.auth_key = self._sender.auth_key | ||||
|         self._session.save() | ||||
| 
 | ||||
|         await self._sender.send(self._init_with( | ||||
|             functions.help.GetConfigRequest())) | ||||
| 
 | ||||
|         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. | ||||
| 
 | ||||
|         This method is **not** asynchronous (don't use ``await`` on it). | ||||
|         Property which is ``True`` if the user has connected. | ||||
| 
 | ||||
|         Example | ||||
|             .. code-block:: python | ||||
| 
 | ||||
|                 while client.is_connected(): | ||||
|                 while client.connected: | ||||
|                     await asyncio.sleep(1) | ||||
|         """ | ||||
|         sender = getattr(self, '_sender', None) | ||||
|         return sender and sender.is_connected() | ||||
|         return sender and sender.connected | ||||
| 
 | ||||
|     def disconnect(self: 'TelegramClient'): | ||||
|         """ | ||||
|  | @ -477,7 +488,7 @@ class TelegramBaseClient(abc.ABC): | |||
| 
 | ||||
|         pts, date = self._state_cache[None] | ||||
|         if pts and date: | ||||
|             self.session.set_update_state(0, types.updates.State( | ||||
|             self._session.set_update_state(0, types.updates.State( | ||||
|                 pts=pts, | ||||
|                 qts=0, | ||||
|                 date=date, | ||||
|  | @ -485,7 +496,7 @@ class TelegramBaseClient(abc.ABC): | |||
|                 unread_count=0 | ||||
|             )) | ||||
| 
 | ||||
|         self.session.close() | ||||
|         self._session.close() | ||||
| 
 | ||||
|     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) | ||||
|         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 | ||||
|         # so it's not valid anymore. Set to None to force recreating it. | ||||
|         self._sender.auth_key.key = None | ||||
|         self.session.auth_key = None | ||||
|         self.session.save() | ||||
|         self._session.auth_key = None | ||||
|         self._session.save() | ||||
|         await self._disconnect() | ||||
|         return await self.connect() | ||||
| 
 | ||||
|  | @ -519,8 +530,8 @@ class TelegramBaseClient(abc.ABC): | |||
|         Callback from the sender whenever it needed to generate a | ||||
|         new authorization key. This means we are not authorized. | ||||
|         """ | ||||
|         self.session.auth_key = auth_key | ||||
|         self.session.save() | ||||
|         self._session.auth_key = auth_key | ||||
|         self._session.save() | ||||
| 
 | ||||
|     # endregion | ||||
| 
 | ||||
|  | @ -623,13 +634,13 @@ class TelegramBaseClient(abc.ABC): | |||
|         session = self._exported_sessions.get(cdn_redirect.dc_id) | ||||
|         if not session: | ||||
|             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) | ||||
|             self._exported_sessions[cdn_redirect.dc_id] = session | ||||
| 
 | ||||
|         self._log[__name__].info('Creating new CDN client') | ||||
|         client = TelegramBareClient( | ||||
|             session, self.api_id, self.api_hash, | ||||
|             session, self._api_id, self._api_hash, | ||||
|             proxy=self._sender.connection.conn.proxy, | ||||
|             timeout=self._sender.connection.get_timeout() | ||||
|         ) | ||||
|  |  | |||
|  | @ -225,7 +225,7 @@ class UpdateMethods(UserMethods): | |||
|         if not pts: | ||||
|             return | ||||
| 
 | ||||
|         self.session.catching_up = True | ||||
|         self._session.catching_up = True | ||||
|         try: | ||||
|             while True: | ||||
|                 d = await self(functions.updates.GetDifferenceRequest( | ||||
|  | @ -275,7 +275,7 @@ class UpdateMethods(UserMethods): | |||
|         finally: | ||||
|             # TODO Save new pts to session | ||||
|             self._state_cache._pts_date = (pts, date) | ||||
|             self.session.catching_up = False | ||||
|             self._session.catching_up = False | ||||
| 
 | ||||
|     # endregion | ||||
| 
 | ||||
|  | @ -285,7 +285,7 @@ class UpdateMethods(UserMethods): | |||
|     # 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. | ||||
|     def _handle_update(self: 'TelegramClient', update): | ||||
|         self.session.process_entities(update) | ||||
|         self._session.process_entities(update) | ||||
|         self._entity_cache.add(update) | ||||
| 
 | ||||
|         if isinstance(update, (types.Updates, types.UpdatesCombined)): | ||||
|  | @ -323,7 +323,7 @@ class UpdateMethods(UserMethods): | |||
|     async def _update_loop(self: 'TelegramClient'): | ||||
|         # Pings' ID don't really need to be secure, just "random" | ||||
|         rnd = lambda: random.randrange(-2**63, 2**63) | ||||
|         while self.is_connected(): | ||||
|         while self.connected: | ||||
|             try: | ||||
|                 await asyncio.wait_for( | ||||
|                     self.disconnected, timeout=60, loop=self._loop | ||||
|  | @ -347,7 +347,7 @@ class UpdateMethods(UserMethods): | |||
|             # inserted because this is a rather expensive operation | ||||
|             # (default's sqlite3 takes ~0.1s to commit changes). Do | ||||
|             # 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 | ||||
|             # for Telegram to keep delivering updates, otherwise they will | ||||
|  | @ -425,7 +425,7 @@ class UpdateMethods(UserMethods): | |||
|                 ) | ||||
|                 break | ||||
|             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)) | ||||
|                     self._log[__name__].exception('Unhandled exception on %s', | ||||
|                                                   name) | ||||
|  |  | |||
|  | @ -24,18 +24,6 @@ if typing.TYPE_CHECKING: | |||
|     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( | ||||
|         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, | ||||
|             attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, | ||||
|             thumb: 'hints.FileLike' = None, | ||||
|             allow_cache: bool = True, | ||||
|             parse_mode: str = (), | ||||
|             voice_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. | ||||
| 
 | ||||
|                 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 | ||||
|                 in chunks of 10 if more than 10 are given. | ||||
|                 sent as an album in the order in which they appear. Currently, | ||||
|                 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): | ||||
|                 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. | ||||
|                 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): | ||||
|                 See the `TelegramClient.parse_mode | ||||
|                 <telethon.client.messageparse.MessageParseMethods.parse_mode>` | ||||
|  | @ -203,16 +186,10 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | |||
|             voice_note (`bool`, optional): | ||||
|                 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): | ||||
|                 If ``True`` the video will be sent as a video note, | ||||
|                 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`): | ||||
|                 The matrix (list of lists), row list or button to be shown | ||||
|                 after sending the message. This parameter will only work if | ||||
|  | @ -267,52 +244,14 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | |||
|         if not caption: | ||||
|             caption = '' | ||||
| 
 | ||||
|         # First check if the user passed an iterable, in which case | ||||
|         # we may want to send as an album if all are photo files. | ||||
|         # First check if the user passed an iterable -> send as album | ||||
|         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 | ||||
|             images = [] | ||||
|             if force_document: | ||||
|                 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, | ||||
|                     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 | ||||
|             return await self._send_album( | ||||
|                 entity, file, caption=file, | ||||
|                 progress_callback=progress_callback, reply_to=reply_to, | ||||
|                 parse_mode=parse_mode, silent=silent | ||||
|             ) | ||||
| 
 | ||||
|         entity = await self.get_input_entity(entity) | ||||
|         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, force_document=force_document, | ||||
|             progress_callback=progress_callback, | ||||
|             attributes=attributes,  allow_cache=allow_cache, thumb=thumb, | ||||
|             attributes=attributes, thumb=thumb, | ||||
|             voice_note=voice_note, video_note=video_note, | ||||
|             supports_streaming=supports_streaming | ||||
|         ) | ||||
|  | @ -343,7 +282,6 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | |||
|             entities=msg_entities, reply_markup=markup, silent=silent | ||||
|         ) | ||||
|         msg = self._get_response_message(request, await self(request), entity) | ||||
|         await self._cache_media(msg, file, file_handle, image=image) | ||||
| 
 | ||||
|         return msg | ||||
| 
 | ||||
|  | @ -351,15 +289,6 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | |||
|                           progress_callback=None, reply_to=None, | ||||
|                           parse_mode=(), silent=None): | ||||
|         """Specialized version of .send_file for albums""" | ||||
|         # We don't care if the user wants to avoid cache, we will use it | ||||
|         # anyway. Why? The cached version will be exactly the same thing | ||||
|         # 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) | ||||
|         if not utils.is_list_like(caption): | ||||
|             caption = (caption,) | ||||
|  | @ -370,7 +299,6 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | |||
| 
 | ||||
|         reply_to = utils.get_message_id(reply_to) | ||||
| 
 | ||||
|         # Need to upload the media first, but only if they're not cached yet | ||||
|         media = [] | ||||
|         for file in files: | ||||
|             # Albums want :tl:`InputMedia` which, in theory, includes | ||||
|  | @ -382,10 +310,12 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | |||
|                 r = await self(functions.messages.UploadMediaRequest( | ||||
|                     entity, media=fm | ||||
|                 )) | ||||
|                 self.session.cache_file( | ||||
|                     fh.md5, fh.size, utils.get_input_photo(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: | ||||
|                 caption, msg_entities = captions.pop() | ||||
|  | @ -414,13 +344,14 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | |||
|         # Sent photo IDs -> messages | ||||
|         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( | ||||
|             self: 'TelegramClient', | ||||
|             file: 'hints.FileLike', | ||||
|             *, | ||||
|             part_size_kb: float = None, | ||||
|             file_name: str = None, | ||||
|             use_cache: type = None, | ||||
|             progress_callback: 'hints.ProgressCallback' = None) -> 'types.TypeInputFile': | ||||
|         """ | ||||
|         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`` | ||||
|                 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): | ||||
|                 A callback function accepting two parameters: | ||||
|                 ``(sent bytes, total)``. | ||||
|  | @ -537,19 +461,11 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | |||
|         hash_md5 = hashlib.md5() | ||||
|         if not is_large: | ||||
|             # Calculate the MD5 hash before anything else. | ||||
|             # As this needs to be done always for small files, | ||||
|             # might as well do it before anything else and | ||||
|             # check the cache. | ||||
|             # This needs to be done always for small files. | ||||
|             if isinstance(file, str): | ||||
|                 with open(file, 'rb') as stream: | ||||
|                     file = stream.read() | ||||
|             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 | ||||
|         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( | ||||
|             self, file, force_document=False, | ||||
|             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): | ||||
|         if not file: | ||||
|             return None, None, None | ||||
|  | @ -626,12 +542,10 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | |||
| 
 | ||||
|         media = None | ||||
|         file_handle = None | ||||
|         use_cache = types.InputPhoto if as_image else types.InputDocument | ||||
|         if not isinstance(file, str) or os.path.isfile(file): | ||||
|             file_handle = await self.upload_file( | ||||
|                 _resize_photo_if_needed(file, as_image), | ||||
|                 progress_callback=progress_callback, | ||||
|                 use_cache=use_cache if allow_cache else None | ||||
|                 progress_callback=progress_callback | ||||
|             ) | ||||
|         elif re.match('https?://', file): | ||||
|             if as_image: | ||||
|  | @ -652,12 +566,6 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | |||
|                 'Failed to convert {} to media. Not an existing 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: | ||||
|             media = types.InputMediaUploadedPhoto(file_handle) | ||||
|         else: | ||||
|  | @ -685,16 +593,4 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): | |||
|             ) | ||||
|         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 | ||||
|  |  | |||
|  | @ -52,7 +52,7 @@ class UserMethods(TelegramBaseClient): | |||
|                             exceptions.append(e) | ||||
|                             results.append(None) | ||||
|                             continue | ||||
|                         self.session.process_entities(result) | ||||
|                         self._session.process_entities(result) | ||||
|                         self._entity_cache.add(result) | ||||
|                         exceptions.append(None) | ||||
|                         results.append(result) | ||||
|  | @ -63,7 +63,7 @@ class UserMethods(TelegramBaseClient): | |||
|                         return results | ||||
|                 else: | ||||
|                     result = await future | ||||
|                     self.session.process_entities(result) | ||||
|                     self._session.process_entities(result) | ||||
|                     self._entity_cache.add(result) | ||||
|                     return result | ||||
|             except (errors.ServerError, errors.RpcCallFailError, | ||||
|  | @ -377,7 +377,7 @@ class UserMethods(TelegramBaseClient): | |||
| 
 | ||||
|         # No InputPeer, cached peer, or known string. Fetch from disk cache | ||||
|         try: | ||||
|             return self.session.get_input_entity(peer) | ||||
|             return self._session.get_input_entity(peer) | ||||
|         except ValueError: | ||||
|             pass | ||||
| 
 | ||||
|  | @ -513,7 +513,7 @@ class UserMethods(TelegramBaseClient): | |||
|             try: | ||||
|                 # Nobody with this username, maybe it's an exact name/title | ||||
|                 return await self.get_entity( | ||||
|                     self.session.get_input_entity(string)) | ||||
|                     self._session.get_input_entity(string)) | ||||
|             except ValueError: | ||||
|                 pass | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,10 +5,5 @@ with Telegram's servers and the protocol used (TCP full, abridged, etc.). | |||
| from .mtprotoplainsender import MTProtoPlainSender | ||||
| from .authenticator import do_authentication | ||||
| from .mtprotosender import MTProtoSender | ||||
| from .connection import ( | ||||
|     Connection, | ||||
|     ConnectionTcpFull, ConnectionTcpIntermediate, ConnectionTcpAbridged, | ||||
|     ConnectionTcpObfuscated, ConnectionTcpMTProxyAbridged, | ||||
|     ConnectionTcpMTProxyIntermediate, | ||||
|     ConnectionTcpMTProxyRandomizedIntermediate, ConnectionHttp, TcpMTProxy | ||||
| ) | ||||
| from .codec import BaseCodec, FullCodec, IntermediateCodec, AbridgedCodec | ||||
| from .connection import BaseConnection, AsyncioConnection | ||||
|  |  | |||
							
								
								
									
										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 | ||||
| from zlib import crc32 | ||||
| import zlib | ||||
| 
 | ||||
| from .connection import Connection, PacketCodec | ||||
| from .basecodec import BaseCodec | ||||
| from ...errors import InvalidChecksumError | ||||
| 
 | ||||
| 
 | ||||
| class FullPacketCodec(PacketCodec): | ||||
|     tag = None | ||||
| 
 | ||||
|     def __init__(self, connection): | ||||
|         super().__init__(connection) | ||||
| class FullCodec(BaseCodec): | ||||
|     """ | ||||
|     Default Telegram codec. Sends 12 additional bytes and | ||||
|     needs to calculate the CRC value of the packet itself. | ||||
|     """ | ||||
|     def __init__(self): | ||||
|         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 | ||||
|         # total length, sequence number, packet and checksum (CRC32) | ||||
|         length = len(data) + 12 | ||||
|         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 | ||||
|         return data + crc | ||||
| 
 | ||||
|     async def read_packet(self, reader): | ||||
|         packet_len_seq = await reader.readexactly(8)  # 4 and 4 | ||||
|         packet_len, seq = struct.unpack('<ii', packet_len_seq) | ||||
|         body = await reader.readexactly(packet_len - 8) | ||||
|     def decode_header(self, header): | ||||
|         length, seq = struct.unpack('<ii', header) | ||||
|         return length - 8 | ||||
| 
 | ||||
|     def decode_body(self, header, body): | ||||
|         checksum = struct.unpack('<I', body[-4:])[0] | ||||
|         body = body[:-4] | ||||
| 
 | ||||
|         valid_checksum = crc32(packet_len_seq + body) | ||||
|         valid_checksum = zlib.crc32(header + body) | ||||
|         if checksum != valid_checksum: | ||||
|             raise InvalidChecksumError(checksum, valid_checksum) | ||||
| 
 | ||||
|         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 .tcpfull import ConnectionTcpFull | ||||
| from .tcpintermediate import ConnectionTcpIntermediate | ||||
| from .tcpabridged import ConnectionTcpAbridged | ||||
| from .tcpobfuscated import ConnectionTcpObfuscated | ||||
| from .tcpmtproxy import ( | ||||
|     TcpMTProxy, | ||||
|     ConnectionTcpMTProxyAbridged, | ||||
|     ConnectionTcpMTProxyIntermediate, | ||||
|     ConnectionTcpMTProxyRandomizedIntermediate | ||||
| ) | ||||
| from .http import ConnectionHttp | ||||
| from .baseconnection import BaseConnection | ||||
| from .asyncioconnection import AsyncioConnection | ||||
|  |  | |||
							
								
								
									
										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 | ||||
|     def address_info(proxy_info): | ||||
|         raise NotImplementedError('New proxy format is not implemented') | ||||
|         if proxy_info is None: | ||||
|             raise ValueError("No proxy info specified for MTProxy connection") | ||||
|         return proxy_info[:2] | ||||
|  |  | |||
|  | @ -122,7 +122,8 @@ class MTProtoSender: | |||
|         await self._connect() | ||||
|         self._user_connected = True | ||||
| 
 | ||||
|     def is_connected(self): | ||||
|     @property | ||||
|     def connected(self): | ||||
|         return self._user_connected | ||||
| 
 | ||||
|     async def disconnect(self): | ||||
|  |  | |||
|  | @ -144,24 +144,3 @@ class Session(ABC): | |||
|         to use a cached username to avoid extra RPC). | ||||
|         """ | ||||
|         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 .. import utils | ||||
| from ..tl import TLObject | ||||
| from ..tl.types import ( | ||||
|     PeerUser, PeerChat, PeerChannel, | ||||
|     InputPeerUser, InputPeerChat, InputPeerChannel, | ||||
|     InputPhoto, InputDocument | ||||
|     InputPeerUser, InputPeerChat, InputPeerChannel | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| 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): | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|  | @ -34,7 +17,6 @@ class MemorySession(Session): | |||
|         self._auth_key = None | ||||
|         self._takeout_id = None | ||||
| 
 | ||||
|         self._files = {} | ||||
|         self._entities = set() | ||||
|         self._update_states = {} | ||||
| 
 | ||||
|  | @ -228,17 +210,3 @@ class MemorySession(Session): | |||
|                 return InputPeerChannel(entity_id, entity_hash) | ||||
|         else: | ||||
|             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 | ||||
| 
 | ||||
| from telethon.tl import types | ||||
| from .memory import MemorySession, _SentFileType | ||||
| from .memory import MemorySession | ||||
| from .. import utils | ||||
| from ..crypto import AuthKey | ||||
| from ..tl.types import ( | ||||
|     InputPhoto, InputDocument, PeerUser, PeerChat, PeerChannel | ||||
|     PeerUser, PeerChat, PeerChannel | ||||
| ) | ||||
| 
 | ||||
| try: | ||||
|  | @ -17,7 +17,7 @@ except ImportError as e: | |||
|     sqlite3_err = type(e) | ||||
| 
 | ||||
| EXTENSION = '.session' | ||||
| CURRENT_VERSION = 5  # database version | ||||
| CURRENT_VERSION = 6  # database version | ||||
| 
 | ||||
| 
 | ||||
| class SQLiteSession(MemorySession): | ||||
|  | @ -87,15 +87,6 @@ class SQLiteSession(MemorySession): | |||
|                     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 ( | ||||
|                     id integer primary key, | ||||
|                     pts integer, | ||||
|  | @ -143,6 +134,9 @@ class SQLiteSession(MemorySession): | |||
|         if old == 4: | ||||
|             old += 1 | ||||
|             c.execute("alter table sessions add column takeout_id integer") | ||||
|         if old == 5: | ||||
|             old += 1 | ||||
|             c.execute('drop table sent_files') | ||||
|         c.close() | ||||
| 
 | ||||
|     @staticmethod | ||||
|  | @ -300,26 +294,3 @@ class SQLiteSession(MemorySession): | |||
|                 utils.get_peer_id(PeerChat(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. | ||||
|         """ | ||||
|         if not self.cl.is_connected(): | ||||
|         if not self.cl.connected: | ||||
|             return | ||||
| 
 | ||||
|         # The user needs to configure a chat where the message should be sent. | ||||
|  |  | |||
|  | @ -116,9 +116,8 @@ class InteractiveTelegramClient(TelegramClient): | |||
|         try: | ||||
|             loop.run_until_complete(self.connect()) | ||||
|         except IOError: | ||||
|             # We handle IOError and not ConnectionError because | ||||
|             # PySocks' errors do not subclass ConnectionError | ||||
|             # (so this will work with and without proxies). | ||||
|             # To avoid issues in the future, we except the most | ||||
|             # generic IOError as possible (instead of ConnectionError) | ||||
|             print('Initial connection failed. Retrying...') | ||||
|             loop.run_until_complete(self.connect()) | ||||
| 
 | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ def get_env(name, message, cast=str): | |||
| session = os.environ.get('TG_SESSION', 'printer') | ||||
| api_id = get_env('TG_API_ID', 'Enter your API ID: ', int) | ||||
| 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) | ||||
| 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') | ||||
| api_id = get_env('TG_API_ID', 'Enter your API ID: ', int) | ||||
| 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. | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user