From 9d32b28f94e4c0f263e80e758fa530a3471cf91b Mon Sep 17 00:00:00 2001 From: bakatrouble Date: Thu, 21 Feb 2019 20:12:11 +0300 Subject: [PATCH 01/73] Implement extendable session storage and JSON session storage --- pyrogram/client/client.py | 59 ++------- pyrogram/client/ext/base_client.py | 14 +-- pyrogram/client/ext/syncer.py | 41 +------ pyrogram/client/session_storage/__init__.py | 21 ++++ .../session_storage/base_session_storage.py | 50 ++++++++ .../session_storage/json_session_storage.py | 116 ++++++++++++++++++ .../session_storage/session_storage_mixin.py | 73 +++++++++++ 7 files changed, 278 insertions(+), 96 deletions(-) create mode 100644 pyrogram/client/session_storage/__init__.py create mode 100644 pyrogram/client/session_storage/base_session_storage.py create mode 100644 pyrogram/client/session_storage/json_session_storage.py create mode 100644 pyrogram/client/session_storage/session_storage_mixin.py diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index f62c046c..9a9f8482 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -36,7 +36,7 @@ from importlib import import_module from pathlib import Path from signal import signal, SIGINT, SIGTERM, SIGABRT from threading import Thread -from typing import Union, List +from typing import Union, List, Type from pyrogram.api import functions, types from pyrogram.api.core import Object @@ -56,6 +56,7 @@ from pyrogram.session import Auth, Session from .dispatcher import Dispatcher from .ext import utils, Syncer, BaseClient from .methods import Methods +from .session_storage import BaseSessionStorage, JsonSessionStorage, SessionDoesNotExist log = logging.getLogger(__name__) @@ -199,8 +200,9 @@ class Client(Methods, BaseClient): config_file: str = BaseClient.CONFIG_FILE, plugins: dict = None, no_updates: bool = None, - takeout: bool = None): - super().__init__() + takeout: bool = None, + session_storage_cls: Type[BaseSessionStorage] = JsonSessionStorage): + super().__init__(session_storage_cls(self)) self.session_name = session_name self.api_id = int(api_id) if api_id else None @@ -296,8 +298,8 @@ class Client(Methods, BaseClient): now = time.time() if abs(now - self.date) > Client.OFFLINE_SLEEP: - self.peers_by_username = {} - self.peers_by_phone = {} + self.peers_by_username.clear() + self.peers_by_phone.clear() self.get_initial_dialogs() self.get_contacts() @@ -1101,33 +1103,10 @@ class Client(Methods, BaseClient): def load_session(self): try: - with open(os.path.join(self.workdir, "{}.session".format(self.session_name)), encoding="utf-8") as f: - s = json.load(f) - except FileNotFoundError: - self.dc_id = 1 - self.date = 0 + self.session_storage.load_session(self.session_name) + except SessionDoesNotExist: + log.info('Session {} was not found, initializing new one') self.auth_key = Auth(self.dc_id, self.test_mode, self.ipv6, self._proxy).create() - else: - self.dc_id = s["dc_id"] - self.test_mode = s["test_mode"] - self.auth_key = base64.b64decode("".join(s["auth_key"])) - self.user_id = s["user_id"] - self.date = s.get("date", 0) - - for k, v in s.get("peers_by_id", {}).items(): - self.peers_by_id[int(k)] = utils.get_input_peer(int(k), v) - - for k, v in s.get("peers_by_username", {}).items(): - peer = self.peers_by_id.get(v, None) - - if peer: - self.peers_by_username[k] = peer - - for k, v in s.get("peers_by_phone", {}).items(): - peer = self.peers_by_id.get(v, None) - - if peer: - self.peers_by_phone[k] = peer def load_plugins(self): if self.plugins.get("enabled", False): @@ -1234,23 +1213,7 @@ class Client(Methods, BaseClient): log.warning('No plugin loaded from "{}"'.format(root)) def save_session(self): - auth_key = base64.b64encode(self.auth_key).decode() - auth_key = [auth_key[i: i + 43] for i in range(0, len(auth_key), 43)] - - os.makedirs(self.workdir, exist_ok=True) - - with open(os.path.join(self.workdir, "{}.session".format(self.session_name)), "w", encoding="utf-8") as f: - json.dump( - dict( - dc_id=self.dc_id, - test_mode=self.test_mode, - auth_key=auth_key, - user_id=self.user_id, - date=self.date - ), - f, - indent=4 - ) + self.session_storage.save_session(self.session_name) def get_initial_dialogs_chunk(self, offset_date: int = 0): diff --git a/pyrogram/client/ext/base_client.py b/pyrogram/client/ext/base_client.py index d2c348a8..87f11e23 100644 --- a/pyrogram/client/ext/base_client.py +++ b/pyrogram/client/ext/base_client.py @@ -24,9 +24,10 @@ from threading import Lock from pyrogram import __version__ from ..style import Markdown, HTML from ...session.internals import MsgId +from ..session_storage import SessionStorageMixin, BaseSessionStorage -class BaseClient: +class BaseClient(SessionStorageMixin): class StopTransmission(StopIteration): pass @@ -67,20 +68,13 @@ class BaseClient: 13: "video_note" } - def __init__(self): + def __init__(self, session_storage: BaseSessionStorage): + self.session_storage = session_storage self.bot_token = None - self.dc_id = None - self.auth_key = None - self.user_id = None - self.date = None self.rnd_id = MsgId self.channels_pts = {} - self.peers_by_id = {} - self.peers_by_username = {} - self.peers_by_phone = {} - self.markdown = Markdown(self.peers_by_id) self.html = HTML(self.peers_by_id) diff --git a/pyrogram/client/ext/syncer.py b/pyrogram/client/ext/syncer.py index e169d2a3..8930b13e 100644 --- a/pyrogram/client/ext/syncer.py +++ b/pyrogram/client/ext/syncer.py @@ -81,47 +81,12 @@ class Syncer: @classmethod def sync(cls, client): - temporary = os.path.join(client.workdir, "{}.sync".format(client.session_name)) - persistent = os.path.join(client.workdir, "{}.session".format(client.session_name)) - + client.date = int(time.time()) try: - auth_key = base64.b64encode(client.auth_key).decode() - auth_key = [auth_key[i: i + 43] for i in range(0, len(auth_key), 43)] - - data = dict( - dc_id=client.dc_id, - test_mode=client.test_mode, - auth_key=auth_key, - user_id=client.user_id, - date=int(time.time()), - peers_by_id={ - k: getattr(v, "access_hash", None) - for k, v in client.peers_by_id.copy().items() - }, - peers_by_username={ - k: utils.get_peer_id(v) - for k, v in client.peers_by_username.copy().items() - }, - peers_by_phone={ - k: utils.get_peer_id(v) - for k, v in client.peers_by_phone.copy().items() - } - ) - - os.makedirs(client.workdir, exist_ok=True) - - with open(temporary, "w", encoding="utf-8") as f: - json.dump(data, f, indent=4) - - f.flush() - os.fsync(f.fileno()) + client.session_storage.save_session(client.session_name, sync=True) except Exception as e: log.critical(e, exc_info=True) else: - shutil.move(temporary, persistent) log.info("Synced {}".format(client.session_name)) finally: - try: - os.remove(temporary) - except OSError: - pass + client.session_storage.sync_cleanup(client.session_name) diff --git a/pyrogram/client/session_storage/__init__.py b/pyrogram/client/session_storage/__init__.py new file mode 100644 index 00000000..6ee92ebc --- /dev/null +++ b/pyrogram/client/session_storage/__init__.py @@ -0,0 +1,21 @@ +# 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 .session_storage_mixin import SessionStorageMixin +from .base_session_storage import BaseSessionStorage, SessionDoesNotExist +from .json_session_storage import JsonSessionStorage diff --git a/pyrogram/client/session_storage/base_session_storage.py b/pyrogram/client/session_storage/base_session_storage.py new file mode 100644 index 00000000..75e416b4 --- /dev/null +++ b/pyrogram/client/session_storage/base_session_storage.py @@ -0,0 +1,50 @@ +# 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 abc + +import pyrogram + + +class SessionDoesNotExist(Exception): + pass + + +class BaseSessionStorage(abc.ABC): + def __init__(self, client: 'pyrogram.client.BaseClient'): + self.client = client + self.dc_id = 1 + self.test_mode = None + self.auth_key = None + self.user_id = None + self.date = 0 + self.peers_by_id = {} + self.peers_by_username = {} + self.peers_by_phone = {} + + @abc.abstractmethod + def load_session(self, name: str): + ... + + @abc.abstractmethod + def save_session(self, name: str, sync=False): + ... + + @abc.abstractmethod + def sync_cleanup(self, name: str): + ... diff --git a/pyrogram/client/session_storage/json_session_storage.py b/pyrogram/client/session_storage/json_session_storage.py new file mode 100644 index 00000000..679a21f3 --- /dev/null +++ b/pyrogram/client/session_storage/json_session_storage.py @@ -0,0 +1,116 @@ +# 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 base64 +import json +import logging +import os +import shutil + +from ..ext import utils +from . import BaseSessionStorage, SessionDoesNotExist + + +log = logging.getLogger(__name__) + + +class JsonSessionStorage(BaseSessionStorage): + def _get_file_name(self, name: str): + if not name.endswith('.session'): + name += '.session' + return os.path.join(self.client.workdir, name) + + def load_session(self, name: str): + file_path = self._get_file_name(name) + log.info('Loading JSON session from {}'.format(file_path)) + + try: + with open(file_path, encoding='utf-8') as f: + s = json.load(f) + except FileNotFoundError: + raise SessionDoesNotExist() + + self.dc_id = s["dc_id"] + self.test_mode = s["test_mode"] + self.auth_key = base64.b64decode("".join(s["auth_key"])) # join split key + self.user_id = s["user_id"] + self.date = s.get("date", 0) + + for k, v in s.get("peers_by_id", {}).items(): + self.peers_by_id[int(k)] = utils.get_input_peer(int(k), v) + + for k, v in s.get("peers_by_username", {}).items(): + peer = self.peers_by_id.get(v, None) + + if peer: + self.peers_by_username[k] = peer + + for k, v in s.get("peers_by_phone", {}).items(): + peer = self.peers_by_id.get(v, None) + + if peer: + self.peers_by_phone[k] = peer + + def save_session(self, name: str, sync=False): + file_path = self._get_file_name(name) + + if sync: + file_path += '.tmp' + + log.info('Saving JSON session to {}, sync={}'.format(file_path, sync)) + + auth_key = base64.b64encode(self.auth_key).decode() + auth_key = [auth_key[i: i + 43] for i in range(0, len(auth_key), 43)] # split key in lines of 43 chars + + os.makedirs(self.client.workdir, exist_ok=True) + + data = { + 'dc_id': self.dc_id, + 'test_mode': self.test_mode, + 'auth_key': auth_key, + 'user_id': self.user_id, + 'date': self.date, + 'peers_by_id': { + k: getattr(v, "access_hash", None) + for k, v in self.peers_by_id.copy().items() + }, + 'peers_by_username': { + k: utils.get_peer_id(v) + for k, v in self.peers_by_username.copy().items() + }, + 'peers_by_phone': { + k: utils.get_peer_id(v) + for k, v in self.peers_by_phone.copy().items() + } + } + + with open(file_path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=4) + + f.flush() + os.fsync(f.fileno()) + + # execution won't be here if an error has occurred earlier + if sync: + shutil.move(file_path, self._get_file_name(name)) + + def sync_cleanup(self, name: str): + try: + os.remove(self._get_file_name(name) + '.tmp') + except OSError: + pass diff --git a/pyrogram/client/session_storage/session_storage_mixin.py b/pyrogram/client/session_storage/session_storage_mixin.py new file mode 100644 index 00000000..bfe9a590 --- /dev/null +++ b/pyrogram/client/session_storage/session_storage_mixin.py @@ -0,0 +1,73 @@ +# 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 Dict + + +class SessionStorageMixin: + @property + def dc_id(self) -> int: + return self.session_storage.dc_id + + @dc_id.setter + def dc_id(self, val): + self.session_storage.dc_id = val + + @property + def test_mode(self) -> bool: + return self.session_storage.test_mode + + @test_mode.setter + def test_mode(self, val): + self.session_storage.test_mode = val + + @property + def auth_key(self) -> bytes: + return self.session_storage.auth_key + + @auth_key.setter + def auth_key(self, val): + self.session_storage.auth_key = val + + @property + def user_id(self): + return self.session_storage.user_id + + @user_id.setter + def user_id(self, val) -> int: + self.session_storage.user_id = val + + @property + def date(self) -> int: + return self.session_storage.date + + @date.setter + def date(self, val): + self.session_storage.date = val + + @property + def peers_by_id(self) -> Dict[str, int]: + return self.session_storage.peers_by_id + + @property + def peers_by_username(self) -> Dict[str, int]: + return self.session_storage.peers_by_username + + @property + def peers_by_phone(self) -> Dict[str, int]: + return self.session_storage.peers_by_phone From 431a983d5b66522604f0685ef078d40735cea64c Mon Sep 17 00:00:00 2001 From: bakatrouble Date: Thu, 21 Feb 2019 21:18:53 +0300 Subject: [PATCH 02/73] Fix logging and cleanup imports in client.py --- pyrogram/client/client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 9a9f8482..0e8d5554 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -16,9 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -import base64 import binascii -import json import logging import math import mimetypes @@ -1105,7 +1103,10 @@ class Client(Methods, BaseClient): try: self.session_storage.load_session(self.session_name) except SessionDoesNotExist: - log.info('Session {} was not found, initializing new one') + session_name = self.session_name[:32] + if session_name != self.session_name: + session_name += '...' + log.info('Could not load session "{}", initializing new one'.format(self.session_name)) self.auth_key = Auth(self.dc_id, self.test_mode, self.ipv6, self._proxy).create() def load_plugins(self): From b04cf9ec9297ce0884943879b1fac07fa7e2933f Mon Sep 17 00:00:00 2001 From: bakatrouble Date: Thu, 21 Feb 2019 21:43:57 +0300 Subject: [PATCH 03/73] Add string session storage --- pyrogram/client/session_storage/__init__.py | 1 + .../session_storage/string_session_storage.py | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 pyrogram/client/session_storage/string_session_storage.py diff --git a/pyrogram/client/session_storage/__init__.py b/pyrogram/client/session_storage/__init__.py index 6ee92ebc..ced103ce 100644 --- a/pyrogram/client/session_storage/__init__.py +++ b/pyrogram/client/session_storage/__init__.py @@ -19,3 +19,4 @@ from .session_storage_mixin import SessionStorageMixin from .base_session_storage import BaseSessionStorage, SessionDoesNotExist from .json_session_storage import JsonSessionStorage +from .string_session_storage import StringSessionStorage diff --git a/pyrogram/client/session_storage/string_session_storage.py b/pyrogram/client/session_storage/string_session_storage.py new file mode 100644 index 00000000..9b6ebf0e --- /dev/null +++ b/pyrogram/client/session_storage/string_session_storage.py @@ -0,0 +1,38 @@ +import base64 +import binascii +import struct + +from . import BaseSessionStorage, SessionDoesNotExist + + +def StringSessionStorage(print_session: bool = False): + class StringSessionStorageClass(BaseSessionStorage): + """ + Packs session data as following (forcing little-endian byte order): + Char dc_id (1 byte, unsigned) + Boolean test_mode (1 byte) + Long long user_id (8 bytes, signed) + Bytes auth_key (256 bytes) + + Uses Base64 encoding for printable representation + """ + PACK_FORMAT = ' Date: Fri, 22 Feb 2019 00:03:58 +0300 Subject: [PATCH 04/73] Refactor session storages: use session_name arg to detect storage type --- pyrogram/client/client.py | 34 +++++++----- pyrogram/client/ext/syncer.py | 4 +- pyrogram/client/session_storage/__init__.py | 4 +- .../session_storage/base_session_storage.py | 17 ++++-- .../session_storage/json_session_storage.py | 14 ++--- .../session_storage/string_session_storage.py | 53 +++++++++---------- 6 files changed, 71 insertions(+), 55 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 0e8d5554..f17a054b 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -49,12 +49,15 @@ from pyrogram.api.errors import ( from pyrogram.client.handlers import DisconnectHandler from pyrogram.client.handlers.handler import Handler from pyrogram.client.methods.password.utils import compute_check +from pyrogram.client.session_storage import BaseSessionConfig from pyrogram.crypto import AES from pyrogram.session import Auth, Session from .dispatcher import Dispatcher from .ext import utils, Syncer, BaseClient from .methods import Methods -from .session_storage import BaseSessionStorage, JsonSessionStorage, SessionDoesNotExist +from .session_storage import SessionDoesNotExist +from .session_storage.json_session_storage import JsonSessionStorage +from .session_storage.string_session_storage import StringSessionStorage log = logging.getLogger(__name__) @@ -176,7 +179,7 @@ class Client(Methods, BaseClient): """ def __init__(self, - session_name: str, + session_name: Union[str, BaseSessionConfig], api_id: Union[int, str] = None, api_hash: str = None, app_version: str = None, @@ -198,11 +201,21 @@ class Client(Methods, BaseClient): config_file: str = BaseClient.CONFIG_FILE, plugins: dict = None, no_updates: bool = None, - takeout: bool = None, - session_storage_cls: Type[BaseSessionStorage] = JsonSessionStorage): - super().__init__(session_storage_cls(self)) + takeout: bool = None): - self.session_name = session_name + if isinstance(session_name, str): + if session_name.startswith(':'): + session_storage = StringSessionStorage(self, session_name) + else: + session_storage = JsonSessionStorage(self, session_name) + elif isinstance(session_name, BaseSessionConfig): + session_storage = session_name.session_storage_cls(self, session_name) + else: + raise RuntimeError('Wrong session_name passed, expected str or BaseSessionConfig subclass') + + super().__init__(session_storage) + + self.session_name = str(session_name) # TODO: build correct session name self.api_id = int(api_id) if api_id else None self.api_hash = api_hash self.app_version = app_version @@ -1101,12 +1114,9 @@ class Client(Methods, BaseClient): def load_session(self): try: - self.session_storage.load_session(self.session_name) + self.session_storage.load_session() except SessionDoesNotExist: - session_name = self.session_name[:32] - if session_name != self.session_name: - session_name += '...' - log.info('Could not load session "{}", initializing new one'.format(self.session_name)) + log.info('Could not load session "{}", initiate new one'.format(self.session_name)) self.auth_key = Auth(self.dc_id, self.test_mode, self.ipv6, self._proxy).create() def load_plugins(self): @@ -1214,7 +1224,7 @@ class Client(Methods, BaseClient): log.warning('No plugin loaded from "{}"'.format(root)) def save_session(self): - self.session_storage.save_session(self.session_name) + self.session_storage.save_session() def get_initial_dialogs_chunk(self, offset_date: int = 0): diff --git a/pyrogram/client/ext/syncer.py b/pyrogram/client/ext/syncer.py index 8930b13e..70955624 100644 --- a/pyrogram/client/ext/syncer.py +++ b/pyrogram/client/ext/syncer.py @@ -83,10 +83,10 @@ class Syncer: def sync(cls, client): client.date = int(time.time()) try: - client.session_storage.save_session(client.session_name, sync=True) + client.session_storage.save_session(sync=True) except Exception as e: log.critical(e, exc_info=True) else: log.info("Synced {}".format(client.session_name)) finally: - client.session_storage.sync_cleanup(client.session_name) + client.session_storage.sync_cleanup() diff --git a/pyrogram/client/session_storage/__init__.py b/pyrogram/client/session_storage/__init__.py index ced103ce..611ec9b7 100644 --- a/pyrogram/client/session_storage/__init__.py +++ b/pyrogram/client/session_storage/__init__.py @@ -17,6 +17,4 @@ # along with Pyrogram. If not, see . from .session_storage_mixin import SessionStorageMixin -from .base_session_storage import BaseSessionStorage, SessionDoesNotExist -from .json_session_storage import JsonSessionStorage -from .string_session_storage import StringSessionStorage +from .base_session_storage import BaseSessionStorage, BaseSessionConfig, SessionDoesNotExist diff --git a/pyrogram/client/session_storage/base_session_storage.py b/pyrogram/client/session_storage/base_session_storage.py index 75e416b4..a5c879f1 100644 --- a/pyrogram/client/session_storage/base_session_storage.py +++ b/pyrogram/client/session_storage/base_session_storage.py @@ -17,6 +17,7 @@ # along with Pyrogram. If not, see . import abc +from typing import Type import pyrogram @@ -26,8 +27,9 @@ class SessionDoesNotExist(Exception): class BaseSessionStorage(abc.ABC): - def __init__(self, client: 'pyrogram.client.BaseClient'): + def __init__(self, client: 'pyrogram.client.BaseClient', session_data): self.client = client + self.session_data = session_data self.dc_id = 1 self.test_mode = None self.auth_key = None @@ -38,13 +40,20 @@ class BaseSessionStorage(abc.ABC): self.peers_by_phone = {} @abc.abstractmethod - def load_session(self, name: str): + def load_session(self): ... @abc.abstractmethod - def save_session(self, name: str, sync=False): + def save_session(self, sync=False): ... @abc.abstractmethod - def sync_cleanup(self, name: str): + def sync_cleanup(self): + ... + + +class BaseSessionConfig(abc.ABC): + @property + @abc.abstractmethod + def session_storage_cls(self) -> Type[BaseSessionStorage]: ... diff --git a/pyrogram/client/session_storage/json_session_storage.py b/pyrogram/client/session_storage/json_session_storage.py index 679a21f3..f41091af 100644 --- a/pyrogram/client/session_storage/json_session_storage.py +++ b/pyrogram/client/session_storage/json_session_storage.py @@ -35,8 +35,8 @@ class JsonSessionStorage(BaseSessionStorage): name += '.session' return os.path.join(self.client.workdir, name) - def load_session(self, name: str): - file_path = self._get_file_name(name) + def load_session(self): + file_path = self._get_file_name(self.session_data) log.info('Loading JSON session from {}'.format(file_path)) try: @@ -66,8 +66,8 @@ class JsonSessionStorage(BaseSessionStorage): if peer: self.peers_by_phone[k] = peer - def save_session(self, name: str, sync=False): - file_path = self._get_file_name(name) + def save_session(self, sync=False): + file_path = self._get_file_name(self.session_data) if sync: file_path += '.tmp' @@ -107,10 +107,10 @@ class JsonSessionStorage(BaseSessionStorage): # execution won't be here if an error has occurred earlier if sync: - shutil.move(file_path, self._get_file_name(name)) + shutil.move(file_path, self._get_file_name(self.session_data)) - def sync_cleanup(self, name: str): + def sync_cleanup(self): try: - os.remove(self._get_file_name(name) + '.tmp') + os.remove(self._get_file_name(self.session_data) + '.tmp') except OSError: pass diff --git a/pyrogram/client/session_storage/string_session_storage.py b/pyrogram/client/session_storage/string_session_storage.py index 9b6ebf0e..c01a2b35 100644 --- a/pyrogram/client/session_storage/string_session_storage.py +++ b/pyrogram/client/session_storage/string_session_storage.py @@ -5,34 +5,33 @@ import struct from . import BaseSessionStorage, SessionDoesNotExist -def StringSessionStorage(print_session: bool = False): - class StringSessionStorageClass(BaseSessionStorage): - """ - Packs session data as following (forcing little-endian byte order): - Char dc_id (1 byte, unsigned) - Boolean test_mode (1 byte) - Long long user_id (8 bytes, signed) - Bytes auth_key (256 bytes) +class StringSessionStorage(BaseSessionStorage): + """ + Packs session data as following (forcing little-endian byte order): + Char dc_id (1 byte, unsigned) + Boolean test_mode (1 byte) + Long long user_id (8 bytes, signed) + Bytes auth_key (256 bytes) - Uses Base64 encoding for printable representation - """ - PACK_FORMAT = ' Date: Fri, 22 Feb 2019 01:34:08 +0300 Subject: [PATCH 05/73] Add bot_token argument (closes #123) --- pyrogram/client/client.py | 25 ++++++++++++++++++++----- pyrogram/client/ext/base_client.py | 2 +- pyrogram/client/ext/syncer.py | 1 + 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index f62c046c..da2ddc5b 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -29,6 +29,7 @@ import struct import tempfile import threading import time +import warnings from configparser import ConfigParser from datetime import datetime from hashlib import sha256, md5 @@ -67,9 +68,8 @@ class Client(Methods, BaseClient): Args: session_name (``str``): - Name to uniquely identify a session of either a User or a Bot. - For Users: pass a string of your choice, e.g.: "my_main_account". - For Bots: pass your Bot API token, e.g.: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" + Name to uniquely identify a session of either a User or a Bot, e.g.: "my_main_account". + You still can use bot token here, but it will be deprecated in next release. Note: as long as a valid User session file exists, Pyrogram won't ask you again to input your phone number. api_id (``int``, *optional*): @@ -144,6 +144,10 @@ class Client(Methods, BaseClient): a new Telegram account in case the phone number you passed is not registered yet. Only applicable for new sessions. + bot_token (``str``, *optional*): + Pass your Bot API token to create a bot session, e.g.: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" + Only applicable for new sessions. + last_name (``str``, *optional*): Same purpose as *first_name*; pass a Last Name to avoid entering it manually. It can be an empty string: "". Only applicable for new sessions. @@ -192,6 +196,7 @@ class Client(Methods, BaseClient): password: str = None, recovery_code: callable = None, force_sms: bool = False, + bot_token: str = None, first_name: str = None, last_name: str = None, workers: int = BaseClient.WORKERS, @@ -218,6 +223,7 @@ class Client(Methods, BaseClient): self.password = password self.recovery_code = recovery_code self.force_sms = force_sms + self.bot_token = bot_token self.first_name = first_name self.last_name = last_name self.workers = workers @@ -263,8 +269,13 @@ class Client(Methods, BaseClient): raise ConnectionError("Client has already been started") if self.BOT_TOKEN_RE.match(self.session_name): + self.is_bot = True self.bot_token = self.session_name self.session_name = self.session_name.split(":")[0] + warnings.warn('\nYou are using a bot token as session name.\n' + 'It will be deprecated in next update, please use session file name to load ' + 'existing sessions and bot_token argument to create new sessions.', + DeprecationWarning, stacklevel=2) self.load_config() self.load_session() @@ -284,11 +295,12 @@ class Client(Methods, BaseClient): if self.bot_token is None: self.authorize_user() else: + self.is_bot = True self.authorize_bot() self.save_session() - if self.bot_token is None: + if not self.is_bot: if self.takeout: self.takeout_id = self.send(functions.account.InitTakeoutSession()).id log.warning("Takeout session {} initiated".format(self.takeout_id)) @@ -1113,6 +1125,8 @@ class Client(Methods, BaseClient): self.auth_key = base64.b64decode("".join(s["auth_key"])) self.user_id = s["user_id"] self.date = s.get("date", 0) + # TODO: replace default with False once token session name will be deprecated + self.is_bot = s.get("is_bot", self.is_bot) for k, v in s.get("peers_by_id", {}).items(): self.peers_by_id[int(k)] = utils.get_input_peer(int(k), v) @@ -1246,7 +1260,8 @@ class Client(Methods, BaseClient): test_mode=self.test_mode, auth_key=auth_key, user_id=self.user_id, - date=self.date + date=self.date, + is_bot=self.is_bot, ), f, indent=4 diff --git a/pyrogram/client/ext/base_client.py b/pyrogram/client/ext/base_client.py index d2c348a8..8ca784aa 100644 --- a/pyrogram/client/ext/base_client.py +++ b/pyrogram/client/ext/base_client.py @@ -68,7 +68,7 @@ class BaseClient: } def __init__(self): - self.bot_token = None + self.is_bot = False self.dc_id = None self.auth_key = None self.user_id = None diff --git a/pyrogram/client/ext/syncer.py b/pyrogram/client/ext/syncer.py index e169d2a3..71dc3f35 100644 --- a/pyrogram/client/ext/syncer.py +++ b/pyrogram/client/ext/syncer.py @@ -94,6 +94,7 @@ class Syncer: auth_key=auth_key, user_id=client.user_id, date=int(time.time()), + is_bot=client.is_bot, peers_by_id={ k: getattr(v, "access_hash", None) for k, v in client.peers_by_id.copy().items() From 9c4e9e166e528d2ef990bcb3f2093a877d65b642 Mon Sep 17 00:00:00 2001 From: bakatrouble Date: Fri, 22 Feb 2019 02:13:51 +0300 Subject: [PATCH 06/73] Merge #221, string sessions now work for bots too --- pyrogram/client/client.py | 17 +++++++++-------- pyrogram/client/ext/base_client.py | 1 - .../session_storage/base_session_storage.py | 1 + .../session_storage/json_session_storage.py | 2 ++ .../session_storage/session_storage_mixin.py | 8 ++++++++ .../session_storage/string_session_storage.py | 13 ++++++++++--- 6 files changed, 30 insertions(+), 12 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 42a2566a..429abab3 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -281,14 +281,15 @@ class Client(Methods, BaseClient): if self.is_started: raise ConnectionError("Client has already been started") - if self.BOT_TOKEN_RE.match(self.session_name): - self.is_bot = True - self.bot_token = self.session_name - self.session_name = self.session_name.split(":")[0] - warnings.warn('\nYou are using a bot token as session name.\n' - 'It will be deprecated in next update, please use session file name to load ' - 'existing sessions and bot_token argument to create new sessions.', - DeprecationWarning, stacklevel=2) + if isinstance(self.session_storage, JsonSessionStorage): + if self.BOT_TOKEN_RE.match(self.session_storage.session_data): + self.is_bot = True + self.bot_token = self.session_storage.session_data + self.session_storage.session_data = self.session_storage.session_data.split(":")[0] + warnings.warn('\nYou are using a bot token as session name.\n' + 'It will be deprecated in next update, please use session file name to load ' + 'existing sessions and bot_token argument to create new sessions.', + DeprecationWarning, stacklevel=2) self.load_config() self.load_session() diff --git a/pyrogram/client/ext/base_client.py b/pyrogram/client/ext/base_client.py index a354ba76..3f40865f 100644 --- a/pyrogram/client/ext/base_client.py +++ b/pyrogram/client/ext/base_client.py @@ -70,7 +70,6 @@ class BaseClient(SessionStorageMixin): def __init__(self, session_storage: BaseSessionStorage): self.session_storage = session_storage - self.is_bot = False self.rnd_id = MsgId self.channels_pts = {} diff --git a/pyrogram/client/session_storage/base_session_storage.py b/pyrogram/client/session_storage/base_session_storage.py index a5c879f1..92473956 100644 --- a/pyrogram/client/session_storage/base_session_storage.py +++ b/pyrogram/client/session_storage/base_session_storage.py @@ -35,6 +35,7 @@ class BaseSessionStorage(abc.ABC): self.auth_key = None self.user_id = None self.date = 0 + self.is_bot = False self.peers_by_id = {} self.peers_by_username = {} self.peers_by_phone = {} diff --git a/pyrogram/client/session_storage/json_session_storage.py b/pyrogram/client/session_storage/json_session_storage.py index f41091af..1e1e0ca4 100644 --- a/pyrogram/client/session_storage/json_session_storage.py +++ b/pyrogram/client/session_storage/json_session_storage.py @@ -50,6 +50,7 @@ class JsonSessionStorage(BaseSessionStorage): self.auth_key = base64.b64decode("".join(s["auth_key"])) # join split key self.user_id = s["user_id"] self.date = s.get("date", 0) + self.is_bot = s.get('is_bot', self.client.is_bot) for k, v in s.get("peers_by_id", {}).items(): self.peers_by_id[int(k)] = utils.get_input_peer(int(k), v) @@ -85,6 +86,7 @@ class JsonSessionStorage(BaseSessionStorage): 'auth_key': auth_key, 'user_id': self.user_id, 'date': self.date, + 'is_bot': self.is_bot, 'peers_by_id': { k: getattr(v, "access_hash", None) for k, v in self.peers_by_id.copy().items() diff --git a/pyrogram/client/session_storage/session_storage_mixin.py b/pyrogram/client/session_storage/session_storage_mixin.py index bfe9a590..7d783ca7 100644 --- a/pyrogram/client/session_storage/session_storage_mixin.py +++ b/pyrogram/client/session_storage/session_storage_mixin.py @@ -60,6 +60,14 @@ class SessionStorageMixin: def date(self, val): self.session_storage.date = val + @property + def is_bot(self): + return self.session_storage.is_bot + + @is_bot.setter + def is_bot(self, val) -> int: + self.session_storage.is_bot = val + @property def peers_by_id(self) -> Dict[str, int]: return self.session_storage.peers_by_id diff --git a/pyrogram/client/session_storage/string_session_storage.py b/pyrogram/client/session_storage/string_session_storage.py index c01a2b35..5b1a8cc1 100644 --- a/pyrogram/client/session_storage/string_session_storage.py +++ b/pyrogram/client/session_storage/string_session_storage.py @@ -11,24 +11,31 @@ class StringSessionStorage(BaseSessionStorage): Char dc_id (1 byte, unsigned) Boolean test_mode (1 byte) Long long user_id (8 bytes, signed) + Boolean is_bot (1 byte) Bytes auth_key (256 bytes) Uses Base64 encoding for printable representation """ - PACK_FORMAT = ' Date: Fri, 22 Feb 2019 03:37:19 +0300 Subject: [PATCH 07/73] add in-memory session storage, refactor session storages, remove mixin --- pyrogram/client/client.py | 112 +++++++++--------- pyrogram/client/ext/base_client.py | 10 +- pyrogram/client/ext/syncer.py | 4 +- .../client/methods/contacts/get_contacts.py | 2 +- pyrogram/client/session_storage/__init__.py | 6 +- .../{session_storage_mixin.py => abstract.py} | 89 ++++++++++---- .../session_storage/base_session_storage.py | 60 ---------- .../{json_session_storage.py => json.py} | 65 +++++----- pyrogram/client/session_storage/memory.py | 85 +++++++++++++ .../{string_session_storage.py => string.py} | 19 +-- pyrogram/session/session.py | 3 +- 11 files changed, 267 insertions(+), 188 deletions(-) rename pyrogram/client/session_storage/{session_storage_mixin.py => abstract.py} (50%) delete mode 100644 pyrogram/client/session_storage/base_session_storage.py rename pyrogram/client/session_storage/{json_session_storage.py => json.py} (58%) create mode 100644 pyrogram/client/session_storage/memory.py rename pyrogram/client/session_storage/{string_session_storage.py => string.py} (62%) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 429abab3..42bd73d6 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -50,15 +50,15 @@ from pyrogram.api.errors import ( from pyrogram.client.handlers import DisconnectHandler from pyrogram.client.handlers.handler import Handler from pyrogram.client.methods.password.utils import compute_check -from pyrogram.client.session_storage import BaseSessionConfig from pyrogram.crypto import AES from pyrogram.session import Auth, Session from .dispatcher import Dispatcher from .ext import utils, Syncer, BaseClient from .methods import Methods -from .session_storage import SessionDoesNotExist -from .session_storage.json_session_storage import JsonSessionStorage -from .session_storage.string_session_storage import StringSessionStorage +from .session_storage import ( + SessionDoesNotExist, SessionStorage, MemorySessionStorage, JsonSessionStorage, + StringSessionStorage +) log = logging.getLogger(__name__) @@ -183,7 +183,7 @@ class Client(Methods, BaseClient): """ def __init__(self, - session_name: Union[str, BaseSessionConfig], + session_name: Union[str, SessionStorage], api_id: Union[int, str] = None, api_hash: str = None, app_version: str = None, @@ -209,14 +209,16 @@ class Client(Methods, BaseClient): takeout: bool = None): if isinstance(session_name, str): - if session_name.startswith(':'): + if session_name == ':memory:': + session_storage = MemorySessionStorage(self) + elif session_name.startswith(':'): session_storage = StringSessionStorage(self, session_name) else: session_storage = JsonSessionStorage(self, session_name) - elif isinstance(session_name, BaseSessionConfig): - session_storage = session_name.session_storage_cls(self, session_name) + elif isinstance(session_name, SessionStorage): + session_storage = session_name else: - raise RuntimeError('Wrong session_name passed, expected str or BaseSessionConfig subclass') + raise RuntimeError('Wrong session_name passed, expected str or SessionConfig subclass') super().__init__(session_storage) @@ -230,7 +232,7 @@ class Client(Methods, BaseClient): self.ipv6 = ipv6 # TODO: Make code consistent, use underscore for private/protected fields self._proxy = proxy - self.test_mode = test_mode + self.session_storage.test_mode = test_mode self.phone_number = phone_number self.phone_code = phone_code self.password = password @@ -282,10 +284,10 @@ class Client(Methods, BaseClient): raise ConnectionError("Client has already been started") if isinstance(self.session_storage, JsonSessionStorage): - if self.BOT_TOKEN_RE.match(self.session_storage.session_data): - self.is_bot = True - self.bot_token = self.session_storage.session_data - self.session_storage.session_data = self.session_storage.session_data.split(":")[0] + if self.BOT_TOKEN_RE.match(self.session_storage._session_name): + self.session_storage.is_bot = True + self.bot_token = self.session_storage._session_name + self.session_storage._session_name = self.session_storage._session_name.split(":")[0] warnings.warn('\nYou are using a bot token as session name.\n' 'It will be deprecated in next update, please use session file name to load ' 'existing sessions and bot_token argument to create new sessions.', @@ -297,33 +299,33 @@ class Client(Methods, BaseClient): self.session = Session( self, - self.dc_id, - self.auth_key + self.session_storage.dc_id, + self.session_storage.auth_key ) self.session.start() self.is_started = True try: - if self.user_id is None: + if self.session_storage.user_id is None: if self.bot_token is None: self.authorize_user() else: - self.is_bot = True + self.session_storage.is_bot = True self.authorize_bot() self.save_session() - if not self.is_bot: + if not self.session_storage.is_bot: 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: - self.peers_by_username.clear() - self.peers_by_phone.clear() + if abs(now - self.session_storage.date) > Client.OFFLINE_SLEEP: + self.session_storage.peers_by_username.clear() + self.session_storage.peers_by_phone.clear() self.get_initial_dialogs() self.get_contacts() @@ -512,19 +514,20 @@ class Client(Methods, BaseClient): except UserMigrate as e: self.session.stop() - self.dc_id = e.x - self.auth_key = Auth(self.dc_id, self.test_mode, self.ipv6, self._proxy).create() + self.session_storage.dc_id = e.x + self.session_storage.auth_key = Auth(self.session_storage.dc_id, self.session_storage.test_mode, + self.ipv6, self._proxy).create() self.session = Session( self, - self.dc_id, - self.auth_key + self.session_storage.dc_id, + self.session_storage.auth_key ) self.session.start() self.authorize_bot() else: - self.user_id = r.user.id + self.session_storage.user_id = r.user.id print("Logged in successfully as @{}".format(r.user.username)) @@ -564,19 +567,19 @@ class Client(Methods, BaseClient): except (PhoneMigrate, NetworkMigrate) as e: self.session.stop() - self.dc_id = e.x + self.session_storage.dc_id = e.x - self.auth_key = Auth( - self.dc_id, - self.test_mode, + self.session_storage.auth_key = Auth( + self.session_storage.dc_id, + self.session_storage.test_mode, self.ipv6, self._proxy ).create() self.session = Session( self, - self.dc_id, - self.auth_key + self.session_storage.dc_id, + self.session_storage.auth_key ) self.session.start() @@ -752,7 +755,7 @@ class Client(Methods, BaseClient): assert self.send(functions.help.AcceptTermsOfService(terms_of_service.id)) self.password = None - self.user_id = r.user.id + self.session_storage.user_id = r.user.id print("Logged in successfully as {}".format(r.user.first_name)) @@ -776,13 +779,13 @@ class Client(Methods, BaseClient): access_hash=access_hash ) - self.peers_by_id[user_id] = input_peer + self.session_storage.peers_by_id[user_id] = input_peer if username is not None: - self.peers_by_username[username.lower()] = input_peer + self.session_storage.peers_by_username[username.lower()] = input_peer if phone is not None: - self.peers_by_phone[phone] = input_peer + self.session_storage.peers_by_phone[phone] = input_peer if isinstance(entity, (types.Chat, types.ChatForbidden)): chat_id = entity.id @@ -792,7 +795,7 @@ class Client(Methods, BaseClient): chat_id=chat_id ) - self.peers_by_id[peer_id] = input_peer + self.session_storage.peers_by_id[peer_id] = input_peer if isinstance(entity, (types.Channel, types.ChannelForbidden)): channel_id = entity.id @@ -810,10 +813,10 @@ class Client(Methods, BaseClient): access_hash=access_hash ) - self.peers_by_id[peer_id] = input_peer + self.session_storage.peers_by_id[peer_id] = input_peer if username is not None: - self.peers_by_username[username.lower()] = input_peer + self.session_storage.peers_by_username[username.lower()] = input_peer def download_worker(self): name = threading.current_thread().name @@ -1127,10 +1130,11 @@ class Client(Methods, BaseClient): def load_session(self): try: - self.session_storage.load_session() + self.session_storage.load() except SessionDoesNotExist: log.info('Could not load session "{}", initiate new one'.format(self.session_name)) - self.auth_key = Auth(self.dc_id, self.test_mode, self.ipv6, self._proxy).create() + self.session_storage.auth_key = Auth(self.session_storage.dc_id, self.session_storage.test_mode, + self.ipv6, self._proxy).create() def load_plugins(self): if self.plugins.get("enabled", False): @@ -1237,7 +1241,7 @@ class Client(Methods, BaseClient): log.warning('No plugin loaded from "{}"'.format(root)) def save_session(self): - self.session_storage.save_session() + self.session_storage.save() def get_initial_dialogs_chunk(self, offset_date: int = 0): @@ -1257,7 +1261,7 @@ class Client(Methods, BaseClient): log.warning("get_dialogs flood: waiting {} seconds".format(e.x)) time.sleep(e.x) else: - log.info("Total peers: {}".format(len(self.peers_by_id))) + log.info("Total peers: {}".format(len(self.session_storage.peers_by_id))) return r def get_initial_dialogs(self): @@ -1293,7 +1297,7 @@ class Client(Methods, BaseClient): ``KeyError`` in case the peer doesn't exist in the internal database. """ try: - return self.peers_by_id[peer_id] + return self.session_storage.peers_by_id[peer_id] except KeyError: if type(peer_id) is str: if peer_id in ("self", "me"): @@ -1304,17 +1308,17 @@ class Client(Methods, BaseClient): try: int(peer_id) except ValueError: - if peer_id not in self.peers_by_username: + if peer_id not in self.session_storage.peers_by_username: self.send( functions.contacts.ResolveUsername( username=peer_id ) ) - return self.peers_by_username[peer_id] + return self.session_storage.peers_by_username[peer_id] else: try: - return self.peers_by_phone[peer_id] + return self.session_storage.peers_by_phone[peer_id] except KeyError: raise PeerIdInvalid @@ -1341,7 +1345,7 @@ class Client(Methods, BaseClient): ) try: - return self.peers_by_id[peer_id] + return self.session_storage.peers_by_id[peer_id] except KeyError: raise PeerIdInvalid @@ -1411,7 +1415,7 @@ class Client(Methods, BaseClient): file_id = file_id or self.rnd_id() md5_sum = md5() if not is_big and not is_missing_part else None - session = Session(self, self.dc_id, self.auth_key, is_media=True) + session = Session(self, self.session_storage.dc_id, self.session_storage.auth_key, is_media=True) session.start() try: @@ -1492,7 +1496,7 @@ class Client(Methods, BaseClient): session = self.media_sessions.get(dc_id, None) if session is None: - if dc_id != self.dc_id: + if dc_id != self.session_storage.dc_id: exported_auth = self.send( functions.auth.ExportAuthorization( dc_id=dc_id @@ -1502,7 +1506,7 @@ class Client(Methods, BaseClient): session = Session( self, dc_id, - Auth(dc_id, self.test_mode, self.ipv6, self._proxy).create(), + Auth(dc_id, self.session_storage.test_mode, self.ipv6, self._proxy).create(), is_media=True ) @@ -1520,7 +1524,7 @@ class Client(Methods, BaseClient): session = Session( self, dc_id, - self.auth_key, + self.session_storage.auth_key, is_media=True ) @@ -1588,7 +1592,7 @@ class Client(Methods, BaseClient): cdn_session = Session( self, r.dc_id, - Auth(r.dc_id, self.test_mode, self.ipv6, self._proxy).create(), + Auth(r.dc_id, self.session_storage.test_mode, self.ipv6, self._proxy).create(), is_media=True, is_cdn=True ) diff --git a/pyrogram/client/ext/base_client.py b/pyrogram/client/ext/base_client.py index 3f40865f..732a600f 100644 --- a/pyrogram/client/ext/base_client.py +++ b/pyrogram/client/ext/base_client.py @@ -24,10 +24,10 @@ from threading import Lock from pyrogram import __version__ from ..style import Markdown, HTML from ...session.internals import MsgId -from ..session_storage import SessionStorageMixin, BaseSessionStorage +from ..session_storage import SessionStorage -class BaseClient(SessionStorageMixin): +class BaseClient: class StopTransmission(StopIteration): pass @@ -68,14 +68,14 @@ class BaseClient(SessionStorageMixin): 13: "video_note" } - def __init__(self, session_storage: BaseSessionStorage): + def __init__(self, session_storage: SessionStorage): self.session_storage = session_storage self.rnd_id = MsgId self.channels_pts = {} - self.markdown = Markdown(self.peers_by_id) - self.html = HTML(self.peers_by_id) + self.markdown = Markdown(self.session_storage.peers_by_id) + self.html = HTML(self.session_storage.peers_by_id) self.session = None self.media_sessions = {} diff --git a/pyrogram/client/ext/syncer.py b/pyrogram/client/ext/syncer.py index 70955624..e13212be 100644 --- a/pyrogram/client/ext/syncer.py +++ b/pyrogram/client/ext/syncer.py @@ -81,9 +81,9 @@ class Syncer: @classmethod def sync(cls, client): - client.date = int(time.time()) + client.session_storage.date = int(time.time()) try: - client.session_storage.save_session(sync=True) + client.session_storage.save(sync=True) except Exception as e: log.critical(e, exc_info=True) else: diff --git a/pyrogram/client/methods/contacts/get_contacts.py b/pyrogram/client/methods/contacts/get_contacts.py index 29b7e176..35b24592 100644 --- a/pyrogram/client/methods/contacts/get_contacts.py +++ b/pyrogram/client/methods/contacts/get_contacts.py @@ -44,5 +44,5 @@ class GetContacts(BaseClient): log.warning("get_contacts flood: waiting {} seconds".format(e.x)) time.sleep(e.x) else: - log.info("Total contacts: {}".format(len(self.peers_by_phone))) + log.info("Total contacts: {}".format(len(self.session_storage.peers_by_phone))) return [pyrogram.User._parse(self, user) for user in contacts.users] diff --git a/pyrogram/client/session_storage/__init__.py b/pyrogram/client/session_storage/__init__.py index 611ec9b7..ad2d8900 100644 --- a/pyrogram/client/session_storage/__init__.py +++ b/pyrogram/client/session_storage/__init__.py @@ -16,5 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from .session_storage_mixin import SessionStorageMixin -from .base_session_storage import BaseSessionStorage, BaseSessionConfig, SessionDoesNotExist +from .abstract import SessionStorage, SessionDoesNotExist +from .memory import MemorySessionStorage +from .json import JsonSessionStorage +from .string import StringSessionStorage diff --git a/pyrogram/client/session_storage/session_storage_mixin.py b/pyrogram/client/session_storage/abstract.py similarity index 50% rename from pyrogram/client/session_storage/session_storage_mixin.py rename to pyrogram/client/session_storage/abstract.py index 7d783ca7..e8f4441e 100644 --- a/pyrogram/client/session_storage/session_storage_mixin.py +++ b/pyrogram/client/session_storage/abstract.py @@ -16,66 +16,103 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from typing import Dict +import abc +from typing import Type + +import pyrogram -class SessionStorageMixin: +class SessionDoesNotExist(Exception): + pass + + +class SessionStorage(abc.ABC): + def __init__(self, client: 'pyrogram.client.BaseClient'): + self._client = client + + @abc.abstractmethod + def load(self): + ... + + @abc.abstractmethod + def save(self, sync=False): + ... + + @abc.abstractmethod + def sync_cleanup(self): + ... + @property - def dc_id(self) -> int: - return self.session_storage.dc_id + @abc.abstractmethod + def dc_id(self): + ... @dc_id.setter + @abc.abstractmethod def dc_id(self, val): - self.session_storage.dc_id = val + ... @property - def test_mode(self) -> bool: - return self.session_storage.test_mode + @abc.abstractmethod + def test_mode(self): + ... @test_mode.setter + @abc.abstractmethod def test_mode(self, val): - self.session_storage.test_mode = val + ... @property - def auth_key(self) -> bytes: - return self.session_storage.auth_key + @abc.abstractmethod + def auth_key(self): + ... @auth_key.setter + @abc.abstractmethod def auth_key(self, val): - self.session_storage.auth_key = val + ... @property + @abc.abstractmethod def user_id(self): - return self.session_storage.user_id + ... @user_id.setter - def user_id(self, val) -> int: - self.session_storage.user_id = val + @abc.abstractmethod + def user_id(self, val): + ... @property - def date(self) -> int: - return self.session_storage.date + @abc.abstractmethod + def date(self): + ... @date.setter + @abc.abstractmethod def date(self, val): - self.session_storage.date = val + ... @property + @abc.abstractmethod def is_bot(self): - return self.session_storage.is_bot + ... @is_bot.setter - def is_bot(self, val) -> int: - self.session_storage.is_bot = val + @abc.abstractmethod + def is_bot(self, val): + ... @property - def peers_by_id(self) -> Dict[str, int]: - return self.session_storage.peers_by_id + @abc.abstractmethod + def peers_by_id(self): + ... @property - def peers_by_username(self) -> Dict[str, int]: - return self.session_storage.peers_by_username + @abc.abstractmethod + def peers_by_username(self): + ... @property - def peers_by_phone(self) -> Dict[str, int]: - return self.session_storage.peers_by_phone + @abc.abstractmethod + def peers_by_phone(self): + ... diff --git a/pyrogram/client/session_storage/base_session_storage.py b/pyrogram/client/session_storage/base_session_storage.py deleted file mode 100644 index 92473956..00000000 --- a/pyrogram/client/session_storage/base_session_storage.py +++ /dev/null @@ -1,60 +0,0 @@ -# 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 abc -from typing import Type - -import pyrogram - - -class SessionDoesNotExist(Exception): - pass - - -class BaseSessionStorage(abc.ABC): - def __init__(self, client: 'pyrogram.client.BaseClient', session_data): - self.client = client - self.session_data = session_data - self.dc_id = 1 - self.test_mode = None - self.auth_key = None - self.user_id = None - self.date = 0 - self.is_bot = False - self.peers_by_id = {} - self.peers_by_username = {} - self.peers_by_phone = {} - - @abc.abstractmethod - def load_session(self): - ... - - @abc.abstractmethod - def save_session(self, sync=False): - ... - - @abc.abstractmethod - def sync_cleanup(self): - ... - - -class BaseSessionConfig(abc.ABC): - @property - @abc.abstractmethod - def session_storage_cls(self) -> Type[BaseSessionStorage]: - ... diff --git a/pyrogram/client/session_storage/json_session_storage.py b/pyrogram/client/session_storage/json.py similarity index 58% rename from pyrogram/client/session_storage/json_session_storage.py rename to pyrogram/client/session_storage/json.py index 1e1e0ca4..170089a4 100644 --- a/pyrogram/client/session_storage/json_session_storage.py +++ b/pyrogram/client/session_storage/json.py @@ -22,21 +22,26 @@ import logging import os import shutil +import pyrogram from ..ext import utils -from . import BaseSessionStorage, SessionDoesNotExist +from . import MemorySessionStorage, SessionDoesNotExist log = logging.getLogger(__name__) -class JsonSessionStorage(BaseSessionStorage): +class JsonSessionStorage(MemorySessionStorage): + def __init__(self, client: 'pyrogram.client.ext.BaseClient', session_name: str): + super(JsonSessionStorage, self).__init__(client) + self._session_name = session_name + def _get_file_name(self, name: str): if not name.endswith('.session'): name += '.session' - return os.path.join(self.client.workdir, name) + return os.path.join(self._client.workdir, name) - def load_session(self): - file_path = self._get_file_name(self.session_data) + def load(self): + file_path = self._get_file_name(self._session_name) log.info('Loading JSON session from {}'.format(file_path)) try: @@ -45,59 +50,59 @@ class JsonSessionStorage(BaseSessionStorage): except FileNotFoundError: raise SessionDoesNotExist() - self.dc_id = s["dc_id"] - self.test_mode = s["test_mode"] - self.auth_key = base64.b64decode("".join(s["auth_key"])) # join split key - self.user_id = s["user_id"] - self.date = s.get("date", 0) - self.is_bot = s.get('is_bot', self.client.is_bot) + self._dc_id = s["dc_id"] + self._test_mode = s["test_mode"] + self._auth_key = base64.b64decode("".join(s["auth_key"])) # join split key + self._user_id = s["user_id"] + self._date = s.get("date", 0) + self._is_bot = s.get('is_bot', self._is_bot) for k, v in s.get("peers_by_id", {}).items(): - self.peers_by_id[int(k)] = utils.get_input_peer(int(k), v) + self._peers_by_id[int(k)] = utils.get_input_peer(int(k), v) for k, v in s.get("peers_by_username", {}).items(): - peer = self.peers_by_id.get(v, None) + peer = self._peers_by_id.get(v, None) if peer: - self.peers_by_username[k] = peer + self._peers_by_username[k] = peer for k, v in s.get("peers_by_phone", {}).items(): - peer = self.peers_by_id.get(v, None) + peer = self._peers_by_id.get(v, None) if peer: - self.peers_by_phone[k] = peer + self._peers_by_phone[k] = peer - def save_session(self, sync=False): - file_path = self._get_file_name(self.session_data) + def save(self, sync=False): + file_path = self._get_file_name(self._session_name) if sync: file_path += '.tmp' log.info('Saving JSON session to {}, sync={}'.format(file_path, sync)) - auth_key = base64.b64encode(self.auth_key).decode() + auth_key = base64.b64encode(self._auth_key).decode() auth_key = [auth_key[i: i + 43] for i in range(0, len(auth_key), 43)] # split key in lines of 43 chars - os.makedirs(self.client.workdir, exist_ok=True) + os.makedirs(self._client.workdir, exist_ok=True) data = { - 'dc_id': self.dc_id, - 'test_mode': self.test_mode, + 'dc_id': self._dc_id, + 'test_mode': self._test_mode, 'auth_key': auth_key, - 'user_id': self.user_id, - 'date': self.date, - 'is_bot': self.is_bot, + 'user_id': self._user_id, + 'date': self._date, + 'is_bot': self._is_bot, 'peers_by_id': { k: getattr(v, "access_hash", None) - for k, v in self.peers_by_id.copy().items() + for k, v in self._peers_by_id.copy().items() }, 'peers_by_username': { k: utils.get_peer_id(v) - for k, v in self.peers_by_username.copy().items() + for k, v in self._peers_by_username.copy().items() }, 'peers_by_phone': { k: utils.get_peer_id(v) - for k, v in self.peers_by_phone.copy().items() + for k, v in self._peers_by_phone.copy().items() } } @@ -109,10 +114,10 @@ class JsonSessionStorage(BaseSessionStorage): # execution won't be here if an error has occurred earlier if sync: - shutil.move(file_path, self._get_file_name(self.session_data)) + shutil.move(file_path, self._get_file_name(self._session_name)) def sync_cleanup(self): try: - os.remove(self._get_file_name(self.session_data) + '.tmp') + os.remove(self._get_file_name(self._session_name) + '.tmp') except OSError: pass diff --git a/pyrogram/client/session_storage/memory.py b/pyrogram/client/session_storage/memory.py new file mode 100644 index 00000000..f456f8eb --- /dev/null +++ b/pyrogram/client/session_storage/memory.py @@ -0,0 +1,85 @@ +import pyrogram +from . import SessionStorage, SessionDoesNotExist + + +class MemorySessionStorage(SessionStorage): + def __init__(self, client: 'pyrogram.client.ext.BaseClient'): + super(MemorySessionStorage, self).__init__(client) + self._dc_id = 1 + self._test_mode = None + self._auth_key = None + self._user_id = None + self._date = 0 + self._is_bot = False + self._peers_by_id = {} + self._peers_by_username = {} + self._peers_by_phone = {} + + def load(self): + raise SessionDoesNotExist() + + def save(self, sync=False): + pass + + def sync_cleanup(self): + pass + + @property + def dc_id(self): + return self._dc_id + + @dc_id.setter + def dc_id(self, val): + self._dc_id = val + + @property + def test_mode(self): + return self._test_mode + + @test_mode.setter + def test_mode(self, val): + self._test_mode = val + + @property + def auth_key(self): + return self._auth_key + + @auth_key.setter + def auth_key(self, val): + self._auth_key = val + + @property + def user_id(self): + return self._user_id + + @user_id.setter + def user_id(self, val): + self._user_id = val + + @property + def date(self): + return self._date + + @date.setter + def date(self, val): + self._date = val + + @property + def is_bot(self): + return self._is_bot + + @is_bot.setter + def is_bot(self, val): + self._is_bot = val + + @property + def peers_by_id(self): + return self._peers_by_id + + @property + def peers_by_username(self): + return self._peers_by_username + + @property + def peers_by_phone(self): + return self._peers_by_phone diff --git a/pyrogram/client/session_storage/string_session_storage.py b/pyrogram/client/session_storage/string.py similarity index 62% rename from pyrogram/client/session_storage/string_session_storage.py rename to pyrogram/client/session_storage/string.py index 5b1a8cc1..f8ec740a 100644 --- a/pyrogram/client/session_storage/string_session_storage.py +++ b/pyrogram/client/session_storage/string.py @@ -2,10 +2,11 @@ import base64 import binascii import struct -from . import BaseSessionStorage, SessionDoesNotExist +import pyrogram +from . import MemorySessionStorage, SessionDoesNotExist -class StringSessionStorage(BaseSessionStorage): +class StringSessionStorage(MemorySessionStorage): """ Packs session data as following (forcing little-endian byte order): Char dc_id (1 byte, unsigned) @@ -18,22 +19,26 @@ class StringSessionStorage(BaseSessionStorage): """ PACK_FORMAT = '. import abc -from typing import Type +from typing import Type, Union import pyrogram +from pyrogram.api import types class SessionDoesNotExist(Exception): @@ -102,17 +103,41 @@ class SessionStorage(abc.ABC): def is_bot(self, val): ... - @property @abc.abstractmethod - def peers_by_id(self): + def clear_cache(self): ... - @property @abc.abstractmethod - def peers_by_username(self): + def cache_peer(self, entity: Union[types.User, + types.Chat, types.ChatForbidden, + types.Channel, types.ChannelForbidden]): ... - @property @abc.abstractmethod - def peers_by_phone(self): + def get_peer_by_id(self, val: int): + ... + + @abc.abstractmethod + def get_peer_by_username(self, val: str): + ... + + @abc.abstractmethod + def get_peer_by_phone(self, val: str): + ... + + def get_peer(self, peer_id: Union[int, str]): + if isinstance(peer_id, int): + return self.get_peer_by_id(peer_id) + else: + peer_id = peer_id.lstrip('+@') + if peer_id.isdigit(): + return self.get_peer_by_phone(peer_id) + return self.get_peer_by_username(peer_id) + + @abc.abstractmethod + def peers_count(self): + ... + + @abc.abstractmethod + def contacts_count(self): ... diff --git a/pyrogram/client/session_storage/json.py b/pyrogram/client/session_storage/json.py index 170089a4..aaa6b96f 100644 --- a/pyrogram/client/session_storage/json.py +++ b/pyrogram/client/session_storage/json.py @@ -58,19 +58,19 @@ class JsonSessionStorage(MemorySessionStorage): self._is_bot = s.get('is_bot', self._is_bot) for k, v in s.get("peers_by_id", {}).items(): - self._peers_by_id[int(k)] = utils.get_input_peer(int(k), v) + self._peers_cache['i' + k] = utils.get_input_peer(int(k), v) for k, v in s.get("peers_by_username", {}).items(): - peer = self._peers_by_id.get(v, None) - - if peer: - self._peers_by_username[k] = peer + try: + self._peers_cache['u' + k] = self.get_peer_by_id(v) + except KeyError: + pass for k, v in s.get("peers_by_phone", {}).items(): - peer = self._peers_by_id.get(v, None) - - if peer: - self._peers_by_phone[k] = peer + try: + self._peers_cache['p' + k] = self.get_peer_by_id(v) + except KeyError: + pass def save(self, sync=False): file_path = self._get_file_name(self._session_name) @@ -93,16 +93,19 @@ class JsonSessionStorage(MemorySessionStorage): 'date': self._date, 'is_bot': self._is_bot, 'peers_by_id': { - k: getattr(v, "access_hash", None) - for k, v in self._peers_by_id.copy().items() + k[1:]: getattr(v, "access_hash", None) + for k, v in self._peers_cache.copy().items() + if k[0] == 'i' }, 'peers_by_username': { - k: utils.get_peer_id(v) - for k, v in self._peers_by_username.copy().items() + k[1:]: utils.get_peer_id(v) + for k, v in self._peers_cache.copy().items() + if k[0] == 'u' }, 'peers_by_phone': { - k: utils.get_peer_id(v) - for k, v in self._peers_by_phone.copy().items() + k[1:]: utils.get_peer_id(v) + for k, v in self._peers_cache.copy().items() + if k[0] == 'p' } } diff --git a/pyrogram/client/session_storage/memory.py b/pyrogram/client/session_storage/memory.py index f456f8eb..d5f92f0d 100644 --- a/pyrogram/client/session_storage/memory.py +++ b/pyrogram/client/session_storage/memory.py @@ -1,4 +1,5 @@ import pyrogram +from pyrogram.api import types from . import SessionStorage, SessionDoesNotExist @@ -11,9 +12,7 @@ class MemorySessionStorage(SessionStorage): self._user_id = None self._date = 0 self._is_bot = False - self._peers_by_id = {} - self._peers_by_username = {} - self._peers_by_phone = {} + self._peers_cache = {} def load(self): raise SessionDoesNotExist() @@ -72,14 +71,48 @@ class MemorySessionStorage(SessionStorage): def is_bot(self, val): self._is_bot = val - @property - def peers_by_id(self): - return self._peers_by_id + def clear_cache(self): + keys = list(filter(lambda k: k[0] in 'up', self._peers_cache.keys())) + for key in keys: + try: + del self._peers_cache[key] + except KeyError: + pass - @property - def peers_by_username(self): - return self._peers_by_username + def cache_peer(self, entity): + if isinstance(entity, types.User): + input_peer = types.InputPeerUser( + user_id=entity.id, + access_hash=entity.access_hash + ) + self._peers_cache['i' + str(entity.id)] = input_peer + if entity.username: + self._peers_cache['u' + entity.username.lower()] = input_peer + if entity.phone: + self._peers_cache['p' + entity.phone] = input_peer + elif isinstance(entity, (types.Chat, types.ChatForbidden)): + self._peers_cache['i-' + str(entity.id)] = types.InputPeerChat(chat_id=entity.id) + elif isinstance(entity, (types.Channel, types.ChannelForbidden)): + input_peer = types.InputPeerChannel( + channel_id=entity.id, + access_hash=entity.access_hash + ) + self._peers_cache['i-100' + str(entity.id)] = input_peer + username = getattr(entity, "username", None) + if username: + self._peers_cache['u' + username.lower()] = input_peer - @property - def peers_by_phone(self): - return self._peers_by_phone + def get_peer_by_id(self, val): + return self._peers_cache['i' + str(val)] + + def get_peer_by_username(self, val): + return self._peers_cache['u' + val.lower()] + + def get_peer_by_phone(self, val): + return self._peers_cache['p' + val] + + def peers_count(self): + return len(list(filter(lambda k: k[0] == 'i', self._peers_cache.keys()))) + + def contacts_count(self): + return len(list(filter(lambda k: k[0] == 'p', self._peers_cache.keys()))) diff --git a/pyrogram/client/style/html.py b/pyrogram/client/style/html.py index 9a72a565..88e317cd 100644 --- a/pyrogram/client/style/html.py +++ b/pyrogram/client/style/html.py @@ -29,14 +29,15 @@ from pyrogram.api.types import ( InputMessageEntityMentionName as Mention, ) from . import utils +from ..session_storage import SessionStorage class HTML: HTML_RE = re.compile(r"<(\w+)(?: href=([\"'])([^<]+)\2)?>([^>]+)") MENTION_RE = re.compile(r"tg://user\?id=(\d+)") - def __init__(self, peers_by_id): - self.peers_by_id = peers_by_id + def __init__(self, session_storage: SessionStorage): + self.session_storage = session_storage def parse(self, message: str): entities = [] @@ -52,7 +53,10 @@ class HTML: if mention: user_id = int(mention.group(1)) - input_user = self.peers_by_id.get(user_id, None) + try: + input_user = self.session_storage.get_peer_by_id(user_id) + except KeyError: + input_user = None entity = ( Mention(start, len(body), input_user) diff --git a/pyrogram/client/style/markdown.py b/pyrogram/client/style/markdown.py index 05a11a25..6793b643 100644 --- a/pyrogram/client/style/markdown.py +++ b/pyrogram/client/style/markdown.py @@ -29,6 +29,7 @@ from pyrogram.api.types import ( InputMessageEntityMentionName as Mention ) from . import utils +from ..session_storage import SessionStorage class Markdown: @@ -52,8 +53,8 @@ class Markdown: )) MENTION_RE = re.compile(r"tg://user\?id=(\d+)") - def __init__(self, peers_by_id: dict): - self.peers_by_id = peers_by_id + def __init__(self, session_storage: SessionStorage): + self.session_storage = session_storage def parse(self, message: str): message = utils.add_surrogates(str(message)).strip() @@ -69,7 +70,10 @@ class Markdown: if mention: user_id = int(mention.group(1)) - input_user = self.peers_by_id.get(user_id, None) + try: + input_user = self.session_storage.get_peer_by_id(user_id) + except KeyError: + input_user = None entity = ( Mention(start, len(text), input_user) From 03b92b3302a9d316d4e693efa8b9d87b0b991fd0 Mon Sep 17 00:00:00 2001 From: bakatrouble Date: Tue, 26 Feb 2019 21:06:30 +0300 Subject: [PATCH 09/73] Implement SQLite session storage --- pyrogram/client/client.py | 2 +- pyrogram/client/session_storage/__init__.py | 1 + pyrogram/client/session_storage/json.py | 6 +- .../client/session_storage/sqlite/0001.sql | 21 +++ .../client/session_storage/sqlite/__init__.py | 132 ++++++++++++++++++ 5 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 pyrogram/client/session_storage/sqlite/0001.sql create mode 100644 pyrogram/client/session_storage/sqlite/__init__.py diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index ad755977..5fc805c0 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -57,7 +57,7 @@ from .ext import utils, Syncer, BaseClient from .methods import Methods from .session_storage import ( SessionDoesNotExist, SessionStorage, MemorySessionStorage, JsonSessionStorage, - StringSessionStorage + StringSessionStorage, SQLiteSessionStorage ) log = logging.getLogger(__name__) diff --git a/pyrogram/client/session_storage/__init__.py b/pyrogram/client/session_storage/__init__.py index ad2d8900..adfcf813 100644 --- a/pyrogram/client/session_storage/__init__.py +++ b/pyrogram/client/session_storage/__init__.py @@ -20,3 +20,4 @@ from .abstract import SessionStorage, SessionDoesNotExist from .memory import MemorySessionStorage from .json import JsonSessionStorage from .string import StringSessionStorage +from .sqlite import SQLiteSessionStorage diff --git a/pyrogram/client/session_storage/json.py b/pyrogram/client/session_storage/json.py index aaa6b96f..570e1525 100644 --- a/pyrogram/client/session_storage/json.py +++ b/pyrogram/client/session_storage/json.py @@ -29,6 +29,8 @@ from . import MemorySessionStorage, SessionDoesNotExist log = logging.getLogger(__name__) +EXTENSION = '.session' + class JsonSessionStorage(MemorySessionStorage): def __init__(self, client: 'pyrogram.client.ext.BaseClient', session_name: str): @@ -36,8 +38,8 @@ class JsonSessionStorage(MemorySessionStorage): self._session_name = session_name def _get_file_name(self, name: str): - if not name.endswith('.session'): - name += '.session' + if not name.endswith(EXTENSION): + name += EXTENSION return os.path.join(self._client.workdir, name) def load(self): diff --git a/pyrogram/client/session_storage/sqlite/0001.sql b/pyrogram/client/session_storage/sqlite/0001.sql new file mode 100644 index 00000000..d81e9554 --- /dev/null +++ b/pyrogram/client/session_storage/sqlite/0001.sql @@ -0,0 +1,21 @@ +create table sessions ( + dc_id integer primary key, + test_mode integer, + auth_key blob, + user_id integer, + date integer, + is_bot integer +); + +create table peers_cache ( + id integer primary key, + hash integer, + username text, + phone integer +); + +create table migrations ( + name text primary key +); + +insert into migrations (name) values ('0001'); diff --git a/pyrogram/client/session_storage/sqlite/__init__.py b/pyrogram/client/session_storage/sqlite/__init__.py new file mode 100644 index 00000000..75931109 --- /dev/null +++ b/pyrogram/client/session_storage/sqlite/__init__.py @@ -0,0 +1,132 @@ +# 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 logging +import os +import sqlite3 + +import pyrogram +from ....api import types +from ...ext import utils +from .. import MemorySessionStorage, SessionDoesNotExist + + +log = logging.getLogger(__name__) + +EXTENSION = '.session.sqlite3' +MIGRATIONS = ['0001'] + + +class SQLiteSessionStorage(MemorySessionStorage): + def __init__(self, client: 'pyrogram.client.ext.BaseClient', session_name: str): + super(SQLiteSessionStorage, self).__init__(client) + self._session_name = session_name + self._conn = None # type: sqlite3.Connection + + def _get_file_name(self, name: str): + if not name.endswith(EXTENSION): + name += EXTENSION + return os.path.join(self._client.workdir, name) + + def _apply_migrations(self, new_db=False): + migrations = MIGRATIONS.copy() + if not new_db: + cursor = self._conn.cursor() + cursor.execute('select name from migrations') + for row in cursor.fetchone(): + migrations.remove(row) + for name in migrations: + with open(os.path.join(os.path.dirname(__file__), '{}.sql'.format(name))) as script: + self._conn.executescript(script.read()) + + def load(self): + file_path = self._get_file_name(self._session_name) + log.info('Loading SQLite session from {}'.format(file_path)) + + if os.path.isfile(file_path): + self._conn = sqlite3.connect(file_path) + self._apply_migrations() + else: + self._conn = sqlite3.connect(file_path) + self._apply_migrations(new_db=True) + + cursor = self._conn.cursor() + cursor.execute('select dc_id, test_mode, auth_key, user_id, "date", is_bot from sessions') + row = cursor.fetchone() + if not row: + raise SessionDoesNotExist() + + self._dc_id = row[0] + self._test_mode = bool(row[1]) + self._auth_key = row[2] + self._user_id = row[3] + self._date = row[4] + self._is_bot = bool(row[5]) + + def cache_peer(self, entity): + peer_id = username = phone = access_hash = None + + if isinstance(entity, types.User): + peer_id = entity.id + username = entity.username.lower() if entity.username else None + phone = entity.phone or None + access_hash = entity.access_hash + elif isinstance(entity, (types.Chat, types.ChatForbidden)): + peer_id = -entity.id + # input_peer = types.InputPeerChat(chat_id=entity.id) + elif isinstance(entity, (types.Channel, types.ChannelForbidden)): + peer_id = int('-100' + str(entity.id)) + username = entity.username.lower() if hasattr(entity, 'username') and entity.username else None + access_hash = entity.access_hash + + self._conn.execute('insert or replace into peers_cache values (?, ?, ?, ?)', + (peer_id, access_hash, username, phone)) + + def get_peer_by_id(self, val): + cursor = self._conn.cursor() + cursor.execute('select id, hash from peers_cache where id = ?', (val,)) + row = cursor.fetchone() + if not row: + raise KeyError(val) + return utils.get_input_peer(row[0], row[1]) + + def get_peer_by_username(self, val): + cursor = self._conn.cursor() + cursor.execute('select id, hash from peers_cache where username = ?', (val,)) + row = cursor.fetchone() + if not row: + raise KeyError(val) + return utils.get_input_peer(row[0], row[1]) + + def get_peer_by_phone(self, val): + cursor = self._conn.cursor() + cursor.execute('select id, hash from peers_cache where phone = ?', (val,)) + row = cursor.fetchone() + if not row: + raise KeyError(val) + return utils.get_input_peer(row[0], row[1]) + + def save(self, sync=False): + log.info('Committing SQLite session') + self._conn.execute('delete from sessions') + self._conn.execute('insert into sessions values (?, ?, ?, ?, ?, ?)', + (self._dc_id, self._test_mode, self._auth_key, self._user_id, self._date, self._is_bot)) + self._conn.commit() + + def sync_cleanup(self): + pass From 10fc340efff40dc54e35bc687af5966b3ad077f5 Mon Sep 17 00:00:00 2001 From: bakatrouble Date: Tue, 26 Feb 2019 21:43:23 +0300 Subject: [PATCH 10/73] Add session migrating from json; add some indexes to sqlite sessions --- pyrogram/client/client.py | 2 +- .../client/session_storage/sqlite/0001.sql | 3 ++ .../client/session_storage/sqlite/__init__.py | 29 +++++++++++++++---- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 5fc805c0..d2bc3ee4 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -214,7 +214,7 @@ class Client(Methods, BaseClient): elif session_name.startswith(':'): session_storage = StringSessionStorage(self, session_name) else: - session_storage = JsonSessionStorage(self, session_name) + session_storage = SQLiteSessionStorage(self, session_name) elif isinstance(session_name, SessionStorage): session_storage = session_name else: diff --git a/pyrogram/client/session_storage/sqlite/0001.sql b/pyrogram/client/session_storage/sqlite/0001.sql index d81e9554..c6c51d24 100644 --- a/pyrogram/client/session_storage/sqlite/0001.sql +++ b/pyrogram/client/session_storage/sqlite/0001.sql @@ -18,4 +18,7 @@ create table migrations ( name text primary key ); +create index username_idx on peers_cache(username); +create index phone_idx on peers_cache(phone); + insert into migrations (name) values ('0001'); diff --git a/pyrogram/client/session_storage/sqlite/__init__.py b/pyrogram/client/session_storage/sqlite/__init__.py index 75931109..4fc7ff64 100644 --- a/pyrogram/client/session_storage/sqlite/__init__.py +++ b/pyrogram/client/session_storage/sqlite/__init__.py @@ -18,17 +18,18 @@ import logging import os +import shutil import sqlite3 import pyrogram from ....api import types from ...ext import utils -from .. import MemorySessionStorage, SessionDoesNotExist +from .. import MemorySessionStorage, SessionDoesNotExist, JsonSessionStorage log = logging.getLogger(__name__) -EXTENSION = '.session.sqlite3' +EXTENSION = '.session' MIGRATIONS = ['0001'] @@ -54,13 +55,32 @@ class SQLiteSessionStorage(MemorySessionStorage): with open(os.path.join(os.path.dirname(__file__), '{}.sql'.format(name))) as script: self._conn.executescript(script.read()) + def _migrate_from_json(self): + jss = JsonSessionStorage(self._client, self._session_name) + jss.load() + file_path = self._get_file_name(self._session_name) + self._conn = sqlite3.connect(file_path + '.tmp') + self._apply_migrations(new_db=True) + self._dc_id, self._test_mode, self._auth_key, self._user_id, self._date, self._is_bot = \ + jss.dc_id, jss.test_mode, jss.auth_key, jss.user_id, jss.date, jss.is_bot + self.save() + self._conn.close() + shutil.move(file_path + '.tmp', file_path) + log.warning('Session was migrated from JSON, loading...') + self.load() + def load(self): file_path = self._get_file_name(self._session_name) log.info('Loading SQLite session from {}'.format(file_path)) if os.path.isfile(file_path): - self._conn = sqlite3.connect(file_path) - self._apply_migrations() + try: + self._conn = sqlite3.connect(file_path) + self._apply_migrations() + except sqlite3.DatabaseError: + log.warning('Trying to migrate session from JSON...') + self._migrate_from_json() + return else: self._conn = sqlite3.connect(file_path) self._apply_migrations(new_db=True) @@ -88,7 +108,6 @@ class SQLiteSessionStorage(MemorySessionStorage): access_hash = entity.access_hash elif isinstance(entity, (types.Chat, types.ChatForbidden)): peer_id = -entity.id - # input_peer = types.InputPeerChat(chat_id=entity.id) elif isinstance(entity, (types.Channel, types.ChannelForbidden)): peer_id = int('-100' + str(entity.id)) username = entity.username.lower() if hasattr(entity, 'username') and entity.username else None From 033622cfb85efbd8a09abf8b0949f9ccc0495b90 Mon Sep 17 00:00:00 2001 From: bakatrouble Date: Wed, 27 Feb 2019 22:49:23 +0300 Subject: [PATCH 11/73] Cleanup json session storage specific code as it is used only for migrations --- pyrogram/client/ext/syncer.py | 2 - pyrogram/client/session_storage/abstract.py | 4 -- pyrogram/client/session_storage/json.py | 67 +------------------ pyrogram/client/session_storage/memory.py | 3 - .../client/session_storage/sqlite/__init__.py | 3 - pyrogram/client/session_storage/string.py | 3 - 6 files changed, 1 insertion(+), 81 deletions(-) diff --git a/pyrogram/client/ext/syncer.py b/pyrogram/client/ext/syncer.py index e13212be..9e7d2303 100644 --- a/pyrogram/client/ext/syncer.py +++ b/pyrogram/client/ext/syncer.py @@ -88,5 +88,3 @@ class Syncer: log.critical(e, exc_info=True) else: log.info("Synced {}".format(client.session_name)) - finally: - client.session_storage.sync_cleanup() diff --git a/pyrogram/client/session_storage/abstract.py b/pyrogram/client/session_storage/abstract.py index 39517a01..134d5c8c 100644 --- a/pyrogram/client/session_storage/abstract.py +++ b/pyrogram/client/session_storage/abstract.py @@ -38,10 +38,6 @@ class SessionStorage(abc.ABC): @abc.abstractmethod def save(self, sync=False): ... - - @abc.abstractmethod - def sync_cleanup(self): - ... @property @abc.abstractmethod diff --git a/pyrogram/client/session_storage/json.py b/pyrogram/client/session_storage/json.py index 570e1525..4a48d3c1 100644 --- a/pyrogram/client/session_storage/json.py +++ b/pyrogram/client/session_storage/json.py @@ -59,70 +59,5 @@ class JsonSessionStorage(MemorySessionStorage): self._date = s.get("date", 0) self._is_bot = s.get('is_bot', self._is_bot) - for k, v in s.get("peers_by_id", {}).items(): - self._peers_cache['i' + k] = utils.get_input_peer(int(k), v) - - for k, v in s.get("peers_by_username", {}).items(): - try: - self._peers_cache['u' + k] = self.get_peer_by_id(v) - except KeyError: - pass - - for k, v in s.get("peers_by_phone", {}).items(): - try: - self._peers_cache['p' + k] = self.get_peer_by_id(v) - except KeyError: - pass - def save(self, sync=False): - file_path = self._get_file_name(self._session_name) - - if sync: - file_path += '.tmp' - - log.info('Saving JSON session to {}, sync={}'.format(file_path, sync)) - - auth_key = base64.b64encode(self._auth_key).decode() - auth_key = [auth_key[i: i + 43] for i in range(0, len(auth_key), 43)] # split key in lines of 43 chars - - os.makedirs(self._client.workdir, exist_ok=True) - - data = { - 'dc_id': self._dc_id, - 'test_mode': self._test_mode, - 'auth_key': auth_key, - 'user_id': self._user_id, - 'date': self._date, - 'is_bot': self._is_bot, - 'peers_by_id': { - k[1:]: getattr(v, "access_hash", None) - for k, v in self._peers_cache.copy().items() - if k[0] == 'i' - }, - 'peers_by_username': { - k[1:]: utils.get_peer_id(v) - for k, v in self._peers_cache.copy().items() - if k[0] == 'u' - }, - 'peers_by_phone': { - k[1:]: utils.get_peer_id(v) - for k, v in self._peers_cache.copy().items() - if k[0] == 'p' - } - } - - with open(file_path, "w", encoding="utf-8") as f: - json.dump(data, f, indent=4) - - f.flush() - os.fsync(f.fileno()) - - # execution won't be here if an error has occurred earlier - if sync: - shutil.move(file_path, self._get_file_name(self._session_name)) - - def sync_cleanup(self): - try: - os.remove(self._get_file_name(self._session_name) + '.tmp') - except OSError: - pass + pass diff --git a/pyrogram/client/session_storage/memory.py b/pyrogram/client/session_storage/memory.py index d5f92f0d..c0610e70 100644 --- a/pyrogram/client/session_storage/memory.py +++ b/pyrogram/client/session_storage/memory.py @@ -20,9 +20,6 @@ class MemorySessionStorage(SessionStorage): def save(self, sync=False): pass - def sync_cleanup(self): - pass - @property def dc_id(self): return self._dc_id diff --git a/pyrogram/client/session_storage/sqlite/__init__.py b/pyrogram/client/session_storage/sqlite/__init__.py index 4fc7ff64..0308a4dc 100644 --- a/pyrogram/client/session_storage/sqlite/__init__.py +++ b/pyrogram/client/session_storage/sqlite/__init__.py @@ -146,6 +146,3 @@ class SQLiteSessionStorage(MemorySessionStorage): self._conn.execute('insert into sessions values (?, ?, ?, ?, ?, ?)', (self._dc_id, self._test_mode, self._auth_key, self._user_id, self._date, self._is_bot)) self._conn.commit() - - def sync_cleanup(self): - pass diff --git a/pyrogram/client/session_storage/string.py b/pyrogram/client/session_storage/string.py index f8ec740a..11051323 100644 --- a/pyrogram/client/session_storage/string.py +++ b/pyrogram/client/session_storage/string.py @@ -44,6 +44,3 @@ class StringSessionStorage(MemorySessionStorage): encoded = ':' + base64.b64encode(packed, b'-_').decode('latin-1').rstrip('=') split = '\n'.join(['"{}"'.format(encoded[i: i + 50]) for i in range(0, len(encoded), 50)]) print('Created session string:\n{}'.format(split)) - - def sync_cleanup(self): - pass From 8cc61f00ed74fc8290b4d75cc1503275a42d5136 Mon Sep 17 00:00:00 2001 From: bakatrouble Date: Fri, 1 Mar 2019 21:23:01 +0300 Subject: [PATCH 12/73] Fix threading with sqlite storage --- .../client/session_storage/sqlite/__init__.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/pyrogram/client/session_storage/sqlite/__init__.py b/pyrogram/client/session_storage/sqlite/__init__.py index 0308a4dc..a16e75e8 100644 --- a/pyrogram/client/session_storage/sqlite/__init__.py +++ b/pyrogram/client/session_storage/sqlite/__init__.py @@ -20,6 +20,7 @@ import logging import os import shutil import sqlite3 +from threading import Lock import pyrogram from ....api import types @@ -38,6 +39,7 @@ class SQLiteSessionStorage(MemorySessionStorage): super(SQLiteSessionStorage, self).__init__(client) self._session_name = session_name self._conn = None # type: sqlite3.Connection + self._lock = Lock() def _get_file_name(self, name: str): if not name.endswith(EXTENSION): @@ -45,6 +47,7 @@ class SQLiteSessionStorage(MemorySessionStorage): return os.path.join(self._client.workdir, name) def _apply_migrations(self, new_db=False): + self._conn.execute('PRAGMA read_uncommitted = true') migrations = MIGRATIONS.copy() if not new_db: cursor = self._conn.cursor() @@ -75,14 +78,14 @@ class SQLiteSessionStorage(MemorySessionStorage): if os.path.isfile(file_path): try: - self._conn = sqlite3.connect(file_path) + self._conn = sqlite3.connect(file_path, isolation_level='EXCLUSIVE', check_same_thread=False) self._apply_migrations() except sqlite3.DatabaseError: log.warning('Trying to migrate session from JSON...') self._migrate_from_json() return else: - self._conn = sqlite3.connect(file_path) + self._conn = sqlite3.connect(file_path, isolation_level='EXCLUSIVE', check_same_thread=False) self._apply_migrations(new_db=True) cursor = self._conn.cursor() @@ -113,8 +116,9 @@ class SQLiteSessionStorage(MemorySessionStorage): username = entity.username.lower() if hasattr(entity, 'username') and entity.username else None access_hash = entity.access_hash - self._conn.execute('insert or replace into peers_cache values (?, ?, ?, ?)', - (peer_id, access_hash, username, phone)) + with self._lock: + self._conn.execute('insert or replace into peers_cache values (?, ?, ?, ?)', + (peer_id, access_hash, username, phone)) def get_peer_by_id(self, val): cursor = self._conn.cursor() @@ -142,7 +146,8 @@ class SQLiteSessionStorage(MemorySessionStorage): def save(self, sync=False): log.info('Committing SQLite session') - self._conn.execute('delete from sessions') - self._conn.execute('insert into sessions values (?, ?, ?, ?, ?, ?)', - (self._dc_id, self._test_mode, self._auth_key, self._user_id, self._date, self._is_bot)) - self._conn.commit() + with self._lock: + self._conn.execute('delete from sessions') + self._conn.execute('insert into sessions values (?, ?, ?, ?, ?, ?)', + (self._dc_id, self._test_mode, self._auth_key, self._user_id, self._date, self._is_bot)) + self._conn.commit() From 85700b0ffc191458677b6d29452cff121a5d4d13 Mon Sep 17 00:00:00 2001 From: bakatrouble Date: Fri, 1 Mar 2019 21:23:53 +0300 Subject: [PATCH 13/73] Do not cache entities without access_hash --- pyrogram/client/client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 2bcf294f..33b3f137 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -762,6 +762,8 @@ class Client(Methods, BaseClient): types.Chat, types.ChatForbidden, types.Channel, types.ChannelForbidden]]): for entity in entities: + if isinstance(entity, (types.User, types.Channel, types.ChannelForbidden)) and not entity.access_hash: + continue self.session_storage.cache_peer(entity) def download_worker(self): From 2e05c81a5c3727143d3c3190d673c32fe713523e Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 8 Jun 2019 11:33:52 +0200 Subject: [PATCH 14/73] Update docs about Telegram data centers --- docs/source/faq.rst | 23 ++++++++++++++------ pyrogram/client/methods/users/get_user_dc.py | 6 +++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/docs/source/faq.rst b/docs/source/faq.rst index 1800a032..6ff16559 100644 --- a/docs/source/faq.rst +++ b/docs/source/faq.rst @@ -134,14 +134,23 @@ in a bunch of seconds: import logging logging.basicConfig(level=logging.INFO) -Another way to confirm you aren't able to connect to Telegram is by pinging these IP addresses and see whether ping -fails or not: +Another way to confirm you aren't able to connect to Telegram is by pinging the IP addresses below and see whether ping +fails or not. -- DC1: ``149.154.175.50`` -- DC2: ``149.154.167.51`` -- DC3: ``149.154.175.100`` -- DC4: ``149.154.167.91`` -- DC5: ``91.108.56.149`` +What are the IP addresses of Telegram Data Centers? +--------------------------------------------------- + +Telegram is currently composed by a decentralized, multi-DC infrastructure (each of which can work independently) spread +in 5 different locations. However, two of the less busy DCs have been lately dismissed and their IP addresses are now +kept as aliases. + +- **DC1** - MIA, Miami FL, USA: ``149.154.175.50`` +- **DC2** - AMS, Amsterdam, NL: ``149.154.167.51`` +- **DC3*** - MIA, Miami FL, USA: ``149.154.175.100`` +- **DC4*** - AMS, Amsterdam, NL: ``149.154.167.91`` +- **DC5** - SIN, Singapore, SG: ``91.108.56.149`` + +***** Alias DC I keep getting PEER_ID_INVALID error! ------------------------------------------- diff --git a/pyrogram/client/methods/users/get_user_dc.py b/pyrogram/client/methods/users/get_user_dc.py index 89e97526..7cb43d83 100644 --- a/pyrogram/client/methods/users/get_user_dc.py +++ b/pyrogram/client/methods/users/get_user_dc.py @@ -26,6 +26,12 @@ class GetUserDC(BaseClient): def get_user_dc(self, user_id: Union[int, str]) -> Union[int, None]: """Get the assigned data center (DC) of a user. + .. note:: + + This information is approximate: it is based on where the user stores their profile pictures and does not by + any means tell you the user location. More info at + `FAQs <../faq#what-are-the-ip-addresses-of-telegram-data-centers>`_. + Parameters: user_id (``int`` | ``str``): Unique identifier (int) or username (str) of the target chat. From 2db2ca3283ed433e4e8be5512eca1b0fc571a987 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 8 Jun 2019 13:49:01 +0200 Subject: [PATCH 15/73] Remove ChatMembers type --- docs/source/api/types.rst | 2 - .../client/methods/chats/get_chat_member.py | 9 ++- .../client/methods/chats/get_chat_members.py | 48 +++++++------ .../client/methods/chats/iter_chat_members.py | 2 +- pyrogram/client/types/__init__.py | 2 + .../client/types/user_and_chats/__init__.py | 4 +- .../types/user_and_chats/chat_members.py | 71 ------------------- 7 files changed, 38 insertions(+), 100 deletions(-) delete mode 100644 pyrogram/client/types/user_and_chats/chat_members.py diff --git a/docs/source/api/types.rst b/docs/source/api/types.rst index 4eef9638..0aab7b5e 100644 --- a/docs/source/api/types.rst +++ b/docs/source/api/types.rst @@ -30,7 +30,6 @@ Users & Chats - :class:`ChatPreview` - :class:`ChatPhoto` - :class:`ChatMember` - - :class:`ChatMembers` - :class:`ChatPermissions` - :class:`Dialog` - :class:`Dialogs` @@ -123,7 +122,6 @@ Details .. autoclass:: ChatPreview() .. autoclass:: ChatPhoto() .. autoclass:: ChatMember() -.. autoclass:: ChatMembers() .. autoclass:: ChatPermissions() .. autoclass:: Dialog() .. autoclass:: Dialogs() diff --git a/pyrogram/client/methods/chats/get_chat_member.py b/pyrogram/client/methods/chats/get_chat_member.py index b84836c6..b0d0641a 100644 --- a/pyrogram/client/methods/chats/get_chat_member.py +++ b/pyrogram/client/methods/chats/get_chat_member.py @@ -51,13 +51,18 @@ class GetChatMember(BaseClient): user = self.resolve_peer(user_id) if isinstance(chat, types.InputPeerChat): - full_chat = self.send( + r = self.send( functions.messages.GetFullChat( chat_id=chat.chat_id ) ) - for member in pyrogram.ChatMembers._parse(self, full_chat).chat_members: + members = r.full_chat.participants.participants + users = {i.id: i for i in r.users} + + for member in members: + member = pyrogram.ChatMember._parse(self, member, users) + if isinstance(user, types.InputPeerSelf): if member.user.is_self: return member diff --git a/pyrogram/client/methods/chats/get_chat_members.py b/pyrogram/client/methods/chats/get_chat_members.py index 1c966f36..0b4613d8 100644 --- a/pyrogram/client/methods/chats/get_chat_members.py +++ b/pyrogram/client/methods/chats/get_chat_members.py @@ -18,7 +18,7 @@ import logging import time -from typing import Union +from typing import Union, List import pyrogram from pyrogram.api import functions, types @@ -45,7 +45,7 @@ class GetChatMembers(BaseClient): limit: int = 200, query: str = "", filter: str = Filters.ALL - ) -> "pyrogram.ChatMembers": + ) -> List["pyrogram.ChatMember"]: """Get a chunk of the members list of a chat. You can get up to 200 chat members at once. @@ -59,15 +59,16 @@ class GetChatMembers(BaseClient): offset (``int``, *optional*): Sequential number of the first member to be returned. - Defaults to 0 [1]_. + Only applicable to supergroups and channels. Defaults to 0 [1]_. limit (``int``, *optional*): Limits the number of members to be retrieved. + Only applicable to supergroups and channels. Defaults to 200, which is also the maximum server limit allowed per method call. query (``str``, *optional*): Query string to filter members based on their display names and usernames. - Defaults to "" (empty string) [2]_. + Only applicable to supergroups and channels. Defaults to "" (empty string) [2]_. filter (``str``, *optional*): Filter used to select the kind of members you want to retrieve. Only applicable for supergroups @@ -78,6 +79,7 @@ class GetChatMembers(BaseClient): *"bots"* - bots only, *"recent"* - recent members only, *"administrators"* - chat administrators only. + Only applicable to supergroups and channels. 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 @@ -86,7 +88,7 @@ class GetChatMembers(BaseClient): .. [2] A query string is applicable only for *"all"*, *"kicked"* and *"restricted"* filters only. Returns: - :obj:`ChatMembers`: On success, an object containing a list of chat members is returned. + List of :obj:`ChatMember`: On success, a list of chat members is returned. Raises: RPCError: In case of a Telegram RPC error. @@ -95,14 +97,16 @@ class GetChatMembers(BaseClient): peer = self.resolve_peer(chat_id) if isinstance(peer, types.InputPeerChat): - return pyrogram.ChatMembers._parse( - self, - self.send( - functions.messages.GetFullChat( - chat_id=peer.chat_id - ) + r = self.send( + functions.messages.GetFullChat( + chat_id=peer.chat_id ) ) + + members = r.full_chat.participants.participants + users = {i.id: i for i in r.users} + + return pyrogram.List(pyrogram.ChatMember._parse(self, member, users) for member in members) elif isinstance(peer, types.InputPeerChannel): filter = filter.lower() @@ -123,18 +127,20 @@ class GetChatMembers(BaseClient): while True: try: - return pyrogram.ChatMembers._parse( - self, - self.send( - functions.channels.GetParticipants( - channel=peer, - filter=filter, - offset=offset, - limit=limit, - hash=0 - ) + r = self.send( + functions.channels.GetParticipants( + channel=peer, + filter=filter, + offset=offset, + limit=limit, + hash=0 ) ) + + members = r.participants + users = {i.id: i for i in r.users} + + return pyrogram.List(pyrogram.ChatMember._parse(self, member, users) for member in members) except FloodWait as e: log.warning("Sleeping for {}s".format(e.x)) time.sleep(e.x) diff --git a/pyrogram/client/methods/chats/iter_chat_members.py b/pyrogram/client/methods/chats/iter_chat_members.py index 961f6d98..fe117694 100644 --- a/pyrogram/client/methods/chats/iter_chat_members.py +++ b/pyrogram/client/methods/chats/iter_chat_members.py @@ -106,7 +106,7 @@ class IterChatMembers(BaseClient): limit=limit, query=q, filter=filter - ).chat_members + ) if not chat_members: break diff --git a/pyrogram/client/types/__init__.py b/pyrogram/client/types/__init__.py index 3d430c44..8a725a9e 100644 --- a/pyrogram/client/types/__init__.py +++ b/pyrogram/client/types/__init__.py @@ -20,6 +20,8 @@ from .keyboards import * from .inline_mode import * from .input_media import * from .input_message_content import * +from .list import List from .messages_and_media import * +from .object import Object from .update import * from .user_and_chats import * diff --git a/pyrogram/client/types/user_and_chats/__init__.py b/pyrogram/client/types/user_and_chats/__init__.py index 2059589a..cf032850 100644 --- a/pyrogram/client/types/user_and_chats/__init__.py +++ b/pyrogram/client/types/user_and_chats/__init__.py @@ -18,7 +18,6 @@ from .chat import Chat from .chat_member import ChatMember -from .chat_members import ChatMembers from .chat_permissions import ChatPermissions from .chat_photo import ChatPhoto from .chat_preview import ChatPreview @@ -28,6 +27,5 @@ from .user import User from .user_status import UserStatus __all__ = [ - "Chat", "ChatMember", "ChatMembers", "ChatPermissions", "ChatPhoto", "ChatPreview", "Dialog", "Dialogs", "User", - "UserStatus" + "Chat", "ChatMember", "ChatPermissions", "ChatPhoto", "ChatPreview", "Dialog", "Dialogs", "User", "UserStatus" ] diff --git a/pyrogram/client/types/user_and_chats/chat_members.py b/pyrogram/client/types/user_and_chats/chat_members.py deleted file mode 100644 index 6abdd719..00000000 --- a/pyrogram/client/types/user_and_chats/chat_members.py +++ /dev/null @@ -1,71 +0,0 @@ -# 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 List - -import pyrogram -from pyrogram.api import types -from .chat_member import ChatMember -from ..object import Object - - -class ChatMembers(Object): - """Contains information about the members list of a chat. - - Parameters: - total_count (``int``): - Total number of members the chat has. - - chat_members (List of :obj:`ChatMember `): - Requested chat members. - """ - - __slots__ = ["total_count", "chat_members"] - - def __init__( - self, - *, - client: "pyrogram.BaseClient" = None, - total_count: int, - chat_members: List[ChatMember] - ): - super().__init__(client) - - self.total_count = total_count - self.chat_members = chat_members - - @staticmethod - def _parse(client, members): - users = {i.id: i for i in members.users} - chat_members = [] - - if isinstance(members, types.channels.ChannelParticipants): - total_count = members.count - members = members.participants - else: - members = members.full_chat.participants.participants - total_count = len(members) - - for member in members: - chat_members.append(ChatMember._parse(client, member, users)) - - return ChatMembers( - total_count=total_count, - chat_members=chat_members, - client=client - ) From c8fd446cb6185b52d9da9090a2c4b9811dd9162a Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 8 Jun 2019 14:00:00 +0200 Subject: [PATCH 16/73] Remove Dialogs type --- docs/source/api/types.rst | 2 - pyrogram/client/methods/chats/get_dialogs.py | 35 +++++++- pyrogram/client/methods/chats/iter_dialogs.py | 4 +- .../client/types/user_and_chats/__init__.py | 3 +- .../client/types/user_and_chats/dialogs.py | 87 ------------------- 5 files changed, 35 insertions(+), 96 deletions(-) delete mode 100644 pyrogram/client/types/user_and_chats/dialogs.py diff --git a/docs/source/api/types.rst b/docs/source/api/types.rst index 0aab7b5e..957cfa52 100644 --- a/docs/source/api/types.rst +++ b/docs/source/api/types.rst @@ -32,7 +32,6 @@ Users & Chats - :class:`ChatMember` - :class:`ChatPermissions` - :class:`Dialog` - - :class:`Dialogs` Messages & Media ^^^^^^^^^^^^^^^^ @@ -124,7 +123,6 @@ Details .. autoclass:: ChatMember() .. autoclass:: ChatPermissions() .. autoclass:: Dialog() -.. autoclass:: Dialogs() .. Messages & Media .. autoclass:: Message() diff --git a/pyrogram/client/methods/chats/get_dialogs.py b/pyrogram/client/methods/chats/get_dialogs.py index 8d5baa15..8c374a44 100644 --- a/pyrogram/client/methods/chats/get_dialogs.py +++ b/pyrogram/client/methods/chats/get_dialogs.py @@ -18,6 +18,7 @@ import logging import time +from typing import List import pyrogram from pyrogram.api import functions, types @@ -33,7 +34,7 @@ class GetDialogs(BaseClient): offset_date: int = 0, limit: int = 100, pinned_only: bool = False - ) -> "pyrogram.Dialogs": + ) -> List["pyrogram.Dialog"]: """Get a chunk of the user's dialogs. You can get up to 100 dialogs at once. @@ -53,7 +54,7 @@ class GetDialogs(BaseClient): Defaults to False. Returns: - :obj:`Dialogs`: On success, an object containing a list of dialogs is returned. + List of :obj:`Dialog`: On success, a list of dialogs is returned. Raises: RPCError: In case of a Telegram RPC error. @@ -80,4 +81,32 @@ class GetDialogs(BaseClient): else: break - return pyrogram.Dialogs._parse(self, r) + users = {i.id: i for i in r.users} + chats = {i.id: i for i in r.chats} + + messages = {} + + for message in r.messages: + to_id = message.to_id + + if isinstance(to_id, types.PeerUser): + if message.out: + chat_id = to_id.user_id + else: + chat_id = message.from_id + elif isinstance(to_id, types.PeerChat): + chat_id = -to_id.chat_id + else: + chat_id = int("-100" + str(to_id.channel_id)) + + messages[chat_id] = pyrogram.Message._parse(self, message, users, chats) + + parsed_dialogs = [] + + for dialog in r.dialogs: + if not isinstance(dialog, types.Dialog): + continue + + parsed_dialogs.append(pyrogram.Dialog._parse(self, dialog, messages, users, chats)) + + return pyrogram.List(parsed_dialogs) diff --git a/pyrogram/client/methods/chats/iter_dialogs.py b/pyrogram/client/methods/chats/iter_dialogs.py index e7fb7330..fce9fb99 100644 --- a/pyrogram/client/methods/chats/iter_dialogs.py +++ b/pyrogram/client/methods/chats/iter_dialogs.py @@ -55,7 +55,7 @@ class IterDialogs(BaseClient): pinned_dialogs = self.get_dialogs( pinned_only=True - ).dialogs + ) for dialog in pinned_dialogs: yield dialog @@ -69,7 +69,7 @@ class IterDialogs(BaseClient): dialogs = self.get_dialogs( offset_date=offset_date, limit=limit - ).dialogs + ) if not dialogs: return diff --git a/pyrogram/client/types/user_and_chats/__init__.py b/pyrogram/client/types/user_and_chats/__init__.py index cf032850..922ac86a 100644 --- a/pyrogram/client/types/user_and_chats/__init__.py +++ b/pyrogram/client/types/user_and_chats/__init__.py @@ -22,10 +22,9 @@ from .chat_permissions import ChatPermissions from .chat_photo import ChatPhoto from .chat_preview import ChatPreview from .dialog import Dialog -from .dialogs import Dialogs from .user import User from .user_status import UserStatus __all__ = [ - "Chat", "ChatMember", "ChatPermissions", "ChatPhoto", "ChatPreview", "Dialog", "Dialogs", "User", "UserStatus" + "Chat", "ChatMember", "ChatPermissions", "ChatPhoto", "ChatPreview", "Dialog", "User", "UserStatus" ] diff --git a/pyrogram/client/types/user_and_chats/dialogs.py b/pyrogram/client/types/user_and_chats/dialogs.py deleted file mode 100644 index 56cdfc72..00000000 --- a/pyrogram/client/types/user_and_chats/dialogs.py +++ /dev/null @@ -1,87 +0,0 @@ -# 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 List - -import pyrogram -from pyrogram.api import types -from .dialog import Dialog -from ..messages_and_media import Message -from ..object import Object - - -class Dialogs(Object): - """Contains a user's dialogs chunk. - - Parameters: - total_count (``int``): - Total number of dialogs the user has. - - dialogs (List of :obj:`Dialog`): - Requested dialogs. - """ - - __slots__ = ["total_count", "dialogs"] - - def __init__( - self, - *, - client: "pyrogram.BaseClient" = None, - total_count: int, - dialogs: List[Dialog] - ): - super().__init__(client) - - self.total_count = total_count - self.dialogs = dialogs - - @staticmethod - def _parse(client, dialogs: types.messages.Dialogs) -> "Dialogs": - users = {i.id: i for i in dialogs.users} - chats = {i.id: i for i in dialogs.chats} - - messages = {} - - for message in dialogs.messages: - to_id = message.to_id - - if isinstance(to_id, types.PeerUser): - if message.out: - chat_id = to_id.user_id - else: - chat_id = message.from_id - elif isinstance(to_id, types.PeerChat): - chat_id = -to_id.chat_id - else: - chat_id = int("-100" + str(to_id.channel_id)) - - messages[chat_id] = Message._parse(client, message, users, chats) - - parsed_dialogs = [] - - for dialog in dialogs.dialogs: - if not isinstance(dialog, types.Dialog): - continue - - parsed_dialogs.append(Dialog._parse(client, dialog, messages, users, chats)) - - return Dialogs( - total_count=getattr(dialogs, "count", len(dialogs.dialogs)), - dialogs=parsed_dialogs, - client=client - ) From 797de058e8cbaa0128d6879f390c658f550f2872 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 8 Jun 2019 14:08:39 +0200 Subject: [PATCH 17/73] Remove ProfilePhotos type --- docs/source/api/types.rst | 2 - .../methods/users/get_profile_photos.py | 28 ++++----- .../methods/users/iter_profile_photos.py | 2 +- .../types/messages_and_media/__init__.py | 4 +- .../messages_and_media/profile_photos.py | 57 ------------------- 5 files changed, 14 insertions(+), 79 deletions(-) delete mode 100644 pyrogram/client/types/messages_and_media/profile_photos.py diff --git a/docs/source/api/types.rst b/docs/source/api/types.rst index 957cfa52..a18e01a6 100644 --- a/docs/source/api/types.rst +++ b/docs/source/api/types.rst @@ -43,7 +43,6 @@ Messages & Media - :class:`Messages` - :class:`MessageEntity` - :class:`Photo` - - :class:`ProfilePhotos` - :class:`Thumbnail` - :class:`Audio` - :class:`Document` @@ -129,7 +128,6 @@ Details .. autoclass:: Messages() .. autoclass:: MessageEntity() .. autoclass:: Photo() -.. autoclass:: ProfilePhotos() .. autoclass:: Thumbnail() .. autoclass:: Audio() .. autoclass:: Document() diff --git a/pyrogram/client/methods/users/get_profile_photos.py b/pyrogram/client/methods/users/get_profile_photos.py index e4e202e0..32e7e513 100644 --- a/pyrogram/client/methods/users/get_profile_photos.py +++ b/pyrogram/client/methods/users/get_profile_photos.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from typing import Union +from typing import Union, List import pyrogram from pyrogram.api import functions, types @@ -29,7 +29,7 @@ class GetProfilePhotos(BaseClient): chat_id: Union[int, str], offset: int = 0, limit: int = 100 - ) -> "pyrogram.ProfilePhotos": + ) -> List["pyrogram.Photo"]: """Get a list of profile pictures for a user or a chat. Parameters: @@ -47,7 +47,7 @@ class GetProfilePhotos(BaseClient): Values between 1—100 are accepted. Defaults to 100. Returns: - :obj:`ProfilePhotos`: On success, an object containing a list of the profile photos is returned. + List of :obj:`Photo`: On success, a list of profile photos is returned. Raises: RPCError: In case of a Telegram RPC error. @@ -55,17 +55,16 @@ class GetProfilePhotos(BaseClient): peer_id = self.resolve_peer(chat_id) if isinstance(peer_id, types.InputPeerUser): - return pyrogram.ProfilePhotos._parse( - self, - self.send( - functions.photos.GetUserPhotos( - user_id=peer_id, - offset=offset, - max_id=0, - limit=limit - ) + r = self.send( + functions.photos.GetUserPhotos( + user_id=peer_id, + offset=offset, + max_id=0, + limit=limit ) ) + + return pyrogram.List(pyrogram.Photo._parse(self, photo) for photo in r.photos) else: new_chat_photos = pyrogram.Messages._parse( self, @@ -86,7 +85,4 @@ class GetProfilePhotos(BaseClient): ) ) - return pyrogram.ProfilePhotos( - total_count=new_chat_photos.total_count, - profile_photos=[m.new_chat_photo for m in new_chat_photos.messages][:limit] - ) + return pyrogram.List([m.new_chat_photo for m in new_chat_photos.messages][:limit]) diff --git a/pyrogram/client/methods/users/iter_profile_photos.py b/pyrogram/client/methods/users/iter_profile_photos.py index 1773634e..49317f87 100644 --- a/pyrogram/client/methods/users/iter_profile_photos.py +++ b/pyrogram/client/methods/users/iter_profile_photos.py @@ -63,7 +63,7 @@ class IterProfilePhotos(BaseClient): chat_id=chat_id, offset=offset, limit=limit - ).photos + ) if not photos: return diff --git a/pyrogram/client/types/messages_and_media/__init__.py b/pyrogram/client/types/messages_and_media/__init__.py index 17a6e36a..2de2c6a3 100644 --- a/pyrogram/client/types/messages_and_media/__init__.py +++ b/pyrogram/client/types/messages_and_media/__init__.py @@ -28,7 +28,6 @@ from .messages import Messages from .photo import Photo from .poll import Poll from .poll_option import PollOption -from .profile_photos import ProfilePhotos from .sticker import Sticker from .stripped_thumbnail import StrippedThumbnail from .thumbnail import Thumbnail @@ -39,6 +38,5 @@ from .voice import Voice __all__ = [ "Animation", "Audio", "Contact", "Document", "Game", "Location", "Message", "MessageEntity", "Messages", "Photo", - "Thumbnail", "StrippedThumbnail", "Poll", "PollOption", "Sticker", "ProfilePhotos", "Venue", "Video", "VideoNote", - "Voice" + "Thumbnail", "StrippedThumbnail", "Poll", "PollOption", "Sticker", "Venue", "Video", "VideoNote", "Voice" ] diff --git a/pyrogram/client/types/messages_and_media/profile_photos.py b/pyrogram/client/types/messages_and_media/profile_photos.py deleted file mode 100644 index 11b8e4dd..00000000 --- a/pyrogram/client/types/messages_and_media/profile_photos.py +++ /dev/null @@ -1,57 +0,0 @@ -# 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 List - -import pyrogram -from .photo import Photo -from ..object import Object - - -class ProfilePhotos(Object): - """Contains a user's profile pictures. - - Parameters: - total_count (``int``): - Total number of profile pictures the target user has. - - profile_photos (List of :obj:`Photo`): - Requested profile pictures. - """ - - __slots__ = ["total_count", "profile_photos"] - - def __init__( - self, - *, - client: "pyrogram.BaseClient" = None, - total_count: int, - profile_photos: List[Photo] - ): - super().__init__(client) - - self.total_count = total_count - self.profile_photos = profile_photos - - @staticmethod - def _parse(client, photos) -> "ProfilePhotos": - return ProfilePhotos( - total_count=getattr(photos, "count", len(photos.photos)), - profile_photos=[Photo._parse(client, photo) for photo in photos.photos], - client=client - ) From cfbc5298dfaad33fa537839f909fef1b81200980 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 8 Jun 2019 15:13:52 +0200 Subject: [PATCH 18/73] Remove Messages type --- docs/source/api/types.rst | 2 - pyrogram/client/ext/dispatcher.py | 3 +- pyrogram/client/ext/utils.py | 58 +++++- .../handlers/deleted_messages_handler.py | 13 +- .../methods/messages/forward_messages.py | 26 +-- .../client/methods/messages/get_history.py | 13 +- .../client/methods/messages/get_messages.py | 16 +- .../client/methods/messages/iter_history.py | 2 +- .../methods/messages/send_media_group.py | 6 +- .../methods/users/get_profile_photos.py | 5 +- .../types/messages_and_media/__init__.py | 5 +- .../types/messages_and_media/message.py | 2 +- .../types/messages_and_media/messages.py | 170 ------------------ 13 files changed, 100 insertions(+), 221 deletions(-) delete mode 100644 pyrogram/client/types/messages_and_media/messages.py diff --git a/docs/source/api/types.rst b/docs/source/api/types.rst index a18e01a6..66d409c4 100644 --- a/docs/source/api/types.rst +++ b/docs/source/api/types.rst @@ -40,7 +40,6 @@ Messages & Media :columns: 5 - :class:`Message` - - :class:`Messages` - :class:`MessageEntity` - :class:`Photo` - :class:`Thumbnail` @@ -125,7 +124,6 @@ Details .. Messages & Media .. autoclass:: Message() -.. autoclass:: Messages() .. autoclass:: MessageEntity() .. autoclass:: Photo() .. autoclass:: Thumbnail() diff --git a/pyrogram/client/ext/dispatcher.py b/pyrogram/client/ext/dispatcher.py index 2f1ec2b9..12d5a5de 100644 --- a/pyrogram/client/ext/dispatcher.py +++ b/pyrogram/client/ext/dispatcher.py @@ -21,6 +21,7 @@ import threading from collections import OrderedDict from queue import Queue from threading import Thread +from . import utils import pyrogram from pyrogram.api import types @@ -68,7 +69,7 @@ class Dispatcher: lambda upd, usr, cht: (pyrogram.Message._parse(self.client, upd.message, usr, cht), MessageHandler), Dispatcher.DELETE_MESSAGES_UPDATES: - lambda upd, usr, cht: (pyrogram.Messages._parse_deleted(self.client, upd), DeletedMessagesHandler), + lambda upd, usr, cht: (utils.parse_deleted_messages(self.client, upd), DeletedMessagesHandler), Dispatcher.CALLBACK_QUERY_UPDATES: lambda upd, usr, cht: (pyrogram.CallbackQuery._parse(self.client, upd, usr), CallbackQueryHandler), diff --git a/pyrogram/client/ext/utils.py b/pyrogram/client/ext/utils.py index e1959309..41270d39 100644 --- a/pyrogram/client/ext/utils.py +++ b/pyrogram/client/ext/utils.py @@ -18,8 +18,9 @@ import struct from base64 import b64decode, b64encode -from typing import Union +from typing import Union, List +import pyrogram from . import BaseClient from ...api import types @@ -135,3 +136,58 @@ def get_input_media_from_file_id( ) raise ValueError("Unknown media type: {}".format(file_id_str)) + + +def parse_messages(client, messages: types.messages.Messages, replies: int = 1) -> List["pyrogram.Message"]: + users = {i.id: i for i in messages.users} + chats = {i.id: i for i in messages.chats} + + if not messages.messages: + return pyrogram.List() + + parsed_messages = [ + pyrogram.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=replies - 1 + ) + + 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 pyrogram.List(parsed_messages) + + +def parse_deleted_messages(client, update) -> List["pyrogram.Message"]: + messages = update.messages + channel_id = getattr(update, "channel_id", None) + + parsed_messages = [] + + for message in messages: + parsed_messages.append( + pyrogram.Message( + message_id=message, + chat=pyrogram.Chat( + id=int("-100" + str(channel_id)), + type="channel", + client=client + ) if channel_id is not None else None, + client=client + ) + ) + + return pyrogram.List(parsed_messages) diff --git a/pyrogram/client/handlers/deleted_messages_handler.py b/pyrogram/client/handlers/deleted_messages_handler.py index b6651fba..3230b9bd 100644 --- a/pyrogram/client/handlers/deleted_messages_handler.py +++ b/pyrogram/client/handlers/deleted_messages_handler.py @@ -20,16 +20,15 @@ from .handler import Handler class DeletedMessagesHandler(Handler): - """The deleted Messages handler class. Used to handle deleted messages coming from any chat - (private, group, channel). It is intended to be used with - :meth:`~Client.add_handler` + """The deleted messages handler class. Used to handle deleted messages coming from any chat + (private, group, channel). It is intended to be used with :meth:`~Client.add_handler` For a nicer way to register this handler, have a look at the :meth:`~Client.on_deleted_messages` decorator. Parameters: callback (``callable``): - Pass a function that will be called when one or more Messages have been deleted. + Pass a function that will be called when one or more messages have been deleted. It takes *(client, messages)* as positional arguments (look at the section below for a detailed description). filters (:obj:`Filters`): @@ -40,12 +39,12 @@ class DeletedMessagesHandler(Handler): client (:obj:`Client`): The Client itself, useful when you want to call other API methods inside the message handler. - messages (:obj:`Messages`): - The deleted messages. + messages (List of :obj:`Message`): + The deleted messages, as list. """ def __init__(self, callback: callable, filters=None): super().__init__(callback, filters) def check(self, messages): - return super().check(messages.messages[0]) + return super().check(messages[0]) diff --git a/pyrogram/client/methods/messages/forward_messages.py b/pyrogram/client/methods/messages/forward_messages.py index bc9ad331..c69df608 100644 --- a/pyrogram/client/methods/messages/forward_messages.py +++ b/pyrogram/client/methods/messages/forward_messages.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from typing import Union, Iterable +from typing import Union, Iterable, List import pyrogram from pyrogram.api import functions, types @@ -32,7 +32,7 @@ class ForwardMessages(BaseClient): disable_notification: bool = None, as_copy: bool = False, remove_caption: bool = False - ) -> "pyrogram.Messages": + ) -> List["pyrogram.Message"]: """Forward messages of any kind. Parameters: @@ -64,9 +64,9 @@ class ForwardMessages(BaseClient): Defaults to False. Returns: - :obj:`Message` | :obj:`Messages`: In case *message_ids* was an integer, the single forwarded message is - returned, otherwise, in case *message_ids* was an iterable, the returned value will be an object containing - a list of messages, even if such iterable contained just a single element. + :obj:`Message` | List of :obj:`Message`: In case *message_ids* was an integer, the single forwarded message + is returned, otherwise, in case *message_ids* was an iterable, the returned value will be a list of + messages, even if such iterable contained just a single element. Raises: RPCError: In case of a Telegram RPC error. @@ -79,9 +79,9 @@ class ForwardMessages(BaseClient): forwarded_messages = [] for chunk in [message_ids[i:i + 200] for i in range(0, len(message_ids), 200)]: - messages = self.get_messages(chat_id=from_chat_id, message_ids=chunk) # type: pyrogram.Messages + messages = self.get_messages(chat_id=from_chat_id, message_ids=chunk) - for message in messages.messages: + for message in messages: forwarded_messages.append( message.forward( chat_id, @@ -91,11 +91,7 @@ class ForwardMessages(BaseClient): ) ) - return pyrogram.Messages( - client=self, - total_count=len(forwarded_messages), - messages=forwarded_messages - ) if is_iterable else forwarded_messages[0] + return pyrogram.List(forwarded_messages) if is_iterable else forwarded_messages[0] else: r = self.send( functions.messages.ForwardMessages( @@ -121,8 +117,4 @@ class ForwardMessages(BaseClient): ) ) - return pyrogram.Messages( - client=self, - total_count=len(forwarded_messages), - messages=forwarded_messages - ) if is_iterable else forwarded_messages[0] + return pyrogram.List(forwarded_messages) if is_iterable else forwarded_messages[0] diff --git a/pyrogram/client/methods/messages/get_history.py b/pyrogram/client/methods/messages/get_history.py index c0810474..8adafe22 100644 --- a/pyrogram/client/methods/messages/get_history.py +++ b/pyrogram/client/methods/messages/get_history.py @@ -18,10 +18,11 @@ import logging import time -from typing import Union +from typing import Union, List import pyrogram from pyrogram.api import functions +from pyrogram.client.ext import utils from pyrogram.errors import FloodWait from ...ext import BaseClient @@ -37,7 +38,7 @@ class GetHistory(BaseClient): offset_id: int = 0, offset_date: int = 0, reverse: bool = False - ) -> "pyrogram.Messages": + ) -> List["pyrogram.Message"]: """Retrieve a chunk of the history of a chat. You can get up to 100 messages at once. @@ -67,15 +68,17 @@ class GetHistory(BaseClient): Pass True to retrieve the messages in reversed order (from older to most recent). Returns: - :obj:`Messages` - On success, an object containing a list of the retrieved messages. + List of :obj:`Message` - On success, a list of the retrieved messages is returned. Raises: RPCError: In case of a Telegram RPC error. """ + offset_id = offset_id or (1 if reverse else 0) + while True: try: - messages = pyrogram.Messages._parse( + messages = utils.parse_messages( self, self.send( functions.messages.GetHistory( @@ -97,6 +100,6 @@ class GetHistory(BaseClient): break if reverse: - messages.messages.reverse() + messages.reverse() return messages diff --git a/pyrogram/client/methods/messages/get_messages.py b/pyrogram/client/methods/messages/get_messages.py index 7a60f276..0f901174 100644 --- a/pyrogram/client/methods/messages/get_messages.py +++ b/pyrogram/client/methods/messages/get_messages.py @@ -18,12 +18,12 @@ import logging import time -from typing import Union, Iterable +from typing import Union, Iterable, List import pyrogram from pyrogram.api import functions, types from pyrogram.errors import FloodWait -from ...ext import BaseClient +from ...ext import BaseClient, utils log = logging.getLogger(__name__) @@ -35,7 +35,7 @@ class GetMessages(BaseClient): message_ids: Union[int, Iterable[int]] = None, reply_to_message_ids: Union[int, Iterable[int]] = None, replies: int = 1 - ) -> Union["pyrogram.Message", "pyrogram.Messages"]: + ) -> Union["pyrogram.Message", List["pyrogram.Message"]]: """Get one or more messages that belong to a specific chat. You can retrieve up to 200 messages at once. @@ -60,9 +60,9 @@ class GetMessages(BaseClient): Defaults to 1. Returns: - :obj:`Message` | :obj:`Messages`: In case *message_ids* was an integer, the single requested message is - returned, otherwise, in case *message_ids* was an iterable, the returned value will be an object containing - a list of messages, even if such iterable contained just a single element. + :obj:`Message` | List of :obj:`Message`: In case *message_ids* was an integer, the single requested message is + returned, otherwise, in case *message_ids* was an iterable, the returned value will be a list of messages, + even if such iterable contained just a single element. Raises: RPCError: In case of a Telegram RPC error. @@ -99,6 +99,6 @@ class GetMessages(BaseClient): else: break - messages = pyrogram.Messages._parse(self, r, replies=replies) + messages = utils.parse_messages(self, r, replies=replies) - return messages if is_iterable else messages.messages[0] + return messages if is_iterable else messages[0] diff --git a/pyrogram/client/methods/messages/iter_history.py b/pyrogram/client/methods/messages/iter_history.py index 57da3da5..15c48c95 100644 --- a/pyrogram/client/methods/messages/iter_history.py +++ b/pyrogram/client/methods/messages/iter_history.py @@ -80,7 +80,7 @@ class IterHistory(BaseClient): offset_id=offset_id, offset_date=offset_date, reverse=reverse - ).messages + ) if not messages: return diff --git a/pyrogram/client/methods/messages/send_media_group.py b/pyrogram/client/methods/messages/send_media_group.py index fb029a66..194a2202 100644 --- a/pyrogram/client/methods/messages/send_media_group.py +++ b/pyrogram/client/methods/messages/send_media_group.py @@ -38,7 +38,7 @@ class SendMediaGroup(BaseClient): media: List[Union["pyrogram.InputMediaPhoto", "pyrogram.InputMediaVideo"]], disable_notification: bool = None, reply_to_message_id: int = None - ): + ) -> List["pyrogram.Message"]: """Send a group of photos or videos as an album. Parameters: @@ -58,7 +58,7 @@ class SendMediaGroup(BaseClient): If the message is a reply, ID of the original message. Returns: - :obj:`Messages`: On success, an object is returned containing all the single messages sent. + List of :obj:`Message`: On success, a list of the sent messages is returned. Raises: RPCError: In case of a Telegram RPC error. @@ -158,7 +158,7 @@ class SendMediaGroup(BaseClient): else: break - return pyrogram.Messages._parse( + return utils.parse_messages( self, types.messages.Messages( messages=[m.message for m in filter( diff --git a/pyrogram/client/methods/users/get_profile_photos.py b/pyrogram/client/methods/users/get_profile_photos.py index 32e7e513..eaf632e2 100644 --- a/pyrogram/client/methods/users/get_profile_photos.py +++ b/pyrogram/client/methods/users/get_profile_photos.py @@ -20,6 +20,7 @@ from typing import Union, List import pyrogram from pyrogram.api import functions, types +from pyrogram.client.ext import utils from ...ext import BaseClient @@ -66,7 +67,7 @@ class GetProfilePhotos(BaseClient): return pyrogram.List(pyrogram.Photo._parse(self, photo) for photo in r.photos) else: - new_chat_photos = pyrogram.Messages._parse( + r = utils.parse_messages( self, self.send( functions.messages.Search( @@ -85,4 +86,4 @@ class GetProfilePhotos(BaseClient): ) ) - return pyrogram.List([m.new_chat_photo for m in new_chat_photos.messages][:limit]) + return pyrogram.List([message.new_chat_photo for message in r][:limit]) diff --git a/pyrogram/client/types/messages_and_media/__init__.py b/pyrogram/client/types/messages_and_media/__init__.py index 2de2c6a3..b9bcb460 100644 --- a/pyrogram/client/types/messages_and_media/__init__.py +++ b/pyrogram/client/types/messages_and_media/__init__.py @@ -24,7 +24,6 @@ from .game import Game from .location import Location from .message import Message from .message_entity import MessageEntity -from .messages import Messages from .photo import Photo from .poll import Poll from .poll_option import PollOption @@ -37,6 +36,6 @@ from .video_note import VideoNote from .voice import Voice __all__ = [ - "Animation", "Audio", "Contact", "Document", "Game", "Location", "Message", "MessageEntity", "Messages", "Photo", - "Thumbnail", "StrippedThumbnail", "Poll", "PollOption", "Sticker", "Venue", "Video", "VideoNote", "Voice" + "Animation", "Audio", "Contact", "Document", "Game", "Location", "Message", "MessageEntity", "Photo", "Thumbnail", + "StrippedThumbnail", "Poll", "PollOption", "Sticker", "Venue", "Video", "VideoNote", "Voice" ] diff --git a/pyrogram/client/types/messages_and_media/message.py b/pyrogram/client/types/messages_and_media/message.py index f7dff7b5..f7e99d0a 100644 --- a/pyrogram/client/types/messages_and_media/message.py +++ b/pyrogram/client/types/messages_and_media/message.py @@ -2617,7 +2617,7 @@ class Message(Object, Update): ) if self.photo: - file_id = self.photo.sizes[-1].file_id + file_id = self.photo.file_id elif self.audio: file_id = self.audio.file_id elif self.document: diff --git a/pyrogram/client/types/messages_and_media/messages.py b/pyrogram/client/types/messages_and_media/messages.py deleted file mode 100644 index ee516f20..00000000 --- a/pyrogram/client/types/messages_and_media/messages.py +++ /dev/null @@ -1,170 +0,0 @@ -# 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 List, Union - -import pyrogram -from pyrogram.api import types -from .message import Message -from ..object import Object -from ..update import Update -from ..user_and_chats import Chat - - -class Messages(Object, Update): - """Contains a chat's messages. - - Parameters: - total_count (``int``): - Total number of messages the target chat has. - - messages (List of :obj:`Message`): - Requested messages. - """ - - __slots__ = ["total_count", "messages"] - - def __init__( - self, - *, - client: "pyrogram.BaseClient" = None, - total_count: int, - messages: List[Message] - ): - super().__init__(client) - - self.total_count = total_count - self.messages = messages - - @staticmethod - def _parse(client, messages: types.messages.Messages, replies: int = 1) -> "Messages": - 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=replies - 1 - ).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=total_count, - messages=parsed_messages, - client=client - ) - - @staticmethod - def _parse_deleted(client, update) -> "Messages": - messages = update.messages - channel_id = getattr(update, "channel_id", None) - - parsed_messages = [] - - for message in messages: - parsed_messages.append( - Message( - message_id=message, - chat=Chat( - id=int("-100" + str(channel_id)), - type="channel", - client=client - ) if channel_id is not None else None, - client=client - ) - ) - - return Messages( - total_count=len(parsed_messages), - messages=parsed_messages, - client=client - ) - - def forward( - self, - chat_id: Union[int, str], - disable_notification: bool = None, - as_copy: bool = False, - remove_caption: bool = False - ): - """Bound method *forward* of :obj:`Message`. - - Parameters: - 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). - - disable_notification (``bool``, *optional*): - Sends messages silently. - Users will receive a notification with no sound. - - as_copy (``bool``, *optional*): - Pass True to forward messages without the forward header (i.e.: send a copy of the message content). - Defaults to False. - - remove_caption (``bool``, *optional*): - If set to True and *as_copy* is enabled as well, media captions are not preserved when copying the - message. Has no effect if *as_copy* is not enabled. - Defaults to False. - - Returns: - On success, a :obj:`Messages` containing forwarded messages is returned. - - Raises: - RPCError: In case of a Telegram RPC error. - """ - forwarded_messages = [] - - for message in self.messages: - forwarded_messages.append( - message.forward( - chat_id=chat_id, - as_copy=as_copy, - disable_notification=disable_notification, - remove_caption=remove_caption - ) - ) - - return Messages( - total_count=len(forwarded_messages), - messages=forwarded_messages, - client=self._client - ) From a769fdfd20ad94f54e5caee0079ffc39a15de9a3 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 8 Jun 2019 15:14:28 +0200 Subject: [PATCH 19/73] Remove GameHighScores type --- docs/source/api/types.rst | 2 - .../methods/bots/get_game_high_scores.py | 21 ++++--- pyrogram/client/types/keyboards/__init__.py | 5 +- .../types/keyboards/game_high_scores.py | 60 ------------------- 4 files changed, 12 insertions(+), 76 deletions(-) delete mode 100644 pyrogram/client/types/keyboards/game_high_scores.py diff --git a/docs/source/api/types.rst b/docs/source/api/types.rst index 66d409c4..118c4261 100644 --- a/docs/source/api/types.rst +++ b/docs/source/api/types.rst @@ -71,7 +71,6 @@ Keyboards - :class:`ForceReply` - :class:`CallbackQuery` - :class:`GameHighScore` - - :class:`GameHighScores` - :class:`CallbackGame` Input Media @@ -150,7 +149,6 @@ Details .. autoclass:: ForceReply() .. autoclass:: CallbackQuery() .. autoclass:: GameHighScore() -.. autoclass:: GameHighScores() .. autoclass:: CallbackGame() .. Input Media diff --git a/pyrogram/client/methods/bots/get_game_high_scores.py b/pyrogram/client/methods/bots/get_game_high_scores.py index e1472b9e..e6459bac 100644 --- a/pyrogram/client/methods/bots/get_game_high_scores.py +++ b/pyrogram/client/methods/bots/get_game_high_scores.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from typing import Union +from typing import Union, List import pyrogram from pyrogram.api import functions @@ -29,7 +29,7 @@ class GetGameHighScores(BaseClient): user_id: Union[int, str], chat_id: Union[int, str], message_id: int = None - ) -> "pyrogram.GameHighScores": + ) -> List["pyrogram.GameHighScore"]: """Get data for high score tables. Parameters: @@ -49,20 +49,19 @@ class GetGameHighScores(BaseClient): Required if inline_message_id is not specified. Returns: - :obj:`GameHighScores`: On success. + List of :obj:`GameHighScore`: On success. Raises: RPCError: In case of a Telegram RPC error. """ # TODO: inline_message_id - return pyrogram.GameHighScores._parse( - self, - self.send( - functions.messages.GetGameHighScores( - peer=self.resolve_peer(chat_id), - id=message_id, - user_id=self.resolve_peer(user_id) - ) + r = self.send( + functions.messages.GetGameHighScores( + peer=self.resolve_peer(chat_id), + id=message_id, + user_id=self.resolve_peer(user_id) ) ) + + return pyrogram.List(pyrogram.GameHighScore._parse(self, score, r.users) for score in r.scores) diff --git a/pyrogram/client/types/keyboards/__init__.py b/pyrogram/client/types/keyboards/__init__.py index dae33e10..90376504 100644 --- a/pyrogram/client/types/keyboards/__init__.py +++ b/pyrogram/client/types/keyboards/__init__.py @@ -20,7 +20,6 @@ from .callback_game import CallbackGame from .callback_query import CallbackQuery from .force_reply import ForceReply from .game_high_score import GameHighScore -from .game_high_scores import GameHighScores from .inline_keyboard_button import InlineKeyboardButton from .inline_keyboard_markup import InlineKeyboardMarkup from .keyboard_button import KeyboardButton @@ -28,6 +27,6 @@ from .reply_keyboard_markup import ReplyKeyboardMarkup from .reply_keyboard_remove import ReplyKeyboardRemove __all__ = [ - "CallbackGame", "CallbackQuery", "ForceReply", "GameHighScore", "GameHighScores", "InlineKeyboardButton", - "InlineKeyboardMarkup", "KeyboardButton", "ReplyKeyboardMarkup", "ReplyKeyboardRemove" + "CallbackGame", "CallbackQuery", "ForceReply", "GameHighScore", "InlineKeyboardButton", "InlineKeyboardMarkup", + "KeyboardButton", "ReplyKeyboardMarkup", "ReplyKeyboardRemove" ] diff --git a/pyrogram/client/types/keyboards/game_high_scores.py b/pyrogram/client/types/keyboards/game_high_scores.py deleted file mode 100644 index ea557cd5..00000000 --- a/pyrogram/client/types/keyboards/game_high_scores.py +++ /dev/null @@ -1,60 +0,0 @@ -# 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 List - -import pyrogram -from pyrogram.api import types -from pyrogram.client.types.object import Object -from .game_high_score import GameHighScore - - -class GameHighScores(Object): - """The high scores table for a game. - - Parameters: - total_count (``int``): - Total number of scores the target game has. - - game_high_scores (List of :obj:`GameHighScore`): - Game scores. - """ - - __slots__ = ["total_count", "game_high_scores"] - - def __init__( - self, - *, - client: "pyrogram.BaseClient" = None, - total_count: int, - game_high_scores: List[GameHighScore] - ): - super().__init__(client) - - self.total_count = total_count - self.game_high_scores = game_high_scores - - @staticmethod - def _parse(client, game_high_scores: types.messages.HighScores) -> "GameHighScores": - return GameHighScores( - total_count=len(game_high_scores.scores), - game_high_scores=[ - GameHighScore._parse(client, score, game_high_scores.users) - for score in game_high_scores.scores], - client=client - ) From 09c5b239be029abde40c9613877ab4bc33307947 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 8 Jun 2019 15:21:32 +0200 Subject: [PATCH 20/73] Add FOLDER_DEAC_AUTOFIX_ALL error. Weird new 500-class error --- compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv b/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv index 4dfe5994..c85fe7e0 100644 --- a/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv +++ b/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv @@ -8,4 +8,5 @@ REG_ID_GENERATE_FAILED Telegram is having internal problems. Please try again la RANDOM_ID_DUPLICATE Telegram is having internal problems. Please try again later WORKER_BUSY_TOO_LONG_RETRY Telegram is having internal problems. Please try again later INTERDC_X_CALL_ERROR Telegram is having internal problems. Please try again later -INTERDC_X_CALL_RICH_ERROR Telegram is having internal problems. Please try again later \ No newline at end of file +INTERDC_X_CALL_RICH_ERROR Telegram is having internal problems. Please try again later +FOLDER_DEAC_AUTOFIX_ALL Telegram is having internal problems. Please try again later \ No newline at end of file From 94d90efc80961a58253dcbc59f9c573ff4f12cc9 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 8 Jun 2019 15:27:09 +0200 Subject: [PATCH 21/73] Rename section from "Keyboards" to "Bots & Keyboards" --- docs/source/api/types.rst | 6 +++--- pyrogram/client/filters/filters.py | 2 +- pyrogram/client/types/__init__.py | 2 +- .../types/{keyboards => bots_and_keyboards}/__init__.py | 0 .../{keyboards => bots_and_keyboards}/callback_game.py | 0 .../{keyboards => bots_and_keyboards}/callback_query.py | 0 .../types/{keyboards => bots_and_keyboards}/force_reply.py | 0 .../{keyboards => bots_and_keyboards}/game_high_score.py | 0 .../inline_keyboard_button.py | 0 .../inline_keyboard_markup.py | 0 .../{keyboards => bots_and_keyboards}/keyboard_button.py | 0 .../reply_keyboard_markup.py | 0 .../reply_keyboard_remove.py | 0 13 files changed, 5 insertions(+), 5 deletions(-) rename pyrogram/client/types/{keyboards => bots_and_keyboards}/__init__.py (100%) rename pyrogram/client/types/{keyboards => bots_and_keyboards}/callback_game.py (100%) rename pyrogram/client/types/{keyboards => bots_and_keyboards}/callback_query.py (100%) rename pyrogram/client/types/{keyboards => bots_and_keyboards}/force_reply.py (100%) rename pyrogram/client/types/{keyboards => bots_and_keyboards}/game_high_score.py (100%) rename pyrogram/client/types/{keyboards => bots_and_keyboards}/inline_keyboard_button.py (100%) rename pyrogram/client/types/{keyboards => bots_and_keyboards}/inline_keyboard_markup.py (100%) rename pyrogram/client/types/{keyboards => bots_and_keyboards}/keyboard_button.py (100%) rename pyrogram/client/types/{keyboards => bots_and_keyboards}/reply_keyboard_markup.py (100%) rename pyrogram/client/types/{keyboards => bots_and_keyboards}/reply_keyboard_remove.py (100%) diff --git a/docs/source/api/types.rst b/docs/source/api/types.rst index 118c4261..644f8bb2 100644 --- a/docs/source/api/types.rst +++ b/docs/source/api/types.rst @@ -57,8 +57,8 @@ Messages & Media - :class:`Poll` - :class:`PollOption` -Keyboards -^^^^^^^^^ +Bots & Keyboards +^^^^^^^^^^^^^^^^ .. hlist:: :columns: 4 @@ -140,7 +140,7 @@ Details .. autoclass:: Poll() .. autoclass:: PollOption() -.. Keyboards +.. Bots & Keyboards .. autoclass:: ReplyKeyboardMarkup() .. autoclass:: KeyboardButton() .. autoclass:: ReplyKeyboardRemove() diff --git a/pyrogram/client/filters/filters.py b/pyrogram/client/filters/filters.py index 1d962e85..fb0a3615 100644 --- a/pyrogram/client/filters/filters.py +++ b/pyrogram/client/filters/filters.py @@ -19,7 +19,7 @@ import re from .filter import Filter -from ..types.keyboards import InlineKeyboardMarkup, ReplyKeyboardMarkup +from ..types.bots_and_keyboards import InlineKeyboardMarkup, ReplyKeyboardMarkup def create(name: str, func: callable, **kwargs) -> type: diff --git a/pyrogram/client/types/__init__.py b/pyrogram/client/types/__init__.py index 8a725a9e..8fa55482 100644 --- a/pyrogram/client/types/__init__.py +++ b/pyrogram/client/types/__init__.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from .keyboards import * +from .bots_and_keyboards import * from .inline_mode import * from .input_media import * from .input_message_content import * diff --git a/pyrogram/client/types/keyboards/__init__.py b/pyrogram/client/types/bots_and_keyboards/__init__.py similarity index 100% rename from pyrogram/client/types/keyboards/__init__.py rename to pyrogram/client/types/bots_and_keyboards/__init__.py diff --git a/pyrogram/client/types/keyboards/callback_game.py b/pyrogram/client/types/bots_and_keyboards/callback_game.py similarity index 100% rename from pyrogram/client/types/keyboards/callback_game.py rename to pyrogram/client/types/bots_and_keyboards/callback_game.py diff --git a/pyrogram/client/types/keyboards/callback_query.py b/pyrogram/client/types/bots_and_keyboards/callback_query.py similarity index 100% rename from pyrogram/client/types/keyboards/callback_query.py rename to pyrogram/client/types/bots_and_keyboards/callback_query.py diff --git a/pyrogram/client/types/keyboards/force_reply.py b/pyrogram/client/types/bots_and_keyboards/force_reply.py similarity index 100% rename from pyrogram/client/types/keyboards/force_reply.py rename to pyrogram/client/types/bots_and_keyboards/force_reply.py diff --git a/pyrogram/client/types/keyboards/game_high_score.py b/pyrogram/client/types/bots_and_keyboards/game_high_score.py similarity index 100% rename from pyrogram/client/types/keyboards/game_high_score.py rename to pyrogram/client/types/bots_and_keyboards/game_high_score.py diff --git a/pyrogram/client/types/keyboards/inline_keyboard_button.py b/pyrogram/client/types/bots_and_keyboards/inline_keyboard_button.py similarity index 100% rename from pyrogram/client/types/keyboards/inline_keyboard_button.py rename to pyrogram/client/types/bots_and_keyboards/inline_keyboard_button.py diff --git a/pyrogram/client/types/keyboards/inline_keyboard_markup.py b/pyrogram/client/types/bots_and_keyboards/inline_keyboard_markup.py similarity index 100% rename from pyrogram/client/types/keyboards/inline_keyboard_markup.py rename to pyrogram/client/types/bots_and_keyboards/inline_keyboard_markup.py diff --git a/pyrogram/client/types/keyboards/keyboard_button.py b/pyrogram/client/types/bots_and_keyboards/keyboard_button.py similarity index 100% rename from pyrogram/client/types/keyboards/keyboard_button.py rename to pyrogram/client/types/bots_and_keyboards/keyboard_button.py diff --git a/pyrogram/client/types/keyboards/reply_keyboard_markup.py b/pyrogram/client/types/bots_and_keyboards/reply_keyboard_markup.py similarity index 100% rename from pyrogram/client/types/keyboards/reply_keyboard_markup.py rename to pyrogram/client/types/bots_and_keyboards/reply_keyboard_markup.py diff --git a/pyrogram/client/types/keyboards/reply_keyboard_remove.py b/pyrogram/client/types/bots_and_keyboards/reply_keyboard_remove.py similarity index 100% rename from pyrogram/client/types/keyboards/reply_keyboard_remove.py rename to pyrogram/client/types/bots_and_keyboards/reply_keyboard_remove.py From 43493733c93eed960354f036f0c981baf54de360 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 8 Jun 2019 15:28:03 +0200 Subject: [PATCH 22/73] Rearrange code --- pyrogram/client/ext/__init__.py | 3 +-- pyrogram/client/ext/dispatcher.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pyrogram/client/ext/__init__.py b/pyrogram/client/ext/__init__.py index 58897c55..dde1952e 100644 --- a/pyrogram/client/ext/__init__.py +++ b/pyrogram/client/ext/__init__.py @@ -19,6 +19,5 @@ from .base_client import BaseClient from .dispatcher import Dispatcher from .emoji import Emoji -from .syncer import Syncer from .file_data import FileData - +from .syncer import Syncer diff --git a/pyrogram/client/ext/dispatcher.py b/pyrogram/client/ext/dispatcher.py index 12d5a5de..b6760345 100644 --- a/pyrogram/client/ext/dispatcher.py +++ b/pyrogram/client/ext/dispatcher.py @@ -21,10 +21,10 @@ import threading from collections import OrderedDict from queue import Queue from threading import Thread -from . import utils import pyrogram from pyrogram.api import types +from . import utils from ..handlers import ( CallbackQueryHandler, MessageHandler, DeletedMessagesHandler, UserStatusHandler, RawUpdateHandler, InlineQueryHandler, PollHandler From 5a0bcdaf425cbef9d13f63c9c02cbd95ae8e8f9e Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 8 Jun 2019 16:51:03 +0200 Subject: [PATCH 23/73] Update robots.txt --- docs/robots.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/robots.txt b/docs/robots.txt index 0ecbac7b..1b9e8da6 100644 --- a/docs/robots.txt +++ b/docs/robots.txt @@ -1,3 +1,8 @@ User-agent: * + Allow: / + +Disallow: /dev/* +Disallow: /old/* + Sitemap: https://docs.pyrogram.org/sitemap.xml \ No newline at end of file From 3ab624c7060dbdbbc4cc4ff1b33075a55a53bf58 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 8 Jun 2019 18:54:33 +0200 Subject: [PATCH 24/73] Add FOLDER_ID_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 fa6ff67e..815ce3fb 100644 --- a/compiler/error/source/400_BAD_REQUEST.tsv +++ b/compiler/error/source/400_BAD_REQUEST.tsv @@ -99,4 +99,5 @@ RESULT_ID_DUPLICATE The result contains items with duplicated identifiers ACCESS_TOKEN_INVALID The bot access token is invalid INVITE_HASH_EXPIRED The chat invite link is no longer valid USER_BANNED_IN_CHANNEL You are limited, check @SpamBot for details -MESSAGE_EDIT_TIME_EXPIRED You can no longer edit this message \ No newline at end of file +MESSAGE_EDIT_TIME_EXPIRED You can no longer edit this message +FOLDER_ID_INVALID The folder id is invalid \ No newline at end of file From 00755347533c03f33f315aa8db5b8b1af615a61c Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 8 Jun 2019 19:13:27 +0200 Subject: [PATCH 25/73] Add a bunch of new errors - MEGAGROUP_PREHISTORY_HIDDEN - CHAT_LINK_EXISTS - LINK_NOT_MODIFIED - BROADCAST_ID_INVALID - MEGAGROUP_ID_INVALID --- compiler/error/source/400_BAD_REQUEST.tsv | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/compiler/error/source/400_BAD_REQUEST.tsv b/compiler/error/source/400_BAD_REQUEST.tsv index 815ce3fb..8e82c9f6 100644 --- a/compiler/error/source/400_BAD_REQUEST.tsv +++ b/compiler/error/source/400_BAD_REQUEST.tsv @@ -100,4 +100,9 @@ ACCESS_TOKEN_INVALID The bot access token is invalid INVITE_HASH_EXPIRED The chat invite link is no longer valid USER_BANNED_IN_CHANNEL You are limited, check @SpamBot for details MESSAGE_EDIT_TIME_EXPIRED You can no longer edit this message -FOLDER_ID_INVALID The folder id is invalid \ No newline at end of file +FOLDER_ID_INVALID The folder id is invalid +MEGAGROUP_PREHISTORY_HIDDEN The action failed because the supergroup has the pre-history hidden +CHAT_LINK_EXISTS The action failed because the supergroup is linked to a channel +LINK_NOT_MODIFIED The chat link was not modified because you tried to link to the same target +BROADCAST_ID_INVALID The channel is invalid +MEGAGROUP_ID_INVALID The supergroup is invalid \ No newline at end of file From 5f3b7b97aaa5ca8a1b8f302051a949de9823eb01 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 8 Jun 2019 19:15:19 +0200 Subject: [PATCH 26/73] Add archive_chats and unarchive_chats methods --- docs/source/api/methods.rst | 4 ++ pyrogram/client/methods/chats/__init__.py | 6 +- .../client/methods/chats/archive_chats.py | 58 +++++++++++++++++++ .../client/methods/chats/unarchive_chats.py | 58 +++++++++++++++++++ 4 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 pyrogram/client/methods/chats/archive_chats.py create mode 100644 pyrogram/client/methods/chats/unarchive_chats.py diff --git a/docs/source/api/methods.rst b/docs/source/api/methods.rst index ed150e4c..e688903f 100644 --- a/docs/source/api/methods.rst +++ b/docs/source/api/methods.rst @@ -104,6 +104,8 @@ Chats - :meth:`~Client.get_dialogs_count` - :meth:`~Client.restrict_chat` - :meth:`~Client.update_chat_username` + - :meth:`~Client.archive_chats` + - :meth:`~Client.unarchive_chats` Users ^^^^^ @@ -233,6 +235,8 @@ Details .. automethod:: Client.get_dialogs_count() .. automethod:: Client.restrict_chat() .. automethod:: Client.update_chat_username() +.. automethod:: Client.archive_chats() +.. automethod:: Client.unarchive_chats() .. Users .. automethod:: Client.get_me() diff --git a/pyrogram/client/methods/chats/__init__.py b/pyrogram/client/methods/chats/__init__.py index c0176939..969628ee 100644 --- a/pyrogram/client/methods/chats/__init__.py +++ b/pyrogram/client/methods/chats/__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 .archive_chats import ArchiveChats from .delete_chat_photo import DeleteChatPhoto from .export_chat_invite_link import ExportChatInviteLink from .get_chat import GetChat @@ -36,6 +37,7 @@ from .restrict_chat_member import RestrictChatMember from .set_chat_description import SetChatDescription from .set_chat_photo import SetChatPhoto from .set_chat_title import SetChatTitle +from .unarchive_chats import UnarchiveChats from .unban_chat_member import UnbanChatMember from .unpin_chat_message import UnpinChatMessage from .update_chat_username import UpdateChatUsername @@ -64,6 +66,8 @@ class Chats( IterChatMembers, UpdateChatUsername, RestrictChat, - GetDialogsCount + GetDialogsCount, + ArchiveChats, + UnarchiveChats ): pass diff --git a/pyrogram/client/methods/chats/archive_chats.py b/pyrogram/client/methods/chats/archive_chats.py new file mode 100644 index 00000000..3c929983 --- /dev/null +++ b/pyrogram/client/methods/chats/archive_chats.py @@ -0,0 +1,58 @@ +# 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, List + +from pyrogram.api import functions, types +from ...ext import BaseClient + + +class ArchiveChats(BaseClient): + def archive_chats( + self, + chat_ids: Union[int, str, List[Union[int, str]]], + ) -> bool: + """Archive one or more chats. + + Parameters: + chat_ids (``int`` | ``str`` | List[``int``, ``str``]): + Unique identifier (int) or username (str) of the target chat. + You can also pass a list of ids (int) or usernames (str). + + Returns: + ``bool``: On success, True is returned. + + Raises: + RPCError: In case of a Telegram RPC error. + """ + + if not isinstance(chat_ids, list): + chat_ids = [chat_ids] + + self.send( + functions.folders.EditPeerFolders( + folder_peers=[ + types.InputFolderPeer( + peer=self.resolve_peer(chat), + folder_id=1 + ) for chat in chat_ids + ] + ) + ) + + return True diff --git a/pyrogram/client/methods/chats/unarchive_chats.py b/pyrogram/client/methods/chats/unarchive_chats.py new file mode 100644 index 00000000..56bcc6f8 --- /dev/null +++ b/pyrogram/client/methods/chats/unarchive_chats.py @@ -0,0 +1,58 @@ +# 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, List + +from pyrogram.api import functions, types +from ...ext import BaseClient + + +class UnarchiveChats(BaseClient): + def unarchive_chats( + self, + chat_ids: Union[int, str, List[Union[int, str]]], + ) -> bool: + """Unarchive one or more chats. + + Parameters: + chat_ids (``int`` | ``str`` | List[``int``, ``str``]): + Unique identifier (int) or username (str) of the target chat. + You can also pass a list of ids (int) or usernames (str). + + Returns: + ``bool``: On success, True is returned. + + Raises: + RPCError: In case of a Telegram RPC error. + """ + + if not isinstance(chat_ids, list): + chat_ids = [chat_ids] + + self.send( + functions.folders.EditPeerFolders( + folder_peers=[ + types.InputFolderPeer( + peer=self.resolve_peer(chat), + folder_id=0 + ) for chat in chat_ids + ] + ) + ) + + return True From 9b12e823b43427bc3a33809ea4cffdc4f9d6f1a2 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 8 Jun 2019 19:25:12 +0200 Subject: [PATCH 27/73] Fix Message bound methods' docstrings --- .../types/messages_and_media/message.py | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/pyrogram/client/types/messages_and_media/message.py b/pyrogram/client/types/messages_and_media/message.py index f7dff7b5..78165e78 100644 --- a/pyrogram/client/types/messages_and_media/message.py +++ b/pyrogram/client/types/messages_and_media/message.py @@ -661,7 +661,7 @@ class Message(Object, Update): reply_to_message_id: int = None, reply_markup=None ) -> "Message": - """Bound method *reply* :obj:`Message `. + """Bound method *reply* of :obj:`Message`. Use as a shortcut for: @@ -748,7 +748,7 @@ class Message(Object, Update): progress: callable = None, progress_args: tuple = () ) -> "Message": - """Bound method *reply_animation* :obj:`Message `. + """Bound method *reply_animation* :obj:`Message`. Use as a shortcut for: @@ -882,7 +882,7 @@ class Message(Object, Update): progress: callable = None, progress_args: tuple = () ) -> "Message": - """Bound method *reply_audio* :obj:`Message `. + """Bound method *reply_audio* of :obj:`Message`. Use as a shortcut for: @@ -1010,7 +1010,7 @@ class Message(Object, Update): "pyrogram.ForceReply" ] = None ) -> "Message": - """Bound method *reply_cached_media* :obj:`Message `. + """Bound method *reply_cached_media* of :obj:`Message`. Use as a shortcut for: @@ -1077,7 +1077,7 @@ class Message(Object, Update): ) def reply_chat_action(self, action: str) -> bool: - """Bound method *reply_chat_action* :obj:`Message `. + """Bound method *reply_chat_action* of :obj:`Message`. Use as a shortcut for: @@ -1130,7 +1130,7 @@ class Message(Object, Update): "pyrogram.ForceReply" ] = None ) -> "Message": - """Bound method *reply_contact* :obj:`Message `. + """Bound method *reply_contact* of :obj:`Message`. Use as a shortcut for: @@ -1217,7 +1217,7 @@ class Message(Object, Update): progress: callable = None, progress_args: tuple = () ) -> "Message": - """Bound method *reply_document* :obj:`Message `. + """Bound method *reply_document* of :obj:`Message`. Use as a shortcut for: @@ -1331,7 +1331,7 @@ class Message(Object, Update): "pyrogram.ForceReply" ] = None ) -> "Message": - """Bound method *reply_game* :obj:`Message `. + """Bound method *reply_game* of :obj:`Message`. Use as a shortcut for: @@ -1396,7 +1396,7 @@ class Message(Object, Update): reply_to_message_id: int = None, hide_via: bool = None ) -> "Message": - """Bound method *reply_inline_bot_result* :obj:`Message `. + """Bound method *reply_inline_bot_result* of :obj:`Message`. Use as a shortcut for: @@ -1470,7 +1470,7 @@ class Message(Object, Update): "pyrogram.ForceReply" ] = None ) -> "Message": - """Bound method *reply_location* :obj:`Message `. + """Bound method *reply_location* of :obj:`Message`. Use as a shortcut for: @@ -1538,7 +1538,7 @@ class Message(Object, Update): disable_notification: bool = None, reply_to_message_id: int = None ) -> "Message": - """Bound method *reply_media_group* :obj:`Message `. + """Bound method *reply_media_group* of :obj:`Message`. Use as a shortcut for: @@ -1610,7 +1610,7 @@ class Message(Object, Update): progress: callable = None, progress_args: tuple = () ) -> "Message": - """Bound method *reply_photo* :obj:`Message `. + """Bound method *reply_photo* of :obj:`Message`. Use as a shortcut for: @@ -1724,7 +1724,7 @@ class Message(Object, Update): "pyrogram.ForceReply" ] = None ) -> "Message": - """Bound method *reply_poll* :obj:`Message `. + """Bound method *reply_poll* of :obj:`Message`. Use as a shortcut for: @@ -1800,7 +1800,7 @@ class Message(Object, Update): progress: callable = None, progress_args: tuple = () ) -> "Message": - """Bound method *reply_sticker* :obj:`Message `. + """Bound method *reply_sticker* of :obj:`Message`. Use as a shortcut for: @@ -1903,7 +1903,7 @@ class Message(Object, Update): "pyrogram.ForceReply" ] = None ) -> "Message": - """Bound method *reply_venue* :obj:`Message `. + """Bound method *reply_venue* of :obj:`Message`. Use as a shortcut for: @@ -2005,7 +2005,7 @@ class Message(Object, Update): progress: callable = None, progress_args: tuple = () ) -> "Message": - """Bound method *reply_video* :obj:`Message `. + """Bound method *reply_video* of :obj:`Message`. Use as a shortcut for: @@ -2140,7 +2140,7 @@ class Message(Object, Update): progress: callable = None, progress_args: tuple = () ) -> "Message": - """Bound method *reply_video_note* :obj:`Message `. + """Bound method *reply_video_note* of :obj:`Message`. Use as a shortcut for: @@ -2258,7 +2258,7 @@ class Message(Object, Update): progress: callable = None, progress_args: tuple = () ) -> "Message": - """Bound method *reply_voice* :obj:`Message `. + """Bound method *reply_voice* of :obj:`Message`. Use as a shortcut for: @@ -2368,7 +2368,7 @@ class Message(Object, Update): "pyrogram.ForceReply" ] = None ) -> "Message": - """Bound method *edit* :obj:`Message `. + """Bound method *edit* of :obj:`Message`. Use as a shortcut for: @@ -2425,7 +2425,7 @@ class Message(Object, Update): "pyrogram.ForceReply" ] = None ) -> "Message": - """Bound method *edit_caption* :obj:`Message `. + """Bound method *edit_caption* of :obj:`Message`. Use as a shortcut for: @@ -2468,7 +2468,7 @@ class Message(Object, Update): ) def edit_media(self, media: InputMedia, reply_markup: "pyrogram.InlineKeyboardMarkup" = None) -> "Message": - """Bound method *edit_media* :obj:`Message `. + """Bound method *edit_media* of :obj:`Message`. Use as a shortcut for: @@ -2506,7 +2506,7 @@ class Message(Object, Update): ) def edit_reply_markup(self, reply_markup: "pyrogram.InlineKeyboardMarkup" = None) -> "Message": - """Bound method *edit_reply_markup* :obj:`Message `. + """Bound method *edit_reply_markup* of :obj:`Message`. Use as a shortcut for: @@ -2547,7 +2547,7 @@ class Message(Object, Update): as_copy: bool = False, remove_caption: bool = False ) -> "Message": - """Bound method *forward* :obj:`Message `. + """Bound method *forward* of :obj:`Message`. Use as a shortcut for: @@ -2690,7 +2690,7 @@ class Message(Object, Update): ) def delete(self, revoke: bool = True): - """Bound method *delete* :obj:`Message `. + """Bound method *delete* of :obj:`Message`. Use as a shortcut for: @@ -2726,7 +2726,7 @@ class Message(Object, Update): ) def click(self, x: int or str, y: int = 0, quote: bool = None, timeout: int = 10): - """Bound method *click* :obj:`Message `. + """Bound method *click* of :obj:`Message`. Use as a shortcut for clicking a button attached to the message instead of: @@ -2853,7 +2853,7 @@ class Message(Object, Update): progress: callable = None, progress_args: tuple = () ) -> str: - """Bound method *download* :obj:`Message `. + """Bound method *download* of :obj:`Message`. Use as a shortcut for: @@ -2902,7 +2902,7 @@ class Message(Object, Update): ) def pin(self, disable_notification: bool = None) -> "Message": - """Bound method *pin* :obj:`Message `. + """Bound method *pin* of :obj:`Message`. Use as a shortcut for: From 6e3d8ca20b25ee90ff3b9a6caa0494b6d16c89a2 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 8 Jun 2019 19:25:57 +0200 Subject: [PATCH 28/73] Add .archive() and .unarchive() bound methods to Chat --- docs/source/api/bound-methods.rst | 13 ++++++ pyrogram/client/types/user_and_chats/chat.py | 46 ++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/docs/source/api/bound-methods.rst b/docs/source/api/bound-methods.rst index 0622e6b8..ac348d03 100644 --- a/docs/source/api/bound-methods.rst +++ b/docs/source/api/bound-methods.rst @@ -59,6 +59,15 @@ Message - :meth:`~Message.reply_video_note` - :meth:`~Message.reply_voice` +Chat +^^^^ + +.. hlist:: + :columns: 2 + + - :meth:`~Chat.archive` + - :meth:`~Chat.unarchive` + CallbackQuery ^^^^^^^^^^^^^ @@ -109,6 +118,10 @@ Details .. automethod:: Message.reply_video_note() .. automethod:: Message.reply_voice() +.. Chat +.. automethod:: Chat.archive() +.. automethod:: Chat.unarchive() + .. CallbackQuery .. automethod:: CallbackQuery.answer() diff --git a/pyrogram/client/types/user_and_chats/chat.py b/pyrogram/client/types/user_and_chats/chat.py index ca9acd65..7296a903 100644 --- a/pyrogram/client/types/user_and_chats/chat.py +++ b/pyrogram/client/types/user_and_chats/chat.py @@ -257,3 +257,49 @@ class Chat(Object): return Chat._parse_user_chat(client, chat) else: return Chat._parse_channel_chat(client, chat) + + def archive(self): + """Bound method *archive* of :obj:`Chat`. + + Use as a shortcut for: + + .. code-block:: python + + client.archive_chats(-100123456789) + + Example: + .. code-block:: python + + chat.archive() + + Returns: + True on success. + + Raises: + RPCError: In case of a Telegram RPC error. + """ + + return self._client.archive_chats(self.id) + + def unarchive(self): + """Bound method *unarchive* of :obj:`Chat`. + + Use as a shortcut for: + + .. code-block:: python + + client.unarchive_chats(-100123456789) + + Example: + .. code-block:: python + + chat.unarchive() + + Returns: + True on success. + + Raises: + RPCError: In case of a Telegram RPC error. + """ + + return self._client.unarchive_chats(self.id) From 34616ebf613802c767e78a6b5404684e41060d00 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 8 Jun 2019 19:29:55 +0200 Subject: [PATCH 29/73] Add .archive() and .unarchive() bound methods to User --- docs/source/api/bound-methods.rst | 13 ++++++ pyrogram/client/types/user_and_chats/user.py | 46 ++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/docs/source/api/bound-methods.rst b/docs/source/api/bound-methods.rst index ac348d03..d01a4383 100644 --- a/docs/source/api/bound-methods.rst +++ b/docs/source/api/bound-methods.rst @@ -68,6 +68,15 @@ Chat - :meth:`~Chat.archive` - :meth:`~Chat.unarchive` +User +^^^^ + +.. hlist:: + :columns: 2 + + - :meth:`~User.archive` + - :meth:`~User.unarchive` + CallbackQuery ^^^^^^^^^^^^^ @@ -122,6 +131,10 @@ Details .. automethod:: Chat.archive() .. automethod:: Chat.unarchive() +.. User +.. automethod:: User.archive() +.. automethod:: User.unarchive() + .. CallbackQuery .. automethod:: CallbackQuery.answer() diff --git a/pyrogram/client/types/user_and_chats/user.py b/pyrogram/client/types/user_and_chats/user.py index 50dd8361..05877b63 100644 --- a/pyrogram/client/types/user_and_chats/user.py +++ b/pyrogram/client/types/user_and_chats/user.py @@ -160,3 +160,49 @@ class User(Object): restriction_reason=user.restriction_reason, client=client ) + + def archive(self): + """Bound method *archive* of :obj:`User`. + + Use as a shortcut for: + + .. code-block:: python + + client.archive_chats(123456789) + + Example: + .. code-block:: python + + user.archive() + + Returns: + True on success. + + Raises: + RPCError: In case of a Telegram RPC error. + """ + + return self._client.archive_chats(self.id) + + def unarchive(self): + """Bound method *unarchive* of :obj:`User`. + + Use as a shortcut for: + + .. code-block:: python + + client.unarchive_chats(123456789) + + Example: + .. code-block:: python + + user.unarchive() + + Returns: + True on success. + + Raises: + RPCError: In case of a Telegram RPC error. + """ + + return self._client.unarchive_chats(self.id) From 6e6dd54d4020248072053959b4120ba6d9ba4641 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 8 Jun 2019 19:55:40 +0200 Subject: [PATCH 30/73] Add missing attributes to the Chat type --- pyrogram/client/types/user_and_chats/chat.py | 19 ++++++++++++++----- pyrogram/client/types/user_and_chats/user.py | 14 +++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/pyrogram/client/types/user_and_chats/chat.py b/pyrogram/client/types/user_and_chats/chat.py index 7296a903..2d88d3ed 100644 --- a/pyrogram/client/types/user_and_chats/chat.py +++ b/pyrogram/client/types/user_and_chats/chat.py @@ -36,14 +36,17 @@ class Chat(Object): Type of chat, can be either "private", "bot", "group", "supergroup" or "channel". is_verified (``bool``, *optional*): - True, if this chat has been verified by Telegram. Supergroups and channels only. + True, if this chat has been verified by Telegram. Supergroups, channels and bots only. is_restricted (``bool``, *optional*): - True, if this chat has been restricted. Supergroups and channels only. + True, if this chat has been restricted. Supergroups, channels and bots only. See *restriction_reason* for details. is_scam (``bool``, *optional*): - True, if this chat has been flagged for scam. Supergroups and channels only. + True, if this chat has been flagged for scam. Supergroups, channels and bots only. + + is_support (``bool``): + True, if this chat is part of the Telegram support team. Users and bots only. title (``str``, *optional*): Title, for supergroups, channels and basic group chats. @@ -92,8 +95,8 @@ class Chat(Object): """ __slots__ = [ - "id", "type", "is_verified", "is_restricted", "is_scam", "title", "username", "first_name", "last_name", - "photo", "description", "invite_link", "pinned_message", "sticker_set_name", "can_set_sticker_set", + "id", "type", "is_verified", "is_restricted", "is_scam", "is_support", "title", "username", "first_name", + "last_name", "photo", "description", "invite_link", "pinned_message", "sticker_set_name", "can_set_sticker_set", "members_count", "restriction_reason", "permissions" ] @@ -106,6 +109,7 @@ class Chat(Object): is_verified: bool = None, is_restricted: bool = None, is_scam: bool = None, + is_support: bool = None, title: str = None, username: str = None, first_name: str = None, @@ -127,6 +131,7 @@ class Chat(Object): self.is_verified = is_verified self.is_restricted = is_restricted self.is_scam = is_scam + self.is_support = is_support self.title = title self.username = username self.first_name = first_name @@ -148,6 +153,10 @@ class Chat(Object): return Chat( id=peer_id, type="bot" if user.bot else "private", + is_verified=getattr(user, "verified", None), + is_restricted=getattr(user, "restricted", None), + is_scam=getattr(user, "scam", None), + is_support=getattr(user, "support", None), username=user.username, first_name=user.first_name, last_name=user.last_name, diff --git a/pyrogram/client/types/user_and_chats/user.py b/pyrogram/client/types/user_and_chats/user.py index 05877b63..f47e8c42 100644 --- a/pyrogram/client/types/user_and_chats/user.py +++ b/pyrogram/client/types/user_and_chats/user.py @@ -52,12 +52,12 @@ class User(Object): True, if this user has been restricted. Bots only. See *restriction_reason* for details. - is_support (``bool``): - True, if this user is part of the Telegram support team. - is_scam (``bool``): True, if this user has been flagged for scam. + is_support (``bool``): + True, if this user is part of the Telegram support team. + first_name (``str``): User's or bot's first name. @@ -86,7 +86,7 @@ class User(Object): __slots__ = [ "id", "is_self", "is_contact", "is_mutual_contact", "is_deleted", "is_bot", "is_verified", "is_restricted", - "is_support", "is_scam", "first_name", "last_name", "status", "username", "language_code", "phone_number", + "is_scam", "is_support", "first_name", "last_name", "status", "username", "language_code", "phone_number", "photo", "restriction_reason" ] @@ -102,8 +102,8 @@ class User(Object): is_bot: bool, is_verified: bool, is_restricted: bool, - is_support: bool, is_scam: bool, + is_support: bool, first_name: str, last_name: str = None, status: UserStatus = None, @@ -123,8 +123,8 @@ class User(Object): self.is_bot = is_bot self.is_verified = is_verified self.is_restricted = is_restricted - self.is_support = is_support self.is_scam = is_scam + self.is_support = is_support self.first_name = first_name self.last_name = last_name self.status = status @@ -148,8 +148,8 @@ class User(Object): is_bot=user.bot, is_verified=user.verified, is_restricted=user.restricted, - is_support=user.support, is_scam=user.scam, + is_support=user.support, first_name=user.first_name, last_name=user.last_name, status=UserStatus._parse(client, user.status, user.id, user.bot), From af08606087548943bd1bf891a35d1d11a494ba2d Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 9 Jun 2019 13:01:24 +0200 Subject: [PATCH 31/73] Fix get_profile_photos not working when passing "me"/"self" as argument --- pyrogram/client/methods/users/get_profile_photos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/client/methods/users/get_profile_photos.py b/pyrogram/client/methods/users/get_profile_photos.py index e4e202e0..2b1d0c54 100644 --- a/pyrogram/client/methods/users/get_profile_photos.py +++ b/pyrogram/client/methods/users/get_profile_photos.py @@ -54,7 +54,7 @@ class GetProfilePhotos(BaseClient): """ peer_id = self.resolve_peer(chat_id) - if isinstance(peer_id, types.InputPeerUser): + if isinstance(peer_id, (types.InputPeerUser, types.InputPeerSelf)): return pyrogram.ProfilePhotos._parse( self, self.send( From 6ddb28c3e4b3dbec4a0e7431983c4bdcba4a3874 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 10 Jun 2019 17:41:55 +0200 Subject: [PATCH 32/73] Small docs layout fixup --- docs/source/api/client.rst | 3 +++ docs/source/api/filters.rst | 3 +++ docs/source/license.rst | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/source/api/client.rst b/docs/source/api/client.rst index 9527ca73..d1b8c4b0 100644 --- a/docs/source/api/client.rst +++ b/docs/source/api/client.rst @@ -13,4 +13,7 @@ This is the Client class. It exposes high-level methods for an easy access to th with app: app.send_message("me", "Hi!") +Details +------- + .. autoclass:: pyrogram.Client() diff --git a/docs/source/api/filters.rst b/docs/source/api/filters.rst index 87faa801..6cb01cda 100644 --- a/docs/source/api/filters.rst +++ b/docs/source/api/filters.rst @@ -1,5 +1,8 @@ Update Filters ============== +Details +------- + .. autoclass:: pyrogram.Filters :members: diff --git a/docs/source/license.rst b/docs/source/license.rst index 43f59d73..38302bdc 100644 --- a/docs/source/license.rst +++ b/docs/source/license.rst @@ -2,7 +2,7 @@ About the License ================= .. image:: https://www.gnu.org/graphics/lgplv3-with-text-154x68.png - :align: left + :align: right Pyrogram is free software and is currently licensed under the terms of the `GNU Lesser General Public License v3 or later (LGPLv3+)`_. In short: you may use, redistribute and/or modify it From 10e5dbb6e87941ce52fb7e9d546448e09cbdd232 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 11 Jun 2019 14:44:30 +0200 Subject: [PATCH 33/73] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 4 ++-- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- .github/ISSUE_TEMPLATE/question.md | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index e0556d54..59410e25 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,7 +4,7 @@ about: Create a bug report affecting the library labels: "bug" --- - + ## Checklist - [ ] I am sure the error is coming from Pyrogram's code and not elsewhere. @@ -15,7 +15,7 @@ labels: "bug" A clear and concise description of the problem. ## Steps to Reproduce -[A minimal, complete and verifiable example](https://stackoverflow.com/help/mcve). +[A minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). ## Traceback The full traceback (if applicable). \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 70a39192..4d2f447c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -4,7 +4,7 @@ about: Suggest ideas, new features or enhancements labels: "enhancement" --- - + ## Checklist - [ ] I believe the idea is awesome and would benefit the library. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 737304d9..88d91ecd 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -5,11 +5,11 @@ title: For Q&A purposes, please read this template body labels: "question" --- - + # Important This place is for issues about Pyrogram, it's **not a forum**. -If you'd like to post a question, please move to https://stackoverflow.com or join the Telegram community by following the description in https://t.me/pyrogram. +If you'd like to post a question, please move to https://stackoverflow.com or join the Telegram community at https://t.me/pyrogram. Thanks. \ No newline at end of file From c8b757aceeee047a3847d808995b156f8a3afb00 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 11 Jun 2019 15:59:15 +0200 Subject: [PATCH 34/73] Move advanced utility methods somewhere else in the docs index --- docs/source/api/methods.rst | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/source/api/methods.rst b/docs/source/api/methods.rst index e688903f..58836efa 100644 --- a/docs/source/api/methods.rst +++ b/docs/source/api/methods.rst @@ -31,9 +31,6 @@ Utilities - :meth:`~Client.run` - :meth:`~Client.add_handler` - :meth:`~Client.remove_handler` - - :meth:`~Client.send` - - :meth:`~Client.resolve_peer` - - :meth:`~Client.save_file` - :meth:`~Client.stop_transmission` Messages @@ -159,6 +156,16 @@ Bots - :meth:`~Client.set_game_score` - :meth:`~Client.get_game_high_scores` +Advanced Usage (Raw API) +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. hlist:: + :columns: 4 + + - :meth:`~Client.send` + - :meth:`~Client.resolve_peer` + - :meth:`~Client.save_file` + ----- Details @@ -172,9 +179,6 @@ Details .. automethod:: Client.run() .. automethod:: Client.add_handler() .. automethod:: Client.remove_handler() -.. automethod:: Client.send() -.. automethod:: Client.resolve_peer() -.. automethod:: Client.save_file() .. automethod:: Client.stop_transmission() .. Messages @@ -269,3 +273,8 @@ Details .. automethod:: Client.send_game() .. automethod:: Client.set_game_score() .. automethod:: Client.get_game_high_scores() + +.. Advanced Usage +.. automethod:: Client.send() +.. automethod:: Client.resolve_peer() +.. automethod:: Client.save_file() \ No newline at end of file From 7baa00353d7a9f755203a32bae1f77394bf32121 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 11 Jun 2019 15:59:39 +0200 Subject: [PATCH 35/73] Add a FAQ about DC migration --- docs/source/faq.rst | 23 ++++++++++++++++---- pyrogram/client/methods/users/get_user_dc.py | 9 ++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/source/faq.rst b/docs/source/faq.rst index 6ff16559..f08da03f 100644 --- a/docs/source/faq.rst +++ b/docs/source/faq.rst @@ -152,11 +152,26 @@ kept as aliases. ***** Alias DC -I keep getting PEER_ID_INVALID error! -------------------------------------------- +I want to migrate my account from DCX to DCY. +--------------------------------------------- -The error in question is ``[400 PEER_ID_INVALID]``, and could mean several -things: +This question is often asked by people who find their account(s) always being connected to DC1 - USA (for example), but +are connecting from a place far away (e.g DC4 - Europe), thus resulting in slower interactions when using the API +because of the great physical distance between the user and its associated DC. + +When registering an account for the first time, is up to Telegram to decide which DC the new user is going to be created +in, based on the phone number origin. + +Even though Telegram `documentations `_ state the server might +decide to automatically migrate a user in case of prolonged usages from a distant, unusual location and albeit this +mechanism is also `confirmed `_ to exist by Telegram itself, +it's currently not possible to have your account migrated, in any way, simply because the feature was once planned but +not yet implemented. + +I keep getting PEER_ID_INVALID error! +------------------------------------- + +The error in question is ``[400 PEER_ID_INVALID]``, and could mean several things: - The chat id you tried to use is simply wrong, double check it. - The chat id refers to a group or channel you are not a member of. diff --git a/pyrogram/client/methods/users/get_user_dc.py b/pyrogram/client/methods/users/get_user_dc.py index 7cb43d83..718f4c44 100644 --- a/pyrogram/client/methods/users/get_user_dc.py +++ b/pyrogram/client/methods/users/get_user_dc.py @@ -28,9 +28,9 @@ class GetUserDC(BaseClient): .. note:: - This information is approximate: it is based on where the user stores their profile pictures and does not by - any means tell you the user location. More info at - `FAQs <../faq#what-are-the-ip-addresses-of-telegram-data-centers>`_. + This information is approximate: it is based on where Telegram stores a user profile pictures and does not + by any means tell you the user location (i.e. a user might travel far away, but will still connect to its + assigned DC). More info at `FAQs <../faq#what-are-the-ip-addresses-of-telegram-data-centers>`_. Parameters: user_id (``int`` | ``str``): @@ -39,7 +39,8 @@ class GetUserDC(BaseClient): For a contact that exists in your Telegram address book you can use his phone number (str). Returns: - ``int`` | ``None``: The DC identifier as integer, or None in case it wasn't possible to get it. + ``int`` | ``None``: The DC identifier as integer, or None in case it wasn't possible to get it (i.e. the + user has no profile picture or has the privacy setting enabled). Raises: RPCError: In case of a Telegram RPC error. From b9b50bad94afe50073e0b85677538f0bcdaf74b3 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 11 Jun 2019 16:46:10 +0200 Subject: [PATCH 36/73] Fix get_users and get_contacts not returning pretty-printable lists --- pyrogram/client/methods/contacts/get_contacts.py | 2 +- pyrogram/client/methods/users/get_users.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyrogram/client/methods/contacts/get_contacts.py b/pyrogram/client/methods/contacts/get_contacts.py index 0c231670..8ca321dc 100644 --- a/pyrogram/client/methods/contacts/get_contacts.py +++ b/pyrogram/client/methods/contacts/get_contacts.py @@ -47,4 +47,4 @@ class GetContacts(BaseClient): time.sleep(e.x) else: log.info("Total contacts: {}".format(len(self.peers_by_phone))) - return [pyrogram.User._parse(self, user) for user in contacts.users] + return pyrogram.List(pyrogram.User._parse(self, user) for user in contacts.users) diff --git a/pyrogram/client/methods/users/get_users.py b/pyrogram/client/methods/users/get_users.py index 4ec0e893..f76e6802 100644 --- a/pyrogram/client/methods/users/get_users.py +++ b/pyrogram/client/methods/users/get_users.py @@ -56,7 +56,7 @@ class GetUsers(BaseClient): ) ) - users = [] + users = pyrogram.List() for i in r: users.append(pyrogram.User._parse(self, i)) From fd0a40442a9745d77c8252ea72d82dcfe1d70dd0 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 11 Jun 2019 18:31:38 +0200 Subject: [PATCH 37/73] Fix plugins not getting reloaded properly when restarting a client --- pyrogram/client/client.py | 59 ++++++++++++++++++++----------- pyrogram/client/ext/dispatcher.py | 1 + 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 6dc8ce97..fd5db423 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -1085,29 +1085,34 @@ class Client(Methods, BaseClient): self._proxy["password"] = parser.get("proxy", "password", fallback=None) or None if self.plugins: - self.plugins["enabled"] = bool(self.plugins.get("enabled", True)) - self.plugins["include"] = "\n".join(self.plugins.get("include", [])) or None - self.plugins["exclude"] = "\n".join(self.plugins.get("exclude", [])) or None + self.plugins = { + "enabled": bool(self.plugins.get("enabled", True)), + "root": self.plugins.get("root", None), + "include": self.plugins.get("include", []), + "exclude": self.plugins.get("exclude", []) + } else: try: section = parser["plugins"] self.plugins = { "enabled": section.getboolean("enabled", True), - "root": section.get("root"), - "include": section.get("include") or None, - "exclude": section.get("exclude") or None + "root": section.get("root", None), + "include": section.get("include", []), + "exclude": section.get("exclude", []) } - except KeyError: - self.plugins = {} - if self.plugins: - for option in ["include", "exclude"]: - if self.plugins[option] is not None: - self.plugins[option] = [ - (i.split()[0], i.split()[1:] or None) - for i in self.plugins[option].strip().split("\n") - ] + include = self.plugins["include"] + exclude = self.plugins["exclude"] + + if include: + self.plugins["include"] = include.strip().split("\n") + + if exclude: + self.plugins["exclude"] = exclude.strip().split("\n") + + except KeyError: + self.plugins = None def load_session(self): try: @@ -1142,14 +1147,26 @@ class Client(Methods, BaseClient): self.peers_by_phone[k] = peer def load_plugins(self): - if self.plugins.get("enabled", False): - root = self.plugins["root"] - include = self.plugins["include"] - exclude = self.plugins["exclude"] + if self.plugins: + plugins = self.plugins.copy() + + for option in ["include", "exclude"]: + if plugins[option]: + plugins[option] = [ + (i.split()[0], i.split()[1:] or None) + for i in self.plugins[option] + ] + else: + return + + if plugins.get("enabled", False): + root = plugins["root"] + include = plugins["include"] + exclude = plugins["exclude"] count = 0 - if include is None: + if not include: for path in sorted(Path(root).rglob("*.py")): module_path = '.'.join(path.parent.parts + (path.stem,)) module = import_module(module_path) @@ -1206,7 +1223,7 @@ class Client(Methods, BaseClient): log.warning('[{}] [LOAD] Ignoring non-existent function "{}" from "{}"'.format( self.session_name, name, module_path)) - if exclude is not None: + if exclude: for path, handlers in exclude: module_path = root + "." + path warn_non_existent_functions = True diff --git a/pyrogram/client/ext/dispatcher.py b/pyrogram/client/ext/dispatcher.py index 2f1ec2b9..a15cb299 100644 --- a/pyrogram/client/ext/dispatcher.py +++ b/pyrogram/client/ext/dispatcher.py @@ -106,6 +106,7 @@ class Dispatcher: worker.join() self.workers_list.clear() + self.groups.clear() def add_handler(self, handler, group: int): if group not in self.groups: From 83af58258c2b6343f8684fb5bbaf80f05bf33a21 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 11 Jun 2019 20:36:09 +0200 Subject: [PATCH 38/73] Fix download_media ignoring the file_name argument --- pyrogram/client/methods/messages/download_media.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyrogram/client/methods/messages/download_media.py b/pyrogram/client/methods/messages/download_media.py index d822a88b..bd8de2d6 100644 --- a/pyrogram/client/methods/messages/download_media.py +++ b/pyrogram/client/methods/messages/download_media.py @@ -86,6 +86,7 @@ class DownloadMedia(BaseClient): error_message = "This message doesn't contain any downloadable media" available_media = ("audio", "document", "photo", "sticker", "animation", "video", "voice", "video_note") + media_file_name = None file_size = None mime_type = None date = None @@ -105,13 +106,13 @@ class DownloadMedia(BaseClient): file_id_str = media else: file_id_str = media.file_id - file_name = getattr(media, "file_name", "") + media_file_name = getattr(media, "file_name", "") file_size = getattr(media, "file_size", None) mime_type = getattr(media, "mime_type", None) date = getattr(media, "date", None) data = FileData( - file_name=file_name, + file_name=media_file_name, file_size=file_size, mime_type=mime_type, date=date From 92625795ef8ea2968097c7fb388b84ad5fa63830 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 11 Jun 2019 20:50:36 +0200 Subject: [PATCH 39/73] Update develop version --- pyrogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index b15bd0c5..ac184844 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -24,7 +24,7 @@ if sys.version_info[:3] in [(3, 5, 0), (3, 5, 1), (3, 5, 2)]: # Monkey patch the standard "typing" module because Python versions from 3.5.0 to 3.5.2 have a broken one. sys.modules["typing"] = typing -__version__ = "0.14.1" +__version__ = "0.15.0-develop" __license__ = "GNU Lesser General Public License v3 or later (LGPLv3+)" __copyright__ = "Copyright (C) 2017-2019 Dan " From 684aef3ded762805a04874cf4fd55a6ce757cfab Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 11 Jun 2019 21:12:00 +0200 Subject: [PATCH 40/73] Fix files downloaded with no file name --- pyrogram/client/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index fd5db423..1106a416 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -849,7 +849,9 @@ class Client(Methods, BaseClient): media_type_str = Client.MEDIA_TYPE_ID[data.media_type] - if not data.file_name: + file_name = file_name or data.file_name + + if not file_name: guessed_extension = self.guess_extension(data.mime_type) if data.media_type in (0, 1, 2, 14): From 4f2928e7b564b7b31e48099aa4b9154ef1de6fd6 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 12 Jun 2019 11:37:43 +0200 Subject: [PATCH 41/73] Improve get_profile_photos and get_profile_photos_count --- .../methods/users/get_profile_photos.py | 25 +++++++++-------- .../methods/users/get_profile_photos_count.py | 28 ++++++++++++++++++- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/pyrogram/client/methods/users/get_profile_photos.py b/pyrogram/client/methods/users/get_profile_photos.py index e2845c8d..3ffeae39 100644 --- a/pyrogram/client/methods/users/get_profile_photos.py +++ b/pyrogram/client/methods/users/get_profile_photos.py @@ -21,6 +21,7 @@ from typing import Union, List import pyrogram from pyrogram.api import functions, types from pyrogram.client.ext import utils + from ...ext import BaseClient @@ -55,18 +56,7 @@ class GetProfilePhotos(BaseClient): """ peer_id = self.resolve_peer(chat_id) - if isinstance(peer_id, (types.InputPeerUser, types.InputPeerSelf)): - r = self.send( - functions.photos.GetUserPhotos( - user_id=peer_id, - offset=offset, - max_id=0, - limit=limit - ) - ) - - return pyrogram.List(pyrogram.Photo._parse(self, photo) for photo in r.photos) - else: + if isinstance(peer_id, types.InputPeerChannel): r = utils.parse_messages( self, self.send( @@ -87,3 +77,14 @@ class GetProfilePhotos(BaseClient): ) return pyrogram.List([message.new_chat_photo for message in r][:limit]) + else: + r = self.send( + functions.photos.GetUserPhotos( + user_id=peer_id, + offset=offset, + max_id=0, + limit=limit + ) + ) + + return pyrogram.List(pyrogram.Photo._parse(self, photo) for photo in r.photos) diff --git a/pyrogram/client/methods/users/get_profile_photos_count.py b/pyrogram/client/methods/users/get_profile_photos_count.py index a65e0ada..bf00a10b 100644 --- a/pyrogram/client/methods/users/get_profile_photos_count.py +++ b/pyrogram/client/methods/users/get_profile_photos_count.py @@ -18,6 +18,8 @@ from typing import Union +from pyrogram.api import functions, types + from ...ext import BaseClient @@ -38,4 +40,28 @@ class GetProfilePhotosCount(BaseClient): RPCError: In case of a Telegram RPC error. """ - return self.get_profile_photos(chat_id, limit=1).total_count + peer_id = self.resolve_peer(chat_id) + + if isinstance(peer_id, types.InputPeerChannel): + r = self.send( + functions.messages.GetSearchCounters( + peer=peer_id, + filters=[types.InputMessagesFilterChatPhotos()], + ) + ) + + return r[0].count + else: + r = self.send( + functions.photos.GetUserPhotos( + user_id=peer_id, + offset=0, + max_id=0, + limit=1 + ) + ) + + if isinstance(r, types.photos.Photos): + return len(r.photos) + else: + return r.count From df6e174b5521629dd8b6f1d7d4c33b25fc7db32d Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 12 Jun 2019 11:38:06 +0200 Subject: [PATCH 42/73] Fix InputPhoneContact docstring --- pyrogram/client/types/input_media/input_phone_contact.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/client/types/input_media/input_phone_contact.py b/pyrogram/client/types/input_media/input_phone_contact.py index f60dd39d..9c03694d 100644 --- a/pyrogram/client/types/input_media/input_phone_contact.py +++ b/pyrogram/client/types/input_media/input_phone_contact.py @@ -24,7 +24,7 @@ from ..object import Object class InputPhoneContact(Object): """A Phone Contact to be added in your Telegram address book. - It is intended to be used with :meth:`~Client.add_contacts() ` + It is intended to be used with :meth:`~pyrogram.Client.add_contacts()` Parameters: phone (``str``): From b86373d28c0de0a905132a7cb48ba6c792fb8ea1 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 12 Jun 2019 11:43:24 +0200 Subject: [PATCH 43/73] Improve get_history_count --- .../methods/messages/get_history_count.py | 48 +++++++------------ 1 file changed, 16 insertions(+), 32 deletions(-) diff --git a/pyrogram/client/methods/messages/get_history_count.py b/pyrogram/client/methods/messages/get_history_count.py index 48614030..9f3e2637 100644 --- a/pyrogram/client/methods/messages/get_history_count.py +++ b/pyrogram/client/methods/messages/get_history_count.py @@ -17,12 +17,10 @@ # along with Pyrogram. If not, see . import logging -import time from typing import Union from pyrogram.api import types, functions from pyrogram.client.ext import BaseClient -from pyrogram.errors import FloodWait log = logging.getLogger(__name__) @@ -51,34 +49,20 @@ class GetHistoryCount(BaseClient): RPCError: In case of a Telegram RPC error. """ - peer = self.resolve_peer(chat_id) + r = self.send( + functions.messages.GetHistory( + peer=self.resolve_peer(chat_id), + offset_id=0, + offset_date=0, + add_offset=0, + limit=1, + max_id=0, + min_id=0, + hash=0 + ) + ) - if not isinstance(peer, types.InputPeerChannel): - offset = 0 - limit = 100 - - while True: - try: - r = self.send( - functions.messages.GetHistory( - peer=peer, - offset_id=1, - offset_date=0, - add_offset=-offset - limit, - limit=limit, - max_id=0, - min_id=0, - hash=0 - ) - ) - except FloodWait as e: - log.warning("Sleeping for {}s".format(e.x)) - time.sleep(e.x) - continue - - if not r.messages: - return offset - - offset += len(r.messages) - - return self.get_history(chat_id=chat_id, limit=1).total_count + if isinstance(r, types.messages.Messages): + return len(r.messages) + else: + return r.count From aaaa119318294fc531755c388589850f3f935b17 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 12 Jun 2019 12:41:55 +0200 Subject: [PATCH 44/73] Hint about which DC exactly is having problems. --- compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv b/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv index c85fe7e0..446fe908 100644 --- a/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv +++ b/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv @@ -7,6 +7,6 @@ HISTORY_GET_FAILED Telegram is having internal problems. Please try again later REG_ID_GENERATE_FAILED Telegram is having internal problems. Please try again later RANDOM_ID_DUPLICATE Telegram is having internal problems. Please try again later WORKER_BUSY_TOO_LONG_RETRY Telegram is having internal problems. Please try again later -INTERDC_X_CALL_ERROR Telegram is having internal problems. Please try again later -INTERDC_X_CALL_RICH_ERROR Telegram is having internal problems. Please try again later +INTERDC_X_CALL_ERROR Telegram is having internal problems at DC{x}. Please try again later +INTERDC_X_CALL_RICH_ERROR Telegram is having internal problems at DC{x}. Please try again later FOLDER_DEAC_AUTOFIX_ALL Telegram is having internal problems. Please try again later \ No newline at end of file From 1ce749a562250fd002e2fb24c3a5aee85e1d5e10 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 12 Jun 2019 12:42:39 +0200 Subject: [PATCH 45/73] Hint about the Advanced Usage page in the method index --- docs/source/api/methods.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/api/methods.rst b/docs/source/api/methods.rst index 58836efa..ac515f6e 100644 --- a/docs/source/api/methods.rst +++ b/docs/source/api/methods.rst @@ -159,6 +159,8 @@ Bots Advanced Usage (Raw API) ^^^^^^^^^^^^^^^^^^^^^^^^ +Learn more about these methods at :doc:`Advanced Usage <../topics/advanced-usage>`. + .. hlist:: :columns: 4 From 93082ce8941f90a91fd11470d55e5f80f29634b5 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 12 Jun 2019 13:43:28 +0200 Subject: [PATCH 46/73] Reword get_user_dc docstrings --- pyrogram/client/methods/users/get_user_dc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/client/methods/users/get_user_dc.py b/pyrogram/client/methods/users/get_user_dc.py index 718f4c44..75587884 100644 --- a/pyrogram/client/methods/users/get_user_dc.py +++ b/pyrogram/client/methods/users/get_user_dc.py @@ -24,7 +24,7 @@ from ...ext import BaseClient class GetUserDC(BaseClient): def get_user_dc(self, user_id: Union[int, str]) -> Union[int, None]: - """Get the assigned data center (DC) of a user. + """Get the assigned DC (data center) of a user. .. note:: From c8b4f3dac9b6c23cdb9814150c44c82120b54863 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 12 Jun 2019 14:05:59 +0200 Subject: [PATCH 47/73] Add FAQ about Pyrogram stability and reliability --- docs/source/faq.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/source/faq.rst b/docs/source/faq.rst index f08da03f..0d56451a 100644 --- a/docs/source/faq.rst +++ b/docs/source/faq.rst @@ -53,6 +53,22 @@ Why Pyrogram? .. _TgCrypto: https://github.com/pyrogram/tgcrypto +How stable and reliable is Pyrogram? +------------------------------------ + +So far, since its first public release, Pyrogram has always shown itself to be quite reliable in handling client-server +interconnections and just as stable when keeping long running applications online. The only annoying issues faced are +actually coming from Telegram servers internal errors and down times, from which Pyrogram is able to recover itself +automatically. + +To challenge the framework, the creator is constantly keeping a public +`welcome bot `_ online 24/7 on his own, +relatively-busy account for well over a year now. + +In addition to that, about six months ago, one of the most popular Telegram bot has been rewritten +:doc:`using Pyrogram ` and is serving more than 200,000 Monthly Active Users since +then, uninterruptedly and without any need for restarting it. + What can MTProto do more than the Bot API? ------------------------------------------ From a2d1752e89336b7de31b698d03be3a96f8f0cfd9 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 12 Jun 2019 22:26:41 +0200 Subject: [PATCH 48/73] Update information about Telegram DCs. Also add IPv6 addresses Thanks Fela for pointing it out that only DC3 is an alias, DC2 and DC4 are both functioning independently, as per latest information. --- docs/source/faq.rst | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/source/faq.rst b/docs/source/faq.rst index 0d56451a..449076af 100644 --- a/docs/source/faq.rst +++ b/docs/source/faq.rst @@ -156,18 +156,34 @@ fails or not. What are the IP addresses of Telegram Data Centers? --------------------------------------------------- -Telegram is currently composed by a decentralized, multi-DC infrastructure (each of which can work independently) spread -in 5 different locations. However, two of the less busy DCs have been lately dismissed and their IP addresses are now -kept as aliases. +The Telegram cloud is currently composed by a decentralized, multi-DC infrastructure (each of which can work +independently) spread in 5 different locations. However, some of the less busy DCs have been lately dismissed and their +IP addresses are now kept as aliases. -- **DC1** - MIA, Miami FL, USA: ``149.154.175.50`` -- **DC2** - AMS, Amsterdam, NL: ``149.154.167.51`` -- **DC3*** - MIA, Miami FL, USA: ``149.154.175.100`` -- **DC4*** - AMS, Amsterdam, NL: ``149.154.167.91`` -- **DC5** - SIN, Singapore, SG: ``91.108.56.149`` +.. csv-table:: Production Environment + :header: ID, Location, IPv4, IPv6 + :widths: auto + :align: center + + DC1, "MIA, Miami FL, USA", ``149.154.175.50``, ``2001:b28:f23d:f001::a`` + DC2, "AMS, Amsterdam, NL", ``149.154.167.51``, ``2001:67c:4e8:f002::a`` + DC3*, "MIA, Miami FL, USA", ``149.154.175.100``, ``2001:b28:f23d:f003::a`` + DC4, "AMS, Amsterdam, NL", ``149.154.167.91``, ``2001:67c:4e8:f004::a`` + DC5, "SIN, Singapore, SG", ``91.108.56.149``, ``2001:b28:f23f:f005::a`` + +.. csv-table:: Test Environment + :header: ID, Location, IPv4, IPv6 + :widths: auto + :align: center + + DC1, "MIA, Miami FL, USA", ``149.154.175.10``, ``2001:b28:f23d:f001::e`` + DC2, "AMS, Amsterdam, NL", ``149.154.167.40``, ``2001:67c:4e8:f002::e`` + DC3*, "MIA, Miami FL, USA", ``149.154.175.117``, ``2001:b28:f23d:f003::e`` ***** Alias DC +More info about the Test Environment can be found :doc:`here `. + I want to migrate my account from DCX to DCY. --------------------------------------------- From 22199b0fe54fadefeac2b4b56e5e2f5b81f1b8b0 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 14 Jun 2019 02:12:06 +0200 Subject: [PATCH 49/73] Implement editing of messages sent via inline bots - edit_message_text - edit_message_caption - edit_message_media - edit_message_reply_markup --- pyrogram/client/ext/base_client.py | 3 ++ pyrogram/client/ext/utils.py | 12 +++++ .../methods/messages/edit_message_caption.py | 51 +++++++++---------- .../methods/messages/edit_message_media.py | 51 ++++++++++++------- .../messages/edit_message_reply_markup.py | 36 +++++++++---- .../methods/messages/edit_message_text.py | 34 ++++++++++--- 6 files changed, 122 insertions(+), 65 deletions(-) diff --git a/pyrogram/client/ext/base_client.py b/pyrogram/client/ext/base_client.py index 9e7cd677..b4c16666 100644 --- a/pyrogram/client/ext/base_client.py +++ b/pyrogram/client/ext/base_client.py @@ -156,3 +156,6 @@ class BaseClient: def get_profile_photos(self, *args, **kwargs): pass + + def edit_message_text(self, *args, **kwargs): + pass diff --git a/pyrogram/client/ext/utils.py b/pyrogram/client/ext/utils.py index 41270d39..fa107fab 100644 --- a/pyrogram/client/ext/utils.py +++ b/pyrogram/client/ext/utils.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . +import base64 import struct from base64 import b64decode, b64encode from typing import Union, List @@ -191,3 +192,14 @@ def parse_deleted_messages(client, update) -> List["pyrogram.Message"]: ) return pyrogram.List(parsed_messages) + + +def unpack_inline_message_id(inline_message_id: str) -> types.InputBotInlineMessageID: + r = inline_message_id + "=" * (-len(inline_message_id) % 4) + r = struct.unpack(" "pyrogram.Message": - """Edit captions of messages. + """Edit caption of media messages. Parameters: - chat_id (``int`` | ``str``): + caption (``str``): + New caption of the media message. + + chat_id (``int`` | ``str``, *optional*): + Required if *inline_message_id* is not specified. 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``): + message_id (``int``, *optional*): + Required if *inline_message_id* is not specified. Message identifier in the chat specified in chat_id. - caption (``str``): - New caption of the message. + inline_message_id (``str``, *optional*): + Required if *chat_id* and *message_id* are not specified. + Identifier of the inline message. parse_mode (``str``, *optional*): Pass "markdown" or "html" if you want Telegram apps to show bold, italic, fixed-width text or inline - URLs in your caption. Defaults to "markdown". + URLs in your message. Defaults to "markdown". reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): An InlineKeyboardMarkup object. Returns: - :obj:`Message`: On success, the edited message is returned. + :obj:`Message` | ``bool``: On success, if the edited message was sent by the bot, the edited message is + returned, otherwise True is returned (message sent via the bot, as inline query result). Raises: RPCError: In case of a Telegram RPC error. """ - style = self.html if parse_mode.lower() == "html" else self.markdown - - r = self.send( - functions.messages.EditMessage( - peer=self.resolve_peer(chat_id), - id=message_id, - reply_markup=reply_markup.write() if reply_markup else None, - **style.parse(caption) - ) + return self.edit_message_text( + text=caption, + chat_id=chat_id, + message_id=message_id, + inline_message_id=inline_message_id, + parse_mode=parse_mode, + reply_markup=reply_markup ) - - for i in r.updates: - if isinstance(i, (types.UpdateEditMessage, types.UpdateEditChannelMessage)): - return pyrogram.Message._parse( - self, i.message, - {i.id: i for i in r.users}, - {i.id: i for i in r.chats} - ) diff --git a/pyrogram/client/methods/messages/edit_message_media.py b/pyrogram/client/methods/messages/edit_message_media.py index 3793c693..2b3ca5d5 100644 --- a/pyrogram/client/methods/messages/edit_message_media.py +++ b/pyrogram/client/methods/messages/edit_message_media.py @@ -32,35 +32,42 @@ from pyrogram.client.types.input_media import InputMedia class EditMessageMedia(BaseClient): def edit_message_media( self, - chat_id: Union[int, str], - message_id: int, media: InputMedia, + chat_id: Union[int, str] = None, + message_id: int = None, + inline_message_id: str = None, reply_markup: "pyrogram.InlineKeyboardMarkup" = None ) -> "pyrogram.Message": - """Edit audio, document, photo, or video messages. + """Edit animation, audio, document, photo or video messages. If a message is a part of a message album, then it can be edited only to a photo or a video. Otherwise, message type can be changed arbitrarily. When inline message is edited, new file can't be uploaded. - Use previously uploaded file via its file_id or specify a URL. On success, if the edited message was sent - by the bot, the edited Message is returned, otherwise True is returned. + Use previously uploaded file via its file_id or specify a URL. Parameters: - chat_id (``int`` | ``str``): + media (:obj:`InputMedia`) + One of the InputMedia objects describing an animation, audio, document, photo or video. + + chat_id (``int`` | ``str``, *optional*): + Required if *inline_message_id* is not specified. 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``): + message_id (``int``, *optional*): + Required if *inline_message_id* is not specified. Message identifier in the chat specified in chat_id. - media (:obj:`InputMedia`) - One of the InputMedia objects describing an animation, audio, document, photo or video. + inline_message_id (``str``, *optional*): + Required if *chat_id* and *message_id* are not specified. + Identifier of the inline message. reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): An InlineKeyboardMarkup object. Returns: - :obj:`Message`: On success, the edited message is returned. + :obj:`Message` | ``bool``: On success, if the edited message was sent by the bot, the edited message is + returned, otherwise True is returned (message sent via the bot, as inline query result). Raises: RPCError: In case of a Telegram RPC error. @@ -92,8 +99,7 @@ class EditMessageMedia(BaseClient): ) else: media = utils.get_input_media_from_file_id(media.media, 2) - - if isinstance(media, InputMediaVideo): + elif isinstance(media, InputMediaVideo): if os.path.exists(media.media): media = self.send( functions.messages.UploadMedia( @@ -130,8 +136,7 @@ class EditMessageMedia(BaseClient): ) else: media = utils.get_input_media_from_file_id(media.media, 4) - - if isinstance(media, InputMediaAudio): + elif isinstance(media, InputMediaAudio): if os.path.exists(media.media): media = self.send( functions.messages.UploadMedia( @@ -167,8 +172,7 @@ class EditMessageMedia(BaseClient): ) else: media = utils.get_input_media_from_file_id(media.media, 9) - - if isinstance(media, InputMediaAnimation): + elif isinstance(media, InputMediaAnimation): if os.path.exists(media.media): media = self.send( functions.messages.UploadMedia( @@ -206,8 +210,7 @@ class EditMessageMedia(BaseClient): ) else: media = utils.get_input_media_from_file_id(media.media, 10) - - if isinstance(media, InputMediaDocument): + elif isinstance(media, InputMediaDocument): if os.path.exists(media.media): media = self.send( functions.messages.UploadMedia( @@ -239,12 +242,22 @@ class EditMessageMedia(BaseClient): else: media = utils.get_input_media_from_file_id(media.media, 5) + if inline_message_id is not None: + return self.send( + functions.messages.EditInlineBotMessage( + id=utils.unpack_inline_message_id(inline_message_id), + media=media, + reply_markup=reply_markup.write() if reply_markup else None, + **style.parse(caption) + ) + ) + r = self.send( functions.messages.EditMessage( peer=self.resolve_peer(chat_id), id=message_id, - reply_markup=reply_markup.write() if reply_markup else None, media=media, + reply_markup=reply_markup.write() if reply_markup else None, **style.parse(caption) ) ) diff --git a/pyrogram/client/methods/messages/edit_message_reply_markup.py b/pyrogram/client/methods/messages/edit_message_reply_markup.py index a058646f..516d7b9c 100644 --- a/pyrogram/client/methods/messages/edit_message_reply_markup.py +++ b/pyrogram/client/methods/messages/edit_message_reply_markup.py @@ -20,43 +20,57 @@ from typing import Union import pyrogram from pyrogram.api import functions, types -from pyrogram.client.ext import BaseClient +from pyrogram.client.ext import BaseClient, utils class EditMessageReplyMarkup(BaseClient): def edit_message_reply_markup( self, - chat_id: Union[int, str], - message_id: int, - reply_markup: "pyrogram.InlineKeyboardMarkup" = None + reply_markup: "pyrogram.InlineKeyboardMarkup" = None, + chat_id: Union[int, str] = None, + message_id: int = None, + inline_message_id: str = None ) -> "pyrogram.Message": """Edit only the reply markup of messages sent by the bot or via the bot (for inline bots). Parameters: - chat_id (``int`` | ``str``): + reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): + An InlineKeyboardMarkup object. + + chat_id (``int`` | ``str``, *optional*): + Required if *inline_message_id* is not specified. 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``): + message_id (``int``, *optional*): + Required if *inline_message_id* is not specified. Message identifier in the chat specified in chat_id. - reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): - An InlineKeyboardMarkup object. + inline_message_id (``str``, *optional*): + Required if *chat_id* and *message_id* are not specified. + Identifier of the inline message. Returns: - :obj:`Message` | ``bool``: In case the edited message is sent by the bot, the edited message is returned, - otherwise, True is returned in case the edited message is send by the user. + :obj:`Message` | ``bool``: On success, if the edited message was sent by the bot, the edited message is + returned, otherwise True is returned (message sent via the bot, as inline query result). Raises: RPCError: In case of a Telegram RPC error. """ + if inline_message_id is not None: + return self.send( + functions.messages.EditInlineBotMessage( + id=utils.unpack_inline_message_id(inline_message_id), + reply_markup=reply_markup.write() if reply_markup else None, + ) + ) r = self.send( functions.messages.EditMessage( peer=self.resolve_peer(chat_id), id=message_id, - reply_markup=reply_markup.write() if reply_markup else None + reply_markup=reply_markup.write() if reply_markup else None, ) ) diff --git a/pyrogram/client/methods/messages/edit_message_text.py b/pyrogram/client/methods/messages/edit_message_text.py index 69283e89..919e5dc1 100644 --- a/pyrogram/client/methods/messages/edit_message_text.py +++ b/pyrogram/client/methods/messages/edit_message_text.py @@ -20,15 +20,16 @@ from typing import Union import pyrogram from pyrogram.api import functions, types -from pyrogram.client.ext import BaseClient +from pyrogram.client.ext import BaseClient, utils class EditMessageText(BaseClient): def edit_message_text( self, - chat_id: Union[int, str], - message_id: int, text: str, + chat_id: Union[int, str] = None, + message_id: int = None, + inline_message_id: str = None, parse_mode: str = "", disable_web_page_preview: bool = None, reply_markup: "pyrogram.InlineKeyboardMarkup" = None @@ -36,16 +37,22 @@ class EditMessageText(BaseClient): """Edit text messages. Parameters: - chat_id (``int`` | ``str``): + text (``str``): + New text of the message. + + chat_id (``int`` | ``str``, *optional*): + Required if *inline_message_id* is not specified. 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``): + message_id (``int``, *optional*): + Required if *inline_message_id* is not specified. Message identifier in the chat specified in chat_id. - text (``str``): - New text of the message. + inline_message_id (``str``, *optional*): + Required if *chat_id* and *message_id* are not specified. + Identifier of the inline message. parse_mode (``str``, *optional*): Pass "markdown" or "html" if you want Telegram apps to show bold, italic, fixed-width text or inline @@ -58,13 +65,24 @@ class EditMessageText(BaseClient): An InlineKeyboardMarkup object. Returns: - :obj:`Message`: On success, the edited message is returned. + :obj:`Message` | ``bool``: On success, if the edited message was sent by the bot, the edited message is + returned, otherwise True is returned (message sent via the bot, as inline query result). Raises: RPCError: In case of a Telegram RPC error. """ style = self.html if parse_mode.lower() == "html" else self.markdown + if inline_message_id is not None: + return self.send( + functions.messages.EditInlineBotMessage( + id=utils.unpack_inline_message_id(inline_message_id), + no_webpage=disable_web_page_preview or None, + reply_markup=reply_markup.write() if reply_markup else None, + **style.parse(text) + ) + ) + r = self.send( functions.messages.EditMessage( peer=self.resolve_peer(chat_id), From da4ff268a41073e0767373532f69c58ac8394333 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 14 Jun 2019 02:46:27 +0200 Subject: [PATCH 50/73] Add edit, edit_caption, edit_media and edit_reply_markup bound methods to the CallbackQuery type --- docs/source/api/bound-methods.rst | 10 +- pyrogram/client/ext/base_client.py | 6 + .../bots_and_keyboards/callback_query.py | 172 ++++++++++++++++++ 3 files changed, 187 insertions(+), 1 deletion(-) diff --git a/docs/source/api/bound-methods.rst b/docs/source/api/bound-methods.rst index d01a4383..38389ae6 100644 --- a/docs/source/api/bound-methods.rst +++ b/docs/source/api/bound-methods.rst @@ -81,9 +81,13 @@ CallbackQuery ^^^^^^^^^^^^^ .. hlist:: - :columns: 2 + :columns: 5 - :meth:`~CallbackQuery.answer` + - :meth:`~CallbackQuery.edit` + - :meth:`~CallbackQuery.edit_caption` + - :meth:`~CallbackQuery.edit_media` + - :meth:`~CallbackQuery.edit_reply_markup` InlineQuery ^^^^^^^^^^^ @@ -137,6 +141,10 @@ Details .. CallbackQuery .. automethod:: CallbackQuery.answer() +.. automethod:: CallbackQuery.edit() +.. automethod:: CallbackQuery.edit_caption() +.. automethod:: CallbackQuery.edit_media() +.. automethod:: CallbackQuery.edit_reply_markup() .. InlineQuery .. automethod:: InlineQuery.answer() diff --git a/pyrogram/client/ext/base_client.py b/pyrogram/client/ext/base_client.py index b4c16666..e584c743 100644 --- a/pyrogram/client/ext/base_client.py +++ b/pyrogram/client/ext/base_client.py @@ -159,3 +159,9 @@ class BaseClient: def edit_message_text(self, *args, **kwargs): pass + + def edit_message_media(self, *args, **kwargs): + pass + + def edit_message_reply_markup(self, *args, **kwargs): + pass diff --git a/pyrogram/client/types/bots_and_keyboards/callback_query.py b/pyrogram/client/types/bots_and_keyboards/callback_query.py index fa0d8be2..df0f6d33 100644 --- a/pyrogram/client/types/bots_and_keyboards/callback_query.py +++ b/pyrogram/client/types/bots_and_keyboards/callback_query.py @@ -172,3 +172,175 @@ class CallbackQuery(Object, Update): url=url, cache_time=cache_time ) + + def edit( + self, + text: str, + parse_mode: str = "", + disable_web_page_preview: bool = None, + reply_markup: "pyrogram.InlineKeyboardMarkup" = None + ) -> Union["pyrogram.Message", bool]: + """Bound method *edit* of :obj:`CallbackQuery`. + + Parameters: + text (``str``): + New text of the message. + + parse_mode (``str``, *optional*): + Pass "markdown" or "html" if you want Telegram apps to show bold, italic, fixed-width text or inline + URLs in your message. Defaults to "markdown". + + disable_web_page_preview (``bool``, *optional*): + Disables link previews for links in this message. + + reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): + An InlineKeyboardMarkup object. + + Returns: + :obj:`Message` | ``bool``: On success, if the edited message was sent by the bot, the edited message is + returned, otherwise True is returned (message sent via the bot, as inline query result). + + Raises: + RPCError: In case of a Telegram RPC error. + """ + chat_id = None + message_id = None + inline_message_id = None + + if self.message is not None: + chat_id = self.message.chat.id + message_id = self.message.message_id + + if self.inline_message_id is not None: + inline_message_id = self.inline_message_id + + return self._client.edit_message_text( + text=text, + chat_id=chat_id, + message_id=message_id, + inline_message_id=inline_message_id, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + reply_markup=reply_markup + ) + + def edit_caption( + self, + caption: str, + parse_mode: str = "", + reply_markup: "pyrogram.InlineKeyboardMarkup" = None + ) -> Union["pyrogram.Message", bool]: + """Bound method *edit_caption* of :obj:`Message`. + + Parameters: + caption (``str``): + New caption of the message. + + parse_mode (``str``, *optional*): + Pass "markdown" or "html" if you want Telegram apps to show bold, italic, fixed-width text or inline + URLs in your message. Defaults to "markdown". + + reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): + An InlineKeyboardMarkup object. + + Returns: + :obj:`Message` | ``bool``: On success, if the edited message was sent by the bot, the edited message is + returned, otherwise True is returned (message sent via the bot, as inline query result). + + Raises: + RPCError: In case of a Telegram RPC error. + """ + chat_id = None + message_id = None + inline_message_id = None + + if self.message is not None: + chat_id = self.message.chat.id + message_id = self.message.message_id + + if self.inline_message_id is not None: + inline_message_id = self.inline_message_id + + return self._client.edit_message_text( + text=caption, + chat_id=chat_id, + message_id=message_id, + inline_message_id=inline_message_id, + parse_mode=parse_mode, + reply_markup=reply_markup + ) + + def edit_media( + self, + media: "pyrogram.InputMedia", + reply_markup: "pyrogram.InlineKeyboardMarkup" = None + ) -> Union["pyrogram.Message", bool]: + """Bound method *edit_media* of :obj:`Message`. + + Parameters: + media (:obj:`InputMedia`): + One of the InputMedia objects describing an animation, audio, document, photo or video. + + reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): + An InlineKeyboardMarkup object. + + Returns: + :obj:`Message` | ``bool``: On success, if the edited message was sent by the bot, the edited message is + returned, otherwise True is returned (message sent via the bot, as inline query result). + + Raises: + RPCError: In case of a Telegram RPC error. + """ + chat_id = None + message_id = None + inline_message_id = None + + if self.message is not None: + chat_id = self.message.chat.id + message_id = self.message.message_id + + if self.inline_message_id is not None: + inline_message_id = self.inline_message_id + + return self._client.edit_message_media( + media=media, + chat_id=chat_id, + message_id=message_id, + inline_message_id=inline_message_id, + reply_markup=reply_markup + ) + + def edit_reply_markup( + self, + reply_markup: "pyrogram.InlineKeyboardMarkup" = None + ) -> Union["pyrogram.Message", bool]: + """Bound method *edit_reply_markup* of :obj:`Message`. + + Parameters: + reply_markup (:obj:`InlineKeyboardMarkup`): + An InlineKeyboardMarkup object. + + Returns: + :obj:`Message` | ``bool``: On success, if the edited message was sent by the bot, the edited message is + returned, otherwise True is returned (message sent via the bot, as inline query result). + + Raises: + RPCError: In case of a Telegram RPC error. + """ + chat_id = None + message_id = None + inline_message_id = None + + if self.message is not None: + chat_id = self.message.chat.id + message_id = self.message.message_id + + if self.inline_message_id is not None: + inline_message_id = self.inline_message_id + + return self._client.edit_message_reply_markup( + reply_markup=reply_markup, + chat_id=chat_id, + message_id=message_id, + inline_message_id=inline_message_id + ) From c485715db1b3ea9578d334624113b8a7f563912f Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 14 Jun 2019 02:47:17 +0200 Subject: [PATCH 51/73] Small docstrings fixup --- .../methods/messages/edit_message_media.py | 2 +- .../client/types/messages_and_media/message.py | 16 +++------------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/pyrogram/client/methods/messages/edit_message_media.py b/pyrogram/client/methods/messages/edit_message_media.py index 2b3ca5d5..d74e2b56 100644 --- a/pyrogram/client/methods/messages/edit_message_media.py +++ b/pyrogram/client/methods/messages/edit_message_media.py @@ -45,7 +45,7 @@ class EditMessageMedia(BaseClient): Use previously uploaded file via its file_id or specify a URL. Parameters: - media (:obj:`InputMedia`) + media (:obj:`InputMedia`): One of the InputMedia objects describing an animation, audio, document, photo or video. chat_id (``int`` | ``str``, *optional*): diff --git a/pyrogram/client/types/messages_and_media/message.py b/pyrogram/client/types/messages_and_media/message.py index c6f3cc77..2a1ba2c8 100644 --- a/pyrogram/client/types/messages_and_media/message.py +++ b/pyrogram/client/types/messages_and_media/message.py @@ -2361,12 +2361,7 @@ class Message(Object, Update): text: str, parse_mode: str = "", disable_web_page_preview: bool = None, - reply_markup: Union[ - "pyrogram.InlineKeyboardMarkup", - "pyrogram.ReplyKeyboardMarkup", - "pyrogram.ReplyKeyboardRemove", - "pyrogram.ForceReply" - ] = None + reply_markup: "pyrogram.InlineKeyboardMarkup" = None ) -> "Message": """Bound method *edit* of :obj:`Message`. @@ -2418,12 +2413,7 @@ class Message(Object, Update): self, caption: str, parse_mode: str = "", - reply_markup: Union[ - "pyrogram.InlineKeyboardMarkup", - "pyrogram.ReplyKeyboardMarkup", - "pyrogram.ReplyKeyboardRemove", - "pyrogram.ForceReply" - ] = None + reply_markup: "pyrogram.InlineKeyboardMarkup" = None ) -> "Message": """Bound method *edit_caption* of :obj:`Message`. @@ -2486,7 +2476,7 @@ class Message(Object, Update): message.edit_media(media) Parameters: - media (:obj:`InputMediaAnimation` | :obj:`InputMediaAudio` | :obj:`InputMediaDocument` | :obj:`InputMediaPhoto` | :obj:`InputMediaVideo`) + media (:obj:`InputMedia`): One of the InputMedia objects describing an animation, audio, document, photo or video. reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): From 3ae77d55c758f91346ad8a122d0da2a3d65fd545 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 14 Jun 2019 02:52:01 +0200 Subject: [PATCH 52/73] Rename edit -> edit_text and reply -> reply_text bound methods --- docs/source/api/bound-methods.rst | 20 +++++++++---------- .../bots_and_keyboards/callback_query.py | 4 ++-- .../types/messages_and_media/message.py | 12 +++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/source/api/bound-methods.rst b/docs/source/api/bound-methods.rst index 38389ae6..2f2200ab 100644 --- a/docs/source/api/bound-methods.rst +++ b/docs/source/api/bound-methods.rst @@ -34,13 +34,13 @@ Message - :meth:`~Message.click` - :meth:`~Message.delete` - :meth:`~Message.download` - - :meth:`~Message.edit` + - :meth:`~Message.forward` + - :meth:`~Message.pin` + - :meth:`~Message.edit_text` - :meth:`~Message.edit_caption` - :meth:`~Message.edit_media` - :meth:`~Message.edit_reply_markup` - - :meth:`~Message.forward` - - :meth:`~Message.pin` - - :meth:`~Message.reply` + - :meth:`~Message.reply_text` - :meth:`~Message.reply_animation` - :meth:`~Message.reply_audio` - :meth:`~Message.reply_cached_media` @@ -84,7 +84,7 @@ CallbackQuery :columns: 5 - :meth:`~CallbackQuery.answer` - - :meth:`~CallbackQuery.edit` + - :meth:`~CallbackQuery.edit_text` - :meth:`~CallbackQuery.edit_caption` - :meth:`~CallbackQuery.edit_media` - :meth:`~CallbackQuery.edit_reply_markup` @@ -106,13 +106,13 @@ Details .. automethod:: Message.click() .. automethod:: Message.delete() .. automethod:: Message.download() -.. automethod:: Message.edit() +.. automethod:: Message.forward() +.. automethod:: Message.pin() +.. automethod:: Message.edit_text() .. automethod:: Message.edit_caption() .. automethod:: Message.edit_media() .. automethod:: Message.edit_reply_markup() -.. automethod:: Message.forward() -.. automethod:: Message.pin() -.. automethod:: Message.reply() +.. automethod:: Message.reply_text() .. automethod:: Message.reply_animation() .. automethod:: Message.reply_audio() .. automethod:: Message.reply_cached_media() @@ -141,7 +141,7 @@ Details .. CallbackQuery .. automethod:: CallbackQuery.answer() -.. automethod:: CallbackQuery.edit() +.. automethod:: CallbackQuery.edit_text() .. automethod:: CallbackQuery.edit_caption() .. automethod:: CallbackQuery.edit_media() .. automethod:: CallbackQuery.edit_reply_markup() diff --git a/pyrogram/client/types/bots_and_keyboards/callback_query.py b/pyrogram/client/types/bots_and_keyboards/callback_query.py index df0f6d33..4bb4a7e4 100644 --- a/pyrogram/client/types/bots_and_keyboards/callback_query.py +++ b/pyrogram/client/types/bots_and_keyboards/callback_query.py @@ -173,14 +173,14 @@ class CallbackQuery(Object, Update): cache_time=cache_time ) - def edit( + def edit_text( self, text: str, parse_mode: str = "", disable_web_page_preview: bool = None, reply_markup: "pyrogram.InlineKeyboardMarkup" = None ) -> Union["pyrogram.Message", bool]: - """Bound method *edit* of :obj:`CallbackQuery`. + """Bound method *edit_text* of :obj:`CallbackQuery`. Parameters: text (``str``): diff --git a/pyrogram/client/types/messages_and_media/message.py b/pyrogram/client/types/messages_and_media/message.py index 2a1ba2c8..cd59b5eb 100644 --- a/pyrogram/client/types/messages_and_media/message.py +++ b/pyrogram/client/types/messages_and_media/message.py @@ -651,7 +651,7 @@ class Message(Object, Update): return parsed_message - def reply( + def reply_text( self, text: str, quote: bool = None, @@ -661,7 +661,7 @@ class Message(Object, Update): reply_to_message_id: int = None, reply_markup=None ) -> "Message": - """Bound method *reply* of :obj:`Message`. + """Bound method *reply_text* of :obj:`Message`. Use as a shortcut for: @@ -676,7 +676,7 @@ class Message(Object, Update): Example: .. code-block:: python - message.reply("hello", quote=True) + message.reply_text("hello", quote=True) Parameters: text (``str``): @@ -2356,14 +2356,14 @@ class Message(Object, Update): progress_args=progress_args ) - def edit( + def edit_text( self, text: str, parse_mode: str = "", disable_web_page_preview: bool = None, reply_markup: "pyrogram.InlineKeyboardMarkup" = None ) -> "Message": - """Bound method *edit* of :obj:`Message`. + """Bound method *edit_text* of :obj:`Message`. Use as a shortcut for: @@ -2378,7 +2378,7 @@ class Message(Object, Update): Example: .. code-block:: python - message.edit("hello") + message.edit_text("hello") Parameters: text (``str``): From 3ed1bb0d86e604d75a809468f3395cd401d060a6 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 14 Jun 2019 03:57:12 +0200 Subject: [PATCH 53/73] Rename CallbackQuery edit_* bound methods to edit_message_* We are editing the message the callback query comes from, not the callback query itself. --- docs/source/api/bound-methods.rst | 18 +++++++++--------- .../types/bots_and_keyboards/callback_query.py | 16 ++++++++-------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/source/api/bound-methods.rst b/docs/source/api/bound-methods.rst index 2f2200ab..83b3dbbe 100644 --- a/docs/source/api/bound-methods.rst +++ b/docs/source/api/bound-methods.rst @@ -81,13 +81,13 @@ CallbackQuery ^^^^^^^^^^^^^ .. hlist:: - :columns: 5 + :columns: 3 - :meth:`~CallbackQuery.answer` - - :meth:`~CallbackQuery.edit_text` - - :meth:`~CallbackQuery.edit_caption` - - :meth:`~CallbackQuery.edit_media` - - :meth:`~CallbackQuery.edit_reply_markup` + - :meth:`~CallbackQuery.edit_message_text` + - :meth:`~CallbackQuery.edit_message_caption` + - :meth:`~CallbackQuery.edit_message_media` + - :meth:`~CallbackQuery.edit_message_reply_markup` InlineQuery ^^^^^^^^^^^ @@ -141,10 +141,10 @@ Details .. CallbackQuery .. automethod:: CallbackQuery.answer() -.. automethod:: CallbackQuery.edit_text() -.. automethod:: CallbackQuery.edit_caption() -.. automethod:: CallbackQuery.edit_media() -.. automethod:: CallbackQuery.edit_reply_markup() +.. automethod:: CallbackQuery.edit_message_text() +.. automethod:: CallbackQuery.edit_message_caption() +.. automethod:: CallbackQuery.edit_message_media() +.. automethod:: CallbackQuery.edit_message_reply_markup() .. InlineQuery .. automethod:: InlineQuery.answer() diff --git a/pyrogram/client/types/bots_and_keyboards/callback_query.py b/pyrogram/client/types/bots_and_keyboards/callback_query.py index 4bb4a7e4..6872d65b 100644 --- a/pyrogram/client/types/bots_and_keyboards/callback_query.py +++ b/pyrogram/client/types/bots_and_keyboards/callback_query.py @@ -173,14 +173,14 @@ class CallbackQuery(Object, Update): cache_time=cache_time ) - def edit_text( + def edit_message_text( self, text: str, parse_mode: str = "", disable_web_page_preview: bool = None, reply_markup: "pyrogram.InlineKeyboardMarkup" = None ) -> Union["pyrogram.Message", bool]: - """Bound method *edit_text* of :obj:`CallbackQuery`. + """Bound method *edit_message_text* of :obj:`CallbackQuery`. Parameters: text (``str``): @@ -224,13 +224,13 @@ class CallbackQuery(Object, Update): reply_markup=reply_markup ) - def edit_caption( + def edit_message_caption( self, caption: str, parse_mode: str = "", reply_markup: "pyrogram.InlineKeyboardMarkup" = None ) -> Union["pyrogram.Message", bool]: - """Bound method *edit_caption* of :obj:`Message`. + """Bound method *edit_message_caption* of :obj:`CallbackQuery`. Parameters: caption (``str``): @@ -270,12 +270,12 @@ class CallbackQuery(Object, Update): reply_markup=reply_markup ) - def edit_media( + def edit_message_media( self, media: "pyrogram.InputMedia", reply_markup: "pyrogram.InlineKeyboardMarkup" = None ) -> Union["pyrogram.Message", bool]: - """Bound method *edit_media* of :obj:`Message`. + """Bound method *edit_message_media* of :obj:`CallbackQuery`. Parameters: media (:obj:`InputMedia`): @@ -310,11 +310,11 @@ class CallbackQuery(Object, Update): reply_markup=reply_markup ) - def edit_reply_markup( + def edit_message_reply_markup( self, reply_markup: "pyrogram.InlineKeyboardMarkup" = None ) -> Union["pyrogram.Message", bool]: - """Bound method *edit_reply_markup* of :obj:`Message`. + """Bound method *edit_message_reply_markup* of :obj:`CallbackQuery`. Parameters: reply_markup (:obj:`InlineKeyboardMarkup`): From 61ed44ff5f1c2dee91da309ce24f6b2b5884be13 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 14 Jun 2019 04:52:05 +0200 Subject: [PATCH 54/73] Add edit_inline_* methods to deal with inline messages only --- docs/source/api/methods.rst | 14 ++- pyrogram/client/ext/base_client.py | 9 ++ pyrogram/client/methods/messages/__init__.py | 10 +- .../methods/messages/edit_inline_caption.py | 58 ++++++++++ .../methods/messages/edit_inline_media.py | 104 ++++++++++++++++++ .../messages/edit_inline_reply_markup.py | 50 +++++++++ .../methods/messages/edit_inline_text.py | 67 +++++++++++ .../methods/messages/edit_message_caption.py | 27 ++--- .../methods/messages/edit_message_media.py | 37 ++----- .../messages/edit_message_reply_markup.py | 34 ++---- .../methods/messages/edit_message_text.py | 36 ++---- 11 files changed, 344 insertions(+), 102 deletions(-) create mode 100644 pyrogram/client/methods/messages/edit_inline_caption.py create mode 100644 pyrogram/client/methods/messages/edit_inline_media.py create mode 100644 pyrogram/client/methods/messages/edit_inline_reply_markup.py create mode 100644 pyrogram/client/methods/messages/edit_inline_text.py diff --git a/docs/source/api/methods.rst b/docs/source/api/methods.rst index ac515f6e..4a3eefd8 100644 --- a/docs/source/api/methods.rst +++ b/docs/source/api/methods.rst @@ -55,11 +55,15 @@ Messages - :meth:`~Client.send_venue` - :meth:`~Client.send_contact` - :meth:`~Client.send_cached_media` - - :meth:`~Client.send_chat_action` - :meth:`~Client.edit_message_text` - :meth:`~Client.edit_message_caption` - - :meth:`~Client.edit_message_reply_markup` - :meth:`~Client.edit_message_media` + - :meth:`~Client.edit_message_reply_markup` + - :meth:`~Client.edit_inline_text` + - :meth:`~Client.edit_inline_caption` + - :meth:`~Client.edit_inline_media` + - :meth:`~Client.edit_inline_reply_markup` + - :meth:`~Client.send_chat_action` - :meth:`~Client.delete_messages` - :meth:`~Client.get_messages` - :meth:`~Client.get_history` @@ -203,8 +207,12 @@ Details .. automethod:: Client.send_chat_action() .. automethod:: Client.edit_message_text() .. automethod:: Client.edit_message_caption() -.. automethod:: Client.edit_message_reply_markup() .. automethod:: Client.edit_message_media() +.. automethod:: Client.edit_message_reply_markup() +.. automethod:: Client.edit_inline_text() +.. automethod:: Client.edit_inline_caption() +.. automethod:: Client.edit_inline_media() +.. automethod:: Client.edit_inline_reply_markup() .. automethod:: Client.delete_messages() .. automethod:: Client.get_messages() .. automethod:: Client.get_history() diff --git a/pyrogram/client/ext/base_client.py b/pyrogram/client/ext/base_client.py index e584c743..c8d1beab 100644 --- a/pyrogram/client/ext/base_client.py +++ b/pyrogram/client/ext/base_client.py @@ -160,8 +160,17 @@ class BaseClient: def edit_message_text(self, *args, **kwargs): pass + def edit_inline_text(self, *args, **kwargs): + pass + def edit_message_media(self, *args, **kwargs): pass + def edit_inline_media(self, *args, **kwargs): + pass + def edit_message_reply_markup(self, *args, **kwargs): pass + + def edit_inline_reply_markup(self, *args, **kwargs): + pass diff --git a/pyrogram/client/methods/messages/__init__.py b/pyrogram/client/methods/messages/__init__.py index 07df7a64..aa0b0c94 100644 --- a/pyrogram/client/methods/messages/__init__.py +++ b/pyrogram/client/methods/messages/__init__.py @@ -18,6 +18,10 @@ from .delete_messages import DeleteMessages from .download_media import DownloadMedia +from .edit_inline_caption import EditInlineCaption +from .edit_inline_media import EditInlineMedia +from .edit_inline_reply_markup import EditInlineReplyMarkup +from .edit_inline_text import EditInlineText from .edit_message_caption import EditMessageCaption from .edit_message_media import EditMessageMedia from .edit_message_reply_markup import EditMessageReplyMarkup @@ -82,6 +86,10 @@ class Messages( SendCachedMedia, GetHistoryCount, SendAnimatedSticker, - ReadHistory + ReadHistory, + EditInlineText, + EditInlineCaption, + EditInlineMedia, + EditInlineReplyMarkup ): pass diff --git a/pyrogram/client/methods/messages/edit_inline_caption.py b/pyrogram/client/methods/messages/edit_inline_caption.py new file mode 100644 index 00000000..a9bbc551 --- /dev/null +++ b/pyrogram/client/methods/messages/edit_inline_caption.py @@ -0,0 +1,58 @@ +# 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.client.ext import BaseClient + + +class EditInlineCaption(BaseClient): + def edit_inline_caption( + self, + inline_message_id: str, + caption: str, + parse_mode: str = "", + reply_markup: "pyrogram.InlineKeyboardMarkup" = None + ) -> bool: + """Edit the caption of **inline** media messages. + + Parameters: + inline_message_id (``str``): + Identifier of the inline message. + + caption (``str``): + New caption of the media message. + + parse_mode (``str``, *optional*): + Pass "markdown" or "html" if you want Telegram apps to show bold, italic, fixed-width text or inline + URLs in your message. Defaults to "markdown". + + reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): + An InlineKeyboardMarkup object. + + Returns: + ``bool``: On success, True is returned. + + Raises: + RPCError: In case of a Telegram RPC error. + """ + return self.edit_inline_text( + inline_message_id=inline_message_id, + text=caption, + parse_mode=parse_mode, + reply_markup=reply_markup + ) diff --git a/pyrogram/client/methods/messages/edit_inline_media.py b/pyrogram/client/methods/messages/edit_inline_media.py new file mode 100644 index 00000000..87e692fd --- /dev/null +++ b/pyrogram/client/methods/messages/edit_inline_media.py @@ -0,0 +1,104 @@ +# 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 functions, types +from pyrogram.client.ext import BaseClient, utils +from pyrogram.client.types import ( + InputMediaPhoto, InputMediaVideo, InputMediaAudio, + InputMediaAnimation, InputMediaDocument +) +from pyrogram.client.types.input_media import InputMedia + + +class EditInlineMedia(BaseClient): + def edit_inline_media( + self, + inline_message_id: str, + media: InputMedia, + reply_markup: "pyrogram.InlineKeyboardMarkup" = None + ) -> bool: + """Edit **inline** animation, audio, document, photo or video messages. + + When the inline message is edited, a new file can't be uploaded. Use a previously uploaded file via its file_id + or specify a URL. + + Parameters: + inline_message_id (``str``): + Required if *chat_id* and *message_id* are not specified. + Identifier of the inline message. + + media (:obj:`InputMedia`): + One of the InputMedia objects describing an animation, audio, document, photo or video. + + reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): + An InlineKeyboardMarkup object. + + Returns: + ``bool``: On success, True is returned. + + Raises: + RPCError: In case of a Telegram RPC error. + """ + style = self.html if media.parse_mode.lower() == "html" else self.markdown + caption = media.caption + + if isinstance(media, InputMediaPhoto): + if media.media.startswith("http"): + media = types.InputMediaPhotoExternal( + url=media.media + ) + else: + media = utils.get_input_media_from_file_id(media.media, 2) + elif isinstance(media, InputMediaVideo): + if media.media.startswith("http"): + media = types.InputMediaDocumentExternal( + url=media.media + ) + else: + media = utils.get_input_media_from_file_id(media.media, 4) + elif isinstance(media, InputMediaAudio): + if media.media.startswith("http"): + media = types.InputMediaDocumentExternal( + url=media.media + ) + else: + media = utils.get_input_media_from_file_id(media.media, 9) + elif isinstance(media, InputMediaAnimation): + if media.media.startswith("http"): + media = types.InputMediaDocumentExternal( + url=media.media + ) + else: + media = utils.get_input_media_from_file_id(media.media, 10) + elif isinstance(media, InputMediaDocument): + if media.media.startswith("http"): + media = types.InputMediaDocumentExternal( + url=media.media + ) + else: + media = utils.get_input_media_from_file_id(media.media, 5) + + return self.send( + functions.messages.EditInlineBotMessage( + id=utils.unpack_inline_message_id(inline_message_id), + media=media, + reply_markup=reply_markup.write() if reply_markup else None, + **style.parse(caption) + ) + ) diff --git a/pyrogram/client/methods/messages/edit_inline_reply_markup.py b/pyrogram/client/methods/messages/edit_inline_reply_markup.py new file mode 100644 index 00000000..0326ed72 --- /dev/null +++ b/pyrogram/client/methods/messages/edit_inline_reply_markup.py @@ -0,0 +1,50 @@ +# 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 functions +from pyrogram.client.ext import BaseClient, utils + + +class EditInlineReplyMarkup(BaseClient): + def edit_inline_reply_markup( + self, + inline_message_id: str, + reply_markup: "pyrogram.InlineKeyboardMarkup" = None + ) -> bool: + """Edit only the reply markup of **inline** messages sent via the bot (for inline bots). + + Parameters: + inline_message_id (``str``): + Identifier of the inline message. + + reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): + An InlineKeyboardMarkup object. + + Returns: + ``bool``: On success, True is returned. + + Raises: + RPCError: In case of a Telegram RPC error. + """ + return self.send( + functions.messages.EditInlineBotMessage( + id=utils.unpack_inline_message_id(inline_message_id), + reply_markup=reply_markup.write() if reply_markup else None, + ) + ) diff --git a/pyrogram/client/methods/messages/edit_inline_text.py b/pyrogram/client/methods/messages/edit_inline_text.py new file mode 100644 index 00000000..927fd80f --- /dev/null +++ b/pyrogram/client/methods/messages/edit_inline_text.py @@ -0,0 +1,67 @@ +# 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 functions +from pyrogram.client.ext import BaseClient, utils + + +class EditInlineText(BaseClient): + def edit_inline_text( + self, + inline_message_id: str, + text: str, + parse_mode: str = "", + disable_web_page_preview: bool = None, + reply_markup: "pyrogram.InlineKeyboardMarkup" = None + ) -> bool: + """Edit the text of **inline** messages. + + Parameters: + inline_message_id (``str``): + Identifier of the inline message. + + text (``str``): + New text of the message. + + parse_mode (``str``, *optional*): + Pass "markdown" or "html" if you want Telegram apps to show bold, italic, fixed-width text or inline + URLs in your message. Defaults to "markdown". + + disable_web_page_preview (``bool``, *optional*): + Disables link previews for links in this message. + + reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): + An InlineKeyboardMarkup object. + + Returns: + ``bool``: On success, True is returned. + + Raises: + RPCError: In case of a Telegram RPC error. + """ + style = self.html if parse_mode.lower() == "html" else self.markdown + + return self.send( + functions.messages.EditInlineBotMessage( + id=utils.unpack_inline_message_id(inline_message_id), + no_webpage=disable_web_page_preview or None, + reply_markup=reply_markup.write() if reply_markup else None, + **style.parse(text) + ) + ) diff --git a/pyrogram/client/methods/messages/edit_message_caption.py b/pyrogram/client/methods/messages/edit_message_caption.py index e9866573..52c22726 100644 --- a/pyrogram/client/methods/messages/edit_message_caption.py +++ b/pyrogram/client/methods/messages/edit_message_caption.py @@ -25,32 +25,25 @@ from pyrogram.client.ext import BaseClient class EditMessageCaption(BaseClient): def edit_message_caption( self, + chat_id: Union[int, str], + message_id: int, caption: str, - chat_id: Union[int, str] = None, - message_id: int = None, - inline_message_id: str = None, parse_mode: str = "", reply_markup: "pyrogram.InlineKeyboardMarkup" = None ) -> "pyrogram.Message": - """Edit caption of media messages. + """Edit the caption of media messages. Parameters: - caption (``str``): - New caption of the media message. - - chat_id (``int`` | ``str``, *optional*): - Required if *inline_message_id* is not specified. + 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``, *optional*): - Required if *inline_message_id* is not specified. + message_id (``int``): Message identifier in the chat specified in chat_id. - inline_message_id (``str``, *optional*): - Required if *chat_id* and *message_id* are not specified. - Identifier of the inline message. + caption (``str``): + New caption of the media message. parse_mode (``str``, *optional*): Pass "markdown" or "html" if you want Telegram apps to show bold, italic, fixed-width text or inline @@ -60,17 +53,15 @@ class EditMessageCaption(BaseClient): An InlineKeyboardMarkup object. Returns: - :obj:`Message` | ``bool``: On success, if the edited message was sent by the bot, the edited message is - returned, otherwise True is returned (message sent via the bot, as inline query result). + :obj:`Message`: On success, the edited message is returned. Raises: RPCError: In case of a Telegram RPC error. """ return self.edit_message_text( - text=caption, chat_id=chat_id, message_id=message_id, - inline_message_id=inline_message_id, + text=caption, parse_mode=parse_mode, reply_markup=reply_markup ) diff --git a/pyrogram/client/methods/messages/edit_message_media.py b/pyrogram/client/methods/messages/edit_message_media.py index d74e2b56..b65804fd 100644 --- a/pyrogram/client/methods/messages/edit_message_media.py +++ b/pyrogram/client/methods/messages/edit_message_media.py @@ -32,42 +32,33 @@ from pyrogram.client.types.input_media import InputMedia class EditMessageMedia(BaseClient): def edit_message_media( self, + chat_id: Union[int, str], + message_id: int, media: InputMedia, - chat_id: Union[int, str] = None, - message_id: int = None, - inline_message_id: str = None, reply_markup: "pyrogram.InlineKeyboardMarkup" = None ) -> "pyrogram.Message": """Edit animation, audio, document, photo or video messages. - If a message is a part of a message album, then it can be edited only to a photo or a video. Otherwise, - message type can be changed arbitrarily. When inline message is edited, new file can't be uploaded. - Use previously uploaded file via its file_id or specify a URL. + If a message is a part of a message album, then it can be edited only to a photo or a video. Otherwise, the + message type can be changed arbitrarily. Parameters: - media (:obj:`InputMedia`): - One of the InputMedia objects describing an animation, audio, document, photo or video. - - chat_id (``int`` | ``str``, *optional*): - Required if *inline_message_id* is not specified. + 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``, *optional*): - Required if *inline_message_id* is not specified. + message_id (``int``): Message identifier in the chat specified in chat_id. - inline_message_id (``str``, *optional*): - Required if *chat_id* and *message_id* are not specified. - Identifier of the inline message. + media (:obj:`InputMedia`): + One of the InputMedia objects describing an animation, audio, document, photo or video. reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): An InlineKeyboardMarkup object. Returns: - :obj:`Message` | ``bool``: On success, if the edited message was sent by the bot, the edited message is - returned, otherwise True is returned (message sent via the bot, as inline query result). + :obj:`Message`: On success, the edited message is returned. Raises: RPCError: In case of a Telegram RPC error. @@ -242,16 +233,6 @@ class EditMessageMedia(BaseClient): else: media = utils.get_input_media_from_file_id(media.media, 5) - if inline_message_id is not None: - return self.send( - functions.messages.EditInlineBotMessage( - id=utils.unpack_inline_message_id(inline_message_id), - media=media, - reply_markup=reply_markup.write() if reply_markup else None, - **style.parse(caption) - ) - ) - r = self.send( functions.messages.EditMessage( peer=self.resolve_peer(chat_id), diff --git a/pyrogram/client/methods/messages/edit_message_reply_markup.py b/pyrogram/client/methods/messages/edit_message_reply_markup.py index 516d7b9c..51b77a6a 100644 --- a/pyrogram/client/methods/messages/edit_message_reply_markup.py +++ b/pyrogram/client/methods/messages/edit_message_reply_markup.py @@ -20,52 +20,36 @@ from typing import Union import pyrogram from pyrogram.api import functions, types -from pyrogram.client.ext import BaseClient, utils +from pyrogram.client.ext import BaseClient class EditMessageReplyMarkup(BaseClient): def edit_message_reply_markup( self, + chat_id: Union[int, str], + message_id: int, reply_markup: "pyrogram.InlineKeyboardMarkup" = None, - chat_id: Union[int, str] = None, - message_id: int = None, - inline_message_id: str = None ) -> "pyrogram.Message": - """Edit only the reply markup of messages sent by the bot or via the bot (for inline bots). + """Edit only the reply markup of messages sent by the bot. Parameters: - reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): - An InlineKeyboardMarkup object. - - chat_id (``int`` | ``str``, *optional*): - Required if *inline_message_id* is not specified. + 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``, *optional*): - Required if *inline_message_id* is not specified. + message_id (``int``): Message identifier in the chat specified in chat_id. - inline_message_id (``str``, *optional*): - Required if *chat_id* and *message_id* are not specified. - Identifier of the inline message. + reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): + An InlineKeyboardMarkup object. Returns: - :obj:`Message` | ``bool``: On success, if the edited message was sent by the bot, the edited message is - returned, otherwise True is returned (message sent via the bot, as inline query result). + :obj:`Message`: On success, the edited message is returned. Raises: RPCError: In case of a Telegram RPC error. """ - if inline_message_id is not None: - return self.send( - functions.messages.EditInlineBotMessage( - id=utils.unpack_inline_message_id(inline_message_id), - reply_markup=reply_markup.write() if reply_markup else None, - ) - ) - r = self.send( functions.messages.EditMessage( peer=self.resolve_peer(chat_id), diff --git a/pyrogram/client/methods/messages/edit_message_text.py b/pyrogram/client/methods/messages/edit_message_text.py index 919e5dc1..7e4345c6 100644 --- a/pyrogram/client/methods/messages/edit_message_text.py +++ b/pyrogram/client/methods/messages/edit_message_text.py @@ -20,39 +20,32 @@ from typing import Union import pyrogram from pyrogram.api import functions, types -from pyrogram.client.ext import BaseClient, utils +from pyrogram.client.ext import BaseClient class EditMessageText(BaseClient): def edit_message_text( self, + chat_id: Union[int, str], + message_id: int, text: str, - chat_id: Union[int, str] = None, - message_id: int = None, - inline_message_id: str = None, parse_mode: str = "", disable_web_page_preview: bool = None, reply_markup: "pyrogram.InlineKeyboardMarkup" = None ) -> "pyrogram.Message": - """Edit text messages. + """Edit the text of messages. Parameters: - text (``str``): - New text of the message. - - chat_id (``int`` | ``str``, *optional*): - Required if *inline_message_id* is not specified. + 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``, *optional*): - Required if *inline_message_id* is not specified. + message_id (``int``): Message identifier in the chat specified in chat_id. - inline_message_id (``str``, *optional*): - Required if *chat_id* and *message_id* are not specified. - Identifier of the inline message. + text (``str``): + New text of the message. parse_mode (``str``, *optional*): Pass "markdown" or "html" if you want Telegram apps to show bold, italic, fixed-width text or inline @@ -65,24 +58,13 @@ class EditMessageText(BaseClient): An InlineKeyboardMarkup object. Returns: - :obj:`Message` | ``bool``: On success, if the edited message was sent by the bot, the edited message is - returned, otherwise True is returned (message sent via the bot, as inline query result). + :obj:`Message`: On success, the edited message is returned. Raises: RPCError: In case of a Telegram RPC error. """ style = self.html if parse_mode.lower() == "html" else self.markdown - if inline_message_id is not None: - return self.send( - functions.messages.EditInlineBotMessage( - id=utils.unpack_inline_message_id(inline_message_id), - no_webpage=disable_web_page_preview or None, - reply_markup=reply_markup.write() if reply_markup else None, - **style.parse(text) - ) - ) - r = self.send( functions.messages.EditMessage( peer=self.resolve_peer(chat_id), From ef8f3bd6e15b2d7da57d20939cf3bba03642edc5 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 14 Jun 2019 04:53:04 +0200 Subject: [PATCH 55/73] Revert: CallbackQuery edit_* bound methods renamed to edit_message_* --- docs/source/api/bound-methods.rst | 18 +-- .../bots_and_keyboards/callback_query.py | 140 ++++++++---------- 2 files changed, 67 insertions(+), 91 deletions(-) diff --git a/docs/source/api/bound-methods.rst b/docs/source/api/bound-methods.rst index 83b3dbbe..e6729da6 100644 --- a/docs/source/api/bound-methods.rst +++ b/docs/source/api/bound-methods.rst @@ -81,13 +81,13 @@ CallbackQuery ^^^^^^^^^^^^^ .. hlist:: - :columns: 3 + :columns: 4 - :meth:`~CallbackQuery.answer` - - :meth:`~CallbackQuery.edit_message_text` - - :meth:`~CallbackQuery.edit_message_caption` - - :meth:`~CallbackQuery.edit_message_media` - - :meth:`~CallbackQuery.edit_message_reply_markup` + - :meth:`~CallbackQuery.edit_text` + - :meth:`~CallbackQuery.edit_caption` + - :meth:`~CallbackQuery.edit_media` + - :meth:`~CallbackQuery.edit_reply_markup` InlineQuery ^^^^^^^^^^^ @@ -141,10 +141,10 @@ Details .. CallbackQuery .. automethod:: CallbackQuery.answer() -.. automethod:: CallbackQuery.edit_message_text() -.. automethod:: CallbackQuery.edit_message_caption() -.. automethod:: CallbackQuery.edit_message_media() -.. automethod:: CallbackQuery.edit_message_reply_markup() +.. automethod:: CallbackQuery.edit_text() +.. automethod:: CallbackQuery.edit_caption() +.. automethod:: CallbackQuery.edit_media() +.. automethod:: CallbackQuery.edit_reply_markup() .. InlineQuery .. automethod:: InlineQuery.answer() diff --git a/pyrogram/client/types/bots_and_keyboards/callback_query.py b/pyrogram/client/types/bots_and_keyboards/callback_query.py index 6872d65b..fcc90e57 100644 --- a/pyrogram/client/types/bots_and_keyboards/callback_query.py +++ b/pyrogram/client/types/bots_and_keyboards/callback_query.py @@ -173,14 +173,16 @@ class CallbackQuery(Object, Update): cache_time=cache_time ) - def edit_message_text( + def edit_text( self, text: str, parse_mode: str = "", disable_web_page_preview: bool = None, reply_markup: "pyrogram.InlineKeyboardMarkup" = None ) -> Union["pyrogram.Message", bool]: - """Bound method *edit_message_text* of :obj:`CallbackQuery`. + """Edit the text of messages attached to this callback query. + + Bound method *edit_message_text* of :obj:`CallbackQuery`. Parameters: text (``str``): @@ -203,34 +205,33 @@ class CallbackQuery(Object, Update): Raises: RPCError: In case of a Telegram RPC error. """ - chat_id = None - message_id = None - inline_message_id = None + if self.inline_message_id is None: + return self._client.edit_message_text( + chat_id=self.message.chat.id, + message_id=self.message.message_id, + text=text, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + reply_markup=reply_markup + ) + else: + return self._client.edit_inline_text( + inline_message_id=self.inline_message_id, + text=text, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + reply_markup=reply_markup + ) - if self.message is not None: - chat_id = self.message.chat.id - message_id = self.message.message_id - - if self.inline_message_id is not None: - inline_message_id = self.inline_message_id - - return self._client.edit_message_text( - text=text, - chat_id=chat_id, - message_id=message_id, - inline_message_id=inline_message_id, - parse_mode=parse_mode, - disable_web_page_preview=disable_web_page_preview, - reply_markup=reply_markup - ) - - def edit_message_caption( + def edit_caption( self, caption: str, parse_mode: str = "", reply_markup: "pyrogram.InlineKeyboardMarkup" = None ) -> Union["pyrogram.Message", bool]: - """Bound method *edit_message_caption* of :obj:`CallbackQuery`. + """Edit the caption of media messages attached to this callback query. + + Bound method *edit_message_caption* of :obj:`CallbackQuery`. Parameters: caption (``str``): @@ -250,32 +251,16 @@ class CallbackQuery(Object, Update): Raises: RPCError: In case of a Telegram RPC error. """ - chat_id = None - message_id = None - inline_message_id = None + return self.edit_text(caption, parse_mode, reply_markup) - if self.message is not None: - chat_id = self.message.chat.id - message_id = self.message.message_id - - if self.inline_message_id is not None: - inline_message_id = self.inline_message_id - - return self._client.edit_message_text( - text=caption, - chat_id=chat_id, - message_id=message_id, - inline_message_id=inline_message_id, - parse_mode=parse_mode, - reply_markup=reply_markup - ) - - def edit_message_media( + def edit_media( self, media: "pyrogram.InputMedia", reply_markup: "pyrogram.InlineKeyboardMarkup" = None ) -> Union["pyrogram.Message", bool]: - """Bound method *edit_message_media* of :obj:`CallbackQuery`. + """Edit animation, audio, document, photo or video messages attached to this callback query. + + Bound method *edit_message_media* of :obj:`CallbackQuery`. Parameters: media (:obj:`InputMedia`): @@ -291,30 +276,27 @@ class CallbackQuery(Object, Update): Raises: RPCError: In case of a Telegram RPC error. """ - chat_id = None - message_id = None - inline_message_id = None + if self.inline_message_id is None: + return self._client.edit_message_media( + chat_id=self.message.chat.id, + message_id=self.message.message_id, + media=media, + reply_markup=reply_markup + ) + else: + return self._client.edit_inline_media( + inline_message_id=self.inline_message_id, + media=media, + reply_markup=reply_markup + ) - if self.message is not None: - chat_id = self.message.chat.id - message_id = self.message.message_id - - if self.inline_message_id is not None: - inline_message_id = self.inline_message_id - - return self._client.edit_message_media( - media=media, - chat_id=chat_id, - message_id=message_id, - inline_message_id=inline_message_id, - reply_markup=reply_markup - ) - - def edit_message_reply_markup( + def edit_reply_markup( self, reply_markup: "pyrogram.InlineKeyboardMarkup" = None ) -> Union["pyrogram.Message", bool]: - """Bound method *edit_message_reply_markup* of :obj:`CallbackQuery`. + """Edit only the reply markup of messages attached to this callback query. + + Bound method *edit_message_reply_markup* of :obj:`CallbackQuery`. Parameters: reply_markup (:obj:`InlineKeyboardMarkup`): @@ -327,20 +309,14 @@ class CallbackQuery(Object, Update): Raises: RPCError: In case of a Telegram RPC error. """ - chat_id = None - message_id = None - inline_message_id = None - - if self.message is not None: - chat_id = self.message.chat.id - message_id = self.message.message_id - - if self.inline_message_id is not None: - inline_message_id = self.inline_message_id - - return self._client.edit_message_reply_markup( - reply_markup=reply_markup, - chat_id=chat_id, - message_id=message_id, - inline_message_id=inline_message_id - ) + if self.inline_message_id is None: + return self._client.edit_message_reply_markup( + chat_id=self.message.chat.id, + message_id=self.message.message_id, + reply_markup=reply_markup + ) + else: + return self._client.edit_inline_reply_markup( + inline_message_id=self.inline_message_id, + reply_markup=reply_markup + ) From 4492d9d20bc1f841f22cb42c7d9f1eca01d89a80 Mon Sep 17 00:00:00 2001 From: ColinShark Date: Sat, 15 Jun 2019 13:55:50 +0200 Subject: [PATCH 56/73] Put italic in quotes, adapt text_mention (#254) * Put italic in quotes, adapt text_mention * Update message_entity.py --- pyrogram/client/types/messages_and_media/message_entity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyrogram/client/types/messages_and_media/message_entity.py b/pyrogram/client/types/messages_and_media/message_entity.py index 5f3483ee..420bd914 100644 --- a/pyrogram/client/types/messages_and_media/message_entity.py +++ b/pyrogram/client/types/messages_and_media/message_entity.py @@ -31,8 +31,8 @@ class MessageEntity(Object): type (``str``): Type of the entity. Can be "mention" (@username), "hashtag", "cashtag", "bot_command", "url", "email", "phone_number", "bold" - (bold text), italic (italic text), "code" (monowidth string), "pre" (monowidth block), "text_link" - (for clickable text URLs), "text_mention" (for users without usernames). + (bold text), "italic" (italic text), "code" (monowidth string), "pre" (monowidth block), "text_link" + (for clickable text URLs), "text_mention" (for custom text mentions based on users' identifiers). offset (``int``): Offset in UTF-16 code units to the start of the entity. From a1aef9cf251dd3083cfdd2a9eae70b846eeaea78 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 15 Jun 2019 15:57:00 +0200 Subject: [PATCH 57/73] Hint about how to ask good questions --- .github/ISSUE_TEMPLATE/question.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 88d91ecd..05f342bc 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -10,6 +10,6 @@ labels: "question" # Important This place is for issues about Pyrogram, it's **not a forum**. -If you'd like to post a question, please move to https://stackoverflow.com or join the Telegram community at https://t.me/pyrogram. +If you'd like to post a question, please move to https://stackoverflow.com or join the Telegram community at https://t.me/pyrogram. Useful information on how to ask good questions can be found here: https://stackoverflow.com/help/how-to-ask. -Thanks. \ No newline at end of file +Thanks. From abc0e992cf43ace3b32c6a6e87b90c59d1a89de2 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 15 Jun 2019 17:59:28 +0200 Subject: [PATCH 58/73] Fix Sticker.set_name being treated as tuple/list-like when should in fact be a string Yes, that little comma messed things up (again) --- pyrogram/client/types/messages_and_media/sticker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/client/types/messages_and_media/sticker.py b/pyrogram/client/types/messages_and_media/sticker.py index 3c171543..78fdda38 100644 --- a/pyrogram/client/types/messages_and_media/sticker.py +++ b/pyrogram/client/types/messages_and_media/sticker.py @@ -94,7 +94,7 @@ class Sticker(Object): self.width = width self.height = height self.emoji = emoji - self.set_name = set_name, + self.set_name = set_name self.thumbs = thumbs # self.mask_position = mask_position From 80d8443be4c11dd44d89184022599891d4f078b4 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 15 Jun 2019 23:02:31 +0200 Subject: [PATCH 59/73] Fix script executions not working outside the current directory Fixes #41 --- pyrogram/client/client.py | 35 +--------------- pyrogram/client/ext/base_client.py | 8 +++- .../client/methods/messages/download_media.py | 42 ++++++++++++++++++- 3 files changed, 47 insertions(+), 38 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 1106a416..9001a37e 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -28,7 +28,6 @@ import tempfile import threading import time from configparser import ConfigParser -from datetime import datetime from hashlib import sha256, md5 from importlib import import_module from pathlib import Path @@ -842,39 +841,7 @@ class Client(Methods, BaseClient): final_file_path = "" try: - data, file_name, done, progress, progress_args, path = packet - - directory, file_name = os.path.split(file_name) - directory = directory or "downloads" - - media_type_str = Client.MEDIA_TYPE_ID[data.media_type] - - file_name = file_name or data.file_name - - if not file_name: - guessed_extension = self.guess_extension(data.mime_type) - - if data.media_type in (0, 1, 2, 14): - extension = ".jpg" - elif data.media_type == 3: - extension = guessed_extension or ".ogg" - elif data.media_type in (4, 10, 13): - extension = guessed_extension or ".mp4" - elif data.media_type == 5: - extension = guessed_extension or ".zip" - elif data.media_type == 8: - extension = guessed_extension or ".webp" - elif data.media_type == 9: - extension = guessed_extension or ".mp3" - else: - continue - - file_name = "{}_{}_{}{}".format( - media_type_str, - datetime.fromtimestamp(data.date or time.time()).strftime("%Y-%m-%d_%H-%M-%S"), - self.rnd_id(), - extension - ) + data, directory, file_name, done, progress, progress_args, path = packet temp_file_path = self.get_file( media_type=data.media_type, diff --git a/pyrogram/client/ext/base_client.py b/pyrogram/client/ext/base_client.py index c8d1beab..def290e6 100644 --- a/pyrogram/client/ext/base_client.py +++ b/pyrogram/client/ext/base_client.py @@ -19,6 +19,8 @@ import os import platform import re +import sys +from pathlib import Path from queue import Queue from threading import Lock @@ -45,6 +47,8 @@ class BaseClient: LANG_CODE = "en" + PARENT_DIR = Path(sys.argv[0]).parent + INVITE_LINK_RE = re.compile(r"^(?:https?://)?(?:www\.)?(?:t(?:elegram)?\.(?:org|me|dog)/joinchat/)([\w-]+)$") BOT_TOKEN_RE = re.compile(r"^\d+:[\w-]+$") DIALOGS_AT_ONCE = 100 @@ -52,8 +56,8 @@ class BaseClient: DOWNLOAD_WORKERS = 1 OFFLINE_SLEEP = 900 WORKERS = 4 - WORKDIR = "." - CONFIG_FILE = "./config.ini" + WORKDIR = PARENT_DIR + CONFIG_FILE = PARENT_DIR / "config.ini" MEDIA_TYPE_ID = { 0: "photo_thumbnail", diff --git a/pyrogram/client/methods/messages/download_media.py b/pyrogram/client/methods/messages/download_media.py index bd8de2d6..143349f7 100644 --- a/pyrogram/client/methods/messages/download_media.py +++ b/pyrogram/client/methods/messages/download_media.py @@ -17,7 +17,10 @@ # along with Pyrogram. If not, see . import binascii +import os import struct +import time +from datetime import datetime from threading import Event from typing import Union @@ -25,12 +28,14 @@ import pyrogram from pyrogram.client.ext import BaseClient, FileData, utils from pyrogram.errors import FileIdInvalid +DEFAULT_DOWNLOAD_DIR = "downloads/" + class DownloadMedia(BaseClient): def download_media( self, message: Union["pyrogram.Message", str], - file_name: str = "", + file_name: str = DEFAULT_DOWNLOAD_DIR, block: bool = True, progress: callable = None, progress_args: tuple = () @@ -169,7 +174,40 @@ class DownloadMedia(BaseClient): done = Event() path = [None] - self.download_queue.put((data, file_name, done, progress, progress_args, path)) + directory, file_name = os.path.split(file_name) + file_name = file_name or data.file_name or "" + + if not os.path.isabs(file_name): + directory = self.PARENT_DIR / (directory or DEFAULT_DOWNLOAD_DIR) + + media_type_str = self.MEDIA_TYPE_ID[data.media_type] + + if not file_name: + guessed_extension = self.guess_extension(data.mime_type) + + if data.media_type in (0, 1, 2, 14): + extension = ".jpg" + elif data.media_type == 3: + extension = guessed_extension or ".ogg" + elif data.media_type in (4, 10, 13): + extension = guessed_extension or ".mp4" + elif data.media_type == 5: + extension = guessed_extension or ".zip" + elif data.media_type == 8: + extension = guessed_extension or ".webp" + elif data.media_type == 9: + extension = guessed_extension or ".mp3" + else: + extension = ".unknown" + + file_name = "{}_{}_{}{}".format( + media_type_str, + datetime.fromtimestamp(data.date or time.time()).strftime("%Y-%m-%d_%H-%M-%S"), + self.rnd_id(), + extension + ) + + self.download_queue.put((data, directory, file_name, done, progress, progress_args, path)) if block: done.wait() From 10de006cc5056b860efa749025b6785b520dadb7 Mon Sep 17 00:00:00 2001 From: ColinShark Date: Mon, 17 Jun 2019 09:47:12 +0200 Subject: [PATCH 60/73] Add returned object --- pyrogram/client/methods/contacts/add_contacts.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyrogram/client/methods/contacts/add_contacts.py b/pyrogram/client/methods/contacts/add_contacts.py index c7e647b0..8e3995ac 100644 --- a/pyrogram/client/methods/contacts/add_contacts.py +++ b/pyrogram/client/methods/contacts/add_contacts.py @@ -34,6 +34,9 @@ class AddContacts(BaseClient): contacts (List of :obj:`InputPhoneContact`): The contact list to be added + Returns: + :obj:`types.contacts.ImportedContacts` + Raises: RPCError: In case of a Telegram RPC error. """ From 1fd31cac1e441f7ae47d77fdece08ae62ebd1b7e Mon Sep 17 00:00:00 2001 From: ColinShark Date: Mon, 17 Jun 2019 14:34:49 +0200 Subject: [PATCH 61/73] Add convenience methods to block and unblock Users --- pyrogram/client/methods/users/__init__.py | 6 ++- pyrogram/client/methods/users/block_user.py | 45 +++++++++++++++++++ pyrogram/client/methods/users/unblock_user.py | 45 +++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 pyrogram/client/methods/users/block_user.py create mode 100644 pyrogram/client/methods/users/unblock_user.py diff --git a/pyrogram/client/methods/users/__init__.py b/pyrogram/client/methods/users/__init__.py index 20b50ce9..f30245d7 100644 --- a/pyrogram/client/methods/users/__init__.py +++ b/pyrogram/client/methods/users/__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 .block_user import BlockUser from .delete_profile_photos import DeleteProfilePhotos from .get_me import GetMe from .get_profile_photos import GetProfilePhotos @@ -24,10 +25,12 @@ from .get_user_dc import GetUserDC from .get_users import GetUsers from .iter_profile_photos import IterProfilePhotos from .set_profile_photo import SetProfilePhoto +from .unblock_user import UnblockUser from .update_username import UpdateUsername class Users( + BlockUser, GetProfilePhotos, SetProfilePhoto, DeleteProfilePhotos, @@ -36,6 +39,7 @@ class Users( UpdateUsername, GetProfilePhotosCount, GetUserDC, - IterProfilePhotos + IterProfilePhotos, + UnblockUser ): pass diff --git a/pyrogram/client/methods/users/block_user.py b/pyrogram/client/methods/users/block_user.py new file mode 100644 index 00000000..1b4cc31a --- /dev/null +++ b/pyrogram/client/methods/users/block_user.py @@ -0,0 +1,45 @@ +# 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 + +import pyrogram +from pyrogram.api import functions, types +from ...ext import BaseClient + + +class BlockUser(BaseClient): + def block_user( + self, + user_id: Union[int, str] + ) -> bool: + """Block a user. + + Returns: + ``bool``: True on success + + Raises: + RPCError: In case of Telegram RPC Error. + """ + return bool( + self.send( + functions.contact.Block( + id=self.resolve_peer(user_id) + ) + ) + ) diff --git a/pyrogram/client/methods/users/unblock_user.py b/pyrogram/client/methods/users/unblock_user.py new file mode 100644 index 00000000..b2212762 --- /dev/null +++ b/pyrogram/client/methods/users/unblock_user.py @@ -0,0 +1,45 @@ +# 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 + +import pyrogram +from pyrogram.api import functions, types +from ...ext import BaseClient + + +class UnblockUser(BaseClient): + def unblock_user( + self, + user_id: Union[int, str] + ) -> bool: + """Unblock a user. + + Returns: + ``bool``: True on success + + Raises: + RPCError: In case of Telegram RPC Error. + """ + return bool( + self.send( + functions.contact.Unblock( + id=self.resolve_peer(user_id) + ) + ) + ) From 682591ea8fdb3f33c2970690b0aafd67c8823c29 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 19 Jun 2019 16:01:23 +0200 Subject: [PATCH 62/73] Update Auth and Session to accommodate Storage Engines --- pyrogram/client/client.py | 24 +-- pyrogram/client/ext/base_client.py | 1 - pyrogram/client/session_storage/abstract.py | 139 ---------------- pyrogram/client/session_storage/json.py | 63 -------- pyrogram/client/session_storage/memory.py | 115 ------------- .../client/session_storage/sqlite/0001.sql | 24 --- .../client/session_storage/sqlite/__init__.py | 153 ------------------ pyrogram/client/session_storage/string.py | 46 ------ .../{session_storage => storage}/__init__.py | 8 +- pyrogram/client/style/html.py | 9 +- pyrogram/client/style/markdown.py | 9 +- pyrogram/session/auth.py | 10 +- pyrogram/session/session.py | 23 ++- 13 files changed, 36 insertions(+), 588 deletions(-) delete mode 100644 pyrogram/client/session_storage/abstract.py delete mode 100644 pyrogram/client/session_storage/json.py delete mode 100644 pyrogram/client/session_storage/memory.py delete mode 100644 pyrogram/client/session_storage/sqlite/0001.sql delete mode 100644 pyrogram/client/session_storage/sqlite/__init__.py delete mode 100644 pyrogram/client/session_storage/string.py rename pyrogram/client/{session_storage => storage}/__init__.py (78%) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 1aa436b5..885c3334 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -26,14 +26,13 @@ import shutil import tempfile import threading import time -import warnings from configparser import ConfigParser from hashlib import sha256, md5 from importlib import import_module from pathlib import Path from signal import signal, SIGINT, SIGTERM, SIGABRT from threading import Thread -from typing import Union, List, Type +from typing import Union, List from pyrogram.api import functions, types from pyrogram.api.core import TLObject @@ -205,24 +204,9 @@ class Client(Methods, BaseClient): no_updates: bool = None, takeout: bool = None ): + super().__init__() - if isinstance(session_name, str): - if session_name == ':memory:': - session_storage = MemorySessionStorage(self) - elif session_name.startswith(':'): - session_storage = StringSessionStorage(self, session_name) - else: - session_storage = SQLiteSessionStorage(self, session_name) - elif isinstance(session_name, SessionStorage): - session_storage = session_name - else: - raise RuntimeError('Wrong session_name passed, expected str or SessionConfig subclass') - - super().__init__(session_storage) - - super().__init__(session_storage) - - self.session_name = str(session_name) # TODO: build correct session name + self.session_name = session_name self.api_id = int(api_id) if api_id else None self.api_hash = api_hash self.app_version = app_version @@ -232,7 +216,7 @@ class Client(Methods, BaseClient): self.ipv6 = ipv6 # TODO: Make code consistent, use underscore for private/protected fields self._proxy = proxy - self.session_storage.test_mode = test_mode + self.test_mode = test_mode self.bot_token = bot_token self.phone_number = phone_number self.phone_code = phone_code diff --git a/pyrogram/client/ext/base_client.py b/pyrogram/client/ext/base_client.py index aaf87823..9276b0eb 100644 --- a/pyrogram/client/ext/base_client.py +++ b/pyrogram/client/ext/base_client.py @@ -27,7 +27,6 @@ from threading import Lock from pyrogram import __version__ from ..style import Markdown, HTML from ...session.internals import MsgId -from ..session_storage import SessionStorage class BaseClient: diff --git a/pyrogram/client/session_storage/abstract.py b/pyrogram/client/session_storage/abstract.py deleted file mode 100644 index 134d5c8c..00000000 --- a/pyrogram/client/session_storage/abstract.py +++ /dev/null @@ -1,139 +0,0 @@ -# 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 abc -from typing import Type, Union - -import pyrogram -from pyrogram.api import types - - -class SessionDoesNotExist(Exception): - pass - - -class SessionStorage(abc.ABC): - def __init__(self, client: 'pyrogram.client.BaseClient'): - self._client = client - - @abc.abstractmethod - def load(self): - ... - - @abc.abstractmethod - def save(self, sync=False): - ... - - @property - @abc.abstractmethod - def dc_id(self): - ... - - @dc_id.setter - @abc.abstractmethod - def dc_id(self, val): - ... - - @property - @abc.abstractmethod - def test_mode(self): - ... - - @test_mode.setter - @abc.abstractmethod - def test_mode(self, val): - ... - - @property - @abc.abstractmethod - def auth_key(self): - ... - - @auth_key.setter - @abc.abstractmethod - def auth_key(self, val): - ... - - @property - @abc.abstractmethod - def user_id(self): - ... - - @user_id.setter - @abc.abstractmethod - def user_id(self, val): - ... - - @property - @abc.abstractmethod - def date(self): - ... - - @date.setter - @abc.abstractmethod - def date(self, val): - ... - - @property - @abc.abstractmethod - def is_bot(self): - ... - - @is_bot.setter - @abc.abstractmethod - def is_bot(self, val): - ... - - @abc.abstractmethod - def clear_cache(self): - ... - - @abc.abstractmethod - def cache_peer(self, entity: Union[types.User, - types.Chat, types.ChatForbidden, - types.Channel, types.ChannelForbidden]): - ... - - @abc.abstractmethod - def get_peer_by_id(self, val: int): - ... - - @abc.abstractmethod - def get_peer_by_username(self, val: str): - ... - - @abc.abstractmethod - def get_peer_by_phone(self, val: str): - ... - - def get_peer(self, peer_id: Union[int, str]): - if isinstance(peer_id, int): - return self.get_peer_by_id(peer_id) - else: - peer_id = peer_id.lstrip('+@') - if peer_id.isdigit(): - return self.get_peer_by_phone(peer_id) - return self.get_peer_by_username(peer_id) - - @abc.abstractmethod - def peers_count(self): - ... - - @abc.abstractmethod - def contacts_count(self): - ... diff --git a/pyrogram/client/session_storage/json.py b/pyrogram/client/session_storage/json.py deleted file mode 100644 index 4a48d3c1..00000000 --- a/pyrogram/client/session_storage/json.py +++ /dev/null @@ -1,63 +0,0 @@ -# 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 base64 -import json -import logging -import os -import shutil - -import pyrogram -from ..ext import utils -from . import MemorySessionStorage, SessionDoesNotExist - - -log = logging.getLogger(__name__) - -EXTENSION = '.session' - - -class JsonSessionStorage(MemorySessionStorage): - def __init__(self, client: 'pyrogram.client.ext.BaseClient', session_name: str): - super(JsonSessionStorage, self).__init__(client) - self._session_name = session_name - - def _get_file_name(self, name: str): - if not name.endswith(EXTENSION): - name += EXTENSION - return os.path.join(self._client.workdir, name) - - def load(self): - file_path = self._get_file_name(self._session_name) - log.info('Loading JSON session from {}'.format(file_path)) - - try: - with open(file_path, encoding='utf-8') as f: - s = json.load(f) - except FileNotFoundError: - raise SessionDoesNotExist() - - self._dc_id = s["dc_id"] - self._test_mode = s["test_mode"] - self._auth_key = base64.b64decode("".join(s["auth_key"])) # join split key - self._user_id = s["user_id"] - self._date = s.get("date", 0) - self._is_bot = s.get('is_bot', self._is_bot) - - def save(self, sync=False): - pass diff --git a/pyrogram/client/session_storage/memory.py b/pyrogram/client/session_storage/memory.py deleted file mode 100644 index c0610e70..00000000 --- a/pyrogram/client/session_storage/memory.py +++ /dev/null @@ -1,115 +0,0 @@ -import pyrogram -from pyrogram.api import types -from . import SessionStorage, SessionDoesNotExist - - -class MemorySessionStorage(SessionStorage): - def __init__(self, client: 'pyrogram.client.ext.BaseClient'): - super(MemorySessionStorage, self).__init__(client) - self._dc_id = 1 - self._test_mode = None - self._auth_key = None - self._user_id = None - self._date = 0 - self._is_bot = False - self._peers_cache = {} - - def load(self): - raise SessionDoesNotExist() - - def save(self, sync=False): - pass - - @property - def dc_id(self): - return self._dc_id - - @dc_id.setter - def dc_id(self, val): - self._dc_id = val - - @property - def test_mode(self): - return self._test_mode - - @test_mode.setter - def test_mode(self, val): - self._test_mode = val - - @property - def auth_key(self): - return self._auth_key - - @auth_key.setter - def auth_key(self, val): - self._auth_key = val - - @property - def user_id(self): - return self._user_id - - @user_id.setter - def user_id(self, val): - self._user_id = val - - @property - def date(self): - return self._date - - @date.setter - def date(self, val): - self._date = val - - @property - def is_bot(self): - return self._is_bot - - @is_bot.setter - def is_bot(self, val): - self._is_bot = val - - def clear_cache(self): - keys = list(filter(lambda k: k[0] in 'up', self._peers_cache.keys())) - for key in keys: - try: - del self._peers_cache[key] - except KeyError: - pass - - def cache_peer(self, entity): - if isinstance(entity, types.User): - input_peer = types.InputPeerUser( - user_id=entity.id, - access_hash=entity.access_hash - ) - self._peers_cache['i' + str(entity.id)] = input_peer - if entity.username: - self._peers_cache['u' + entity.username.lower()] = input_peer - if entity.phone: - self._peers_cache['p' + entity.phone] = input_peer - elif isinstance(entity, (types.Chat, types.ChatForbidden)): - self._peers_cache['i-' + str(entity.id)] = types.InputPeerChat(chat_id=entity.id) - elif isinstance(entity, (types.Channel, types.ChannelForbidden)): - input_peer = types.InputPeerChannel( - channel_id=entity.id, - access_hash=entity.access_hash - ) - self._peers_cache['i-100' + str(entity.id)] = input_peer - username = getattr(entity, "username", None) - if username: - self._peers_cache['u' + username.lower()] = input_peer - - def get_peer_by_id(self, val): - return self._peers_cache['i' + str(val)] - - def get_peer_by_username(self, val): - return self._peers_cache['u' + val.lower()] - - def get_peer_by_phone(self, val): - return self._peers_cache['p' + val] - - def peers_count(self): - return len(list(filter(lambda k: k[0] == 'i', self._peers_cache.keys()))) - - def contacts_count(self): - return len(list(filter(lambda k: k[0] == 'p', self._peers_cache.keys()))) diff --git a/pyrogram/client/session_storage/sqlite/0001.sql b/pyrogram/client/session_storage/sqlite/0001.sql deleted file mode 100644 index c6c51d24..00000000 --- a/pyrogram/client/session_storage/sqlite/0001.sql +++ /dev/null @@ -1,24 +0,0 @@ -create table sessions ( - dc_id integer primary key, - test_mode integer, - auth_key blob, - user_id integer, - date integer, - is_bot integer -); - -create table peers_cache ( - id integer primary key, - hash integer, - username text, - phone integer -); - -create table migrations ( - name text primary key -); - -create index username_idx on peers_cache(username); -create index phone_idx on peers_cache(phone); - -insert into migrations (name) values ('0001'); diff --git a/pyrogram/client/session_storage/sqlite/__init__.py b/pyrogram/client/session_storage/sqlite/__init__.py deleted file mode 100644 index a16e75e8..00000000 --- a/pyrogram/client/session_storage/sqlite/__init__.py +++ /dev/null @@ -1,153 +0,0 @@ -# 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 logging -import os -import shutil -import sqlite3 -from threading import Lock - -import pyrogram -from ....api import types -from ...ext import utils -from .. import MemorySessionStorage, SessionDoesNotExist, JsonSessionStorage - - -log = logging.getLogger(__name__) - -EXTENSION = '.session' -MIGRATIONS = ['0001'] - - -class SQLiteSessionStorage(MemorySessionStorage): - def __init__(self, client: 'pyrogram.client.ext.BaseClient', session_name: str): - super(SQLiteSessionStorage, self).__init__(client) - self._session_name = session_name - self._conn = None # type: sqlite3.Connection - self._lock = Lock() - - def _get_file_name(self, name: str): - if not name.endswith(EXTENSION): - name += EXTENSION - return os.path.join(self._client.workdir, name) - - def _apply_migrations(self, new_db=False): - self._conn.execute('PRAGMA read_uncommitted = true') - migrations = MIGRATIONS.copy() - if not new_db: - cursor = self._conn.cursor() - cursor.execute('select name from migrations') - for row in cursor.fetchone(): - migrations.remove(row) - for name in migrations: - with open(os.path.join(os.path.dirname(__file__), '{}.sql'.format(name))) as script: - self._conn.executescript(script.read()) - - def _migrate_from_json(self): - jss = JsonSessionStorage(self._client, self._session_name) - jss.load() - file_path = self._get_file_name(self._session_name) - self._conn = sqlite3.connect(file_path + '.tmp') - self._apply_migrations(new_db=True) - self._dc_id, self._test_mode, self._auth_key, self._user_id, self._date, self._is_bot = \ - jss.dc_id, jss.test_mode, jss.auth_key, jss.user_id, jss.date, jss.is_bot - self.save() - self._conn.close() - shutil.move(file_path + '.tmp', file_path) - log.warning('Session was migrated from JSON, loading...') - self.load() - - def load(self): - file_path = self._get_file_name(self._session_name) - log.info('Loading SQLite session from {}'.format(file_path)) - - if os.path.isfile(file_path): - try: - self._conn = sqlite3.connect(file_path, isolation_level='EXCLUSIVE', check_same_thread=False) - self._apply_migrations() - except sqlite3.DatabaseError: - log.warning('Trying to migrate session from JSON...') - self._migrate_from_json() - return - else: - self._conn = sqlite3.connect(file_path, isolation_level='EXCLUSIVE', check_same_thread=False) - self._apply_migrations(new_db=True) - - cursor = self._conn.cursor() - cursor.execute('select dc_id, test_mode, auth_key, user_id, "date", is_bot from sessions') - row = cursor.fetchone() - if not row: - raise SessionDoesNotExist() - - self._dc_id = row[0] - self._test_mode = bool(row[1]) - self._auth_key = row[2] - self._user_id = row[3] - self._date = row[4] - self._is_bot = bool(row[5]) - - def cache_peer(self, entity): - peer_id = username = phone = access_hash = None - - if isinstance(entity, types.User): - peer_id = entity.id - username = entity.username.lower() if entity.username else None - phone = entity.phone or None - access_hash = entity.access_hash - elif isinstance(entity, (types.Chat, types.ChatForbidden)): - peer_id = -entity.id - elif isinstance(entity, (types.Channel, types.ChannelForbidden)): - peer_id = int('-100' + str(entity.id)) - username = entity.username.lower() if hasattr(entity, 'username') and entity.username else None - access_hash = entity.access_hash - - with self._lock: - self._conn.execute('insert or replace into peers_cache values (?, ?, ?, ?)', - (peer_id, access_hash, username, phone)) - - def get_peer_by_id(self, val): - cursor = self._conn.cursor() - cursor.execute('select id, hash from peers_cache where id = ?', (val,)) - row = cursor.fetchone() - if not row: - raise KeyError(val) - return utils.get_input_peer(row[0], row[1]) - - def get_peer_by_username(self, val): - cursor = self._conn.cursor() - cursor.execute('select id, hash from peers_cache where username = ?', (val,)) - row = cursor.fetchone() - if not row: - raise KeyError(val) - return utils.get_input_peer(row[0], row[1]) - - def get_peer_by_phone(self, val): - cursor = self._conn.cursor() - cursor.execute('select id, hash from peers_cache where phone = ?', (val,)) - row = cursor.fetchone() - if not row: - raise KeyError(val) - return utils.get_input_peer(row[0], row[1]) - - def save(self, sync=False): - log.info('Committing SQLite session') - with self._lock: - self._conn.execute('delete from sessions') - self._conn.execute('insert into sessions values (?, ?, ?, ?, ?, ?)', - (self._dc_id, self._test_mode, self._auth_key, self._user_id, self._date, self._is_bot)) - self._conn.commit() diff --git a/pyrogram/client/session_storage/string.py b/pyrogram/client/session_storage/string.py deleted file mode 100644 index 11051323..00000000 --- a/pyrogram/client/session_storage/string.py +++ /dev/null @@ -1,46 +0,0 @@ -import base64 -import binascii -import struct - -import pyrogram -from . import MemorySessionStorage, SessionDoesNotExist - - -class StringSessionStorage(MemorySessionStorage): - """ - Packs session data as following (forcing little-endian byte order): - Char dc_id (1 byte, unsigned) - Boolean test_mode (1 byte) - Long long user_id (8 bytes, signed) - Boolean is_bot (1 byte) - Bytes auth_key (256 bytes) - - Uses Base64 encoding for printable representation - """ - PACK_FORMAT = '. -from .abstract import SessionStorage, SessionDoesNotExist -from .memory import MemorySessionStorage -from .json import JsonSessionStorage -from .string import StringSessionStorage -from .sqlite import SQLiteSessionStorage +from .memory_storage import MemoryStorage +from .file_storage import FileStorage +from .storage import Storage diff --git a/pyrogram/client/style/html.py b/pyrogram/client/style/html.py index 894dbd6c..9c0a372c 100644 --- a/pyrogram/client/style/html.py +++ b/pyrogram/client/style/html.py @@ -31,16 +31,14 @@ from pyrogram.api.types import ( ) from pyrogram.errors import PeerIdInvalid from . import utils -from ..session_storage import SessionStorage class HTML: HTML_RE = re.compile(r"<(\w+)(?: href=([\"'])([^<]+)\2)?>([^>]+)") MENTION_RE = re.compile(r"tg://user\?id=(\d+)") - def __init__(self, session_storage: SessionStorage, client: "pyrogram.BaseClient" = None): + def __init__(self, client: "pyrogram.BaseClient" = None): self.client = client - self.session_storage = session_storage def parse(self, message: str): entities = [] @@ -56,9 +54,10 @@ class HTML: if mention: user_id = int(mention.group(1)) + try: - input_user = self.session_storage.get_peer_by_id(user_id) - except KeyError: + input_user = self.client.resolve_peer(user_id) + except PeerIdInvalid: input_user = None entity = ( diff --git a/pyrogram/client/style/markdown.py b/pyrogram/client/style/markdown.py index 68b54bbb..adb86e94 100644 --- a/pyrogram/client/style/markdown.py +++ b/pyrogram/client/style/markdown.py @@ -31,7 +31,6 @@ from pyrogram.api.types import ( ) from pyrogram.errors import PeerIdInvalid from . import utils -from ..session_storage import SessionStorage class Markdown: @@ -55,9 +54,8 @@ class Markdown: )) MENTION_RE = re.compile(r"tg://user\?id=(\d+)") - def __init__(self, session_storage: SessionStorage, client: "pyrogram.BaseClient" = None): + def __init__(self, client: "pyrogram.BaseClient" = None): self.client = client - self.session_storage = session_storage def parse(self, message: str): message = utils.add_surrogates(str(message or "")).strip() @@ -73,9 +71,10 @@ class Markdown: if mention: user_id = int(mention.group(1)) + try: - input_user = self.session_storage.get_peer_by_id(user_id) - except KeyError: + input_user = self.client.resolve_peer(user_id) + except PeerIdInvalid: input_user = None entity = ( diff --git a/pyrogram/session/auth.py b/pyrogram/session/auth.py index fb6e7ca3..b05b2855 100644 --- a/pyrogram/session/auth.py +++ b/pyrogram/session/auth.py @@ -22,10 +22,12 @@ from hashlib import sha1 from io import BytesIO from os import urandom +import pyrogram from pyrogram.api import functions, types from pyrogram.api.core import TLObject, Long, Int from pyrogram.connection import Connection from pyrogram.crypto import AES, RSA, Prime + from .internals import MsgId log = logging.getLogger(__name__) @@ -34,11 +36,11 @@ log = logging.getLogger(__name__) class Auth: MAX_RETRIES = 5 - def __init__(self, dc_id: int, test_mode: bool, ipv6: bool, proxy: dict): + def __init__(self, client: "pyrogram.Client", dc_id: int): self.dc_id = dc_id - self.test_mode = test_mode - self.ipv6 = ipv6 - self.proxy = proxy + self.test_mode = client.storage.test_mode + self.ipv6 = client.ipv6 + self.proxy = client.proxy self.connection = None diff --git a/pyrogram/session/session.py b/pyrogram/session/session.py index bd7f0f26..5947fc0f 100644 --- a/pyrogram/session/session.py +++ b/pyrogram/session/session.py @@ -34,6 +34,7 @@ from pyrogram.api.core import Message, TLObject, MsgContainer, Long, FutureSalt, from pyrogram.connection import Connection from pyrogram.crypto import AES, KDF from pyrogram.errors import RPCError, InternalServerError, AuthKeyDuplicated + from .internals import MsgId, MsgFactory log = logging.getLogger(__name__) @@ -70,12 +71,14 @@ class Session: 64: "[64] invalid container" } - def __init__(self, - client: pyrogram, - dc_id: int, - auth_key: bytes, - is_media: bool = False, - is_cdn: bool = False): + def __init__( + self, + client: pyrogram, + dc_id: int, + auth_key: bytes, + is_media: bool = False, + is_cdn: bool = False + ): if not Session.notice_displayed: print("Pyrogram v{}, {}".format(__version__, __copyright__)) print("Licensed under the terms of the " + __license__, end="\n\n") @@ -113,8 +116,12 @@ class Session: def start(self): while True: - self.connection = Connection(self.dc_id, self.client.session_storage.test_mode, - self.client.ipv6, self.client.proxy) + self.connection = Connection( + self.dc_id, + self.client.storage.test_mode, + self.client.ipv6, + self.client.proxy + ) try: self.connection.connect() From 6177abbfa4d09b63eee17907a3d2462ac2e14a32 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 19 Jun 2019 16:04:06 +0200 Subject: [PATCH 63/73] Add Storage abstract class --- pyrogram/client/storage/storage.py | 98 ++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 pyrogram/client/storage/storage.py diff --git a/pyrogram/client/storage/storage.py b/pyrogram/client/storage/storage.py new file mode 100644 index 00000000..e0810645 --- /dev/null +++ b/pyrogram/client/storage/storage.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 . + + +class Storage: + def __init__(self, name: str): + self.name = name + + def open(self): + raise NotImplementedError + + def save(self): + raise NotImplementedError + + def close(self): + raise NotImplementedError + + def update_peers(self, peers): + raise NotImplementedError + + def get_peer_by_id(self, peer_id): + raise NotImplementedError + + def get_peer_by_username(self, username): + raise NotImplementedError + + def get_peer_by_phone_number(self, phone_number): + raise NotImplementedError + + def export_session_string(self): + raise NotImplementedError + + @property + def peers_count(self): + raise NotImplementedError + + @property + def dc_id(self): + raise NotImplementedError + + @dc_id.setter + def dc_id(self, value): + raise NotImplementedError + + @property + def test_mode(self): + raise NotImplementedError + + @test_mode.setter + def test_mode(self, value): + raise NotImplementedError + + @property + def auth_key(self): + raise NotImplementedError + + @auth_key.setter + def auth_key(self, value): + raise NotImplementedError + + @property + def date(self): + raise NotImplementedError + + @date.setter + def date(self, value): + raise NotImplementedError + + @property + def user_id(self): + raise NotImplementedError + + @user_id.setter + def user_id(self, value): + raise NotImplementedError + + @property + def is_bot(self): + raise NotImplementedError + + @is_bot.setter + def is_bot(self, value): + raise NotImplementedError From 6cc9688e4921a15c82667264eea624844cb48be9 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 19 Jun 2019 16:04:35 +0200 Subject: [PATCH 64/73] Implement FileStorage and MemoryStorage engines --- pyrogram/client/storage/file_storage.py | 102 +++++++++ pyrogram/client/storage/memory_storage.py | 241 ++++++++++++++++++++++ pyrogram/client/storage/schema.sql | 34 +++ 3 files changed, 377 insertions(+) create mode 100644 pyrogram/client/storage/file_storage.py create mode 100644 pyrogram/client/storage/memory_storage.py create mode 100644 pyrogram/client/storage/schema.sql diff --git a/pyrogram/client/storage/file_storage.py b/pyrogram/client/storage/file_storage.py new file mode 100644 index 00000000..ee5000c5 --- /dev/null +++ b/pyrogram/client/storage/file_storage.py @@ -0,0 +1,102 @@ +# 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 base64 +import json +import logging +import os +import sqlite3 +from pathlib import Path +from sqlite3 import DatabaseError +from threading import Lock +from typing import Union + +from .memory_storage import MemoryStorage + +log = logging.getLogger(__name__) + + +class FileStorage(MemoryStorage): + FILE_EXTENSION = ".session" + + def __init__(self, name: str, workdir: Path): + super().__init__(name) + + self.workdir = workdir + self.database = workdir / (self.name + self.FILE_EXTENSION) + self.conn = None # type: sqlite3.Connection + self.lock = Lock() + + # noinspection PyAttributeOutsideInit + def migrate_from_json(self, path: Union[str, Path]): + log.warning("JSON session storage detected! Pyrogram will now convert it into an SQLite session storage...") + + with open(path, encoding="utf-8") as f: + json_session = json.load(f) + + os.remove(path) + + self.open() + + self.dc_id = json_session["dc_id"] + self.test_mode = json_session["test_mode"] + self.auth_key = base64.b64decode("".join(json_session["auth_key"])) + self.user_id = json_session["user_id"] + self.date = json_session.get("date", 0) + self.is_bot = json_session.get("is_bot", False) + + peers_by_id = json_session.get("peers_by_id", {}) + peers_by_phone = json_session.get("peers_by_phone", {}) + + peers = {} + + for k, v in peers_by_id.items(): + if v is None: + type_ = "group" + elif k.startswith("-100"): + type_ = "channel" + else: + type_ = "user" + + peers[int(k)] = [int(k), int(v) if v is not None else None, type_, None, None] + + for k, v in peers_by_phone.items(): + peers[v][4] = k + + # noinspection PyTypeChecker + self.update_peers(peers.values()) + + log.warning("Done! The session has been successfully converted from JSON to SQLite storage") + + def open(self): + database_exists = os.path.isfile(self.database) + + self.conn = sqlite3.connect( + str(self.database), + timeout=1, + check_same_thread=False + ) + + try: + if not database_exists: + self.create() + + with self.conn: + self.conn.execute("VACUUM") + except DatabaseError: + self.migrate_from_json(self.database) diff --git a/pyrogram/client/storage/memory_storage.py b/pyrogram/client/storage/memory_storage.py new file mode 100644 index 00000000..7eb3a7d0 --- /dev/null +++ b/pyrogram/client/storage/memory_storage.py @@ -0,0 +1,241 @@ +# 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 base64 +import inspect +import logging +import sqlite3 +import struct +import time +from pathlib import Path +from threading import Lock +from typing import List, Tuple + +from pyrogram.api import types +from pyrogram.client.storage.storage import Storage + +log = logging.getLogger(__name__) + + +class MemoryStorage(Storage): + SCHEMA_VERSION = 1 + USERNAME_TTL = 8 * 60 * 60 + SESSION_STRING_FMT = ">B?256sI?" + SESSION_STRING_SIZE = 351 + + def __init__(self, name: str): + super().__init__(name) + + self.conn = None # type: sqlite3.Connection + self.lock = Lock() + + def create(self): + with self.lock, self.conn: + with open(Path(__file__).parent / "schema.sql", "r") as schema: + self.conn.executescript(schema.read()) + + self.conn.execute( + "INSERT INTO version VALUES (?)", + (self.SCHEMA_VERSION,) + ) + + self.conn.execute( + "INSERT INTO sessions VALUES (?, ?, ?, ?, ?, ?)", + (1, None, None, 0, None, None) + ) + + def _import_session_string(self, string_session: str): + decoded = base64.urlsafe_b64decode(string_session + "=" * (-len(string_session) % 4)) + return struct.unpack(self.SESSION_STRING_FMT, decoded) + + def export_session_string(self): + packed = struct.pack( + self.SESSION_STRING_FMT, + self.dc_id, + self.test_mode, + self.auth_key, + self.user_id, + self.is_bot + ) + + return base64.urlsafe_b64encode(packed).decode().rstrip("=") + + # noinspection PyAttributeOutsideInit + def open(self): + self.conn = sqlite3.connect(":memory:", check_same_thread=False) + self.create() + + if self.name != ":memory:": + imported_session_string = self._import_session_string(self.name) + + self.dc_id, self.test_mode, self.auth_key, self.user_id, self.is_bot = imported_session_string + self.date = 0 + + self.name = ":memory:" + str(self.user_id or "") + + # noinspection PyAttributeOutsideInit + def save(self): + self.date = int(time.time()) + + with self.lock: + self.conn.commit() + + def close(self): + with self.lock: + self.conn.close() + + def update_peers(self, peers: List[Tuple[int, int, str, str, str]]): + with self.lock: + self.conn.executemany( + "REPLACE INTO peers (id, access_hash, type, username, phone_number)" + "VALUES (?, ?, ?, ?, ?)", + peers + ) + + def clear_peers(self): + with self.lock, self.conn: + self.conn.execute( + "DELETE FROM peers" + ) + + @staticmethod + def _get_input_peer(peer_id: int, access_hash: int, peer_type: str): + if peer_type in ["user", "bot"]: + return types.InputPeerUser( + user_id=peer_id, + access_hash=access_hash + ) + + if peer_type == "group": + return types.InputPeerChat( + chat_id=-peer_id + ) + + if peer_type in ["channel", "supergroup"]: + return types.InputPeerChannel( + channel_id=int(str(peer_id)[4:]), + access_hash=access_hash + ) + + raise ValueError("Invalid peer type") + + def get_peer_by_id(self, peer_id: int): + r = self.conn.execute( + "SELECT id, access_hash, type FROM peers WHERE id = ?", + (peer_id,) + ).fetchone() + + if r is None: + raise KeyError("ID not found") + + return self._get_input_peer(*r) + + def get_peer_by_username(self, username: str): + r = self.conn.execute( + "SELECT id, access_hash, type, last_update_on FROM peers WHERE username = ?", + (username,) + ).fetchone() + + if r is None: + raise KeyError("Username not found") + + if abs(time.time() - r[3]) > self.USERNAME_TTL: + raise KeyError("Username expired") + + return self._get_input_peer(*r[:3]) + + def get_peer_by_phone_number(self, phone_number: str): + r = self.conn.execute( + "SELECT id, access_hash, type FROM peers WHERE phone_number = ?", + (phone_number,) + ).fetchone() + + if r is None: + raise KeyError("Phone number not found") + + return self._get_input_peer(*r) + + @property + def peers_count(self): + return self.conn.execute( + "SELECT COUNT(*) FROM peers" + ).fetchone()[0] + + def _get(self): + attr = inspect.stack()[1].function + + return self.conn.execute( + "SELECT {} FROM sessions".format(attr) + ).fetchone()[0] + + def _set(self, value): + attr = inspect.stack()[1].function + + with self.lock, self.conn: + self.conn.execute( + "UPDATE sessions SET {} = ?".format(attr), + (value,) + ) + + @property + def dc_id(self): + return self._get() + + @dc_id.setter + def dc_id(self, value): + self._set(value) + + @property + def test_mode(self): + return self._get() + + @test_mode.setter + def test_mode(self, value): + self._set(value) + + @property + def auth_key(self): + return self._get() + + @auth_key.setter + def auth_key(self, value): + self._set(value) + + @property + def date(self): + return self._get() + + @date.setter + def date(self, value): + self._set(value) + + @property + def user_id(self): + return self._get() + + @user_id.setter + def user_id(self, value): + self._set(value) + + @property + def is_bot(self): + return self._get() + + @is_bot.setter + def is_bot(self, value): + self._set(value) diff --git a/pyrogram/client/storage/schema.sql b/pyrogram/client/storage/schema.sql new file mode 100644 index 00000000..1f5af6d2 --- /dev/null +++ b/pyrogram/client/storage/schema.sql @@ -0,0 +1,34 @@ +CREATE TABLE sessions ( + dc_id INTEGER PRIMARY KEY, + test_mode INTEGER, + auth_key BLOB, + date INTEGER NOT NULL, + user_id INTEGER, + is_bot INTEGER +); + +CREATE TABLE peers ( + id INTEGER PRIMARY KEY, + access_hash INTEGER, + type INTEGER NOT NULL, + username TEXT, + phone_number TEXT, + last_update_on INTEGER NOT NULL DEFAULT (CAST(STRFTIME('%s', 'now') AS INTEGER)) +); + +CREATE TABLE version ( + number INTEGER PRIMARY KEY +); + +CREATE INDEX idx_peers_id ON peers (id); +CREATE INDEX idx_peers_username ON peers (username); +CREATE INDEX idx_peers_phone_number ON peers (phone_number); + +CREATE TRIGGER trg_peers_last_update_on + AFTER UPDATE + ON peers + BEGIN + UPDATE peers + SET last_update_on = CAST(STRFTIME('%s', 'now') AS INTEGER) + WHERE id = NEW.id; + END; \ No newline at end of file From 8465c4a97798de7d09bab1048d4154c470d59278 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 19 Jun 2019 16:06:37 +0200 Subject: [PATCH 65/73] Instruct Python to add schema.sql file to the package --- MANIFEST.in | 2 +- setup.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 97d04588..79c547f6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,7 @@ ## Include include README.md COPYING COPYING.lesser NOTICE requirements.txt recursive-include compiler *.py *.tl *.tsv *.txt -recursive-include pyrogram mime.types +recursive-include pyrogram mime.types schema.sql ## Exclude prune pyrogram/api/errors/exceptions diff --git a/setup.py b/setup.py index 146dae9e..d4255e03 100644 --- a/setup.py +++ b/setup.py @@ -168,7 +168,8 @@ setup( python_requires="~=3.4", packages=find_packages(exclude=["compiler*"]), package_data={ - "pyrogram.client.ext": ["mime.types"] + "pyrogram.client.ext": ["mime.types"], + "pyrogram.client.storage": ["schema.sql"] }, zip_safe=False, install_requires=requires, From edaced35a758a84dff17328cb0095cfb525b5449 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 19 Jun 2019 16:07:22 +0200 Subject: [PATCH 66/73] Use base64.urlsafe_b64encode/decode instead of manually passing altchars --- pyrogram/client/ext/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyrogram/client/ext/utils.py b/pyrogram/client/ext/utils.py index fa107fab..e0a797e2 100644 --- a/pyrogram/client/ext/utils.py +++ b/pyrogram/client/ext/utils.py @@ -18,16 +18,16 @@ import base64 import struct -from base64 import b64decode, b64encode from typing import Union, List import pyrogram + from . import BaseClient from ...api import types def decode(s: str) -> bytes: - s = b64decode(s + "=" * (-len(s) % 4), "-_") + s = base64.urlsafe_b64decode(s + "=" * (-len(s) % 4)) r = b"" assert s[-1] == 2 @@ -59,7 +59,7 @@ def encode(s: bytes) -> str: r += bytes([i]) - return b64encode(r, b"-_").decode().rstrip("=") + return base64.urlsafe_b64encode(r).decode().rstrip("=") def get_peer_id(input_peer) -> int: From 30192de1ad493acd896448e727443d7b03e65761 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 19 Jun 2019 16:10:37 +0200 Subject: [PATCH 67/73] Update pyrogram/client to accommodate Storage Engines --- pyrogram/client/client.py | 231 ++++++++++-------- pyrogram/client/ext/base_client.py | 8 +- pyrogram/client/ext/syncer.py | 15 +- .../client/methods/contacts/get_contacts.py | 1 - 4 files changed, 141 insertions(+), 114 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 885c3334..2d18d178 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -import binascii import logging import math import mimetypes @@ -51,10 +50,7 @@ from pyrogram.errors import ( from pyrogram.session import Auth, Session from .ext import utils, Syncer, BaseClient, Dispatcher from .methods import Methods -from .session_storage import ( - SessionDoesNotExist, SessionStorage, MemorySessionStorage, JsonSessionStorage, - StringSessionStorage, SQLiteSessionStorage -) +from .storage import Storage, FileStorage, MemoryStorage log = logging.getLogger(__name__) @@ -64,8 +60,13 @@ class Client(Methods, BaseClient): Parameters: session_name (``str``): - Name to uniquely identify a session of either a User or a Bot, e.g.: "my_account". This name will be used - to save a file to disk that stores details needed for reconnecting without asking again for credentials. + Pass a string of your choice to give a name to the client session, e.g.: "*my_account*". This name will be + used to save a file on disk that stores details needed to reconnect without asking again for credentials. + Alternatively, if you don't want a file to be saved on disk, pass the special name "**:memory:**" to start + an in-memory session that will be discarded as soon as you stop the Client. In order to reconnect again + using a memory storage without having to login again, you can use + :meth:`~pyrogram.Client.export_session_string` before stopping the client to get a session string you can + pass here as argument. api_id (``int``, *optional*): The *api_id* part of your Telegram API Key, as integer. E.g.: 12345 @@ -179,7 +180,7 @@ class Client(Methods, BaseClient): def __init__( self, - session_name: str, + session_name: Union[str, Storage], api_id: Union[int, str] = None, api_hash: str = None, app_version: str = None, @@ -226,12 +227,23 @@ class Client(Methods, BaseClient): self.first_name = first_name self.last_name = last_name self.workers = workers - self.workdir = workdir - self.config_file = config_file + self.workdir = Path(workdir) + self.config_file = Path(config_file) self.plugins = plugins self.no_updates = no_updates self.takeout = takeout + if isinstance(session_name, str): + if session_name == ":memory:" or len(session_name) >= MemoryStorage.SESSION_STRING_SIZE: + session_name = re.sub(r"[\n\s]+", "", session_name) + self.storage = MemoryStorage(session_name) + else: + self.storage = FileStorage(session_name, self.workdir) + elif isinstance(session_name, Storage): + self.storage = session_name + else: + raise ValueError("Unknown storage engine") + self.dispatcher = Dispatcher(self, workers) def __enter__(self): @@ -266,50 +278,32 @@ class Client(Methods, BaseClient): if self.is_started: raise ConnectionError("Client has already been started") - if isinstance(self.session_storage, JsonSessionStorage): - if self.BOT_TOKEN_RE.match(self.session_storage._session_name): - self.session_storage.is_bot = True - self.bot_token = self.session_storage._session_name - self.session_storage._session_name = self.session_storage._session_name.split(":")[0] - warnings.warn('\nWARNING: You are using a bot token as session name!\n' - 'This usage will be deprecated soon. Please use a session file name to load ' - 'an existing session and the bot_token argument to create new sessions.\n' - 'More info: https://docs.pyrogram.org/intro/auth#bot-authorization\n') - self.load_config() self.load_session() self.load_plugins() - self.session = Session( - self, - self.session_storage.dc_id, - self.session_storage.auth_key - ) + self.session = Session(self, self.storage.dc_id, self.storage.auth_key) self.session.start() self.is_started = True try: - if self.session_storage.user_id is None: + if self.storage.user_id is None: if self.bot_token is None: - self.is_bot = False + self.storage.is_bot = False self.authorize_user() else: - self.session_storage.is_bot = True + self.storage.is_bot = True self.authorize_bot() - self.save_session() - - if not self.session_storage.is_bot: + if not self.storage.is_bot: 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.session_storage.date) > Client.OFFLINE_SLEEP: - self.session_storage.clear_cache() - + if abs(now - self.storage.date) > Client.OFFLINE_SLEEP: self.get_initial_dialogs() self.get_contacts() else: @@ -508,20 +502,15 @@ class Client(Methods, BaseClient): except UserMigrate as e: self.session.stop() - self.session_storage.dc_id = e.x - self.session_storage.auth_key = Auth(self.session_storage.dc_id, self.session_storage.test_mode, - self.ipv6, self._proxy).create() - - self.session = Session( - self, - self.session_storage.dc_id, - self.session_storage.auth_key - ) + self.storage.dc_id = e.x + self.storage.auth_key = Auth(self, self.storage.dc_id).create() + self.session = Session(self, self.storage.dc_id, self.storage.auth_key) self.session.start() + self.authorize_bot() else: - self.session_storage.user_id = r.user.id + self.storage.user_id = r.user.id print("Logged in successfully as @{}".format(r.user.username)) @@ -562,20 +551,10 @@ class Client(Methods, BaseClient): except (PhoneMigrate, NetworkMigrate) as e: self.session.stop() - self.session_storage.dc_id = e.x + self.storage.dc_id = e.x + self.storage.auth_key = Auth(self, self.storage.dc_id).create() - self.session_storage.auth_key = Auth( - self.session_storage.dc_id, - self.session_storage.test_mode, - self.ipv6, - self._proxy - ).create() - - self.session = Session( - self, - self.session_storage.dc_id, - self.session_storage.auth_key - ) + self.session = Session(self, self.storage.dc_id, self.storage.auth_key) self.session.start() except (PhoneNumberInvalid, PhoneNumberBanned) as e: @@ -755,13 +734,13 @@ class Client(Methods, BaseClient): ) self.password = None - self.session_storage.user_id = r.user.id + self.storage.user_id = r.user.id print("Logged in successfully as {}".format(r.user.first_name)) def fetch_peers( self, - entities: List[ + peers: List[ Union[ types.User, types.Chat, types.ChatForbidden, @@ -770,11 +749,57 @@ class Client(Methods, BaseClient): ] ) -> bool: is_min = False + parsed_peers = [] - for entity in entities: - if isinstance(entity, (types.User, types.Channel, types.ChannelForbidden)) and not entity.access_hash: + for peer in peers: + username = None + phone_number = None + + if isinstance(peer, types.User): + peer_id = peer.id + access_hash = peer.access_hash + + username = peer.username + phone_number = peer.phone + + if peer.bot: + peer_type = "bot" + else: + peer_type = "user" + + if access_hash is None: + is_min = True + continue + + if username is not None: + username = username.lower() + elif isinstance(peer, (types.Chat, types.ChatForbidden)): + peer_id = -peer.id + access_hash = 0 + peer_type = "group" + elif isinstance(peer, (types.Channel, types.ChannelForbidden)): + peer_id = int("-100" + str(peer.id)) + access_hash = peer.access_hash + + username = getattr(peer, "username", None) + + if peer.broadcast: + peer_type = "channel" + else: + peer_type = "supergroup" + + if access_hash is None: + is_min = True + continue + + if username is not None: + username = username.lower() + else: continue - self.session_storage.cache_peer(entity) + + parsed_peers.append((peer_id, access_hash, peer_type, username, phone_number)) + + self.storage.update_peers(parsed_peers) return is_min @@ -1035,12 +1060,23 @@ class Client(Methods, BaseClient): self.plugins = None def load_session(self): - try: - self.session_storage.load() - except SessionDoesNotExist: - log.info('Could not load session "{}", initiate new one'.format(self.session_name)) - self.session_storage.auth_key = Auth(self.session_storage.dc_id, self.session_storage.test_mode, - self.ipv6, self._proxy).create() + self.storage.open() + + session_empty = any([ + self.storage.test_mode is None, + self.storage.auth_key is None, + self.storage.user_id is None, + self.storage.is_bot is None + ]) + + if session_empty: + self.storage.dc_id = 1 + self.storage.date = 0 + + self.storage.test_mode = self.test_mode + self.storage.auth_key = Auth(self, self.storage.dc_id).create() + self.storage.user_id = None + self.storage.is_bot = None def load_plugins(self): if self.plugins: @@ -1164,9 +1200,6 @@ class Client(Methods, BaseClient): log.warning('[{}] No plugin loaded from "{}"'.format( self.session_name, root)) - def save_session(self): - self.session_storage.save() - def get_initial_dialogs_chunk(self, offset_date: int = 0): while True: try: @@ -1184,7 +1217,7 @@ class Client(Methods, BaseClient): log.warning("get_dialogs flood: waiting {} seconds".format(e.x)) time.sleep(e.x) else: - log.info("Total peers: {}".format(self.session_storage.peers_count())) + log.info("Total peers: {}".format(self.storage.peers_count)) return r def get_initial_dialogs(self): @@ -1222,7 +1255,7 @@ class Client(Methods, BaseClient): KeyError: In case the peer doesn't exist in the internal database. """ try: - return self.session_storage.get_peer_by_id(peer_id) + return self.storage.get_peer_by_id(peer_id) except KeyError: if type(peer_id) is str: if peer_id in ("self", "me"): @@ -1234,7 +1267,7 @@ class Client(Methods, BaseClient): int(peer_id) except ValueError: try: - self.session_storage.get_peer_by_username(peer_id) + return self.storage.get_peer_by_username(peer_id) except KeyError: self.send( functions.contacts.ResolveUsername( @@ -1242,10 +1275,10 @@ class Client(Methods, BaseClient): ) ) - return self.session_storage.get_peer_by_username(peer_id) + return self.storage.get_peer_by_username(peer_id) else: try: - return self.session_storage.get_peer_by_phone(peer_id) + return self.storage.get_peer_by_phone_number(peer_id) except KeyError: raise PeerIdInvalid @@ -1253,7 +1286,10 @@ class Client(Methods, BaseClient): self.fetch_peers( self.send( functions.users.GetUsers( - id=[types.InputUser(user_id=peer_id, access_hash=0)] + id=[types.InputUser( + user_id=peer_id, + access_hash=0 + )] ) ) ) @@ -1261,7 +1297,10 @@ class Client(Methods, BaseClient): if str(peer_id).startswith("-100"): self.send( functions.channels.GetChannels( - id=[types.InputChannel(channel_id=int(str(peer_id)[4:]), access_hash=0)] + id=[types.InputChannel( + channel_id=int(str(peer_id)[4:]), + access_hash=0 + )] ) ) else: @@ -1272,7 +1311,7 @@ class Client(Methods, BaseClient): ) try: - return self.session_storage.get_peer_by_id(peer_id) + return self.storage.get_peer_by_id(peer_id) except KeyError: raise PeerIdInvalid @@ -1347,7 +1386,7 @@ class Client(Methods, BaseClient): file_id = file_id or self.rnd_id() md5_sum = md5() if not is_big and not is_missing_part else None - session = Session(self, self.session_storage.dc_id, self.session_storage.auth_key, is_media=True) + session = Session(self, self.storage.dc_id, self.storage.auth_key, is_media=True) session.start() try: @@ -1433,19 +1472,14 @@ class Client(Methods, BaseClient): session = self.media_sessions.get(dc_id, None) if session is None: - if dc_id != self.session_storage.dc_id: + if dc_id != self.storage.dc_id: exported_auth = self.send( functions.auth.ExportAuthorization( dc_id=dc_id ) ) - session = Session( - self, - dc_id, - Auth(dc_id, self.session_storage.test_mode, self.ipv6, self._proxy).create(), - is_media=True - ) + session = Session(self, dc_id, Auth(self, dc_id).create(), is_media=True) session.start() @@ -1458,12 +1492,7 @@ class Client(Methods, BaseClient): ) ) else: - session = Session( - self, - dc_id, - self.session_storage.auth_key, - is_media=True - ) + session = Session(self, dc_id, self.storage.auth_key, is_media=True) session.start() @@ -1548,13 +1577,7 @@ class Client(Methods, BaseClient): cdn_session = self.media_sessions.get(r.dc_id, None) if cdn_session is None: - cdn_session = Session( - self, - r.dc_id, - Auth(r.dc_id, self.session_storage.test_mode, self.ipv6, self._proxy).create(), - is_media=True, - is_cdn=True - ) + cdn_session = Session(self, r.dc_id, Auth(self, r.dc_id).create(), is_media=True, is_cdn=True) cdn_session.start() @@ -1650,3 +1673,11 @@ class Client(Methods, BaseClient): if extensions: return extensions.split(" ")[0] + + def export_session_string(self): + """Export the current session as serialized string. + + Returns: + ``str``: The session serialized into a printable, url-safe string. + """ + return self.storage.export_session_string() diff --git a/pyrogram/client/ext/base_client.py b/pyrogram/client/ext/base_client.py index 9276b0eb..88623f4a 100644 --- a/pyrogram/client/ext/base_client.py +++ b/pyrogram/client/ext/base_client.py @@ -87,13 +87,13 @@ class BaseClient: mime_types_to_extensions[mime_type] = " ".join(extensions) - def __init__(self, session_storage: SessionStorage): - self.session_storage = session_storage + def __init__(self): + self.storage = None self.rnd_id = MsgId - self.markdown = Markdown(self.session_storage, self) - self.html = HTML(self.session_storage, self) + self.markdown = Markdown(self) + self.html = HTML(self) self.session = None self.media_sessions = {} diff --git a/pyrogram/client/ext/syncer.py b/pyrogram/client/ext/syncer.py index 9e7d2303..42e1f95a 100644 --- a/pyrogram/client/ext/syncer.py +++ b/pyrogram/client/ext/syncer.py @@ -16,16 +16,10 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -import base64 -import json import logging -import os -import shutil import time from threading import Thread, Event, Lock -from . import utils - log = logging.getLogger(__name__) @@ -81,10 +75,13 @@ class Syncer: @classmethod def sync(cls, client): - client.session_storage.date = int(time.time()) try: - client.session_storage.save(sync=True) + start = time.time() + client.storage.save() except Exception as e: log.critical(e, exc_info=True) else: - log.info("Synced {}".format(client.session_name)) + log.info('Synced "{}" in {:.6} ms'.format( + client.storage.name, + (time.time() - start) * 1000 + )) diff --git a/pyrogram/client/methods/contacts/get_contacts.py b/pyrogram/client/methods/contacts/get_contacts.py index 79677563..40cb344e 100644 --- a/pyrogram/client/methods/contacts/get_contacts.py +++ b/pyrogram/client/methods/contacts/get_contacts.py @@ -46,5 +46,4 @@ class GetContacts(BaseClient): log.warning("get_contacts flood: waiting {} seconds".format(e.x)) time.sleep(e.x) else: - log.info("Total contacts: {}".format(self.session_storage.contacts_count())) return pyrogram.List(pyrogram.User._parse(self, user) for user in contacts.users) From 0be0e2da5615cd9a403889ce487d3e8878451949 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 19 Jun 2019 16:11:25 +0200 Subject: [PATCH 68/73] Add export_session_string method to docs --- docs/source/api/methods.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/api/methods.rst b/docs/source/api/methods.rst index 4a3eefd8..2a08b37f 100644 --- a/docs/source/api/methods.rst +++ b/docs/source/api/methods.rst @@ -32,6 +32,7 @@ Utilities - :meth:`~Client.add_handler` - :meth:`~Client.remove_handler` - :meth:`~Client.stop_transmission` + - :meth:`~Client.export_session_string` Messages ^^^^^^^^ @@ -186,6 +187,7 @@ Details .. automethod:: Client.add_handler() .. automethod:: Client.remove_handler() .. automethod:: Client.stop_transmission() +.. automethod:: Client.export_session_string() .. Messages .. automethod:: Client.send_message() From 1f04ce38fc4f62ec786efb9c8080993bdfe127f2 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 19 Jun 2019 16:11:53 +0200 Subject: [PATCH 69/73] Fix glossary term --- docs/source/glossary.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index bcb1193c..d5a1bffd 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -58,7 +58,7 @@ Terms Pyrogram --- to automate some behaviours, like sending messages or reacting to text commands or any other event. Session - Also known as *login session*, is a strictly personal piece of information created and held by both parties + Also known as *login session*, is a strictly personal piece of data created and held by both parties (client and server) which is used to grant permission into a single account without having to start a new authorization process from scratch. From d1cd21916a7d20f71afa78767ca660f9f3fa8c8d Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 19 Jun 2019 16:12:06 +0200 Subject: [PATCH 70/73] Add storage-engines.rst page to docs --- docs/source/index.rst | 1 + docs/source/topics/storage-engines.rst | 95 ++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 docs/source/topics/storage-engines.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 0bc175ee..b9682827 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -130,6 +130,7 @@ Meta topics/auto-auth topics/session-settings topics/tgcrypto + topics/storage-engines topics/text-formatting topics/serialize topics/proxy diff --git a/docs/source/topics/storage-engines.rst b/docs/source/topics/storage-engines.rst new file mode 100644 index 00000000..933a21b3 --- /dev/null +++ b/docs/source/topics/storage-engines.rst @@ -0,0 +1,95 @@ +Storage Engines +=============== + +Every time you login to Telegram, some personal piece of data are created and held by both parties (the client, Pyrogram +and the server, Telegram). This session data is uniquely bound to your own account, indefinitely (until you logout or +decide to manually terminate it) and is used to authorize a client to execute API calls on behalf of your identity. + +Persisting Sessions +------------------- + +In order to make a client reconnect successfully between restarts, that is, without having to start a new +authorization process from scratch each time, Pyrogram needs to store the generated session data somewhere. + +Other useful data being stored is peers' cache. In short, peers are all those entities you can chat with, such as users +or bots, basic groups, but also channels and supergroups. Because of how Telegram works, a unique pair of **id** and +**access_hash** is needed to contact a peer. This, plus other useful info such as the peer type, is what is stored +inside a session storage. + +So, if you ever wondered how is Pyrogram able to contact peers just by asking for their ids, it's because of this very +reason: the peer *id* is looked up in the internal database and the available *access_hash* is retrieved, which is then +used to correctly invoke API methods. + +Different Storage Engines +------------------------- + +Let's now talk about how Pyrogram actually stores all the relevant data. Pyrogram offers two different types of storage +engines: a **File Storage** and a **Memory Storage**. These engines are well integrated in the library and require a +minimal effort to set up. Here's how they work: + +File Storage +^^^^^^^^^^^^ + +This is the most common storage engine. It is implemented by using **SQLite**, which will store the session and peers +details. The database will be saved to disk as a single portable file and is designed to efficiently save and retrieve +peers whenever they are needed. + +To use this type of engine, simply pass any name of your choice to the ``session_name`` parameter of the +:obj:`~pyrogram.Client` constructor, as usual: + +.. code-block:: python + + from pyrogram import Client + + with Client("my_account") as app: + print(app.get_me()) + +Once you successfully log in (either with a user or a bot identity), a session file will be created and saved to disk as +``my_account.session``. Any subsequent client restart will make Pyrogram search for a file named that way and the +session database will be automatically loaded. + +Memory Storage +^^^^^^^^^^^^^^ + +In case you don't want to have any session file saved on disk, you can use an in-memory storage by passing the special +session name "**:memory:**" to the ``session_name`` parameter of the :obj:`~pyrogram.Client` constructor: + +.. code-block:: python + + from pyrogram import Client + + with Client(":memory:") as app: + print(app.get_me()) + +This database is still backed by SQLite, but exists purely in memory. However, once you stop a client, the entire +database is discarded and the session details used for logging in again will be lost forever. + +Session Strings +--------------- + +Session strings are useful when you want to run authorized Pyrogram clients on platforms like +`Heroku `_, where their ephemeral filesystems makes it much harder for a file-based storage +engine to properly work as intended. + +In case you want to use an in-memory storage, but also want to keep access to the session you created, call +:meth:`~pyrogram.Client.export_session_string` anytime before stopping the client... + +.. code-block:: python + + from pyrogram import Client + + with Client(":memory:") as app: + print(app.export_session_string()) + +...and save the resulting string somewhere. You can use this string as session name the next time you want to login +using the same session; the storage used will still be completely in-memory: + +.. code-block:: python + + from pyrogram import Client + + session_string = "...ZnUIFD8jsjXTb8g_vpxx48k1zkov9sapD-tzjz-S4WZv70M..." + + with Client(session_string) as app: + print(app.get_me()) + From fa1976c8a0e73c3acd0cf35ee3f831db5c945e42 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 19 Jun 2019 17:09:07 +0200 Subject: [PATCH 71/73] Update TgCrypto required version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d4255e03..45c2871b 100644 --- a/setup.py +++ b/setup.py @@ -174,7 +174,7 @@ setup( zip_safe=False, install_requires=requires, extras_require={ - "fast": ["tgcrypto==1.1.1"] + "fast": ["tgcrypto==1.2.0"] }, cmdclass={ "clean": Clean, From 8c96e5f46aaca4ca2a1593a6c57a24cc60a766ea Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 20 Jun 2019 03:31:37 +0200 Subject: [PATCH 72/73] Smarter session migration --- pyrogram/client/storage/file_storage.py | 68 ++++++++++++++----------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/pyrogram/client/storage/file_storage.py b/pyrogram/client/storage/file_storage.py index ee5000c5..f52a03a9 100644 --- a/pyrogram/client/storage/file_storage.py +++ b/pyrogram/client/storage/file_storage.py @@ -19,12 +19,9 @@ import base64 import json import logging -import os import sqlite3 from pathlib import Path -from sqlite3 import DatabaseError from threading import Lock -from typing import Union from .memory_storage import MemoryStorage @@ -43,25 +40,18 @@ class FileStorage(MemoryStorage): self.lock = Lock() # noinspection PyAttributeOutsideInit - def migrate_from_json(self, path: Union[str, Path]): - log.warning("JSON session storage detected! Pyrogram will now convert it into an SQLite session storage...") - - with open(path, encoding="utf-8") as f: - json_session = json.load(f) - - os.remove(path) - + def migrate_from_json(self, session_json: dict): self.open() - self.dc_id = json_session["dc_id"] - self.test_mode = json_session["test_mode"] - self.auth_key = base64.b64decode("".join(json_session["auth_key"])) - self.user_id = json_session["user_id"] - self.date = json_session.get("date", 0) - self.is_bot = json_session.get("is_bot", False) + self.dc_id = session_json["dc_id"] + self.test_mode = session_json["test_mode"] + self.auth_key = base64.b64decode("".join(session_json["auth_key"])) + self.user_id = session_json["user_id"] + self.date = session_json.get("date", 0) + self.is_bot = session_json.get("is_bot", False) - peers_by_id = json_session.get("peers_by_id", {}) - peers_by_phone = json_session.get("peers_by_phone", {}) + peers_by_id = session_json.get("peers_by_id", {}) + peers_by_phone = session_json.get("peers_by_phone", {}) peers = {} @@ -81,22 +71,40 @@ class FileStorage(MemoryStorage): # noinspection PyTypeChecker self.update_peers(peers.values()) - log.warning("Done! The session has been successfully converted from JSON to SQLite storage") - def open(self): - database_exists = os.path.isfile(self.database) + path = self.database + file_exists = path.is_file() + + if file_exists: + try: + with open(path, encoding="utf-8") as f: + session_json = json.load(f) + except ValueError: + pass + else: + log.warning("JSON session storage detected! Converting it into an SQLite session storage...") + + path.rename(path.name + ".OLD") + + log.warning('The old session file has been renamed to "{}.OLD"'.format(path.name)) + + self.migrate_from_json(session_json) + + log.warning("Done! The session has been successfully converted from JSON to SQLite storage") + + return + + if Path(path.name + ".OLD").is_file(): + log.warning('Old session file detected: "{}.OLD". You can remove this file now'.format(path.name)) self.conn = sqlite3.connect( - str(self.database), + path, timeout=1, check_same_thread=False ) - try: - if not database_exists: - self.create() + if not file_exists: + self.create() - with self.conn: - self.conn.execute("VACUUM") - except DatabaseError: - self.migrate_from_json(self.database) + with self.conn: + self.conn.execute("VACUUM") From f16ca8b9ea78c8af503fc80f3f7a2ca6cd9495c8 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 20 Jun 2019 03:39:12 +0200 Subject: [PATCH 73/73] Small documentation fixes --- docs/releases.py | 3 +-- docs/source/license.rst | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/releases.py b/docs/releases.py index 0c284f0b..0b566ca7 100644 --- a/docs/releases.py +++ b/docs/releases.py @@ -35,8 +35,7 @@ backwards-incompatible changes made in that version. When upgrading to a new version of Pyrogram, you will need to check all the breaking changes in order to find incompatible code in your application, but also to take advantage of new features and improvements. -Releases --------- +**Contents** """.lstrip("\n") diff --git a/docs/source/license.rst b/docs/source/license.rst index 38302bdc..43f59d73 100644 --- a/docs/source/license.rst +++ b/docs/source/license.rst @@ -2,7 +2,7 @@ About the License ================= .. image:: https://www.gnu.org/graphics/lgplv3-with-text-154x68.png - :align: right + :align: left Pyrogram is free software and is currently licensed under the terms of the `GNU Lesser General Public License v3 or later (LGPLv3+)`_. In short: you may use, redistribute and/or modify it