From b53ba81a6adf10268b01156812ab9a115ddf7522 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 3 Jan 2019 11:13:24 +0100 Subject: [PATCH 01/29] Add no_updates parameter in Client Useful to completely disable incoming updates for batch programs --- pyrogram/client/client.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 3dcaaf12..daef9d34 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -151,6 +151,12 @@ class Client(Methods, BaseClient): Define a custom directory for your plugins. The plugins directory is the location in your filesystem where Pyrogram will automatically load your update handlers. Defaults to None (plugins disabled). + + no_updates (``bool``, *optional*): + Pass True to completely disable incoming updates for the current session. + When updates are disabled your client can't receive any new message. + Useful for batch programs that don't need to deal with updates. + Defaults to False (updates enabled and always received). """ def __init__(self, @@ -173,7 +179,8 @@ class Client(Methods, BaseClient): workers: int = BaseClient.WORKERS, workdir: str = BaseClient.WORKDIR, config_file: str = BaseClient.CONFIG_FILE, - plugins_dir: str = None): + plugins_dir: str = None, + no_updates: bool = None): super().__init__() self.session_name = session_name @@ -197,6 +204,7 @@ class Client(Methods, BaseClient): self.workdir = workdir self.config_file = config_file self.plugins_dir = plugins_dir + self.no_updates = no_updates self.dispatcher = Dispatcher(self, workers) @@ -943,6 +951,9 @@ class Client(Methods, BaseClient): if not self.is_started: raise ConnectionError("Client has not been started") + if self.no_updates: + data = functions.InvokeWithoutUpdates(data) + r = self.session.send(data, retries, timeout) self.fetch_peers(getattr(r, "users", [])) From 7c008ca4e353a648de1ebd4ac3bc241ff589c5b7 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 3 Jan 2019 11:42:15 +0100 Subject: [PATCH 02/29] Add a bunch of takeout related errors --- compiler/error/source/400_BAD_REQUEST.tsv | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/error/source/400_BAD_REQUEST.tsv b/compiler/error/source/400_BAD_REQUEST.tsv index c0a5da73..1837d8df 100644 --- a/compiler/error/source/400_BAD_REQUEST.tsv +++ b/compiler/error/source/400_BAD_REQUEST.tsv @@ -80,4 +80,6 @@ USER_ADMIN_INVALID The action requires admin privileges INPUT_USER_DEACTIVATED The target user has been deactivated PASSWORD_RECOVERY_NA The password recovery e-mail is not available PASSWORD_EMPTY The password entered is empty -PHONE_NUMBER_FLOOD This number has tried to login too many times \ No newline at end of file +PHONE_NUMBER_FLOOD This number has tried to login too many times +TAKEOUT_INVALID The takeout id is invalid +TAKEOUT_REQUIRED The method must be invoked inside a takeout session \ No newline at end of file From 4f6990d735d63404ff7c5297e9c7b12d2f76599b Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 3 Jan 2019 12:20:42 +0100 Subject: [PATCH 03/29] Add takeout parameter in Client This lets the client use a takeout session instead of a normal one. Takeout sessions are useful for exporting Telegram data. Methods invoked inside a takeout session are less prone to throw FloodWait exceptions. --- pyrogram/client/client.py | 22 +++++++++++++++++++++- pyrogram/client/ext/base_client.py | 2 ++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index daef9d34..bd72c582 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -157,6 +157,13 @@ class Client(Methods, BaseClient): When updates are disabled your client can't receive any new message. Useful for batch programs that don't need to deal with updates. Defaults to False (updates enabled and always received). + + takeout (``bool``, *optional*): + Pass True to let the client use a takeout session instead of a normal one, implies no_updates. + Useful for exporting your Telegram data. Methods invoked inside a takeout session (such as get_history, + download_media, ...) are less prone to throw FloodWait exceptions. + Only available for users, bots will ignore this parameter. + Defaults to False (normal session). """ def __init__(self, @@ -180,7 +187,8 @@ class Client(Methods, BaseClient): workdir: str = BaseClient.WORKDIR, config_file: str = BaseClient.CONFIG_FILE, plugins_dir: str = None, - no_updates: bool = None): + no_updates: bool = None, + takeout: bool = None): super().__init__() self.session_name = session_name @@ -205,6 +213,7 @@ class Client(Methods, BaseClient): self.config_file = config_file self.plugins_dir = plugins_dir self.no_updates = no_updates + self.takeout = takeout self.dispatcher = Dispatcher(self, workers) @@ -261,6 +270,10 @@ class Client(Methods, BaseClient): self.save_session() if self.bot_token is None: + if self.takeout: + self.takeout_id = self.send(functions.account.InitTakeoutSession()).id + log.warning("Takeout session {} initiated".format(self.takeout_id)) + now = time.time() if abs(now - self.date) > Client.OFFLINE_SLEEP: @@ -316,6 +329,10 @@ class Client(Methods, BaseClient): if not self.is_started: raise ConnectionError("Client is already stopped") + if self.takeout_id: + self.send(functions.account.FinishTakeoutSession()) + log.warning("Takeout session {} finished".format(self.takeout_id)) + Syncer.remove(self) self.dispatcher.stop() @@ -954,6 +971,9 @@ class Client(Methods, BaseClient): if self.no_updates: data = functions.InvokeWithoutUpdates(data) + if self.takeout_id: + data = functions.InvokeWithTakeout(self.takeout_id, data) + r = self.session.send(data, retries, timeout) self.fetch_peers(getattr(r, "users", [])) diff --git a/pyrogram/client/ext/base_client.py b/pyrogram/client/ext/base_client.py index 7aee5302..cecbf5fb 100644 --- a/pyrogram/client/ext/base_client.py +++ b/pyrogram/client/ext/base_client.py @@ -90,6 +90,8 @@ class BaseClient: self.is_started = None self.is_idle = None + self.takeout_id = None + self.updates_queue = Queue() self.updates_workers_list = [] self.download_queue = Queue() From 9256185b5a59fa2f7959a4e5052e3a3b3e2d6f84 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 3 Jan 2019 18:27:47 +0100 Subject: [PATCH 04/29] Update copyright year --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 478683c3..f7d2b44e 100644 --- a/README.rst +++ b/README.rst @@ -61,7 +61,7 @@ and documentation. Any help is appreciated! Copyright & License ------------------- -- Copyright (C) 2017-2018 Dan Tès +- Copyright (C) 2017-2019 Dan Tès - Licensed under the terms of the `GNU Lesser General Public License v3 or later (LGPLv3+)`_ .. _`Telegram`: https://telegram.org/ From d69a93d253c203a43bd0c0da5bc21c08be3a4899 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 3 Jan 2019 20:53:48 +0100 Subject: [PATCH 05/29] Automatically cast message and caption arguments to str --- pyrogram/client/style/html.py | 10 +++++----- pyrogram/client/style/markdown.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyrogram/client/style/html.py b/pyrogram/client/style/html.py index 0014c4d9..9a72a565 100644 --- a/pyrogram/client/style/html.py +++ b/pyrogram/client/style/html.py @@ -38,12 +38,12 @@ class HTML: def __init__(self, peers_by_id): self.peers_by_id = peers_by_id - def parse(self, text): + def parse(self, message: str): entities = [] - text = utils.add_surrogates(text) + message = utils.add_surrogates(str(message)) offset = 0 - for match in self.HTML_RE.finditer(text): + for match in self.HTML_RE.finditer(message): start = match.start() - offset style, url, body = match.group(1, 3, 4) @@ -73,12 +73,12 @@ class HTML: continue entities.append(entity) - text = text.replace(match.group(), body) + message = message.replace(match.group(), body) offset += len(style) * 2 + 5 + (len(url) + 8 if url else 0) # TODO: OrderedDict to be removed in Python3.6 return OrderedDict([ - ("message", utils.remove_surrogates(text)), + ("message", utils.remove_surrogates(message)), ("entities", entities) ]) diff --git a/pyrogram/client/style/markdown.py b/pyrogram/client/style/markdown.py index 7bd96ed6..05a11a25 100644 --- a/pyrogram/client/style/markdown.py +++ b/pyrogram/client/style/markdown.py @@ -56,7 +56,7 @@ class Markdown: self.peers_by_id = peers_by_id def parse(self, message: str): - message = utils.add_surrogates(message).strip() + message = utils.add_surrogates(str(message)).strip() entities = [] offset = 0 From 36f987e9791af3843a41ea95982028961dcbc018 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 3 Jan 2019 20:58:38 +0100 Subject: [PATCH 06/29] Add Filters.me Useful to filter messages coming from the current running user. Does the same thing as Filters.user("me") --- pyrogram/client/filters/filters.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyrogram/client/filters/filters.py b/pyrogram/client/filters/filters.py index c0ebfdcf..ea6d7202 100644 --- a/pyrogram/client/filters/filters.py +++ b/pyrogram/client/filters/filters.py @@ -61,6 +61,9 @@ class Filters: create = create + me = create("Me", lambda _, m: bool(m.from_user and m.from_user.is_self)) + """Filter messages coming from you yourself""" + bot = create("Bot", lambda _, m: bool(m.from_user and m.from_user.is_bot)) """Filter messages coming from bots""" From fe4e8c5a4208d3446d5aa169242a39622918b4c8 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 4 Jan 2019 14:36:42 +0100 Subject: [PATCH 07/29] Rename get_history's "reversed" parameter to "reverse" It was colliding with the built-in "reversed" function --- pyrogram/client/methods/messages/get_history.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyrogram/client/methods/messages/get_history.py b/pyrogram/client/methods/messages/get_history.py index c2f4226b..14af0110 100644 --- a/pyrogram/client/methods/messages/get_history.py +++ b/pyrogram/client/methods/messages/get_history.py @@ -30,7 +30,7 @@ class GetHistory(BaseClient): offset: int = 0, offset_id: int = 0, offset_date: int = 0, - reversed: bool = False): + reverse: bool = False): """Use this method to retrieve the history of a chat. You can get up to 100 messages at once. @@ -55,7 +55,7 @@ class GetHistory(BaseClient): offset_date (``int``, *optional*): Pass a date in Unix time as offset to retrieve only older messages starting from that date. - reversed (``bool``, *optional*): + reverse (``bool``, *optional*): Pass True to retrieve the messages in reversed order (from older to most recent). Returns: @@ -72,7 +72,7 @@ class GetHistory(BaseClient): peer=self.resolve_peer(chat_id), offset_id=offset_id, offset_date=offset_date, - add_offset=offset - (limit if reversed else 0), + add_offset=offset - (limit if reverse else 0), limit=limit, max_id=0, min_id=0, @@ -81,7 +81,7 @@ class GetHistory(BaseClient): ) ) - if reversed: + if reverse: messages.messages.reverse() return messages From fe89974523c8ae8d604f96b6c53671fd7bc2ef37 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 4 Jan 2019 14:37:26 +0100 Subject: [PATCH 08/29] Add get_history signature to BaseClient Also make other method parameters generic --- pyrogram/client/ext/base_client.py | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/pyrogram/client/ext/base_client.py b/pyrogram/client/ext/base_client.py index cecbf5fb..93d4e57e 100644 --- a/pyrogram/client/ext/base_client.py +++ b/pyrogram/client/ext/base_client.py @@ -23,8 +23,6 @@ from threading import Lock from pyrogram import __version__ from ..style import Markdown, HTML -from ...api.core import Object -from ...session import Session from ...session.internals import MsgId @@ -99,33 +97,23 @@ class BaseClient: self.disconnect_handler = None - def send(self, data: Object, retries: int = Session.MAX_RETRIES, timeout: float = Session.WAIT_TIMEOUT): + def send(self, *args, **kwargs): pass - def resolve_peer(self, peer_id: int or str): + def resolve_peer(self, *args, **kwargs): pass - def fetch_peers(self, entities): + def fetch_peers(self, *args, **kwargs): pass - def add_handler(self, handler, group: int = 0) -> tuple: + def add_handler(self, *args, **kwargs): pass - def save_file( - self, - path: str, - file_id: int = None, - file_part: int = 0, - progress: callable = None, - progress_args: tuple = () - ): + def save_file(self, *args, **kwargs): pass - def get_messages( - self, - chat_id: int or str, - message_ids: int or list = None, - reply_to_message_ids: int or list = None, - replies: int = 1 - ): + def get_messages(self, *args, **kwargs): + pass + + def get_history(self, *args, **kwargs): pass From 7e3513f8ee0b94643c750dae4b127c6cebbaab7e Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 4 Jan 2019 14:38:08 +0100 Subject: [PATCH 09/29] Wait in case of flood errors in get_messages --- pyrogram/client/methods/messages/get_messages.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/methods/messages/get_messages.py b/pyrogram/client/methods/messages/get_messages.py index f5497629..f24e996f 100644 --- a/pyrogram/client/methods/messages/get_messages.py +++ b/pyrogram/client/methods/messages/get_messages.py @@ -16,12 +16,17 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . +import logging +import time from typing import Union, Iterable import pyrogram from pyrogram.api import functions, types +from pyrogram.api.errors import FloodWait from ...ext import BaseClient +log = logging.getLogger(__name__) + class GetMessages(BaseClient): def get_messages(self, @@ -78,6 +83,15 @@ class GetMessages(BaseClient): else: rpc = functions.messages.GetMessages(id=ids) - messages = pyrogram.Messages._parse(self, self.send(rpc), replies) + while True: + try: + r = self.send(rpc) + except FloodWait as e: + log.warning("Sleeping for {}s".format(e.x)) + time.sleep(e.x) + else: + break + + messages = pyrogram.Messages._parse(self, r, replies=replies) return messages if is_iterable else messages.messages[0] From 04542dbddf5ac4179ea3e8a950f062afaec95b4e Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 4 Jan 2019 14:42:39 +0100 Subject: [PATCH 10/29] Make parsing multiple Messages more efficient This is achieved by not calling get_messages for each single reply. Instead, all the available replies are retrieved in one request only. --- .../types/messages_and_media/messages.py | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/pyrogram/client/types/messages_and_media/messages.py b/pyrogram/client/types/messages_and_media/messages.py index 67dc2367..d89f0bad 100644 --- a/pyrogram/client/types/messages_and_media/messages.py +++ b/pyrogram/client/types/messages_and_media/messages.py @@ -52,9 +52,38 @@ class Messages(PyrogramType, Update): users = {i.id: i for i in messages.users} chats = {i.id: i for i in messages.chats} + total_count = getattr(messages, "count", len(messages.messages)) + + if not messages.messages: + return Messages( + total_count=total_count, + messages=[], + client=client + ) + + parsed_messages = [Message._parse(client, message, users, chats, replies=0) for message in messages.messages] + + if replies: + messages_with_replies = {i.id: getattr(i, "reply_to_msg_id", None) for i in messages.messages} + reply_message_ids = [i[0] for i in filter(lambda x: x[1] is not None, messages_with_replies.items())] + + if reply_message_ids: + reply_messages = client.get_messages( + parsed_messages[0].chat.id, + reply_to_message_ids=reply_message_ids, + replies=0 + ).messages + + for message in parsed_messages: + reply_id = messages_with_replies[message.message_id] + + for reply in reply_messages: + if reply.message_id == reply_id: + message.reply_to_message = reply + return Messages( - total_count=getattr(messages, "count", len(messages.messages)), - messages=[Message._parse(client, message, users, chats, replies) for message in messages.messages], + total_count=total_count, + messages=parsed_messages, client=client ) From fbc18cace0948b48c1569792cf8596d0ea45ec53 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 4 Jan 2019 15:36:43 +0100 Subject: [PATCH 11/29] Update get_history docstrings --- pyrogram/client/methods/messages/get_history.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyrogram/client/methods/messages/get_history.py b/pyrogram/client/methods/messages/get_history.py index 14af0110..d45623f4 100644 --- a/pyrogram/client/methods/messages/get_history.py +++ b/pyrogram/client/methods/messages/get_history.py @@ -31,9 +31,10 @@ class GetHistory(BaseClient): offset_id: int = 0, offset_date: int = 0, reverse: bool = False): - """Use this method to retrieve the history of a chat. + """Use this method to retrieve a chunk of the history of a chat. You can get up to 100 messages at once. + For a more convenient way of getting a chat history see :meth:`iter_history`. Args: chat_id (``int`` | ``str``): @@ -72,7 +73,7 @@ class GetHistory(BaseClient): peer=self.resolve_peer(chat_id), offset_id=offset_id, offset_date=offset_date, - add_offset=offset - (limit if reverse else 0), + add_offset=offset * (-1 if reverse else 1) - (limit if reverse else 0), limit=limit, max_id=0, min_id=0, From 8628d3a56d6fd36929468c13e2c6c73e2a2d89a2 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 4 Jan 2019 15:37:08 +0100 Subject: [PATCH 12/29] Add iter_history method For #170 --- docs/source/pyrogram/Client.rst | 1 + pyrogram/client/methods/messages/__init__.py | 4 +- .../client/methods/messages/iter_history.py | 93 +++++++++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 pyrogram/client/methods/messages/iter_history.py diff --git a/docs/source/pyrogram/Client.rst b/docs/source/pyrogram/Client.rst index 14796ef2..78b5a32b 100644 --- a/docs/source/pyrogram/Client.rst +++ b/docs/source/pyrogram/Client.rst @@ -62,6 +62,7 @@ Messages delete_messages get_messages get_history + iter_history send_poll vote_poll retract_vote diff --git a/pyrogram/client/methods/messages/__init__.py b/pyrogram/client/methods/messages/__init__.py index 237b6493..d5d81044 100644 --- a/pyrogram/client/methods/messages/__init__.py +++ b/pyrogram/client/methods/messages/__init__.py @@ -25,6 +25,7 @@ from .edit_message_text import EditMessageText from .forward_messages import ForwardMessages from .get_history import GetHistory from .get_messages import GetMessages +from .iter_history import IterHistory from .retract_vote import RetractVote from .send_animation import SendAnimation from .send_audio import SendAudio @@ -70,6 +71,7 @@ class Messages( SendPoll, VotePoll, RetractVote, - DownloadMedia + DownloadMedia, + IterHistory ): pass diff --git a/pyrogram/client/methods/messages/iter_history.py b/pyrogram/client/methods/messages/iter_history.py new file mode 100644 index 00000000..ab587988 --- /dev/null +++ b/pyrogram/client/methods/messages/iter_history.py @@ -0,0 +1,93 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2019 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from typing import Union, Generator + +import pyrogram +from ...ext import BaseClient + + +class IterHistory(BaseClient): + def iter_history(self, + chat_id: Union[int, str], + limit: int = 0, + offset: int = 0, + offset_id: int = 0, + offset_date: int = 0, + reverse: bool = False) -> Generator["pyrogram.Message", None, None]: + """Use this method to iterate through a chat history sequentially. + + This convenience method does the same as repeatedly calling :meth:`get_history` in a loop, thus saving you from + the hassle of setting up boilerplate code. It is useful for getting the whole chat history with a single call. + + Args: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + For your personal cloud (Saved Messages) you can simply use "me" or "self". + For a contact that exists in your Telegram address book you can use his phone number (str). + + limit (``int``, *optional*): + Limits the number of messages to be retrieved. + By default, no limit is applied and all messages are returned. + + offset (``int``, *optional*): + Sequential number of the first message to be returned.. + Negative values are also accepted and become useful in case you set offset_id or offset_date. + + offset_id (``int``, *optional*): + Identifier of the first message to be returned. + + offset_date (``int``, *optional*): + Pass a date in Unix time as offset to retrieve only older messages starting from that date. + + reverse (``bool``, *optional*): + Pass True to retrieve the messages in reversed order (from older to most recent). + + Returns: + A generator yielding :obj:`Message ` objects. + + Raises: + :class:`Error ` in case of a Telegram RPC error. + """ + offset_id = offset_id or (1 if reverse else 0) + current = 0 + total = limit or (1 << 31) - 1 + limit = min(100, total) + + while True: + messages = self.get_history( + chat_id=chat_id, + limit=limit, + offset=offset, + offset_id=offset_id, + offset_date=offset_date, + reverse=reverse + ).messages + + if not messages: + return + + offset_id = messages[-1].message_id + (1 if reverse else 0) + + for message in messages: + yield message + + current += 1 + + if current >= total: + return From 4d1d70082b31c03bae90056cd3df0c8db18a89cb Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 4 Jan 2019 16:08:05 +0100 Subject: [PATCH 13/29] Sleep in case of get_dialogs flood waits --- pyrogram/client/methods/chats/get_dialogs.py | 40 +++++++++++++------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/pyrogram/client/methods/chats/get_dialogs.py b/pyrogram/client/methods/chats/get_dialogs.py index f80518b0..35fc7752 100644 --- a/pyrogram/client/methods/chats/get_dialogs.py +++ b/pyrogram/client/methods/chats/get_dialogs.py @@ -16,10 +16,16 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . +import logging +import time + import pyrogram from pyrogram.api import functions, types +from pyrogram.api.errors import FloodWait from ...ext import BaseClient +log = logging.getLogger(__name__) + class GetDialogs(BaseClient): def get_dialogs(self, @@ -29,6 +35,7 @@ class GetDialogs(BaseClient): """Use this method to get the user's dialogs You can get up to 100 dialogs at once. + For a more convenient way of getting a user's dialogs see :meth:`iter_dialogs`. Args: offset_date (``int``): @@ -50,18 +57,25 @@ class GetDialogs(BaseClient): :class:`Error ` in case of a Telegram RPC error. """ - if pinned_only: - r = self.send(functions.messages.GetPinnedDialogs()) - else: - r = self.send( - functions.messages.GetDialogs( - offset_date=offset_date, - offset_id=0, - offset_peer=types.InputPeerEmpty(), - limit=limit, - hash=0, - exclude_pinned=True - ) - ) + while True: + try: + if pinned_only: + r = self.send(functions.messages.GetPinnedDialogs()) + else: + r = self.send( + functions.messages.GetDialogs( + offset_date=offset_date, + offset_id=0, + offset_peer=types.InputPeerEmpty(), + limit=limit, + hash=0, + exclude_pinned=True + ) + ) + except FloodWait as e: + log.warning("Sleeping {}s".format(e.x)) + time.sleep(e.x) + else: + break return pyrogram.Dialogs._parse(self, r) From 948f2b44ed69116e2c84b644a9951353d0e54dec Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 4 Jan 2019 16:10:34 +0100 Subject: [PATCH 14/29] Add iter_dialogs method. Reference #170 --- docs/source/pyrogram/Client.rst | 1 + pyrogram/client/ext/base_client.py | 3 + pyrogram/client/methods/chats/__init__.py | 4 +- pyrogram/client/methods/chats/iter_dialogs.py | 82 +++++++++++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 pyrogram/client/methods/chats/iter_dialogs.py diff --git a/docs/source/pyrogram/Client.rst b/docs/source/pyrogram/Client.rst index 78b5a32b..ff45fad1 100644 --- a/docs/source/pyrogram/Client.rst +++ b/docs/source/pyrogram/Client.rst @@ -93,6 +93,7 @@ Chats get_chat_members get_chat_members_count get_dialogs + iter_dialogs Users ----- diff --git a/pyrogram/client/ext/base_client.py b/pyrogram/client/ext/base_client.py index 93d4e57e..5e03e100 100644 --- a/pyrogram/client/ext/base_client.py +++ b/pyrogram/client/ext/base_client.py @@ -117,3 +117,6 @@ class BaseClient: def get_history(self, *args, **kwargs): pass + + def get_dialogs(self, *args, **kwargs): + pass diff --git a/pyrogram/client/methods/chats/__init__.py b/pyrogram/client/methods/chats/__init__.py index 745678cc..7f7761c5 100644 --- a/pyrogram/client/methods/chats/__init__.py +++ b/pyrogram/client/methods/chats/__init__.py @@ -24,6 +24,7 @@ from .get_chat_members import GetChatMembers from .get_chat_members_count import GetChatMembersCount from .get_chat_preview import GetChatPreview from .get_dialogs import GetDialogs +from .iter_dialogs import IterDialogs from .join_chat import JoinChat from .kick_chat_member import KickChatMember from .leave_chat import LeaveChat @@ -56,6 +57,7 @@ class Chats( UnpinChatMessage, GetDialogs, GetChatMembersCount, - GetChatPreview + GetChatPreview, + IterDialogs ): pass diff --git a/pyrogram/client/methods/chats/iter_dialogs.py b/pyrogram/client/methods/chats/iter_dialogs.py new file mode 100644 index 00000000..6058cd17 --- /dev/null +++ b/pyrogram/client/methods/chats/iter_dialogs.py @@ -0,0 +1,82 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2019 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from typing import Generator + +import pyrogram +from ...ext import BaseClient + + +class IterDialogs(BaseClient): + def iter_dialogs(self, + offset_date: int = 0, + limit: int = 0) -> Generator["pyrogram.Dialog", None, None]: + """Use this method to iterate through a user's dialogs sequentially. + + This convenience method does the same as repeatedly calling :meth:`get_dialogs` in a loop, thus saving you from + the hassle of setting up boilerplate code. It is useful for getting the whole dialogs list with a single call. + + Args: + offset_date (``int``): + The offset date in Unix time taken from the top message of a :obj:`Dialog`. + Defaults to 0 (most recent dialog). + + limit (``str``, *optional*): + Limits the number of dialogs to be retrieved. + By default, no limit is applied and all dialogs are returned. + + Returns: + A generator yielding :obj:`Dialog ` objects. + + Raises: + :class:`Error ` in case of a Telegram RPC error. + """ + current = 0 + total = limit or (1 << 31) - 1 + limit = min(100, total) + + pinned_dialogs = self.get_dialogs( + pinned_only=True + ).dialogs + + for dialog in pinned_dialogs: + yield dialog + + current += 1 + + if current >= total: + return + + while True: + dialogs = self.get_dialogs( + offset_date=offset_date, + limit=limit + ).dialogs + + if not dialogs: + return + + offset_date = dialogs[-1].top_message.date + + for dialog in dialogs: + yield dialog + + current += 1 + + if current >= total: + return From e1cac13f0d35b5a24546000d08108c512733a5d7 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 4 Jan 2019 16:58:57 +0100 Subject: [PATCH 15/29] Fix get_dialogs docstrings --- pyrogram/client/methods/chats/get_dialogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/client/methods/chats/get_dialogs.py b/pyrogram/client/methods/chats/get_dialogs.py index 35fc7752..c5fe6cfb 100644 --- a/pyrogram/client/methods/chats/get_dialogs.py +++ b/pyrogram/client/methods/chats/get_dialogs.py @@ -32,7 +32,7 @@ class GetDialogs(BaseClient): offset_date: int = 0, limit: int = 100, pinned_only: bool = False) -> "pyrogram.Dialogs": - """Use this method to get the user's dialogs + """Use this method to get a chunk of the user's dialogs You can get up to 100 dialogs at once. For a more convenient way of getting a user's dialogs see :meth:`iter_dialogs`. From d7e3397050c9f0d718b550b8b8cababc918688eb Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 4 Jan 2019 16:59:36 +0100 Subject: [PATCH 16/29] Update get_chat_members docstrings --- pyrogram/client/methods/chats/get_chat_members.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/methods/chats/get_chat_members.py b/pyrogram/client/methods/chats/get_chat_members.py index 1d99ec4b..382d7f0f 100644 --- a/pyrogram/client/methods/chats/get_chat_members.py +++ b/pyrogram/client/methods/chats/get_chat_members.py @@ -39,10 +39,12 @@ class GetChatMembers(BaseClient): limit: int = 200, query: str = "", filter: str = Filters.ALL) -> "pyrogram.ChatMembers": - """Use this method to get the members list of a chat. + """Use this method to get a chunk of the members list of a chat. + You can get up to 200 chat members at once. A chat can be either a basic group, a supergroup or a channel. You must be admin to retrieve the members list of a channel (also known as "subscribers"). + For a more convenient way of getting chat members see :meth:`iter_chat_members`. Args: chat_id (``int`` | ``str``): From 153439ac88e5e130deee6186d8dd946b79152a17 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 4 Jan 2019 17:13:44 +0100 Subject: [PATCH 17/29] Add iter_chat_members method. Reference #170 --- docs/source/pyrogram/Client.rst | 1 + pyrogram/client/ext/base_client.py | 3 + pyrogram/client/methods/chats/__init__.py | 4 +- .../client/methods/chats/iter_chat_members.py | 124 ++++++++++++++++++ 4 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 pyrogram/client/methods/chats/iter_chat_members.py diff --git a/docs/source/pyrogram/Client.rst b/docs/source/pyrogram/Client.rst index ff45fad1..0d49068a 100644 --- a/docs/source/pyrogram/Client.rst +++ b/docs/source/pyrogram/Client.rst @@ -92,6 +92,7 @@ Chats get_chat_member get_chat_members get_chat_members_count + iter_chat_members get_dialogs iter_dialogs diff --git a/pyrogram/client/ext/base_client.py b/pyrogram/client/ext/base_client.py index 5e03e100..aaed5786 100644 --- a/pyrogram/client/ext/base_client.py +++ b/pyrogram/client/ext/base_client.py @@ -120,3 +120,6 @@ class BaseClient: def get_dialogs(self, *args, **kwargs): pass + + def get_chat_members(self, *args, **kwargs): + pass diff --git a/pyrogram/client/methods/chats/__init__.py b/pyrogram/client/methods/chats/__init__.py index 7f7761c5..6cc034e4 100644 --- a/pyrogram/client/methods/chats/__init__.py +++ b/pyrogram/client/methods/chats/__init__.py @@ -24,6 +24,7 @@ from .get_chat_members import GetChatMembers from .get_chat_members_count import GetChatMembersCount from .get_chat_preview import GetChatPreview from .get_dialogs import GetDialogs +from .iter_chat_members import IterChatMembers from .iter_dialogs import IterDialogs from .join_chat import JoinChat from .kick_chat_member import KickChatMember @@ -58,6 +59,7 @@ class Chats( GetDialogs, GetChatMembersCount, GetChatPreview, - IterDialogs + IterDialogs, + IterChatMembers ): pass diff --git a/pyrogram/client/methods/chats/iter_chat_members.py b/pyrogram/client/methods/chats/iter_chat_members.py new file mode 100644 index 00000000..bdd8d117 --- /dev/null +++ b/pyrogram/client/methods/chats/iter_chat_members.py @@ -0,0 +1,124 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2019 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from string import ascii_lowercase +from typing import Union, Generator + +import pyrogram +from ...ext import BaseClient + + +class Filters: + ALL = "all" + KICKED = "kicked" + RESTRICTED = "restricted" + BOTS = "bots" + RECENT = "recent" + ADMINISTRATORS = "administrators" + + +QUERIES = [""] + [str(i) for i in range(10)] + list(ascii_lowercase) +QUERYABLE_FILTERS = (Filters.ALL, Filters.KICKED, Filters.RESTRICTED) + + +class IterChatMembers(BaseClient): + def iter_chat_members(self, + chat_id: Union[int, str], + limit: int = 0, + query: str = "", + filter: str = Filters.ALL) -> Generator["pyrogram.ChatMember", None, None]: + """Use this method to iterate through the members of a chat sequentially. + + This convenience method does the same as repeatedly calling :meth:`get_chat_members` in a loop, thus saving you + from the hassle of setting up boilerplate code. It is useful for getting the whole members list of a chat with + a single call. + + Args: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + + limit (``int``, *optional*): + Limits the number of members to be retrieved. + By default, no limit is applied and all members are returned. + + query (``str``, *optional*): + Query string to filter members based on their display names and usernames. + Defaults to "" (empty string) [2]_. + + filter (``str``, *optional*): + Filter used to select the kind of members you want to retrieve. Only applicable for supergroups + and channels. It can be any of the followings: + *"all"* - all kind of members, + *"kicked"* - kicked (banned) members only, + *"restricted"* - restricted members only, + *"bots"* - bots only, + *"recent"* - recent members only, + *"administrators"* - chat administrators only. + Defaults to *"all"*. + + .. [1] Server limit: on supergroups, you can get up to 10,000 members for a single query and up to 200 members + on channels. + + .. [2] A query string is applicable only for *"all"*, *"kicked"* and *"restricted"* filters only. + + Returns: + A generator yielding :obj:`ChatMember ` objects. + + Raises: + :class:`Error ` in case of a Telegram RPC error. + """ + current = 0 + yielded = set() + queries = [query] if query else QUERIES + total = limit or (1 << 31) - 1 + limit = min(200, total) + + if filter not in QUERYABLE_FILTERS: + queries = [""] + + for q in queries: + offset = 0 + + while True: + chat_members = self.get_chat_members( + chat_id=chat_id, + offset=offset, + limit=limit, + query=q, + filter=filter + ).chat_members + + if not chat_members: + break + + offset += len(chat_members) + + for chat_member in chat_members: + user_id = chat_member.user.id + + if user_id in yielded: + continue + + yield chat_member + + yielded.add(chat_member.user.id) + + current += 1 + + if current >= total: + return From b8a3d02eef230b7a52d8a9dc785914b1e2bdf6ab Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 4 Jan 2019 23:12:06 +0100 Subject: [PATCH 18/29] Fix get_messages return type --- pyrogram/client/methods/messages/get_messages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/client/methods/messages/get_messages.py b/pyrogram/client/methods/messages/get_messages.py index f24e996f..b1c90d10 100644 --- a/pyrogram/client/methods/messages/get_messages.py +++ b/pyrogram/client/methods/messages/get_messages.py @@ -33,7 +33,7 @@ class GetMessages(BaseClient): chat_id: Union[int, str], message_ids: Union[int, Iterable[int]] = None, reply_to_message_ids: Union[int, Iterable[int]] = None, - replies: int = 1) -> "pyrogram.Messages": + replies: int = 1) -> Union["pyrogram.Message", "pyrogram.Messages"]: """Use this method to get one or more messages that belong to a specific chat. You can retrieve up to 200 messages at once. From 7e354b12bfac977a9c4e5d1abc3f637b9c1dc117 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 4 Jan 2019 23:15:57 +0100 Subject: [PATCH 19/29] Fix kick_chat_member docstrings and return type --- pyrogram/client/methods/chats/kick_chat_member.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyrogram/client/methods/chats/kick_chat_member.py b/pyrogram/client/methods/chats/kick_chat_member.py index b02e02cc..4cd66ec4 100644 --- a/pyrogram/client/methods/chats/kick_chat_member.py +++ b/pyrogram/client/methods/chats/kick_chat_member.py @@ -27,7 +27,7 @@ class KickChatMember(BaseClient): def kick_chat_member(self, chat_id: Union[int, str], user_id: Union[int, str], - until_date: int = 0) -> "pyrogram.Message": + until_date: int = 0) -> Union["pyrogram.Message", bool]: """Use this method to kick a user from a group, a supergroup or a channel. In the case of supergroups and channels, the user will not be able to return to the group on their own using invite links, etc., unless unbanned first. You must be an administrator in the chat for this to work and must @@ -52,7 +52,7 @@ class KickChatMember(BaseClient): considered to be banned forever. Defaults to 0 (ban forever). Returns: - True on success. + On success, either True or a service :obj:`Message ` will be returned (when applicable). Raises: :class:`Error ` in case of a Telegram RPC error. @@ -93,3 +93,5 @@ class KickChatMember(BaseClient): {i.id: i for i in r.users}, {i.id: i for i in r.chats} ) + else: + return True From 999b9ce667e5723fba63b2685fd281e0485111d3 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 5 Jan 2019 12:06:54 +0100 Subject: [PATCH 20/29] Add MESSAGE_POLL_CLOSED error --- compiler/error/source/400_BAD_REQUEST.tsv | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/error/source/400_BAD_REQUEST.tsv b/compiler/error/source/400_BAD_REQUEST.tsv index 1837d8df..f0e68e52 100644 --- a/compiler/error/source/400_BAD_REQUEST.tsv +++ b/compiler/error/source/400_BAD_REQUEST.tsv @@ -82,4 +82,5 @@ PASSWORD_RECOVERY_NA The password recovery e-mail is not available PASSWORD_EMPTY The password entered is empty PHONE_NUMBER_FLOOD This number has tried to login too many times TAKEOUT_INVALID The takeout id is invalid -TAKEOUT_REQUIRED The method must be invoked inside a takeout session \ No newline at end of file +TAKEOUT_REQUIRED The method must be invoked inside a takeout session +MESSAGE_POLL_CLOSED You can't interact with a closed poll \ No newline at end of file From 0716380737e4818569cc86a1e19ce25e477e5223 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 5 Jan 2019 12:26:05 +0100 Subject: [PATCH 21/29] Add MEDIA_INVALID error --- compiler/error/source/400_BAD_REQUEST.tsv | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/error/source/400_BAD_REQUEST.tsv b/compiler/error/source/400_BAD_REQUEST.tsv index f0e68e52..f01439f5 100644 --- a/compiler/error/source/400_BAD_REQUEST.tsv +++ b/compiler/error/source/400_BAD_REQUEST.tsv @@ -83,4 +83,5 @@ PASSWORD_EMPTY The password entered is empty PHONE_NUMBER_FLOOD This number has tried to login too many times TAKEOUT_INVALID The takeout id is invalid TAKEOUT_REQUIRED The method must be invoked inside a takeout session -MESSAGE_POLL_CLOSED You can't interact with a closed poll \ No newline at end of file +MESSAGE_POLL_CLOSED You can't interact with a closed poll +MEDIA_INVALID The media is invalid \ No newline at end of file From a50dba2b4cb7bfb22f9a42be37ed4c9c3773bf84 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 5 Jan 2019 14:44:10 +0100 Subject: [PATCH 22/29] Add close_poll method --- pyrogram/client/methods/messages/__init__.py | 2 + .../client/methods/messages/close_poll.py | 65 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 pyrogram/client/methods/messages/close_poll.py diff --git a/pyrogram/client/methods/messages/__init__.py b/pyrogram/client/methods/messages/__init__.py index d5d81044..f76d0a22 100644 --- a/pyrogram/client/methods/messages/__init__.py +++ b/pyrogram/client/methods/messages/__init__.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . +from .close_poll import ClosePoll from .delete_messages import DeleteMessages from .download_media import DownloadMedia from .edit_message_caption import EditMessageCaption @@ -70,6 +71,7 @@ class Messages( SendVoice, SendPoll, VotePoll, + ClosePoll, RetractVote, DownloadMedia, IterHistory diff --git a/pyrogram/client/methods/messages/close_poll.py b/pyrogram/client/methods/messages/close_poll.py new file mode 100644 index 00000000..c2d2706b --- /dev/null +++ b/pyrogram/client/methods/messages/close_poll.py @@ -0,0 +1,65 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2019 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from typing import Union + +from pyrogram.api import functions, types +from pyrogram.client.ext import BaseClient + + +class ClosePoll(BaseClient): + def close_poll(self, + chat_id: Union[int, str], + message_id: id) -> bool: + """Use this method to close (stop) a poll. + + Closed polls can't be reopened and nobody will be able to vote in it anymore. + + Args: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + For your personal cloud (Saved Messages) you can simply use "me" or "self". + For a contact that exists in your Telegram address book you can use his phone number (str). + + message_id (``int``): + Unique poll message identifier inside this chat. + + Returns: + On success, True is returned. + + Raises: + :class:`Error ` in case of a Telegram RPC error. + """ + poll = self.get_messages(chat_id, message_id).poll + + self.send( + functions.messages.EditMessage( + peer=self.resolve_peer(chat_id), + id=message_id, + media=types.InputMediaPoll( + poll=types.Poll( + id=poll.id, + closed=True, + question="", + answers=[] + ) + ) + ) + ) + + return True From c7b1d6f70a97674fc59afc21b1334cd10f024b87 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 5 Jan 2019 15:26:40 +0100 Subject: [PATCH 23/29] Take into account that flags:# could be not always the first argument For instance, in Layer 91, Poll's flags:# is at the second position. This mess also happened in the past (thanks tg devs) and eventually will be fixed again with the next Layer update, but from now on Pyrogram will be able to correctly generate code even in such cases. --- compiler/api/compiler.py | 52 +++++++++++++++++-------------- compiler/api/template/mtproto.txt | 2 -- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/compiler/api/compiler.py b/compiler/api/compiler.py index 57e4d22b..9e671e80 100644 --- a/compiler/api/compiler.py +++ b/compiler/api/compiler.py @@ -26,7 +26,7 @@ NOTICE_PATH = "NOTICE" SECTION_RE = re.compile(r"---(\w+)---") LAYER_RE = re.compile(r"//\sLAYER\s(\d+)") COMBINATOR_RE = re.compile(r"^([\w.]+)#([0-9a-f]+)\s(?:.*)=\s([\w<>.]+);(?: // Docs: (.+))?$", re.MULTILINE) -ARGS_RE = re.compile("[^{](\w+):([\w?!.<>]+)") +ARGS_RE = re.compile("[^{](\w+):([\w?!.<>#]+)") FLAGS_RE = re.compile(r"flags\.(\d+)\?") FLAGS_RE_2 = re.compile(r"flags\.(\d+)\?([\w<>.]+)") FLAGS_RE_3 = re.compile(r"flags:#") @@ -288,17 +288,20 @@ def start(): sorted_args = sort_args(c.args) arguments = ", " + ", ".join( - [get_argument_type(i) for i in sorted_args] + [get_argument_type(i) for i in sorted_args if i != ("flags", "#")] ) if c.args else "" fields = "\n ".join( - ["self.{0} = {0} # {1}".format(i[0], i[1]) for i in c.args] + ["self.{0} = {0} # {1}".format(i[0], i[1]) for i in c.args if i != ("flags", "#")] ) if c.args else "pass" docstring_args = [] docs = c.docs.split("|")[1:] if c.docs else None for i, arg in enumerate(sorted_args): + if arg == ("flags", "#"): + continue + arg_name, arg_type = arg is_optional = FLAGS_RE.match(arg_type) flag_number = is_optional.group(1) if is_optional else -1 @@ -338,28 +341,31 @@ def start(): if references: docstring_args += "\n\n See Also:\n This object can be returned by " + references + "." - if c.has_flags: - write_flags = [] - for i in c.args: - flag = FLAGS_RE.match(i[1]) - if flag: - write_flags.append("flags |= (1 << {}) if self.{} is not None else 0".format(flag.group(1), i[0])) - - write_flags = "\n ".join([ - "flags = 0", - "\n ".join(write_flags), - "b.write(Int(flags))" - ]) - else: - write_flags = "# No flags" - - read_flags = "flags = Int.read(b)" if c.has_flags else "# No flags" - - write_types = read_types = "" + write_types = read_types = "" if c.has_flags else "# No flags\n " for arg_name, arg_type in c.args: flag = FLAGS_RE_2.findall(arg_type) + if arg_name == "flags" and arg_type == "#": + write_flags = [] + + for i in c.args: + flag = FLAGS_RE.match(i[1]) + if flag: + write_flags.append( + "flags |= (1 << {}) if self.{} is not None else 0".format(flag.group(1), i[0])) + + write_flags = "\n ".join([ + "flags = 0", + "\n ".join(write_flags), + "b.write(Int(flags))\n " + ]) + + write_types += write_flags + read_types += "flags = Int.read(b)\n " + + continue + if flag: index, flag_type = flag[0] @@ -448,11 +454,9 @@ def start(): object_id=c.id, arguments=arguments, fields=fields, - read_flags=read_flags, read_types=read_types, - write_flags=write_flags, write_types=write_types, - return_arguments=", ".join([i[0] for i in sorted_args]) + return_arguments=", ".join([i[0] for i in sorted_args if i != ("flags", "#")]) ) ) diff --git a/compiler/api/template/mtproto.txt b/compiler/api/template/mtproto.txt index 368d4712..9a65b52d 100644 --- a/compiler/api/template/mtproto.txt +++ b/compiler/api/template/mtproto.txt @@ -16,7 +16,6 @@ class {class_name}(Object): @staticmethod def read(b: BytesIO, *args) -> "{class_name}": - {read_flags} {read_types} return {class_name}({return_arguments}) @@ -24,6 +23,5 @@ class {class_name}(Object): b = BytesIO() b.write(Int(self.ID, False)) - {write_flags} {write_types} return b.getvalue() From 7cb1c99e286ddae9d4c604e3952e7d43e8d0b7f9 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 5 Jan 2019 18:29:48 +0100 Subject: [PATCH 24/29] Update copyright year --- pyrogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index abeb8b43..abc81b3d 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -18,7 +18,7 @@ import sys -__copyright__ = "Copyright (C) 2017-2018 Dan Tès ".replace( +__copyright__ = "Copyright (C) 2017-2019 Dan Tès ".replace( "\xe8", "e" if sys.getfilesystemencoding() != "utf-8" else "\xe8" ) From 7d061a1b5c0d7155ac27bb8e567509d25085da7c Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 5 Jan 2019 23:11:39 +0100 Subject: [PATCH 25/29] Add Game type --- pyrogram/__init__.py | 2 +- pyrogram/client/types/__init__.py | 2 +- .../types/messages_and_media/__init__.py | 1 + .../client/types/messages_and_media/game.py | 98 +++++++++++++++++++ 4 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 pyrogram/client/types/messages_and_media/game.py diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index abc81b3d..281da114 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -32,7 +32,7 @@ from .client.types import ( Location, Message, MessageEntity, Dialog, Dialogs, Photo, PhotoSize, Sticker, User, UserStatus, UserProfilePhotos, Venue, Animation, Video, VideoNote, Voice, CallbackQuery, Messages, ForceReply, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove, - Poll, PollOption, ChatPreview, StopPropagation + Poll, PollOption, ChatPreview, StopPropagation, Game ) from .client import ( Client, ChatAction, ParseMode, Emoji, diff --git a/pyrogram/client/types/__init__.py b/pyrogram/client/types/__init__.py index 0f4628ce..65a9165a 100644 --- a/pyrogram/client/types/__init__.py +++ b/pyrogram/client/types/__init__.py @@ -31,7 +31,7 @@ from .input_media import ( from .messages_and_media import ( Audio, Contact, Document, Animation, Location, Photo, PhotoSize, Sticker, Venue, Video, VideoNote, Voice, UserProfilePhotos, - Message, Messages, MessageEntity, Poll, PollOption + Message, Messages, MessageEntity, Poll, PollOption, Game ) from .user_and_chats import ( Chat, ChatMember, ChatMembers, ChatPhoto, diff --git a/pyrogram/client/types/messages_and_media/__init__.py b/pyrogram/client/types/messages_and_media/__init__.py index b6847bdb..3c0b3c98 100644 --- a/pyrogram/client/types/messages_and_media/__init__.py +++ b/pyrogram/client/types/messages_and_media/__init__.py @@ -34,3 +34,4 @@ from .venue import Venue from .video import Video from .video_note import VideoNote from .voice import Voice +from .game import Game diff --git a/pyrogram/client/types/messages_and_media/game.py b/pyrogram/client/types/messages_and_media/game.py new file mode 100644 index 00000000..01af7226 --- /dev/null +++ b/pyrogram/client/types/messages_and_media/game.py @@ -0,0 +1,98 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2019 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +import pyrogram +from pyrogram.api import types +from .animation import Animation +from .photo import Photo +from ..pyrogram_type import PyrogramType + + +class Game(PyrogramType): + """This object represents a game. + Use BotFather to create and edit games, their short names will act as unique identifiers. + + Args: + id (``int``): + Unique identifier of the game. + + title (``str``): + Title of the game. + + short_name (``str``): + Unique short name of the game. + + description (``str``): + Description of the game. + + photo (:obj:`Photo `): + Photo that will be displayed in the game message in chats. + + animation (:obj:`Animation `, *optional*): + Animation that will be displayed in the game message in chats. + Upload via BotFather. + """ + + def __init__(self, + *, + client: "pyrogram.client.ext.BaseClient", + id: int, + title: str, + short_name: str, + description: str, + photo: Photo, + animation: Animation = None): + super().__init__(client) + + self.id = id + self.title = title + self.short_name = short_name + self.description = description + self.photo = photo + self.animation = animation + + @staticmethod + def _parse(client, message: types.Message) -> "Game": + game = message.media.game # type: types.Game + animation = None + + if game.document: + attributes = {type(i): i for i in game.document.attributes} + + file_name = getattr( + attributes.get( + types.DocumentAttributeFilename, None + ), "file_name", None + ) + + animation = Animation._parse( + client, + game.document, + attributes.get(types.DocumentAttributeVideo, None), + file_name + ) + + return Game( + id=game.id, + title=game.title, + short_name=game.short_name, + description=game.description, + photo=Photo._parse(client, game.photo), + animation=animation, + client=client + ) From d5303285d66a7693728200dcb6a372937e7adb5c Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 5 Jan 2019 23:12:29 +0100 Subject: [PATCH 26/29] Add support for Game inline buttons. Add CallbackGame type --- pyrogram/client/types/bots/__init__.py | 1 + pyrogram/client/types/bots/callback_game.py | 29 +++++++++++++++++++ .../types/bots/inline_keyboard_button.py | 17 +++++++++-- 3 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 pyrogram/client/types/bots/callback_game.py diff --git a/pyrogram/client/types/bots/__init__.py b/pyrogram/client/types/bots/__init__.py index 28cfe724..804701dd 100644 --- a/pyrogram/client/types/bots/__init__.py +++ b/pyrogram/client/types/bots/__init__.py @@ -23,3 +23,4 @@ from .inline_keyboard_markup import InlineKeyboardMarkup from .keyboard_button import KeyboardButton from .reply_keyboard_markup import ReplyKeyboardMarkup from .reply_keyboard_remove import ReplyKeyboardRemove +from .callback_game import CallbackGame \ No newline at end of file diff --git a/pyrogram/client/types/bots/callback_game.py b/pyrogram/client/types/bots/callback_game.py new file mode 100644 index 00000000..be026360 --- /dev/null +++ b/pyrogram/client/types/bots/callback_game.py @@ -0,0 +1,29 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2019 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from ..pyrogram_type import PyrogramType + + +class CallbackGame(PyrogramType): + """A placeholder, currently holds no information. + + Use BotFather to set up your game. + """ + + def __init__(self): + super().__init__(None) diff --git a/pyrogram/client/types/bots/inline_keyboard_button.py b/pyrogram/client/types/bots/inline_keyboard_button.py index f9c1267a..cd30f373 100644 --- a/pyrogram/client/types/bots/inline_keyboard_button.py +++ b/pyrogram/client/types/bots/inline_keyboard_button.py @@ -18,8 +18,9 @@ from pyrogram.api.types import ( KeyboardButtonUrl, KeyboardButtonCallback, - KeyboardButtonSwitchInline + KeyboardButtonSwitchInline, KeyboardButtonGame ) +from .callback_game import CallbackGame from ..pyrogram_type import PyrogramType @@ -58,7 +59,8 @@ class InlineKeyboardButton(PyrogramType): callback_data: bytes = None, url: str = None, switch_inline_query: str = None, - switch_inline_query_current_chat: str = None): + switch_inline_query_current_chat: str = None, + callback_game: CallbackGame = None): super().__init__(None) self.text = text @@ -66,7 +68,7 @@ class InlineKeyboardButton(PyrogramType): self.callback_data = callback_data self.switch_inline_query = switch_inline_query self.switch_inline_query_current_chat = switch_inline_query_current_chat - # self.callback_game = callback_game + self.callback_game = callback_game # self.pay = pay @staticmethod @@ -95,6 +97,12 @@ class InlineKeyboardButton(PyrogramType): switch_inline_query=o.query ) + if isinstance(o, KeyboardButtonGame): + return InlineKeyboardButton( + text=o.text, + callback_game=CallbackGame() + ) + def write(self): if self.callback_data: return KeyboardButtonCallback(self.text, self.callback_data) @@ -107,3 +115,6 @@ class InlineKeyboardButton(PyrogramType): if self.switch_inline_query_current_chat: return KeyboardButtonSwitchInline(self.text, self.switch_inline_query_current_chat, same_peer=True) + + if self.callback_game: + return KeyboardButtonGame(self.text) From 50e5692eae1ad7708ea5c20d6c74d2f2bdf417ae Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 5 Jan 2019 23:12:59 +0100 Subject: [PATCH 27/29] Add Filters.game and Filters.game_score. Also fix Filters.animation name --- pyrogram/client/filters/filters.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pyrogram/client/filters/filters.py b/pyrogram/client/filters/filters.py index ea6d7202..01ffe434 100644 --- a/pyrogram/client/filters/filters.py +++ b/pyrogram/client/filters/filters.py @@ -100,9 +100,12 @@ class Filters: sticker = create("Sticker", lambda _, m: bool(m.sticker)) """Filter messages that contain :obj:`Sticker ` objects.""" - animation = create("GIF", lambda _, m: bool(m.animation)) + animation = create("Animation", lambda _, m: bool(m.animation)) """Filter messages that contain :obj:`Animation ` objects.""" + game = create("Game", lambda _, m: bool(m.game)) + """Filter messages that contain :obj:`Game ` objects.""" + video = create("Video", lambda _, m: bool(m.video)) """Filter messages that contain :obj:`Video ` objects.""" @@ -169,6 +172,9 @@ class Filters: pinned_message = create("PinnedMessage", lambda _, m: bool(m.pinned_message)) """Filter service messages for pinned messages.""" + game_score = create("GameScore", lambda _, m: bool(m.game_score)) + """Filter service messages for game scores.""" + reply_keyboard = create("ReplyKeyboard", lambda _, m: isinstance(m.reply_markup, ReplyKeyboardMarkup)) """Filter messages containing reply keyboard markups""" @@ -193,7 +199,8 @@ class Filters: - channel_chat_created - migrate_to_chat_id - migrate_from_chat_id - - pinned_message""" + - pinned_message + - game_score""" media = create("Media", lambda _, m: bool(m.media)) """Filter media messages. A media message contains any of the following fields set @@ -208,7 +215,8 @@ class Filters: - video_note - contact - location - - venue""" + - venue + - poll""" @staticmethod def command(command: str or list, From bb27633da6d831b71723a37953dd00edd927e37c Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 5 Jan 2019 23:13:47 +0100 Subject: [PATCH 28/29] Add game and game_score in Message --- .../types/messages_and_media/message.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pyrogram/client/types/messages_and_media/message.py b/pyrogram/client/types/messages_and_media/message.py index 830a24de..fddc4d0b 100644 --- a/pyrogram/client/types/messages_and_media/message.py +++ b/pyrogram/client/types/messages_and_media/message.py @@ -121,6 +121,9 @@ class Message(PyrogramType, Update): animation (:obj:`Animation `, *optional*): Message is an animation, information about the animation. + game (:obj:`Game `, *optional*): + Message is a game, information about the game. + video (:obj:`Video `, *optional*): Message is a video, information about the video. @@ -199,6 +202,10 @@ class Message(PyrogramType, Update): Note that the Message object in this field will not contain further reply_to_message fields even if it is itself a reply. + game_score (``int``, *optional*): + The game score for a user. + The reply_to_message field will contain the game Message. + views (``int``, *optional*): Channel post views. @@ -255,6 +262,7 @@ class Message(PyrogramType, Update): photo: "pyrogram.Photo" = None, sticker: "pyrogram.Sticker" = None, animation: "pyrogram.Animation" = None, + game: "pyrogram.Game" = None, video: "pyrogram.Video" = None, voice: "pyrogram.Voice" = None, video_note: "pyrogram.VideoNote" = None, @@ -275,6 +283,7 @@ class Message(PyrogramType, Update): migrate_to_chat_id: int = None, migrate_from_chat_id: int = None, pinned_message: "Message" = None, + game_score: int = None, views: int = None, via_bot: User = None, outgoing: bool = None, @@ -311,6 +320,7 @@ class Message(PyrogramType, Update): self.photo = photo self.sticker = sticker self.animation = animation + self.game = game self.video = video self.voice = voice self.video_note = video_note @@ -331,6 +341,7 @@ class Message(PyrogramType, Update): self.migrate_to_chat_id = migrate_to_chat_id self.migrate_from_chat_id = migrate_from_chat_id self.pinned_message = pinned_message + self.game_score = game_score self.views = views self.via_bot = via_bot self.outgoing = outgoing @@ -407,6 +418,19 @@ class Message(PyrogramType, Update): except MessageIdsEmpty: pass + if isinstance(action, types.MessageActionGameScore): + parsed_message.game_score = action.score + + if message.reply_to_msg_id and replies: + try: + parsed_message.reply_to_message = client.get_messages( + parsed_message.chat.id, + reply_to_message_ids=message.id, + replies=0 + ) + except MessageIdsEmpty: + pass + return parsed_message if isinstance(message, types.Message): @@ -435,6 +459,7 @@ class Message(PyrogramType, Update): location = None contact = None venue = None + game = None audio = None voice = None animation = None @@ -456,6 +481,8 @@ class Message(PyrogramType, Update): contact = Contact._parse(client, media) elif isinstance(media, types.MessageMediaVenue): venue = pyrogram.Venue._parse(client, media) + elif isinstance(media, types.MessageMediaGame): + game = pyrogram.Game._parse(client, message) elif isinstance(media, types.MessageMediaDocument): doc = media.document @@ -543,6 +570,7 @@ class Message(PyrogramType, Update): audio=audio, voice=voice, animation=animation, + game=game, video=video, video_note=video_note, sticker=sticker, From 6451d599b264ce0ea39779a2fbb4618d571a34c5 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 7 Jan 2019 08:30:40 +0100 Subject: [PATCH 29/29] Fix typo --- pyrogram/client/types/input_media/input_media_audio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/client/types/input_media/input_media_audio.py b/pyrogram/client/types/input_media/input_media_audio.py index 5034ed06..a2dc18db 100644 --- a/pyrogram/client/types/input_media/input_media_audio.py +++ b/pyrogram/client/types/input_media/input_media_audio.py @@ -20,7 +20,7 @@ from . import InputMedia class InputMediaAudio(InputMedia): - """This object represents a video to be sent inside an album. + """This object represents an audio to be sent inside an album. It is intended to be used with :obj:`send_media_group() `. Args: