diff --git a/optional-requirements.txt b/optional-requirements.txt new file mode 100644 index 00000000..55bfc014 --- /dev/null +++ b/optional-requirements.txt @@ -0,0 +1,3 @@ +cryptg +pysocks +hachoir3 diff --git a/readthedocs/extra/basic/installation.rst b/readthedocs/extra/basic/installation.rst index e74cdae6..0f812127 100644 --- a/readthedocs/extra/basic/installation.rst +++ b/readthedocs/extra/basic/installation.rst @@ -33,6 +33,13 @@ If you don't have root access, simply pass the ``--user`` flag to the pip command. If you want to install a specific branch, append ``@branch`` to the end of the first install command. +By default the library will use a pure Python implementation for encryption, +which can be really slow when uploading or downloading files. If you don't +mind using a C extension, install `cryptg `__ +via ``pip`` or as an extra: + + ``pip3 install telethon[cryptg]`` + Manual Installation ******************* diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index 34609615..ad027361 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -14,6 +14,76 @@ it can take advantage of new goodies! .. contents:: List of All Versions +Further easing library usage (v0.17.4) +====================================== + +*Published at 2018/02/24* + +Some new things and patches that already deserved their own release. + + +Additions +~~~~~~~~~ + +- New ``pattern`` argument to ``NewMessage`` to easily filter messages. +- New ``.get_participants()`` convenience method to get members from chats. +- ``.send_message()`` now accepts a ``Message`` as the ``message`` parameter. +- You can now ``.get_entity()`` through exact name match instead username. +- Raise ``ProxyConnectionError`` instead looping forever so you can + ``except`` it on your own code and behave accordingly. + +Bug fixes +~~~~~~~~~ + +- ``.parse_username`` would fail with ``www.`` or a trailing slash. +- ``events.MessageChanged`` would fail with ``UpdateDeleteMessages``. +- You can now send ``b'byte strings'`` directly as files again. +- ``.send_file()`` was not respecting the original captions when passing + another message (or media) as the file. +- Downloading media from a different data center would always log a warning + for the first time. + +Internal changes +~~~~~~~~~~~~~~~~ + +- Use ``req_pq_multi`` instead ``req_pq`` when generating ``auth_key``. +- You can use ``.get_me(input_peer=True)`` if all you need is your self ID. +- New addition to the interactive client example to show peer information. +- Avoid special casing ``InputPeerSelf`` on some ``NewMessage`` events, so + you can always safely rely on ``.sender`` to get the right ID. + + +New small convenience functions (v0.17.3) +========================================= + +*Published at 2018/02/18* + +More bug fixes and a few others addition to make events easier to use. + +Additions +~~~~~~~~~ + +- Use ``hachoir`` to extract video and audio metadata before upload. +- New ``.add_event_handler``, ``.add_update_handler`` now deprecated. + +Bug fixes +~~~~~~~~~ + +- ``bot_token`` wouldn't work on ``.start()``, and changes to ``password`` + (now it will ask you for it if you don't provide it, as docstring hinted). +- ``.edit_message()`` was ignoring the formatting (e.g. markdown). +- Added missing case to the ``NewMessage`` event for normal groups. +- Accessing the ``.text`` of the ``NewMessage`` event was failing due + to a bug with the markdown unparser. + +Internal changes +~~~~~~~~~~~~~~~~ + +- ``libssl`` is no longer an optional dependency. Use ``cryptg`` instead, + which you can find on https://github.com/Lonami/cryptg. + + + New small convenience functions (v0.17.2) ========================================= diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..2b650ec4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pyaes +rsa diff --git a/setup.py b/setup.py index 2682e099..00dd7446 100755 --- a/setup.py +++ b/setup.py @@ -110,7 +110,7 @@ def main(): long_description = f.read() with open('telethon/version.py', encoding='utf-8') as f: - version = re.search(r"^__version__\s+=\s+'(.*)'$", + version = re.search(r"^__version__\s*=\s*'(.*)'.*$", f.read(), flags=re.MULTILINE).group(1) setup( name='Telethon', @@ -149,7 +149,10 @@ def main(): 'telethon_generator', 'telethon_tests', 'run_tests.py', 'try_telethon.py' ]), - install_requires=['pyaes', 'rsa'] + install_requires=['pyaes', 'rsa'], + extras_require={ + 'cryptg': ['cryptg'] + } ) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 0af94658..46a172a7 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -1,6 +1,7 @@ import abc import datetime import itertools +import re from .. import utils from ..errors import RPCError @@ -8,14 +9,62 @@ from ..extensions import markdown from ..tl import types, functions +async def _into_id_set(client, chats): + """Helper util to turn the input chat or chats into a set of IDs.""" + if chats is None: + return None + + if not hasattr(chats, '__iter__') or isinstance(chats, str): + chats = (chats,) + + result = set() + for chat in chats: + chat = await client.get_input_entity(chat) + if isinstance(chat, types.InputPeerSelf): + chat = await client.get_me(input_peer=True) + result.add(utils.get_peer_id(chat)) + return result + + class _EventBuilder(abc.ABC): + """ + The common event builder, with builtin support to filter per chat. + + Args: + chats (:obj:`entity`, optional): + May be one or more entities (username/peer/etc.). By default, + only matching chats will be handled. + + blacklist_chats (:obj:`bool`, optional): + Whether to treat the the list of chats as a blacklist (if + it matches it will NOT be handled) or a whitelist (default). + """ + def __init__(self, chats=None, blacklist_chats=False): + self.chats = chats + self.blacklist_chats = blacklist_chats + self._self_id = None + @abc.abstractmethod def build(self, update): """Builds an event for the given update if possible, or returns None""" - @abc.abstractmethod async def resolve(self, client): """Helper method to allow event builders to be resolved before usage""" + self.chats = await _into_id_set(client, self.chats) + self._self_id = (await client.get_me(input_peer=True)).user_id + + def _filter_event(self, event): + """ + If the ID of ``event._chat_peer`` isn't in the chats set (or it is + but the set is a blacklist) returns ``None``, otherwise the event. + """ + if self.chats is not None: + inside = utils.get_peer_id(event._chat_peer) in self.chats + if inside == self.blacklist_chats: + # If this chat matches but it's a blacklist ignore. + # If it doesn't match but it's a whitelist ignore. + return None + return event class _EventCommon(abc.ABC): @@ -98,7 +147,7 @@ class _EventCommon(abc.ABC): there is no caching besides local caching yet. """ if self._chat is None and await self.input_chat: - self._chat = await self._client.get_entity(self._input_chat) + self._chat = await self._client.get_entity(await self._input_chat) return self._chat @@ -106,8 +155,6 @@ class Raw(_EventBuilder): """ Represents a raw event. The event is the update itself. """ - async def resolve(self, client): - pass def build(self, update): return update @@ -129,36 +176,28 @@ class NewMessage(_EventBuilder): If set to ``True``, only **outgoing** messages will be handled. Mutually exclusive with ``incoming`` (can only set one of either). - chats (:obj:`entity`, optional): - May be one or more entities (username/peer/etc.). By default, - only matching chats will be handled. - - blacklist_chats (:obj:`bool`, optional): - Whether to treat the the list of chats as a blacklist (if - it matches it will NOT be handled) or a whitelist (default). - - Notes: - The ``message.from_id`` might not only be an integer or ``None``, - but also ``InputPeerSelf()`` for short private messages (the API - would not return such thing, this is a custom modification). + pattern (:obj:`str`, :obj:`callable`, :obj:`Pattern`, optional): + If set, only messages matching this pattern will be handled. + You can specify a regex-like string which will be matched + against the message, a callable function that returns ``True`` + if a message is acceptable, or a compiled regex pattern. """ def __init__(self, incoming=None, outgoing=None, - chats=None, blacklist_chats=False): + chats=None, blacklist_chats=False, pattern=None): if incoming and outgoing: raise ValueError('Can only set either incoming or outgoing') + super().__init__(chats=chats, blacklist_chats=blacklist_chats) self.incoming = incoming self.outgoing = outgoing - self.chats = chats - self.blacklist_chats = blacklist_chats - - async def resolve(self, client): - if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str): - self.chats = set(utils.get_peer_id(await client.get_input_entity(x)) - for x in self.chats) - elif self.chats is not None: - self.chats = {utils.get_peer_id( - await client.get_input_entity(self.chats))} + if isinstance(pattern, str): + self.pattern = re.compile(pattern).match + elif not pattern or callable(pattern): + self.pattern = pattern + elif hasattr(pattern, 'match') and callable(pattern.match): + self.pattern = pattern.match + else: + raise TypeError('Invalid pattern type given') def build(self, update): if isinstance(update, @@ -174,7 +213,23 @@ class NewMessage(_EventBuilder): silent=update.silent, id=update.id, to_id=types.PeerUser(update.user_id), - from_id=types.InputPeerSelf() if update.out else update.user_id, + from_id=self._self_id if update.out else update.user_id, + message=update.message, + date=update.date, + fwd_from=update.fwd_from, + via_bot_id=update.via_bot_id, + reply_to_msg_id=update.reply_to_msg_id, + entities=update.entities + )) + elif isinstance(update, types.UpdateShortChatMessage): + event = NewMessage.Event(types.Message( + out=update.out, + mentioned=update.mentioned, + media_unread=update.media_unread, + silent=update.silent, + id=update.id, + from_id=update.from_id, + to_id=types.PeerChat(update.chat_id), message=update.message, date=update.date, fwd_from=update.fwd_from, @@ -186,23 +241,18 @@ class NewMessage(_EventBuilder): return # Short-circuit if we let pass all events - if all(x is None for x in (self.incoming, self.outgoing, self.chats)): + if all(x is None for x in (self.incoming, self.outgoing, self.chats, + self.pattern)): return event if self.incoming and event.message.out: return if self.outgoing and not event.message.out: return + if self.pattern and not self.pattern(event.message.message or ''): + return - if self.chats is not None: - inside = utils.get_peer_id(event.message.to_id) in self.chats - if inside == self.blacklist_chats: - # If this chat matches but it's a blacklist ignore. - # If it doesn't match but it's a whitelist ignore. - return - - # Tests passed so return the event - return event + return self._filter_event(event) class Event(_EventCommon): """ @@ -264,9 +314,13 @@ class NewMessage(_EventBuilder): or the edited message otherwise. """ if not self.message.out: - return None + if not isinstance(self.message.to_id, types.PeerUser): + return None + me = await self._client.get_me(input_peer=True) + if self.message.to_id.user_id != me.user_id: + return None - return await self._client.edit_message(self.input_chat, + return await self._client.edit_message(await self.input_chat, self.message, *args, **kwargs) @@ -277,7 +331,7 @@ class NewMessage(_EventBuilder): This is a shorthand for ``client.delete_messages(event.chat, event.message, ...)``. """ - return await self._client.delete_messages(self.input_chat, + return await self._client.delete_messages(await self.input_chat, [self.message], *args, **kwargs) @@ -413,30 +467,7 @@ class NewMessage(_EventBuilder): class ChatAction(_EventBuilder): """ Represents an action in a chat (such as user joined, left, or new pin). - - Args: - chats (:obj:`entity`, optional): - May be one or more entities (username/peer/etc.). By default, - only matching chats will be handled. - - blacklist_chats (:obj:`bool`, optional): - Whether to treat the the list of chats as a blacklist (if - it matches it will NOT be handled) or a whitelist (default). - """ - def __init__(self, chats=None, blacklist_chats=False): - # TODO This can probably be reused in all builders - self.chats = chats - self.blacklist_chats = blacklist_chats - - async def resolve(self, client): - if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str): - self.chats = set(utils.get_peer_id(await client.get_input_entity(x)) - for x in self.chats) - elif self.chats is not None: - self.chats = {utils.get_peer_id( - await client.get_input_entity(self.chats))} - def build(self, update): if isinstance(update, types.UpdateChannelPinnedMessage): # Telegram sends UpdateChannelPinnedMessage and then @@ -494,16 +525,7 @@ class ChatAction(_EventBuilder): else: return - if self.chats is None: - return event - else: - inside = utils.get_peer_id(event._chat_peer) in self.chats - if inside == self.blacklist_chats: - # If this chat matches but it's a blacklist ignore. - # If it doesn't match but it's a whitelist ignore. - return - - return event + return self._filter_event(event) class Event(_EventCommon): """ @@ -649,7 +671,6 @@ class UserUpdate(_EventBuilder): """ Represents an user update (gone online, offline, joined Telegram). """ - def build(self, update): if isinstance(update, types.UpdateUserStatus): event = UserUpdate.Event(update.user_id, @@ -657,10 +678,7 @@ class UserUpdate(_EventBuilder): else: return - return event - - async def resolve(self, client): - pass + return self._filter_event(event) class Event(_EventCommon): """ @@ -800,13 +818,16 @@ class MessageChanged(_EventBuilder): """ Represents a message changed (edited or deleted). """ - def build(self, update): if isinstance(update, (types.UpdateEditMessage, types.UpdateEditChannelMessage)): event = MessageChanged.Event(edit_msg=update.message) - elif isinstance(update, (types.UpdateDeleteMessages, - types.UpdateDeleteChannelMessages)): + elif isinstance(update, types.UpdateDeleteMessages): + event = MessageChanged.Event( + deleted_ids=update.messages, + peer=None + ) + elif isinstance(update, types.UpdateDeleteChannelMessages): event = MessageChanged.Event( deleted_ids=update.messages, peer=types.PeerChannel(update.channel_id) @@ -814,91 +835,32 @@ class MessageChanged(_EventBuilder): else: return - return event + return self._filter_event(event) - async def resolve(self, client): - pass - - class Event(_EventCommon): + class Event(NewMessage.Event): """ Represents the event of an user status update (last seen, joined). + Please note that the ``message`` member will be ``None`` if the + action was a deletion and not an edit. + Members: edited (:obj:`bool`): ``True`` if the message was edited. - message (:obj:`Message`, optional): - The new edited message, if any. - deleted (:obj:`bool`): ``True`` if the message IDs were deleted. deleted_ids (:obj:`List[int]`): A list containing the IDs of the messages that were deleted. - - input_sender (:obj:`InputPeer`): - This is the input version of the user who edited the message. - Similarly to ``input_chat``, this doesn't have things like - username or similar, but still useful in some cases. - - Note that this might not be available if the library can't - find the input chat. - - sender (:obj:`User`): - This property will make an API call the first time to get the - most up to date version of the sender, so use with care as - there is no caching besides local caching yet. - - ``input_sender`` needs to be available (often the case). """ def __init__(self, edit_msg=None, deleted_ids=None, peer=None): - super().__init__(peer if not edit_msg else edit_msg.to_id) + if edit_msg is None: + msg = types.Message((deleted_ids or [0])[0], peer, None, '') + else: + msg = edit_msg + super().__init__(msg) self.edited = bool(edit_msg) - self.message = edit_msg self.deleted = bool(deleted_ids) self.deleted_ids = deleted_ids or [] - self._input_sender = None - self._sender = None - - @property - async def input_sender(self): - """ - This (:obj:`InputPeer`) is the input version of the user who - sent the message. Similarly to ``input_chat``, this doesn't have - things like username or similar, but still useful in some cases. - - Note that this might not be available if the library can't - find the input chat, or if the message a broadcast on a channel. - """ - # TODO Code duplication - if self._input_sender is None: - if self.is_channel and not self.is_group: - return None - - try: - self._input_sender = await self._client.get_input_entity( - self.message.from_id - ) - except (ValueError, TypeError): - # We can rely on self.input_chat for this - self._input_sender = await self._get_input_entity( - self.message.id, - self.message.from_id, - chat=await self.input_chat - ) - - return self._input_sender - - @property - async def sender(self): - """ - This (:obj:`User`) will make an API call the first time to get - the most up to date version of the sender, so use with care as - there is no caching besides local caching yet. - - ``input_sender`` needs to be available (often the case). - """ - if self._sender is None and await self.input_sender: - self._sender = await self._client.get_entity(self._input_sender) - return self._sender diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index 4e41a84e..46974482 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -169,6 +169,7 @@ def unparse(text, entities, delimiters=None, url_fmt=None): entities = tuple(sorted(entities, key=lambda e: e.offset, reverse=True)) text = _add_surrogate(text) + delimiters = {v: k for k, v in delimiters.items()} for entity in entities: s = entity.offset e = entity.offset + entity.length diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index 578386be..588899b4 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -15,6 +15,11 @@ CONN_RESET_ERRNOS = { errno.EINVAL, errno.ENOTCONN } +try: + import socks +except ImportError: + socks = None + MAX_TIMEOUT = 15 # in seconds CONN_RESET_ERRNOS = { errno.EBADF, errno.ENOTSOCK, errno.ENETUNREACH, @@ -81,6 +86,9 @@ class TcpClient: await asyncio.sleep(timeout) timeout = min(timeout * 2, MAX_TIMEOUT) except OSError as e: + # Stop retrying to connect if proxy connection error occurred + if socks and isinstance(e, socks.ProxyConnectionError): + raise # There are some errors that we know how to handle, and # the loop will allow us to retry if e.errno in (errno.EBADF, errno.ENOTSOCK, errno.EINVAL, diff --git a/telethon/network/authenticator.py b/telethon/network/authenticator.py index 65d6e357..f905f0fc 100644 --- a/telethon/network/authenticator.py +++ b/telethon/network/authenticator.py @@ -17,7 +17,7 @@ from ..errors import SecurityError from ..extensions import BinaryReader from ..network import MtProtoPlainSender from ..tl.functions import ( - ReqPqRequest, ReqDHParamsRequest, SetClientDHParamsRequest + ReqPqMultiRequest, ReqDHParamsRequest, SetClientDHParamsRequest ) @@ -53,7 +53,7 @@ async def _do_authentication(connection): sender = MtProtoPlainSender(connection) # Step 1 sending: PQ Request, endianness doesn't matter since it's random - req_pq_request = ReqPqRequest( + req_pq_request = ReqPqMultiRequest( nonce=int.from_bytes(os.urandom(16), 'big', signed=True) ) await sender.send(bytes(req_pq_request)) diff --git a/telethon/session.py b/telethon/session.py index 04794129..c462baff 100644 --- a/telethon/session.py +++ b/telethon/session.py @@ -221,7 +221,7 @@ class Session: c = self._cursor() c.execute('select auth_key from sessions') tuple_ = c.fetchone() - if tuple_: + if tuple_ and tuple_[0]: self._auth_key = AuthKey(data=tuple_[0]) else: self._auth_key = None @@ -424,13 +424,19 @@ class Session: (phone,)) else: username, _ = utils.parse_username(key) - c.execute('select id, hash from entities where username=?', - (username,)) + if username: + c.execute('select id, hash from entities where username=?', + (username,)) if isinstance(key, int): c.execute('select id, hash from entities where id=?', (key,)) result = c.fetchone() + if not result and isinstance(key, str): + # Try exact match by name if phone/username failed + c.execute('select id, hash from entities where name=?', (key,)) + result = c.fetchone() + c.close() if result: i, h = result # unpack resulting tuple diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 155a8b11..82bde4e0 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -3,7 +3,6 @@ import logging import os from asyncio import Lock from datetime import timedelta - from . import version, utils from .crypto import rsa from .errors import ( @@ -78,7 +77,7 @@ class TelegramBareClient: "Refer to telethon.rtfd.io for more information.") self._use_ipv6 = use_ipv6 - + # Determine what session object we have if isinstance(session, str) or session is None: session = Session(session) @@ -554,17 +553,6 @@ class TelegramBareClient: """ self.updates.process(await self(GetStateRequest())) - def add_update_handler(self, handler): - """Adds an update handler (a function which takes a TLObject, - an update, as its parameter) and listens for updates""" - self.updates.handlers.append(handler) - - def remove_update_handler(self, handler): - self.updates.handlers.remove(handler) - - def list_update_handlers(self): - return self.updates.handlers[:] - # endregion # Constant read diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 2ca7b7ab..e1fe9e07 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1,4 +1,5 @@ import asyncio +import getpass import hashlib import io import itertools @@ -6,6 +7,7 @@ import logging import os import re import sys +import warnings from collections import OrderedDict, UserList from datetime import datetime, timedelta from io import BytesIO @@ -23,8 +25,15 @@ try: except ImportError: socks = None +try: + import hachoir + import hachoir.metadata + import hachoir.parser +except ImportError: + hachoir = None + from . import TelegramBareClient -from . import helpers, utils +from . import helpers, utils, events from .errors import ( RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError, @@ -47,7 +56,7 @@ from .tl.functions.messages import ( GetDialogsRequest, GetHistoryRequest, SendMediaRequest, SendMessageRequest, GetChatsRequest, GetAllDraftsRequest, CheckChatInviteRequest, ReadMentionsRequest, SendMultiMediaRequest, - UploadMediaRequest, EditMessageRequest + UploadMediaRequest, EditMessageRequest, GetFullChatRequest ) from .tl.functions import channels @@ -57,7 +66,7 @@ from .tl.functions.users import ( GetUsersRequest ) from .tl.functions.channels import ( - GetChannelsRequest, GetFullChannelRequest + GetChannelsRequest, GetFullChannelRequest, GetParticipantsRequest ) from .tl.types import ( DocumentAttributeAudio, DocumentAttributeFilename, @@ -71,7 +80,8 @@ from .tl.types import ( InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig, InputDocument, InputMediaDocument, Document, MessageEntityTextUrl, InputMessageEntityMentionName, DocumentAttributeVideo, - UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates + UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates, + MessageMediaWebPage, ChannelParticipantsSearch ) from .tl.types.messages import DialogsSlice from .extensions import markdown, html @@ -168,6 +178,9 @@ class TelegramClient(TelegramBareClient): self._phone_code_hash = {} self._phone = None + # Sometimes we need to know who we are, cache the self peer + self._self_input_peer = None + # endregion # region Telegram requests functions @@ -207,8 +220,9 @@ class TelegramClient(TelegramBareClient): async def start(self, phone=lambda: input('Please enter your phone: '), - password=None, bot_token=None, - force_sms=False, code_callback=None, + password=lambda: getpass.getpass( + 'Please enter your password: '), + bot_token=None, force_sms=False, code_callback=None, first_name='New User', last_name=''): """ Convenience method to interactively connect and sign in if required, @@ -265,7 +279,7 @@ class TelegramClient(TelegramBareClient): if not phone and not bot_token: raise ValueError('No phone number or bot token provided.') - if phone and bot_token: + if phone and bot_token and not callable(phone): raise ValueError('Both a phone and a bot token provided, ' 'must only provide one of either') @@ -322,6 +336,9 @@ class TelegramClient(TelegramBareClient): "Two-step verification is enabled for this account. " "Please provide the 'password' argument to 'start()'." ) + # TODO If callable given make it retry on invalid + if callable(password): + password = password() me = await self.sign_in(phone=phone, password=password) # We won't reach here if any step failed (exit by exception) @@ -393,6 +410,9 @@ class TelegramClient(TelegramBareClient): 'and a password only if an RPCError was raised before.' ) + self._self_input_peer = utils.get_input_peer( + result.user, allow_self=False + ) self._set_connected_and_authorized() return result.user @@ -422,6 +442,9 @@ class TelegramClient(TelegramBareClient): last_name=last_name )) + self._self_input_peer = utils.get_input_peer( + result.user, allow_self=False + ) self._set_connected_and_authorized() return result.user @@ -441,16 +464,31 @@ class TelegramClient(TelegramBareClient): self.session.delete() return True - async def get_me(self): + async def get_me(self, input_peer=False): """ Gets "me" (the self user) which is currently authenticated, or None if the request fails (hence, not authenticated). + Args: + input_peer (:obj:`bool`, optional): + Whether to return the ``InputPeerUser`` version or the normal + ``User``. This can be useful if you just need to know the ID + of yourself. + Returns: :obj:`User`: Your own user. """ + if input_peer and self._self_input_peer: + return self._self_input_peer try: - return (await self(GetUsersRequest([InputUserSelf()])))[0] + me = (await self(GetUsersRequest([InputUserSelf()])))[0] + if not self._self_input_peer: + self._self_input_peer = utils.get_input_peer( + me, allow_self=False + ) + if input_peer: + return self._self_input_peer + return me except UnauthorizedError: return None @@ -627,8 +665,8 @@ class TelegramClient(TelegramBareClient): entity (:obj:`entity`): To who will it be sent. - message (:obj:`str`): - The message to be sent. + message (:obj:`str` | :obj:`Message`): + The message to be sent, or another message object to resend. reply_to (:obj:`int` | :obj:`Message`, optional): Whether to reply to a message or not. If an integer is provided, @@ -646,16 +684,37 @@ class TelegramClient(TelegramBareClient): Returns: the sent message """ - entity = await self.get_input_entity(entity) - message, msg_entities = await self._parse_message_text(message, parse_mode) - request = SendMessageRequest( - peer=entity, - message=message, - entities=msg_entities, - no_webpage=not link_preview, - reply_to_msg_id=self._get_message_id(reply_to) - ) + entity = await self.get_input_entity(entity) + if isinstance(message, Message): + if (message.media + and not isinstance(message.media, MessageMediaWebPage)): + return await self.send_file(entity, message.media) + + if utils.get_peer_id(entity) == utils.get_peer_id(message.to_id): + reply_id = message.reply_to_msg_id + else: + reply_id = None + request = SendMessageRequest( + peer=entity, + message=message.message or '', + silent=message.silent, + reply_to_msg_id=reply_id, + reply_markup=message.reply_markup, + entities=message.entities, + no_webpage=not isinstance(message.media, MessageMediaWebPage) + ) + message = message.message + else: + message, msg_ent = await self._parse_message_text(message, parse_mode) + request = SendMessageRequest( + peer=entity, + message=message, + entities=msg_ent, + no_webpage=not link_preview, + reply_to_msg_id=self._get_message_id(reply_to) + ) + result = await self(request) if isinstance(result, UpdateShortSentMessage): @@ -956,11 +1015,64 @@ class TelegramClient(TelegramBareClient): raise TypeError('Invalid message type: {}'.format(type(message))) + def get_participants(self, entity, limit=None, search=''): + """ + Gets the list of participants from the specified entity + + Args: + entity (:obj:`entity`): + The entity from which to retrieve the participants list. + + limit (:obj: `int`): + Limits amount of participants fetched. + + search (:obj: `str`, optional): + Look for participants with this string in name/username. + + Returns: + A list of participants with an additional .total variable on the list + indicating the total amount of members in this group/channel. + """ + entity = self.get_input_entity(entity) + limit = float('inf') if limit is None else int(limit) + if isinstance(entity, InputPeerChannel): + offset = 0 + all_participants = {} + search = ChannelParticipantsSearch(search) + while True: + loop_limit = min(limit - offset, 200) + participants = self(GetParticipantsRequest( + entity, search, offset, loop_limit, hash=0 + )) + if not participants.users: + break + for user in participants.users: + if len(all_participants) < limit: + all_participants[user.id] = user + offset += len(participants.users) + if offset > limit: + break + + users = UserList(all_participants.values()) + users.total = self(GetFullChannelRequest( + entity)).full_chat.participants_count + + elif isinstance(entity, InputPeerChat): + users = self(GetFullChatRequest(entity.chat_id)).users + if len(users) > limit: + users = users[:limit] + users = UserList(users) + users.total = len(users) + else: + users = UserList([entity]) + users.total = 1 + return users + # endregion # region Uploading files - async def send_file(self, entity, file, caption='', + async def send_file(self, entity, file, caption=None, force_document=False, progress_callback=None, reply_to=None, attributes=None, @@ -1019,6 +1131,10 @@ class TelegramClient(TelegramBareClient): If "is_voice_note" in kwargs, despite its value, and the file is sent as a document, it will be sent as a voice note. + Notes: + If the ``hachoir3`` package (``hachoir`` module) is installed, + it will be used to determine metadata from audio and video files. + Returns: The message (or messages) containing the sent file. """ @@ -1068,11 +1184,11 @@ class TelegramClient(TelegramBareClient): if isinstance(file_handle, use_cache): # File was cached, so an instance of use_cache was returned if as_image: - media = InputMediaPhoto(file_handle, caption) + media = InputMediaPhoto(file_handle, caption or '') else: - media = InputMediaDocument(file_handle, caption) + media = InputMediaDocument(file_handle, caption or '') elif as_image: - media = InputMediaUploadedPhoto(file_handle, caption) + media = InputMediaUploadedPhoto(file_handle, caption or '') else: mime_type = None if isinstance(file, str): @@ -1082,12 +1198,32 @@ class TelegramClient(TelegramBareClient): attr_dict = { DocumentAttributeFilename: DocumentAttributeFilename(os.path.basename(file)) - # TODO If the input file is an audio, find out: - # Performer and song title and add DocumentAttributeAudio } + if utils.is_audio(file) and hachoir: + m = hachoir.metadata.extractMetadata( + hachoir.parser.createParser(file) + ) + attr_dict[DocumentAttributeAudio] = DocumentAttributeAudio( + title=m.get('title') if m.has('title') else None, + performer=m.get('author') if m.has('author') else None, + duration=int(m.get('duration').seconds + if m.has('duration') else 0) + ) + if not force_document and utils.is_video(file): - attr_dict[DocumentAttributeVideo] = \ - DocumentAttributeVideo(0, 0, 0) + if hachoir: + m = hachoir.metadata.extractMetadata( + hachoir.parser.createParser(file) + ) + doc = DocumentAttributeVideo( + w=m.get('width') if m.has('width') else 0, + h=m.get('height') if m.has('height') else 0, + duration=int(m.get('duration').seconds + if m.has('duration') else 0) + ) + else: + doc = DocumentAttributeVideo(0, 0, 0) + attr_dict[DocumentAttributeVideo] = doc else: attr_dict = { DocumentAttributeFilename: @@ -1095,8 +1231,11 @@ class TelegramClient(TelegramBareClient): } if 'is_voice_note' in kwargs: - attr_dict[DocumentAttributeAudio] = \ - DocumentAttributeAudio(0, voice=True) + if DocumentAttributeAudio in attr_dict: + attr_dict[DocumentAttributeAudio].voice = True + else: + attr_dict[DocumentAttributeAudio] = \ + DocumentAttributeAudio(0, voice=True) # Now override the attributes if any. As we have a dict of # {cls: instance}, we can override any class with the list @@ -1119,7 +1258,7 @@ class TelegramClient(TelegramBareClient): file=file_handle, mime_type=mime_type, attributes=list(attr_dict.values()), - caption=caption, + caption=caption or '', **input_kw ) @@ -1139,7 +1278,7 @@ class TelegramClient(TelegramBareClient): return msg - async def send_voice_note(self, entity, file, caption='', + async def send_voice_note(self, entity, file, caption=None, progress_callback=None, reply_to=None): """Wrapper method around .send_file() with is_voice_note=()""" return await self.send_file(entity, file, caption, @@ -1147,7 +1286,7 @@ class TelegramClient(TelegramBareClient): reply_to=reply_to, is_voice_note=()) # empty tuple is enough - async def _send_album(self, entity, files, caption='', + async def _send_album(self, entity, files, caption=None, progress_callback=None, reply_to=None): """Specialized version of .send_file for albums""" # We don't care if the user wants to avoid cache, we will use it @@ -1156,6 +1295,7 @@ class TelegramClient(TelegramBareClient): # cache only makes a difference for documents where the user may # want the attributes used on them to change. Caption's ignored. entity = await self.get_input_entity(entity) + caption = caption or '' reply_to = self._get_message_id(reply_to) # Need to upload the media first, but only if they're not cached yet @@ -1479,18 +1619,27 @@ class TelegramClient(TelegramBareClient): file_size = document.size + kind = 'document' possible_names = [] for attr in document.attributes: if isinstance(attr, DocumentAttributeFilename): possible_names.insert(0, attr.file_name) elif isinstance(attr, DocumentAttributeAudio): - possible_names.append('{} - {}'.format( - attr.performer, attr.title - )) + kind = 'audio' + if attr.performer and attr.title: + possible_names.append('{} - {}'.format( + attr.performer, attr.title + )) + elif attr.performer: + possible_names.append(attr.performer) + elif attr.title: + possible_names.append(attr.title) + elif attr.voice: + kind = 'voice' file = self._get_proper_filename( - file, 'document', utils.get_extension(document), + file, kind, utils.get_extension(document), date=date, possible_names=possible_names ) @@ -1711,28 +1860,19 @@ class TelegramClient(TelegramBareClient): # region Event handling - async def on(self, event): + def on(self, event): """ - - Turns the given entity into a valid Telegram user or chat. + Decorator helper method around add_event_handler(). Args: event (:obj:`_EventBuilder` | :obj:`type`): The event builder class or instance to be used, for instance ``events.NewMessage``. """ - if isinstance(event, type): - event = event() - - await event.resolve(self) - - def decorator(f): - self._event_builders.append((event, f)) + async def decorator(f): + await self.add_event_handler(f, event) return f - if self._on_handler not in self.updates.handlers: - self.add_update_handler(self._on_handler) - return decorator async def _on_handler(self, update): @@ -1742,6 +1882,48 @@ class TelegramClient(TelegramBareClient): event._client = self await callback(event) + async def add_event_handler(self, callback, event=None): + """ + Registers the given callback to be called on the specified event. + + Args: + callback (:obj:`callable`): + The callable function accepting one parameter to be used. + + event (:obj:`_EventBuilder` | :obj:`type`, optional): + The event builder class or instance to be used, + for instance ``events.NewMessage``. + + If left unspecified, ``events.Raw`` (the ``Update`` objects + with no further processing) will be passed instead. + """ + + self.updates.handler = self._on_handler + if isinstance(event, type): + event = event() + elif not event: + event = events.Raw() + + await event.resolve(self) + self._event_builders.append((event, callback)) + + def add_update_handler(self, handler): + """Adds an update handler (a function which takes a TLObject, + an update, as its parameter) and listens for updates""" + warnings.warn( + 'add_update_handler is deprecated, use the @client.on syntax ' + 'or add_event_handler(callback, events.Raw) instead (see ' + 'https://telethon.rtfd.io/en/latest/extra/basic/working-' + 'with-updates.html)' + ) + self.add_event_handler(handler, events.Raw) + + def remove_update_handler(self, handler): + pass + + def list_update_handlers(self): + return [] + # endregion # region Small utilities to make users' life easier @@ -1831,9 +2013,9 @@ class TelegramClient(TelegramBareClient): if user.phone == phone: return user else: - string, is_join_chat = utils.parse_username(string) + username, is_join_chat = utils.parse_username(string) if is_join_chat: - invite = await self(CheckChatInviteRequest(string)) + invite = await self(CheckChatInviteRequest(username)) if isinstance(invite, ChatInvite): raise ValueError( 'Cannot get entity from a channel ' @@ -1841,13 +2023,19 @@ class TelegramClient(TelegramBareClient): ) elif isinstance(invite, ChatInviteAlready): return invite.chat - else: - if string in ('me', 'self'): + elif username: + if username in ('me', 'self'): return await self.get_me() - result = await self(ResolveUsernameRequest(string)) + result = await self(ResolveUsernameRequest(username)) for entity in itertools.chain(result.users, result.chats): - if entity.username.lower() == string: + if entity.username.lower() == username: return entity + try: + # Nobody with this username, maybe it's an exact name/title + return await self.get_entity( + self.session.get_input_entity(string)) + except ValueError: + pass raise TypeError( 'Cannot turn "{}" into any entity (user or chat)'.format(string) diff --git a/telethon/update_state.py b/telethon/update_state.py index cc14e258..f52b0d42 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -16,15 +16,15 @@ class UpdateState: WORKER_POLL_TIMEOUT = 5.0 # Avoid waiting forever on the workers def __init__(self, loop=None): - self.handlers = [] + self.handler = None self._loop = loop if loop else asyncio.get_event_loop() # https://core.telegram.org/api/updates self._state = tl.updates.State(0, 0, datetime.now(), 0, 0) def handle_update(self, update): - for handler in self.handlers: - asyncio.ensure_future(handler(update), loop=self._loop) + if self.handler: + asyncio.ensure_future(self.handler(update), loop=self._loop) def process(self, update): """Processes an update object. This method is normally called by diff --git a/telethon/utils.py b/telethon/utils.py index 9460986c..fdadbd1c 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -27,9 +27,11 @@ from .tl.types import ( from .tl.types.contacts import ResolvedPeer USERNAME_RE = re.compile( - r'@|(?:https?://)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?' + r'@|(?:https?://)?(?:www\.)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?' ) +VALID_USERNAME_RE = re.compile(r'^[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]$') + def get_display_name(entity): """Gets the input peer for the given "entity" (user, chat or channel) @@ -323,12 +325,20 @@ def get_input_media(media, user_caption=None, is_photo=False): def is_image(file): """Returns True if the file extension looks like an image file""" - return (mimetypes.guess_type(file)[0] or '').startswith('image/') + return (isinstance(file, str) and + (mimetypes.guess_type(file)[0] or '').startswith('image/')) + + +def is_audio(file): + """Returns True if the file extension looks like an audio file""" + return (isinstance(file, str) and + (mimetypes.guess_type(file)[0] or '').startswith('audio/')) def is_video(file): """Returns True if the file extension looks like a video file""" - return (mimetypes.guess_type(file)[0] or '').startswith('video/') + return (isinstance(file, str) and + (mimetypes.guess_type(file)[0] or '').startswith('video/')) def parse_phone(phone): @@ -346,15 +356,23 @@ def parse_username(username): a string, username or URL. Returns a tuple consisting of both the stripped, lowercase username and whether it is a joinchat/ hash (in which case is not lowercase'd). + + Returns None if the username is not valid. """ username = username.strip() m = USERNAME_RE.match(username) if m: - result = username[m.end():] + username = username[m.end():] is_invite = bool(m.group(1)) - return result if is_invite else result.lower(), is_invite - else: + if is_invite: + return username, True + else: + username = username.rstrip('/') + + if VALID_USERNAME_RE.match(username): return username.lower(), False + else: + return None, False def get_peer_id(peer): diff --git a/telethon/version.py b/telethon/version.py index 4ead720b..8cc14b33 100644 --- a/telethon/version.py +++ b/telethon/version.py @@ -1,3 +1,3 @@ # Versions should comply with PEP440. # This line is parsed in setup.py: -__version__ = '0.17.2' +__version__ = '0.17.4' diff --git a/telethon_examples/interactive_telegram_client.py b/telethon_examples/interactive_telegram_client.py index d45d2ff1..f6986370 100644 --- a/telethon_examples/interactive_telegram_client.py +++ b/telethon_examples/interactive_telegram_client.py @@ -1,12 +1,13 @@ import os from getpass import getpass -from telethon import TelegramClient, ConnectionMode +from telethon.utils import get_display_name + +from telethon import ConnectionMode, TelegramClient from telethon.errors import SessionPasswordNeededError from telethon.tl.types import ( - UpdateShortChatMessage, UpdateShortMessage, PeerChat + PeerChat, UpdateShortChatMessage, UpdateShortMessage ) -from telethon.utils import get_display_name def sprint(string, *args, **kwargs): @@ -47,6 +48,7 @@ class InteractiveTelegramClient(TelegramClient): Telegram through Telethon, such as listing dialogs (open chats), talking to people, downloading media, and receiving updates. """ + def __init__(self, session_user_id, user_phone, api_id, api_hash, proxy=None): """ @@ -182,14 +184,15 @@ class InteractiveTelegramClient(TelegramClient): # Show some information print_title('Chat with "{}"'.format(get_display_name(entity))) print('Available commands:') - print(' !q: Quits the current chat.') - print(' !Q: Quits the current chat and exits.') - print(' !h: prints the latest messages (message History).') - print(' !up : Uploads and sends the Photo from path.') - print(' !uf : Uploads and sends the File from path.') - print(' !d : Deletes a message by its id') - print(' !dm : Downloads the given message Media (if any).') + print(' !q: Quits the current chat.') + print(' !Q: Quits the current chat and exits.') + print(' !h: prints the latest messages (message History).') + print(' !up : Uploads and sends the Photo from path.') + print(' !uf : Uploads and sends the File from path.') + print(' !d : Deletes a message by its id') + print(' !dm : Downloads the given message Media (if any).') print(' !dp: Downloads the current dialog Profile picture.') + print(' !i: Prints information about this chat..') print() # And start a while loop to chat @@ -234,8 +237,7 @@ class InteractiveTelegramClient(TelegramClient): # And print it to the user sprint('[{}:{}] (ID={}) {}: {}'.format( - msg.date.hour, msg.date.minute, msg.id, name, - content)) + msg.date.hour, msg.date.minute, msg.id, name, content)) # Send photo elif msg.startswith('!up '): @@ -264,12 +266,16 @@ class InteractiveTelegramClient(TelegramClient): os.makedirs('usermedia', exist_ok=True) output = self.download_profile_photo(entity, 'usermedia') if output: - print( - 'Profile picture downloaded to {}'.format(output) - ) + print('Profile picture downloaded to', output) else: print('No profile picture found for this user!') + elif msg == '!i': + attributes = list(entity.to_dict().items()) + pad = max(len(x) for x, _ in attributes) + for name, val in attributes: + print("{:<{width}} : {}".format(name, val, width=pad)) + # Send chat message (if any) elif msg: self.send_message(entity, msg, link_preview=False) @@ -356,6 +362,5 @@ class InteractiveTelegramClient(TelegramClient): else: who = self.get_entity(update.from_id) sprint('<< {} @ {} sent "{}"'.format( - get_display_name(which), get_display_name(who), - update.message + get_display_name(which), get_display_name(who), update.message )) diff --git a/telethon_examples/print_updates.py b/telethon_examples/print_updates.py index ab7ba1d4..4c676a81 100755 --- a/telethon_examples/print_updates.py +++ b/telethon_examples/print_updates.py @@ -1,46 +1,36 @@ #!/usr/bin/env python3 # A simple script to print all updates received -from getpass import getpass from os import environ + # environ is used to get API information from environment variables # You could also use a config file, pass them as arguments, # or even hardcode them (not recommended) from telethon import TelegramClient -from telethon.errors import SessionPasswordNeededError + def main(): session_name = environ.get('TG_SESSION', 'session') - user_phone = environ['TG_PHONE'] client = TelegramClient(session_name, int(environ['TG_API_ID']), environ['TG_API_HASH'], proxy=None, - update_workers=4) + update_workers=4, + spawn_read_thread=False) - print('INFO: Connecting to Telegram Servers...', end='', flush=True) - client.connect() - print('Done!') - - if not client.is_user_authorized(): - print('INFO: Unauthorized user') - client.send_code_request(user_phone) - code_ok = False - while not code_ok: - code = input('Enter the auth code: ') - try: - code_ok = client.sign_in(user_phone, code) - except SessionPasswordNeededError: - password = getpass('Two step verification enabled. Please enter your password: ') - code_ok = client.sign_in(password=password) - print('INFO: Client initialized succesfully!') + if 'TG_PHONE' in environ: + client.start(phone=environ['TG_PHONE']) + else: + client.start() client.add_update_handler(update_handler) - input('Press Enter to stop this!\n') + print('(Press Ctrl+C to stop this)') + client.idle() + def update_handler(update): print(update) - print('Press Enter to stop this!') + if __name__ == '__main__': main() diff --git a/telethon_examples/replier.py b/telethon_examples/replier.py index 66026363..ed4cc2fa 100755 --- a/telethon_examples/replier.py +++ b/telethon_examples/replier.py @@ -9,17 +9,12 @@ file, including TG_API_ID, TG_API_HASH, TG_PHONE and optionally TG_SESSION. This script assumes that you have certain files on the working directory, such as "xfiles.m4a" or "anytime.png" for some of the automated replies. """ -from getpass import getpass +import re from collections import defaultdict from datetime import datetime, timedelta from os import environ -import re - -from telethon import TelegramClient -from telethon.errors import SessionPasswordNeededError -from telethon.tl.types import UpdateNewChannelMessage, UpdateShortMessage, MessageService -from telethon.tl.functions.messages import EditMessageRequest +from telethon import TelegramClient, events, utils """Uncomment this for debugging import logging @@ -35,103 +30,57 @@ REACTS = {'emacs': 'Needs more vim', recent_reacts = defaultdict(list) -def update_handler(update): - global recent_reacts - try: - msg = update.message - except AttributeError: - # print(update, 'did not have update.message') - return - if isinstance(msg, MessageService): - print(msg, 'was service msg') - return +if __name__ == '__main__': + # TG_API_ID and TG_API_HASH *must* exist or this won't run! + session_name = environ.get('TG_SESSION', 'session') + client = TelegramClient( + session_name, int(environ['TG_API_ID']), environ['TG_API_HASH'], + spawn_read_thread=False, proxy=None, update_workers=4 + ) - # React to messages in supergroups and PMs - if isinstance(update, UpdateNewChannelMessage): - words = re.split('\W+', msg.message) + @client.on(events.NewMessage) + def my_handler(event: events.NewMessage.Event): + global recent_reacts + + # This utils function gets the unique identifier from peers (to_id) + to_id = utils.get_peer_id(event.message.to_id) + + # Through event.raw_text we access the text of messages without format + words = re.split('\W+', event.raw_text) + + # Try to match some reaction for trigger, response in REACTS.items(): - if len(recent_reacts[msg.to_id.channel_id]) > 3: + if len(recent_reacts[to_id]) > 3: # Silently ignore triggers if we've recently sent 3 reactions break if trigger in words: # Remove recent replies older than 10 minutes - recent_reacts[msg.to_id.channel_id] = [ - a for a in recent_reacts[msg.to_id.channel_id] if + recent_reacts[to_id] = [ + a for a in recent_reacts[to_id] if datetime.now() - a < timedelta(minutes=10) ] - # Send a reaction - client.send_message(msg.to_id, response, reply_to=msg.id) + # Send a reaction as a reply (otherwise, event.respond()) + event.reply(response) # Add this reaction to the list of recent actions - recent_reacts[msg.to_id.channel_id].append(datetime.now()) + recent_reacts[to_id].append(datetime.now()) - if isinstance(update, UpdateShortMessage): - words = re.split('\W+', msg) - for trigger, response in REACTS.items(): - if len(recent_reacts[update.user_id]) > 3: - # Silently ignore triggers if we've recently sent 3 reactions - break + # Automatically send relevant media when we say certain things + # When invoking requests, get_input_entity needs to be called manually + if event.out: + if event.raw_text.lower() == 'x files theme': + client.send_voice_note(event.message.to_id, 'xfiles.m4a', + reply_to=event.message.id) + if event.raw_text.lower() == 'anytime': + client.send_file(event.message.to_id, 'anytime.png', + reply_to=event.message.id) + if '.shrug' in event.text: + event.edit(event.text.replace('.shrug', r'¯\_(ツ)_/¯')) - if trigger in words: - # Send a reaction - client.send_message(update.user_id, response, reply_to=update.id) - # Add this reaction to the list of recent reactions - recent_reacts[update.user_id].append(datetime.now()) + if 'TG_PHONE' in environ: + client.start(phone=environ['TG_PHONE']) + else: + client.start() - # Automatically send relevant media when we say certain things - # When invoking requests, get_input_entity needs to be called manually - if isinstance(update, UpdateNewChannelMessage) and msg.out: - if msg.message.lower() == 'x files theme': - client.send_voice_note(msg.to_id, 'xfiles.m4a', reply_to=msg.id) - if msg.message.lower() == 'anytime': - client.send_file(msg.to_id, 'anytime.png', reply_to=msg.id) - if '.shrug' in msg.message: - client(EditMessageRequest( - client.get_input_entity(msg.to_id), msg.id, - message=msg.message.replace('.shrug', r'¯\_(ツ)_/¯') - )) - - if isinstance(update, UpdateShortMessage) and update.out: - if msg.lower() == 'x files theme': - client.send_voice_note(update.user_id, 'xfiles.m4a', reply_to=update.id) - if msg.lower() == 'anytime': - client.send_file(update.user_id, 'anytime.png', reply_to=update.id) - if '.shrug' in msg: - client(EditMessageRequest( - client.get_input_entity(update.user_id), update.id, - message=msg.replace('.shrug', r'¯\_(ツ)_/¯') - )) - - -if __name__ == '__main__': - session_name = environ.get('TG_SESSION', 'session') - user_phone = environ['TG_PHONE'] - client = TelegramClient( - session_name, int(environ['TG_API_ID']), environ['TG_API_HASH'], - proxy=None, update_workers=4 - ) - try: - print('INFO: Connecting to Telegram Servers...', end='', flush=True) - client.connect() - print('Done!') - - if not client.is_user_authorized(): - print('INFO: Unauthorized user') - client.send_code_request(user_phone) - code_ok = False - while not code_ok: - code = input('Enter the auth code: ') - try: - code_ok = client.sign_in(user_phone, code) - except SessionPasswordNeededError: - password = getpass('Two step verification enabled. ' - 'Please enter your password: ') - code_ok = client.sign_in(password=password) - print('INFO: Client initialized successfully!') - - client.add_update_handler(update_handler) - input('Press Enter to stop this!\n') - except KeyboardInterrupt: - pass - finally: - client.disconnect() + print('(Press Ctrl+C to stop this)') + client.idle() diff --git a/telethon_generator/scheme.tl b/telethon_generator/scheme.tl index 1d03c281..491f0c9e 100644 --- a/telethon_generator/scheme.tl +++ b/telethon_generator/scheme.tl @@ -53,7 +53,10 @@ destroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes; ---functions--- +// Deprecated since somewhere around February of 2018 +// See https://core.telegram.org/mtproto/auth_key req_pq#60469778 nonce:int128 = ResPQ; +req_pq_multi#be7e8ef1 nonce:int128 = ResPQ; req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:bytes q:bytes public_key_fingerprint:long encrypted_data:bytes = Server_DH_Params;