diff --git a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst index 5774f7ee..a99e4761 100644 --- a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst +++ b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst @@ -11,10 +11,10 @@ Accessing the Full API reason not to, like a method not existing or you wanting more control. -The ``TelegramClient`` doesn't offer a method for every single request -the Telegram API supports. However, it's very simple to *call* or *invoke* -any request. Whenever you need something, don't forget to `check the -documentation`__ and look for the `method you need`__. There you can go +The `telethon.telegram_client.TelegramClient` doesn't offer a method for every +single request the Telegram API supports. However, it's very simple to *call* +or *invoke* any request. Whenever you need something, don't forget to `check +the documentation`__ and look for the `method you need`__. There you can go through a sorted list of everything you can do. @@ -30,9 +30,9 @@ You should also refer to the documentation to see what the objects (constructors) Telegram returns look like. Every constructor inherits from a common type, and that's the reason for this distinction. -Say ``client.send_message()`` didn't exist, we could use the `search`__ -to look for "message". There we would find :tl:`SendMessageRequest`, -which we can work with. +Say `telethon.telegram_client.TelegramClient.send_message` didn't exist, +we could use the `search`__ to look for "message". There we would find +:tl:`SendMessageRequest`, which we can work with. Every request is a Python class, and has the parameters needed for you to invoke it. You can also call ``help(request)`` for information on @@ -63,7 +63,7 @@ construct one, for instance: peer = InputPeerUser(user_id, user_hash) -Or we call ``.get_input_entity()``: +Or we call `telethon.telegram_client.TelegramClient.get_input_entity()`: .. code-block:: python @@ -74,7 +74,7 @@ When you're going to invoke an API method, most require you to pass an ``.get_input_entity()`` is more straightforward (and often immediate, if you've seen the user before, know their ID, etc.). If you also need to have information about the whole user, use -``.get_entity()`` instead: +`telethon.telegram_client.TelegramClient.get_entity()` instead: .. code-block:: python @@ -83,7 +83,7 @@ If you also need to have information about the whole user, use In the later case, when you use the entity, the library will cast it to its "input" version for you. If you already have the complete user and want to cache its input version so the library doesn't have to do this -every time its used, simply call ``.get_input_peer``: +every time its used, simply call `telethon.utils.get_input_peer`: .. code-block:: python diff --git a/readthedocs/extra/advanced-usage/sessions.rst b/readthedocs/extra/advanced-usage/sessions.rst index a2031f16..1cbd0b3a 100644 --- a/readthedocs/extra/advanced-usage/sessions.rst +++ b/readthedocs/extra/advanced-usage/sessions.rst @@ -7,7 +7,7 @@ Session Files The first parameter you pass to the constructor of the ``TelegramClient`` is the ``session``, and defaults to be the session name (or full path). That is, if you create a ``TelegramClient('anon')`` instance and connect, an -``anon.session`` file will be created on the working directory. +``anon.session`` file will be created in the working directory. Note that if you pass a string it will be a file in the current working directory, although you can also pass absolute paths. diff --git a/readthedocs/extra/advanced-usage/update-modes.rst b/readthedocs/extra/advanced-usage/update-modes.rst index 5e613bb7..2fd6e036 100644 --- a/readthedocs/extra/advanced-usage/update-modes.rst +++ b/readthedocs/extra/advanced-usage/update-modes.rst @@ -26,7 +26,7 @@ That's it! This is the old way to listen for raw updates, with no further processing. If this feels annoying for you, remember that you can always use :ref:`working-with-updates` but maybe use this for some other cases. -Now let's do something more interesting. Every time an user talks to use, +Now let's do something more interesting. Every time an user talks to us, let's reply to them with the same text reversed: .. code-block:: python diff --git a/readthedocs/extra/basic/installation.rst b/readthedocs/extra/basic/installation.rst index 7a240eff..c8182dbb 100644 --- a/readthedocs/extra/basic/installation.rst +++ b/readthedocs/extra/basic/installation.rst @@ -58,8 +58,7 @@ Manual Installation 5. Done! -To generate the `method documentation`__, ``cd docs`` and then -``python3 generate.py`` (if some pages render bad do it twice). +To generate the `method documentation`__, ``python3 setup.py gen docs``. Optional dependencies diff --git a/readthedocs/extra/troubleshooting/deleted-limited-or-deactivated-accounts.rst b/readthedocs/extra/troubleshooting/deleted-limited-or-deactivated-accounts.rst index 6426ada9..671306c4 100644 --- a/readthedocs/extra/troubleshooting/deleted-limited-or-deactivated-accounts.rst +++ b/readthedocs/extra/troubleshooting/deleted-limited-or-deactivated-accounts.rst @@ -2,14 +2,15 @@ Deleted, Limited or Deactivated Accounts ======================================== -If you're from Iran or Russian, we have bad news for you. -Telegram is much more likely to ban these numbers, -as they are often used to spam other accounts, -likely through the use of libraries like this one. -The best advice we can give you is to not abuse the API, -like calling many requests really quickly, +If you're from Iran or Russia, we have bad news for you. Telegram is much more +likely to ban these numbers, as they are often used to spam other accounts, +likely through the use of libraries like this one. The best advice we can +give you is to not abuse the API, like calling many requests really quickly, and to sign up with these phones through an official application. +We have also had reports from Kazakhstan and China, where connecting +would fail. To solve these connection problems, you should use a proxy. + Telegram may also ban virtual (VoIP) phone numbers, as again, they're likely to be used for spam. diff --git a/telethon/events/messagedeleted.py b/telethon/events/messagedeleted.py index 4116f852..cbbac8cd 100644 --- a/telethon/events/messagedeleted.py +++ b/telethon/events/messagedeleted.py @@ -29,5 +29,12 @@ class MessageDeleted(EventBuilder): super().__init__( chat_peer=peer, msg_id=(deleted_ids or [0])[0] ) + if peer is None: + # If it's not a channel ID, then it was private/small group. + # We can't know which one was exactly unless we logged all + # messages, but we can indicate that it was maybe either of + # both by setting them both to True. + self.is_private = self.is_group = True + self.deleted_id = None if not deleted_ids else deleted_ids[0] self.deleted_ids = deleted_ids diff --git a/telethon/events/newmessage.py b/telethon/events/newmessage.py index 33d009dd..4476aee0 100644 --- a/telethon/events/newmessage.py +++ b/telethon/events/newmessage.py @@ -28,8 +28,16 @@ class NewMessage(EventBuilder): """ def __init__(self, incoming=None, outgoing=None, chats=None, blacklist_chats=False, pattern=None): + if incoming is not None and outgoing is None: + outgoing = not incoming + elif outgoing is not None and incoming is None: + incoming = not incoming + if incoming and outgoing: - raise ValueError('Can only set either incoming or outgoing') + self.incoming = self.outgoing = None # Same as no filter + elif all(x is not None and not x for x in (incoming, outgoing)): + raise ValueError("Don't create an event handler if you " + "don't want neither incoming or outgoing!") super().__init__(chats=chats, blacklist_chats=blacklist_chats) self.incoming = incoming diff --git a/telethon/network/authenticator.py b/telethon/network/authenticator.py index f905f0fc..0a05f624 100644 --- a/telethon/network/authenticator.py +++ b/telethon/network/authenticator.py @@ -11,8 +11,7 @@ from ..tl.types import ( ServerDHInnerData, ClientDHInnerData, DhGenOk, DhGenRetry, DhGenFail ) from .. import helpers as utils -from ..crypto import AES, AuthKey, Factorization -from ..crypto import rsa +from ..crypto import AES, AuthKey, Factorization, rsa from ..errors import SecurityError from ..extensions import BinaryReader from ..network import MtProtoPlainSender diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 20a64b23..5f46551f 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -963,7 +963,7 @@ class TelegramClient(TelegramBareClient): Raises: ``MessageAuthorRequiredError`` if you're not the author of the - message but try editing it anyway. + message but tried editing it anyway. ``MessageNotModifiedError`` if the contents of the message were not modified at all. @@ -1031,7 +1031,8 @@ class TelegramClient(TelegramBareClient): async def iter_messages(self, entity, limit=20, offset_date=None, offset_id=0, max_id=0, min_id=0, add_offset=0, search=None, filter=None, from_user=None, - batch_size=100, wait_time=None, _total=None): + batch_size=100, wait_time=None, ids=None, + _total=None): """ Iterator over the message history for the specified entity. @@ -1059,7 +1060,7 @@ class TelegramClient(TelegramBareClient): max_id (`int`): All the messages with a higher (newer) ID or equal to this will - be excluded + be excluded. min_id (`int`): All the messages with a lower (older) ID or equal to this will @@ -1091,6 +1092,15 @@ class TelegramClient(TelegramBareClient): If left to ``None``, it will default to 1 second only if the limit is higher than 3000. + ids (`int`, `list`): + A single integer ID (or several IDs) for the message that + should be returned. This parameter takes precedence over + the rest (which will be ignored if this is set). This can + for instance be used to get the message with ID 123 from + a channel. Note that if the message doesn't exist, ``None`` + will appear in its place, so that zipping the list of IDs + with the messages can match one-to-one. + _total (`list`, optional): A single-item list to pass the total parameter by reference. @@ -1110,6 +1120,23 @@ class TelegramClient(TelegramBareClient): you think may be good. """ entity = await self.get_input_entity(entity) + if ids: + if not utils.is_list_like(ids): + ids = (ids,) + async for x in self._iter_ids(entity, ids, total=_total): + await yield_(x) + return + + # Telegram doesn't like min_id/max_id. If these IDs are low enough + # (starting from last_id - 100), the request will return nothing. + # + # We can emulate their behaviour locally by setting offset = max_id + # and simply stopping once we hit a message with ID <= min_id. + offset_id = max(offset_id, max_id) + if offset_id and min_id: + if offset_id - min_id <= 1: + return + limit = float('inf') if limit is None else int(limit) if search is not None or filter or from_user: if filter is None: @@ -1123,8 +1150,8 @@ class TelegramClient(TelegramBareClient): offset_id=offset_id, add_offset=add_offset, limit=1, - max_id=max_id, - min_id=min_id, + max_id=0, + min_id=0, hash=0, from_id=self.get_input_entity(from_user) if from_user else None ) @@ -1134,8 +1161,8 @@ class TelegramClient(TelegramBareClient): limit=1, offset_date=offset_date, offset_id=offset_id, - min_id=min_id, - max_id=max_id, + min_id=0, + max_id=0, add_offset=add_offset, hash=0 ) @@ -1166,6 +1193,9 @@ class TelegramClient(TelegramBareClient): for x in itertools.chain(r.users, r.chats)} for message in r.messages: + if message.id <= min_id: + return + if isinstance(message, MessageEmpty) or message.id >= last_id: continue @@ -1175,27 +1205,7 @@ class TelegramClient(TelegramBareClient): # IDs are returned in descending order. last_id = message.id - # Add a few extra attributes to the Message to be friendlier. - # To make messages more friendly, always add message - # to service messages, and action to normal messages. - message.message = getattr(message, 'message', None) - message.action = getattr(message, 'action', None) - message.to = entities[utils.get_peer_id(message.to_id)] - message.sender = ( - None if not message.from_id else - entities[utils.get_peer_id(message.from_id)] - ) - if getattr(message, 'fwd_from', None): - message.fwd_from.sender = ( - None if not message.fwd_from.from_id else - entities[utils.get_peer_id(message.fwd_from.from_id)] - ) - message.fwd_from.channel = ( - None if not message.fwd_from.channel_id else - entities[utils.get_peer_id( - PeerChannel(message.fwd_from.channel_id) - )] - ) + self._make_message_friendly(message, entities) await yield_(message) have += 1 @@ -1210,18 +1220,92 @@ class TelegramClient(TelegramBareClient): await asyncio.sleep(max(wait_time - (time.time() - start), 0)) + @staticmethod + def _make_message_friendly(message, entities): + """ + Add a few extra attributes to the :tl:`Message` to be friendlier. + + To make messages more friendly, always add message + to service messages, and action to normal messages. + """ + # TODO Create an actual friendlier class + message.message = getattr(message, 'message', None) + message.action = getattr(message, 'action', None) + message.to = entities[utils.get_peer_id(message.to_id)] + message.sender = ( + None if not message.from_id else + entities[utils.get_peer_id(message.from_id)] + ) + if getattr(message, 'fwd_from', None): + message.fwd_from.sender = ( + None if not message.fwd_from.from_id else + entities[utils.get_peer_id(message.fwd_from.from_id)] + ) + message.fwd_from.channel = ( + None if not message.fwd_from.channel_id else + entities[utils.get_peer_id( + PeerChannel(message.fwd_from.channel_id) + )] + ) + + @async_generator + async def _iter_ids(self, entity, ids, total): + """ + Special case for `iter_messages` when it should only fetch some IDs. + """ + if total: + total[0] = len(ids) + + if isinstance(entity, InputPeerChannel): + r = await self(channels.GetMessagesRequest(entity, ids)) + else: + r = await self(messages.GetMessagesRequest(ids)) + + entities = {utils.get_peer_id(x): x + for x in itertools.chain(r.users, r.chats)} + + # Telegram seems to return the messages in the order in which + # we asked them for, so we don't need to check it ourselves. + for message in r.messages: + if isinstance(message, MessageEmpty): + await yield_(None) + else: + self._make_message_friendly(message, entities) + await yield_(message) + async def get_messages(self, *args, **kwargs): """ Same as :meth:`iter_messages`, but returns a list instead with an additional ``.total`` attribute on the list. + + If the `limit` is not set, it will be 1 by default unless both + `min_id` **and** `max_id` are set (as *named* arguments), in + which case the entire range will be returned. + + This is so because any integer limit would be rather arbitrary and + it's common to only want to fetch one message, but if a range is + specified it makes sense that it should return the entirety of it. + + If `ids` is present in the *named* arguments and is not a list, + a single :tl:`Message` will be returned for convenience instead + of a list. """ total = [0] kwargs['_total'] = total + if len(args) == 1 and 'limit' not in kwargs: + if 'min_id' in kwargs and 'max_id' in kwargs: + kwargs['limit'] = None + else: + kwargs['limit'] = 1 + msgs = UserList() async for msg in self.iter_messages(*args, **kwargs): msgs.append(msg) msgs.total = total[0] + if 'ids' in kwargs and not utils.is_list_like(kwargs['ids']): + return msgs[0] + return msgs async def get_message_history(self, *args, **kwargs): @@ -1666,7 +1750,7 @@ class TelegramClient(TelegramBareClient): if m.has('duration') else 0) ) else: - doc = DocumentAttributeVideo(0, 0, 0, + doc = DocumentAttributeVideo(0, 1, 1, round_message=video_note) attr_dict[DocumentAttributeVideo] = doc @@ -2448,7 +2532,7 @@ class TelegramClient(TelegramBareClient): async def catch_up(self): state = self.session.get_update_state(0) - if not state: + if not state or not state.pts: return self.session.catching_up = True diff --git a/telethon/utils.py b/telethon/utils.py index 0ba64177..ddf68cca 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -552,8 +552,13 @@ def resolve_id(marked_id): if marked_id >= 0: return marked_id, PeerUser - if str(marked_id).startswith('-100'): - return int(str(marked_id)[4:]), PeerChannel + # There have been report of chat IDs being 10000xyz, which means their + # marked version is -10000xyz, which in turn looks like a channel but + # it becomes 00xyz (= xyz). Hence, we must assert that there are only + # two zeroes. + m = re.match(r'-100([^0]\d*)', str(marked_id)) + if m: + return int(m.group(1)), PeerChannel return -marked_id, PeerChat diff --git a/telethon_examples/interactive_telegram_client.py b/telethon_examples/interactive_telegram_client.py index 1e4cf128..12e1b298 100644 --- a/telethon_examples/interactive_telegram_client.py +++ b/telethon_examples/interactive_telegram_client.py @@ -3,7 +3,8 @@ from getpass import getpass from telethon.utils import get_display_name -from telethon import ConnectionMode, TelegramClient +from telethon import TelegramClient, events +from telethon.network import ConnectionTcpAbridged from telethon.errors import SessionPasswordNeededError from telethon.tl.types import ( PeerChat, UpdateShortChatMessage, UpdateShortMessage @@ -70,11 +71,11 @@ class InteractiveTelegramClient(TelegramClient): # These parameters should be passed always, session name and API session_user_id, api_id, api_hash, - # You can optionally change the connection mode by using this enum. - # This changes how much data will be sent over the network with - # every request, and how it will be formatted. Default is - # ConnectionMode.TCP_FULL, and smallest is TCP_TCP_ABRIDGED. - connection_mode=ConnectionMode.TCP_ABRIDGED, + # You can optionally change the connection mode by passing a + # type or an instance of it. This changes how the sent packets + # look (low-level concept you normally shouldn't worry about). + # Default is ConnectionTcpFull, smallest is ConnectionTcpAbridged. + connection=ConnectionTcpAbridged, # If you're using a proxy, set it here. proxy=proxy, @@ -126,10 +127,11 @@ class InteractiveTelegramClient(TelegramClient): def run(self): """Main loop of the TelegramClient, will wait for user action""" - # Once everything is ready, we can add an update handler. Every - # update object will be passed to the self.update_handler method, - # where we can process it as we need. - self.add_update_handler(self.update_handler) + # Once everything is ready, we can add an event handler. + # + # Events are an abstraction over Telegram's "Updates" and + # are much easier to use. + self.add_event_handler(self.message_handler, events.NewMessage) # Enter a while loop to chat as long as the user wants while True: @@ -334,31 +336,29 @@ class InteractiveTelegramClient(TelegramClient): bytes_to_string(total_bytes), downloaded_bytes / total_bytes) ) - def update_handler(self, update): - """Callback method for received Updates""" + def message_handler(self, event): + """Callback method for received events.NewMessage""" - # We have full control over what we want to do with the updates. - # In our case we only want to react to chat messages, so we use - # isinstance() to behave accordingly on these cases. - if isinstance(update, UpdateShortMessage): - who = self.get_entity(update.user_id) - if update.out: + # Note that accessing ``.sender`` and ``.chat`` may be slow since + # these are not cached and must be queried always! However it lets + # us access the chat title and user name. + if event.is_group: + if event.out: + sprint('>> sent "{}" to chat {}'.format( + event.text, get_display_name(event.chat) + )) + else: + sprint('<< {} @ {} sent "{}"'.format( + get_display_name(event.sender), + get_display_name(event.chat), + event.text + )) + else: + if event.out: sprint('>> "{}" to user {}'.format( - update.message, get_display_name(who) + event.text, get_display_name(event.chat) )) else: sprint('<< {} sent "{}"'.format( - get_display_name(who), update.message - )) - - elif isinstance(update, UpdateShortChatMessage): - which = self.get_entity(PeerChat(update.chat_id)) - if update.out: - sprint('>> sent "{}" to chat {}'.format( - update.message, get_display_name(which) - )) - else: - who = self.get_entity(update.from_id) - sprint('<< {} @ {} sent "{}"'.format( - get_display_name(which), get_display_name(who), update.message + get_display_name(event.chat), event.text )) diff --git a/telethon_examples/print_updates.py b/telethon_examples/print_updates.py index 4c676a81..fff8f7e4 100755 --- a/telethon_examples/print_updates.py +++ b/telethon_examples/print_updates.py @@ -1,5 +1,9 @@ #!/usr/bin/env python3 # A simple script to print all updates received +# +# NOTE: To run this script you MUST have 'TG_API_ID' and 'TG_API_HASH' in +# your environment variables. This is a good way to use these private +# values. See https://superuser.com/q/284342. from os import environ @@ -23,7 +27,7 @@ def main(): else: client.start() - client.add_update_handler(update_handler) + client.add_event_handler(update_handler) print('(Press Ctrl+C to stop this)') client.idle() diff --git a/telethon_examples/replier.py b/telethon_examples/replier.py index ed4cc2fa..312ef2f1 100755 --- a/telethon_examples/replier.py +++ b/telethon_examples/replier.py @@ -2,9 +2,9 @@ """ A example script to automatically send messages based on certain triggers. -The script makes uses of environment variables to determine the API ID, -hash, phone and such to be used. You may want to add these to your .bashrc -file, including TG_API_ID, TG_API_HASH, TG_PHONE and optionally TG_SESSION. +NOTE: To run this script you MUST have 'TG_API_ID' and 'TG_API_HASH' in + your environment variables. This is a good way to use these private + values. See https://superuser.com/q/284342. 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. diff --git a/telethon_generator/data/html/core.html b/telethon_generator/data/html/core.html index 25295494..7d1dfc73 100644 --- a/telethon_generator/data/html/core.html +++ b/telethon_generator/data/html/core.html @@ -4,7 +4,36 @@