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..05f342bc 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. 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. 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/compiler/error/source/400_BAD_REQUEST.tsv b/compiler/error/source/400_BAD_REQUEST.tsv index fa6ff67e..8e82c9f6 100644 --- a/compiler/error/source/400_BAD_REQUEST.tsv +++ b/compiler/error/source/400_BAD_REQUEST.tsv @@ -99,4 +99,10 @@ 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 +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 diff --git a/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv b/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv index 4dfe5994..446fe908 100644 --- a/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv +++ b/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv @@ -7,5 +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 \ No newline at end of file +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 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/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 diff --git a/docs/source/api/bound-methods.rst b/docs/source/api/bound-methods.rst index 0622e6b8..e6729da6 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` @@ -59,13 +59,35 @@ Message - :meth:`~Message.reply_video_note` - :meth:`~Message.reply_voice` -CallbackQuery -^^^^^^^^^^^^^ +Chat +^^^^ .. hlist:: :columns: 2 + - :meth:`~Chat.archive` + - :meth:`~Chat.unarchive` + +User +^^^^ + +.. hlist:: + :columns: 2 + + - :meth:`~User.archive` + - :meth:`~User.unarchive` + +CallbackQuery +^^^^^^^^^^^^^ + +.. hlist:: + :columns: 4 + - :meth:`~CallbackQuery.answer` + - :meth:`~CallbackQuery.edit_text` + - :meth:`~CallbackQuery.edit_caption` + - :meth:`~CallbackQuery.edit_media` + - :meth:`~CallbackQuery.edit_reply_markup` InlineQuery ^^^^^^^^^^^ @@ -84,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() @@ -109,8 +131,20 @@ Details .. automethod:: Message.reply_video_note() .. automethod:: Message.reply_voice() +.. Chat +.. automethod:: Chat.archive() +.. automethod:: Chat.unarchive() + +.. User +.. automethod:: User.archive() +.. automethod:: User.unarchive() + .. CallbackQuery .. automethod:: CallbackQuery.answer() +.. automethod:: CallbackQuery.edit_text() +.. automethod:: CallbackQuery.edit_caption() +.. automethod:: CallbackQuery.edit_media() +.. automethod:: CallbackQuery.edit_reply_markup() .. InlineQuery .. automethod:: InlineQuery.answer() 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/api/methods.rst b/docs/source/api/methods.rst index ed150e4c..2a08b37f 100644 --- a/docs/source/api/methods.rst +++ b/docs/source/api/methods.rst @@ -31,10 +31,8 @@ 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` + - :meth:`~Client.export_session_string` Messages ^^^^^^^^ @@ -58,11 +56,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` @@ -104,6 +106,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 ^^^^^ @@ -157,6 +161,18 @@ Bots - :meth:`~Client.set_game_score` - :meth:`~Client.get_game_high_scores` +Advanced Usage (Raw API) +^^^^^^^^^^^^^^^^^^^^^^^^ + +Learn more about these methods at :doc:`Advanced Usage <../topics/advanced-usage>`. + +.. hlist:: + :columns: 4 + + - :meth:`~Client.send` + - :meth:`~Client.resolve_peer` + - :meth:`~Client.save_file` + ----- Details @@ -170,10 +186,8 @@ 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() +.. automethod:: Client.export_session_string() .. Messages .. automethod:: Client.send_message() @@ -195,8 +209,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() @@ -233,6 +251,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() @@ -265,3 +285,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 diff --git a/docs/source/api/types.rst b/docs/source/api/types.rst index 4eef9638..644f8bb2 100644 --- a/docs/source/api/types.rst +++ b/docs/source/api/types.rst @@ -30,10 +30,8 @@ Users & Chats - :class:`ChatPreview` - :class:`ChatPhoto` - :class:`ChatMember` - - :class:`ChatMembers` - :class:`ChatPermissions` - :class:`Dialog` - - :class:`Dialogs` Messages & Media ^^^^^^^^^^^^^^^^ @@ -42,10 +40,8 @@ Messages & Media :columns: 5 - :class:`Message` - - :class:`Messages` - :class:`MessageEntity` - :class:`Photo` - - :class:`ProfilePhotos` - :class:`Thumbnail` - :class:`Audio` - :class:`Document` @@ -61,8 +57,8 @@ Messages & Media - :class:`Poll` - :class:`PollOption` -Keyboards -^^^^^^^^^ +Bots & Keyboards +^^^^^^^^^^^^^^^^ .. hlist:: :columns: 4 @@ -75,7 +71,6 @@ Keyboards - :class:`ForceReply` - :class:`CallbackQuery` - :class:`GameHighScore` - - :class:`GameHighScores` - :class:`CallbackGame` Input Media @@ -123,17 +118,13 @@ Details .. autoclass:: ChatPreview() .. autoclass:: ChatPhoto() .. autoclass:: ChatMember() -.. autoclass:: ChatMembers() .. autoclass:: ChatPermissions() .. autoclass:: Dialog() -.. autoclass:: Dialogs() .. Messages & Media .. autoclass:: Message() -.. autoclass:: Messages() .. autoclass:: MessageEntity() .. autoclass:: Photo() -.. autoclass:: ProfilePhotos() .. autoclass:: Thumbnail() .. autoclass:: Audio() .. autoclass:: Document() @@ -149,7 +140,7 @@ Details .. autoclass:: Poll() .. autoclass:: PollOption() -.. Keyboards +.. Bots & Keyboards .. autoclass:: ReplyKeyboardMarkup() .. autoclass:: KeyboardButton() .. autoclass:: ReplyKeyboardRemove() @@ -158,7 +149,6 @@ Details .. autoclass:: ForceReply() .. autoclass:: CallbackQuery() .. autoclass:: GameHighScore() -.. autoclass:: GameHighScores() .. autoclass:: CallbackGame() .. Input Media diff --git a/docs/source/faq.rst b/docs/source/faq.rst index 1800a032..449076af 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? ------------------------------------------ @@ -134,20 +150,60 @@ 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? +--------------------------------------------------- + +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. + +.. 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. +--------------------------------------------- + +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 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/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. 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()) + diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index 6cd63655..c444ca0f 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-asyncio" +__version__ = "0.15.0-asyncio" __license__ = "GNU Lesser General Public License v3 or later (LGPLv3+)" __copyright__ = "Copyright (C) 2017-2019 Dan " diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 98bcb8c5..1f1562d1 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -29,7 +29,6 @@ import shutil import tempfile import time from configparser import ConfigParser -from datetime import datetime from hashlib import sha256, md5 from importlib import import_module from pathlib import Path @@ -54,6 +53,7 @@ from pyrogram.session import Auth, Session from .ext import utils, Syncer, BaseClient, Dispatcher from .ext.utils import ainput from .methods import Methods +from .storage import Storage, FileStorage, MemoryStorage log = logging.getLogger(__name__) @@ -63,8 +63,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 @@ -178,7 +183,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, @@ -225,12 +230,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): @@ -271,50 +287,32 @@ 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] - log.warning('\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() await self.load_session() self.load_plugins() - self.session = Session( - self, - self.dc_id, - self.auth_key - ) + self.session = Session(self, self.storage.dc_id, self.storage.auth_key) await self.session.start() self.is_started = True try: - if self.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 await self.authorize_user() else: - self.is_bot = True + self.storage.is_bot = True await self.authorize_bot() - self.save_session() - - if not self.is_bot: + if not self.storage.is_bot: if self.takeout: self.takeout_id = (await 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 = {} - self.peers_by_phone = {} - + if abs(now - self.storage.date) > Client.OFFLINE_SLEEP: await self.get_initial_dialogs() await self.get_contacts() else: @@ -516,19 +514,14 @@ class Client(Methods, BaseClient): except UserMigrate as e: await self.session.stop() - self.dc_id = e.x - self.auth_key = await Auth(self.dc_id, self.test_mode, self.ipv6, self._proxy).create() - - self.session = Session( - self, - self.dc_id, - self.auth_key - ) + self.storage.dc_id = e.x + self.storage.auth_key = await Auth(self, self.storage.dc_id).create() + self.session = Session(self, self.storage.dc_id, self.storage.auth_key) await self.session.start() await self.authorize_bot() else: - self.user_id = r.user.id + self.storage.user_id = r.user.id print("Logged in successfully as @{}".format(r.user.username)) @@ -569,20 +562,10 @@ class Client(Methods, BaseClient): except (PhoneMigrate, NetworkMigrate) as e: await self.session.stop() - self.dc_id = e.x + self.storage.dc_id = e.x + self.storage.auth_key = await Auth(self, self.storage.dc_id).create() - self.auth_key = await Auth( - self.dc_id, - self.test_mode, - self.ipv6, - self._proxy - ).create() - - self.session = Session( - self, - self.dc_id, - self.auth_key - ) + self.session = Session(self, self.storage.dc_id, self.storage.auth_key) await self.session.start() except (PhoneNumberInvalid, PhoneNumberBanned) as e: @@ -762,13 +745,13 @@ class Client(Methods, BaseClient): ) self.password = None - self.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, @@ -777,64 +760,57 @@ class Client(Methods, BaseClient): ] ) -> bool: is_min = False + parsed_peers = [] - for entity in entities: - if isinstance(entity, types.User): - user_id = entity.id + for peer in peers: + username = None + phone_number = None - access_hash = entity.access_hash + 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 - username = entity.username - phone = entity.phone - - input_peer = types.InputPeerUser( - user_id=user_id, - access_hash=access_hash - ) - - self.peers_by_id[user_id] = input_peer - if username is not None: - self.peers_by_username[username.lower()] = input_peer + 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 - if phone is not None: - self.peers_by_phone[phone] = input_peer + username = getattr(peer, "username", None) - if isinstance(entity, (types.Chat, types.ChatForbidden)): - chat_id = entity.id - peer_id = -chat_id - - input_peer = types.InputPeerChat( - chat_id=chat_id - ) - - self.peers_by_id[peer_id] = input_peer - - if isinstance(entity, (types.Channel, types.ChannelForbidden)): - channel_id = entity.id - peer_id = int("-100" + str(channel_id)) - - access_hash = entity.access_hash + if peer.broadcast: + peer_type = "channel" + else: + peer_type = "supergroup" if access_hash is None: is_min = True continue - username = getattr(entity, "username", None) - - input_peer = types.InputPeerChannel( - channel_id=channel_id, - access_hash=access_hash - ) - - self.peers_by_id[peer_id] = input_peer - if username is not None: - self.peers_by_username[username.lower()] = input_peer + username = username.lower() + else: + continue + + parsed_peers.append((peer_id, access_hash, peer_type, username, phone_number)) + + self.storage.update_peers(parsed_peers) return is_min @@ -849,37 +825,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] - - if not data.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 = await self.get_file( media_type=data.media_type, @@ -1092,71 +1038,75 @@ 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 async 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.auth_key = await 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) - # TODO: replace default with False once token session name will be deprecated - self.is_bot = s.get("is_bot", self.is_bot) + self.storage.open() - for k, v in s.get("peers_by_id", {}).items(): - self.peers_by_id[int(k)] = utils.get_input_peer(int(k), v) + 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 + ]) - for k, v in s.get("peers_by_username", {}).items(): - peer = self.peers_by_id.get(v, None) + if session_empty: + self.storage.dc_id = 1 + self.storage.date = 0 - 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 + self.storage.test_mode = self.test_mode + self.storage.auth_key = await Auth(self, self.storage.dc_id).create() + self.storage.user_id = None + self.storage.is_bot = None 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) @@ -1213,7 +1163,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 @@ -1258,28 +1208,7 @@ class Client(Methods, BaseClient): log.warning('[{}] No plugin loaded from "{}"'.format( self.session_name, 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, - is_bot=self.is_bot, - ), - f, - indent=4 - ) - - async def get_initial_dialogs_chunk(self, - offset_date: int = 0): + async def get_initial_dialogs_chunk(self, offset_date: int = 0): while True: try: r = await self.send( @@ -1296,7 +1225,7 @@ class Client(Methods, BaseClient): log.warning("get_dialogs flood: waiting {} seconds".format(e.x)) await asyncio.sleep(e.x) else: - log.info("Total peers: {}".format(len(self.peers_by_id))) + log.info("Total peers: {}".format(self.storage.peers_count)) return r async def get_initial_dialogs(self): @@ -1335,7 +1264,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.storage.get_peer_by_id(peer_id) except KeyError: if type(peer_id) is str: if peer_id in ("self", "me"): @@ -1346,15 +1275,17 @@ class Client(Methods, BaseClient): try: int(peer_id) except ValueError: - if peer_id not in self.peers_by_username: + try: + return self.storage.get_peer_by_username(peer_id) + except KeyError: await self.send(functions.contacts.ResolveUsername(username=peer_id ) ) - return self.peers_by_username[peer_id] + return self.storage.get_peer_by_username(peer_id) else: try: - return self.peers_by_phone[peer_id] + return self.storage.get_peer_by_phone_number(peer_id) except KeyError: raise PeerIdInvalid @@ -1362,7 +1293,10 @@ class Client(Methods, BaseClient): self.fetch_peers( await 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 + )] ) ) ) @@ -1370,7 +1304,10 @@ class Client(Methods, BaseClient): if str(peer_id).startswith("-100"): await 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: @@ -1381,7 +1318,7 @@ class Client(Methods, BaseClient): ) try: - return self.peers_by_id[peer_id] + return self.storage.get_peer_by_id(peer_id) except KeyError: raise PeerIdInvalid @@ -1469,7 +1406,7 @@ class Client(Methods, BaseClient): is_missing_part = file_id is not None file_id = file_id or self.rnd_id() md5_sum = md5() if not is_big and not is_missing_part else None - pool = [Session(self, self.dc_id, self.auth_key, is_media=True) for _ in range(pool_size)] + pool = [Session(self, self.storage.dc_id, self.storage.auth_key, is_media=True) for _ in range(pool_size)] workers = [asyncio.ensure_future(worker(session)) for session in pool for _ in range(workers_count)] queue = asyncio.Queue(16) @@ -1559,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.storage.dc_id: exported_auth = await self.send( functions.auth.ExportAuthorization( dc_id=dc_id @@ -1569,9 +1506,7 @@ class Client(Methods, BaseClient): session = Session( self, dc_id, - await Auth(dc_id, self.test_mode, self.ipv6, self._proxy).create(), - is_media=True - ) + await Auth(self, dc_id).create(), is_media=True) await session.start() @@ -1584,12 +1519,7 @@ class Client(Methods, BaseClient): ) ) else: - session = Session( - self, - dc_id, - self.auth_key, - is_media=True - ) + session = Session(self, dc_id, self.storage.auth_key, is_media=True) await session.start() @@ -1677,10 +1607,7 @@ class Client(Methods, BaseClient): cdn_session = Session( self, r.dc_id, - await Auth(r.dc_id, self.test_mode, self.ipv6, self._proxy).create(), - is_media=True, - is_cdn=True - ) + await Auth(self, r.dc_id).create(), is_media=True, is_cdn=True) await cdn_session.start() @@ -1776,3 +1703,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/__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/base_client.py b/pyrogram/client/ext/base_client.py index 302d3ede..0b1f9bff 100644 --- a/pyrogram/client/ext/base_client.py +++ b/pyrogram/client/ext/base_client.py @@ -20,8 +20,11 @@ import asyncio import os import platform import re +import sys +from pathlib import Path from pyrogram import __version__ + from ..style import Markdown, HTML from ...session.internals import MsgId @@ -44,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 @@ -51,8 +56,8 @@ class BaseClient: DOWNLOAD_WORKERS = 4 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", @@ -83,18 +88,10 @@ class BaseClient: mime_types_to_extensions[mime_type] = " ".join(extensions) def __init__(self): - self.is_bot = None - self.dc_id = None - self.auth_key = None - self.user_id = None - self.date = None + self.storage = None self.rnd_id = MsgId - self.peers_by_id = {} - self.peers_by_username = {} - self.peers_by_phone = {} - self.markdown = Markdown(self) self.html = HTML(self) @@ -155,3 +152,21 @@ class BaseClient: def get_profile_photos(self, *args, **kwargs): pass + + 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/ext/dispatcher.py b/pyrogram/client/ext/dispatcher.py index 5b2894d4..8b32e076 100644 --- a/pyrogram/client/ext/dispatcher.py +++ b/pyrogram/client/ext/dispatcher.py @@ -22,6 +22,8 @@ from collections import OrderedDict import pyrogram from pyrogram.api import types + +from . import utils from ..handlers import ( CallbackQueryHandler, MessageHandler, DeletedMessagesHandler, UserStatusHandler, RawUpdateHandler, InlineQueryHandler, PollHandler @@ -65,7 +67,7 @@ class Dispatcher: return await pyrogram.Message._parse(self.client, update.message, users, chats), MessageHandler async def deleted_messages_parser(update, users, chats): - return pyrogram.Messages._parse_deleted(self.client, update), DeletedMessagesHandler + return utils.parse_deleted_messages(self.client, update), DeletedMessagesHandler async def callback_query_parser(update, users, chats): return await pyrogram.CallbackQuery._parse(self.client, update, users), CallbackQueryHandler @@ -106,6 +108,7 @@ class Dispatcher: await i self.update_worker_tasks.clear() + self.groups.clear() log.info("Stopped {} UpdateWorkerTasks".format(self.workers)) diff --git a/pyrogram/client/ext/syncer.py b/pyrogram/client/ext/syncer.py index 88caa160..8b48e6e2 100644 --- a/pyrogram/client/ext/syncer.py +++ b/pyrogram/client/ext/syncer.py @@ -17,15 +17,9 @@ # along with Pyrogram. If not, see . import asyncio -import base64 -import json import logging -import os -import shutil import time -from . import utils - log = logging.getLogger(__name__) @@ -85,48 +79,13 @@ 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)) - 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()), - is_bot=bool(client.is_bot), - 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()) + start = time.time() + client.storage.save() 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 + log.info('Synced "{}" in {:.6} ms'.format( + client.storage.name, + (time.time() - start) * 1000 + )) diff --git a/pyrogram/client/ext/utils.py b/pyrogram/client/ext/utils.py index cd5c919c..30f7e476 100644 --- a/pyrogram/client/ext/utils.py +++ b/pyrogram/client/ext/utils.py @@ -17,18 +17,20 @@ # along with Pyrogram. If not, see . import asyncio +import base64 import struct import sys -from base64 import b64decode, b64encode from concurrent.futures.thread import ThreadPoolExecutor -from typing import Union +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 @@ -60,7 +62,7 @@ def encode(s: bytes) -> str: r += bytes([i]) - return b64encode(r, b"-_").decode().rstrip("=") + return base64.urlsafe_b64encode(r).decode().rstrip("=") async def ainput(prompt: str = ""): @@ -147,3 +149,69 @@ def get_input_media_from_file_id( ) raise ValueError("Unknown media type: {}".format(file_id_str)) + + +async 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 = [] + + for message in messages.messages: + parsed_messages.append(await pyrogram.Message._parse(client, message, users, chats, replies=0)) + + 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 = await 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) + + +def unpack_inline_message_id(inline_message_id: str) -> types.InputBotInlineMessageID: + r = inline_message_id + "=" * (-len(inline_message_id) % 4) + r = struct.unpack(" type: 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/bots/get_game_high_scores.py b/pyrogram/client/methods/bots/get_game_high_scores.py index a11eaba3..81f6a265 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, - await self.send( - functions.messages.GetGameHighScores( - peer=await self.resolve_peer(chat_id), - id=message_id, - user_id=await self.resolve_peer(user_id) - ) + r = await self.send( + functions.messages.GetGameHighScores( + peer=await self.resolve_peer(chat_id), + id=message_id, + user_id=await 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/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..3f53b25e --- /dev/null +++ b/pyrogram/client/methods/chats/archive_chats.py @@ -0,0 +1,59 @@ +# 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): + async 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] + + await self.send( + functions.folders.EditPeerFolders( + folder_peers=[ + types.InputFolderPeer( + peer=await self.resolve_peer(chat), + folder_id=1 + ) for chat in chat_ids + ] + ) + ) + + return True diff --git a/pyrogram/client/methods/chats/get_chat_member.py b/pyrogram/client/methods/chats/get_chat_member.py index 287f9bf7..46f2f571 100644 --- a/pyrogram/client/methods/chats/get_chat_member.py +++ b/pyrogram/client/methods/chats/get_chat_member.py @@ -21,6 +21,7 @@ from typing import Union import pyrogram from pyrogram.api import functions, types from pyrogram.errors import UserNotParticipant + from ...ext import BaseClient @@ -51,13 +52,18 @@ class GetChatMember(BaseClient): user = await self.resolve_peer(user_id) if isinstance(chat, types.InputPeerChat): - full_chat = await self.send( + r = await 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 8bc0d000..9190e2f3 100644 --- a/pyrogram/client/methods/chats/get_chat_members.py +++ b/pyrogram/client/methods/chats/get_chat_members.py @@ -18,11 +18,12 @@ import asyncio import logging -from typing import Union +from typing import Union, List import pyrogram from pyrogram.api import functions, types from pyrogram.errors import FloodWait + from ...ext import BaseClient log = logging.getLogger(__name__) @@ -45,7 +46,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 +60,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 +80,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 +89,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 +98,16 @@ class GetChatMembers(BaseClient): peer = await self.resolve_peer(chat_id) if isinstance(peer, types.InputPeerChat): - return pyrogram.ChatMembers._parse( - self, - await self.send( - functions.messages.GetFullChat( - chat_id=peer.chat_id - ) + r = await 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 +128,20 @@ class GetChatMembers(BaseClient): while True: try: - return pyrogram.ChatMembers._parse( - self, - await self.send( - functions.channels.GetParticipants( - channel=peer, - filter=filter, - offset=offset, - limit=limit, - hash=0 - ) + r = await 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)) await asyncio.sleep(e.x) diff --git a/pyrogram/client/methods/chats/get_dialogs.py b/pyrogram/client/methods/chats/get_dialogs.py index 6f6af700..f9042a79 100644 --- a/pyrogram/client/methods/chats/get_dialogs.py +++ b/pyrogram/client/methods/chats/get_dialogs.py @@ -18,10 +18,12 @@ import asyncio import logging +from typing import List import pyrogram from pyrogram.api import functions, types from pyrogram.errors import FloodWait + from ...ext import BaseClient log = logging.getLogger(__name__) @@ -33,7 +35,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 +55,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 +82,32 @@ class GetDialogs(BaseClient): else: break - return await 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] = await 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_chat_members.py b/pyrogram/client/methods/chats/iter_chat_members.py index 78e7c01a..c4db1521 100644 --- a/pyrogram/client/methods/chats/iter_chat_members.py +++ b/pyrogram/client/methods/chats/iter_chat_members.py @@ -19,10 +19,10 @@ from string import ascii_lowercase from typing import Union, AsyncGenerator, Optional -from async_generator import async_generator, yield_ - import pyrogram +from async_generator import async_generator, yield_ from pyrogram.api import types + from ...ext import BaseClient @@ -103,13 +103,13 @@ class IterChatMembers(BaseClient): offset = 0 while True: - chat_members = (await self.get_chat_members( + chat_members = await self.get_chat_members( chat_id=chat_id, offset=offset, limit=limit, query=q, filter=filter - )).chat_members + ) if not chat_members: break diff --git a/pyrogram/client/methods/chats/iter_dialogs.py b/pyrogram/client/methods/chats/iter_dialogs.py index a7075072..bbad9b35 100644 --- a/pyrogram/client/methods/chats/iter_dialogs.py +++ b/pyrogram/client/methods/chats/iter_dialogs.py @@ -18,9 +18,9 @@ from typing import AsyncGenerator, Optional +import pyrogram from async_generator import async_generator, yield_ -import pyrogram from ...ext import BaseClient @@ -56,9 +56,9 @@ class IterDialogs(BaseClient): total = limit or (1 << 31) - 1 limit = min(100, total) - pinned_dialogs = (await self.get_dialogs( + pinned_dialogs = await self.get_dialogs( pinned_only=True - )).dialogs + ) for dialog in pinned_dialogs: await yield_(dialog) @@ -69,10 +69,10 @@ class IterDialogs(BaseClient): return while True: - dialogs = (await self.get_dialogs( + dialogs = await self.get_dialogs( offset_date=offset_date, limit=limit - )).dialogs + ) if not dialogs: return diff --git a/pyrogram/client/methods/chats/unarchive_chats.py b/pyrogram/client/methods/chats/unarchive_chats.py new file mode 100644 index 00000000..56768dba --- /dev/null +++ b/pyrogram/client/methods/chats/unarchive_chats.py @@ -0,0 +1,59 @@ +# 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): + async 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] + + await self.send( + functions.folders.EditPeerFolders( + folder_peers=[ + types.InputFolderPeer( + peer=await self.resolve_peer(chat), + folder_id=0 + ) for chat in chat_ids + ] + ) + ) + + return True diff --git a/pyrogram/client/methods/contacts/add_contacts.py b/pyrogram/client/methods/contacts/add_contacts.py index fa3a5367..a2c83802 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. """ diff --git a/pyrogram/client/methods/contacts/get_contacts.py b/pyrogram/client/methods/contacts/get_contacts.py index 329b1a89..2755c2a1 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)) await asyncio.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/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/download_media.py b/pyrogram/client/methods/messages/download_media.py index 72d21d3b..cd9402f0 100644 --- a/pyrogram/client/methods/messages/download_media.py +++ b/pyrogram/client/methods/messages/download_media.py @@ -18,19 +18,25 @@ import asyncio import binascii +import os import struct +import time +from datetime import datetime +from threading import Event from typing import Union import pyrogram from pyrogram.client.ext import BaseClient, FileData, utils from pyrogram.errors import FileIdInvalid +DEFAULT_DOWNLOAD_DIR = "downloads/" + class DownloadMedia(BaseClient): async 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 = () @@ -86,6 +92,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 +112,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 @@ -168,7 +175,40 @@ class DownloadMedia(BaseClient): done = asyncio.Event() path = [None] - self.download_queue.put_nowait((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_nowait((data, directory, file_name, done, progress, progress_args, path)) if block: await done.wait() 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..a8660029 --- /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): + async 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 await 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..97427890 --- /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): + async 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 await 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..1c7b2487 --- /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): + async 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 await 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..216c3c21 --- /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): + async 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 await 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 697623f3..f43e079c 100644 --- a/pyrogram/client/methods/messages/edit_message_caption.py +++ b/pyrogram/client/methods/messages/edit_message_caption.py @@ -19,7 +19,6 @@ from typing import Union import pyrogram -from pyrogram.api import functions, types from pyrogram.client.ext import BaseClient @@ -32,7 +31,7 @@ class EditMessageCaption(BaseClient): parse_mode: str = "", reply_markup: "pyrogram.InlineKeyboardMarkup" = None ) -> "pyrogram.Message": - """Edit captions of messages. + """Edit the caption of media messages. Parameters: chat_id (``int`` | ``str``): @@ -44,11 +43,11 @@ class EditMessageCaption(BaseClient): Message identifier in the chat specified in chat_id. caption (``str``): - New caption of the message. + 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 caption. Defaults to "markdown". + URLs in your message. Defaults to "markdown". reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): An InlineKeyboardMarkup object. @@ -59,21 +58,10 @@ class EditMessageCaption(BaseClient): Raises: RPCError: In case of a Telegram RPC error. """ - style = self.html if parse_mode.lower() == "html" else self.markdown - - r = await self.send( - functions.messages.EditMessage( - peer=await self.resolve_peer(chat_id), - id=message_id, - reply_markup=reply_markup.write() if reply_markup else None, - **await style.parse(caption) - ) + return await self.edit_message_text( + chat_id=chat_id, + message_id=message_id, + text=caption, + parse_mode=parse_mode, + reply_markup=reply_markup ) - - for i in r.updates: - if isinstance(i, (types.UpdateEditMessage, types.UpdateEditChannelMessage)): - return await 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 7acf5bf8..70aacc02 100644 --- a/pyrogram/client/methods/messages/edit_message_media.py +++ b/pyrogram/client/methods/messages/edit_message_media.py @@ -37,12 +37,10 @@ class EditMessageMedia(BaseClient): media: InputMedia, 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. + 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: chat_id (``int`` | ``str``): @@ -53,7 +51,7 @@ class EditMessageMedia(BaseClient): message_id (``int``): Message identifier in the chat specified in chat_id. - media (:obj:`InputMedia`) + media (:obj:`InputMedia`): One of the InputMedia objects describing an animation, audio, document, photo or video. reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): @@ -92,8 +90,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 = await self.send( functions.messages.UploadMedia( @@ -130,8 +127,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 = await self.send( functions.messages.UploadMedia( @@ -167,8 +163,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 = await self.send( functions.messages.UploadMedia( @@ -206,8 +201,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 = await self.send( functions.messages.UploadMedia( @@ -243,8 +237,8 @@ class EditMessageMedia(BaseClient): functions.messages.EditMessage( peer=await 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, **await 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 b6eba080..b2d47701 100644 --- a/pyrogram/client/methods/messages/edit_message_reply_markup.py +++ b/pyrogram/client/methods/messages/edit_message_reply_markup.py @@ -28,9 +28,9 @@ class EditMessageReplyMarkup(BaseClient): self, chat_id: Union[int, str], message_id: int, - reply_markup: "pyrogram.InlineKeyboardMarkup" = None + reply_markup: "pyrogram.InlineKeyboardMarkup" = 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: chat_id (``int`` | ``str``): @@ -45,18 +45,16 @@ class EditMessageReplyMarkup(BaseClient): An InlineKeyboardMarkup object. 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`: On success, the edited message is returned. Raises: RPCError: In case of a Telegram RPC error. """ - r = await self.send( functions.messages.EditMessage( peer=await 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 5d2782f9..5a02a954 100644 --- a/pyrogram/client/methods/messages/edit_message_text.py +++ b/pyrogram/client/methods/messages/edit_message_text.py @@ -33,7 +33,7 @@ class EditMessageText(BaseClient): disable_web_page_preview: bool = None, reply_markup: "pyrogram.InlineKeyboardMarkup" = None ) -> "pyrogram.Message": - """Edit text messages. + """Edit the text of messages. Parameters: chat_id (``int`` | ``str``): diff --git a/pyrogram/client/methods/messages/forward_messages.py b/pyrogram/client/methods/messages/forward_messages.py index e8b77a23..1830f137 100644 --- a/pyrogram/client/methods/messages/forward_messages.py +++ b/pyrogram/client/methods/messages/forward_messages.py @@ -16,10 +16,11 @@ # 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 + from ...ext import BaseClient @@ -32,7 +33,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 +65,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 +80,9 @@ class ForwardMessages(BaseClient): forwarded_messages = [] for chunk in [message_ids[i:i + 200] for i in range(0, len(message_ids), 200)]: - messages = await self.get_messages(chat_id=from_chat_id, message_ids=chunk) # type: pyrogram.Messages + messages = await self.get_messages(chat_id=from_chat_id, message_ids=chunk) - for message in messages.messages: + for message in messages: forwarded_messages.append( await message.forward( chat_id, @@ -91,11 +92,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 = await self.send( functions.messages.ForwardMessages( @@ -116,13 +113,9 @@ class ForwardMessages(BaseClient): if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)): forwarded_messages.append( await pyrogram.Message._parse( - self, i.message, - users, chats + self, i.message, + users, chats + ) ) - ) - 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 5e2c35e0..f6936591 100644 --- a/pyrogram/client/methods/messages/get_history.py +++ b/pyrogram/client/methods/messages/get_history.py @@ -18,11 +18,13 @@ import asyncio import logging -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 log = logging.getLogger(__name__) @@ -37,7 +39,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 +69,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 = await pyrogram.Messages._parse( + messages = await utils.parse_messages( self, await self.send( functions.messages.GetHistory( @@ -97,6 +101,6 @@ class GetHistory(BaseClient): break if reverse: - messages.messages.reverse() + messages.reverse() return messages diff --git a/pyrogram/client/methods/messages/get_history_count.py b/pyrogram/client/methods/messages/get_history_count.py index 1286fffb..e8ca3380 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 = await self.resolve_peer(chat_id) + r = await self.send( + functions.messages.GetHistory( + peer=await 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 = await 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 (await 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 diff --git a/pyrogram/client/methods/messages/get_messages.py b/pyrogram/client/methods/messages/get_messages.py index ac83a7e4..d705a0c6 100644 --- a/pyrogram/client/methods/messages/get_messages.py +++ b/pyrogram/client/methods/messages/get_messages.py @@ -18,12 +18,13 @@ import asyncio import logging -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 +36,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 +61,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 +100,6 @@ class GetMessages(BaseClient): else: break - messages = await pyrogram.Messages._parse(self, r, replies) + messages = await 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 7f13e377..2dca96fe 100644 --- a/pyrogram/client/methods/messages/iter_history.py +++ b/pyrogram/client/methods/messages/iter_history.py @@ -18,9 +18,9 @@ from typing import Union, Optional, AsyncGenerator +import pyrogram from async_generator import async_generator, yield_ -import pyrogram from ...ext import BaseClient @@ -76,14 +76,14 @@ class IterHistory(BaseClient): limit = min(100, total) while True: - messages = (await self.get_history( + messages = await self.get_history( chat_id=chat_id, limit=limit, offset=offset, offset_id=offset_id, offset_date=offset_date, reverse=reverse - )).messages + ) if not messages: return diff --git a/pyrogram/client/methods/messages/send_media_group.py b/pyrogram/client/methods/messages/send_media_group.py index c890f0db..475d5973 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 await pyrogram.Messages._parse( + return await utils.parse_messages( self, types.messages.Messages( messages=[m.message for m in filter( 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..0d1e2e65 --- /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 + +from pyrogram.api import functions + +from ...ext import BaseClient + + +class BlockUser(BaseClient): + async 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( + await self.send( + functions.contact.Block( + id=await self.resolve_peer(user_id) + ) + ) + ) diff --git a/pyrogram/client/methods/users/get_profile_photos.py b/pyrogram/client/methods/users/get_profile_photos.py index a7d06fe2..71fd6056 100644 --- a/pyrogram/client/methods/users/get_profile_photos.py +++ b/pyrogram/client/methods/users/get_profile_photos.py @@ -16,10 +16,12 @@ # 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 +from pyrogram.client.ext import utils + from ...ext import BaseClient @@ -29,7 +31,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,27 +49,15 @@ 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. """ peer_id = await self.resolve_peer(chat_id) - if isinstance(peer_id, types.InputPeerUser): - return pyrogram.ProfilePhotos._parse( - self, - await self.send( - functions.photos.GetUserPhotos( - user_id=peer_id, - offset=offset, - max_id=0, - limit=limit - ) - ) - ) - else: - new_chat_photos = pyrogram.Messages._parse( + if isinstance(peer_id, types.InputPeerChannel): + r = await utils.parse_messages( self, await self.send( functions.messages.Search( @@ -86,7 +76,15 @@ 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([message.new_chat_photo for message in r][:limit]) + else: + r = await 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 4104b7ce..82f41dd9 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 await self.get_profile_photos(chat_id, limit=1).total_count + peer_id = await self.resolve_peer(chat_id) + + if isinstance(peer_id, types.InputPeerChannel): + r = await self.send( + functions.messages.GetSearchCounters( + peer=peer_id, + filters=[types.InputMessagesFilterChatPhotos()], + ) + ) + + return r[0].count + else: + r = await 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 diff --git a/pyrogram/client/methods/users/get_user_dc.py b/pyrogram/client/methods/users/get_user_dc.py index 0e1643bf..36c59d58 100644 --- a/pyrogram/client/methods/users/get_user_dc.py +++ b/pyrogram/client/methods/users/get_user_dc.py @@ -24,7 +24,13 @@ from ...ext import BaseClient class GetUserDC(BaseClient): async 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:: + + 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``): @@ -33,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. diff --git a/pyrogram/client/methods/users/get_users.py b/pyrogram/client/methods/users/get_users.py index 9136865e..e997c887 100644 --- a/pyrogram/client/methods/users/get_users.py +++ b/pyrogram/client/methods/users/get_users.py @@ -57,7 +57,7 @@ class GetUsers(BaseClient): ) ) - users = [] + users = pyrogram.List() for i in r: users.append(pyrogram.User._parse(self, i)) diff --git a/pyrogram/client/methods/users/iter_profile_photos.py b/pyrogram/client/methods/users/iter_profile_photos.py index e32e2a18..bfe3e7f0 100644 --- a/pyrogram/client/methods/users/iter_profile_photos.py +++ b/pyrogram/client/methods/users/iter_profile_photos.py @@ -16,11 +16,11 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from typing import Union, Generator - -from async_generator import async_generator, yield_ +from typing import Union, AsyncGenerator, Optional import pyrogram +from async_generator import async_generator, yield_ + from ...ext import BaseClient @@ -31,7 +31,7 @@ class IterProfilePhotos(BaseClient): chat_id: Union[int, str], offset: int = 0, limit: int = 0, - ) -> Generator["pyrogram.Photo", None, None]: + ) -> Optional[AsyncGenerator["pyrogram.Message", None]]: """Iterate through a chat or a user profile photos sequentially. This convenience method does the same as repeatedly calling :meth:`~Client.get_profile_photos` in a loop, thus @@ -62,11 +62,11 @@ class IterProfilePhotos(BaseClient): limit = min(100, total) while True: - photos = self.get_profile_photos( + photos = await self.get_profile_photos( chat_id=chat_id, offset=offset, limit=limit - ).photos + ) if not photos: return diff --git a/pyrogram/client/methods/users/unblock_user.py b/pyrogram/client/methods/users/unblock_user.py new file mode 100644 index 00000000..6aed10fa --- /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 + +from pyrogram.api import functions + +from ...ext import BaseClient + + +class UnblockUser(BaseClient): + async 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( + await self.send( + functions.contact.Unblock( + id=await self.resolve_peer(user_id) + ) + ) + ) diff --git a/pyrogram/client/storage/__init__.py b/pyrogram/client/storage/__init__.py new file mode 100644 index 00000000..00d2f144 --- /dev/null +++ b/pyrogram/client/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 .memory_storage import MemoryStorage +from .file_storage import FileStorage +from .storage import Storage diff --git a/pyrogram/client/storage/file_storage.py b/pyrogram/client/storage/file_storage.py new file mode 100644 index 00000000..f52a03a9 --- /dev/null +++ b/pyrogram/client/storage/file_storage.py @@ -0,0 +1,110 @@ +# 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 sqlite3 +from pathlib import Path +from threading import Lock + +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, session_json: dict): + self.open() + + 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 = session_json.get("peers_by_id", {}) + peers_by_phone = session_json.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()) + + def open(self): + 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( + path, + timeout=1, + check_same_thread=False + ) + + if not file_exists: + self.create() + + with self.conn: + self.conn.execute("VACUUM") 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 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 diff --git a/pyrogram/client/types/__init__.py b/pyrogram/client/types/__init__.py index 3d430c44..8fa55482 100644 --- a/pyrogram/client/types/__init__.py +++ b/pyrogram/client/types/__init__.py @@ -16,10 +16,12 @@ # 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 * +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/keyboards/__init__.py b/pyrogram/client/types/bots_and_keyboards/__init__.py similarity index 87% rename from pyrogram/client/types/keyboards/__init__.py rename to pyrogram/client/types/bots_and_keyboards/__init__.py index dae33e10..90376504 100644 --- a/pyrogram/client/types/keyboards/__init__.py +++ b/pyrogram/client/types/bots_and_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/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 53% rename from pyrogram/client/types/keyboards/callback_query.py rename to pyrogram/client/types/bots_and_keyboards/callback_query.py index 60cf4cc6..daecaa35 100644 --- a/pyrogram/client/types/keyboards/callback_query.py +++ b/pyrogram/client/types/bots_and_keyboards/callback_query.py @@ -172,3 +172,151 @@ class CallbackQuery(Object, Update): url=url, cache_time=cache_time ) + + def edit_text( + self, + text: str, + parse_mode: str = "", + disable_web_page_preview: bool = None, + reply_markup: "pyrogram.InlineKeyboardMarkup" = None + ) -> Union["pyrogram.Message", bool]: + """Edit the text of messages attached to this callback query. + + Bound method *edit_message_text* 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. + """ + 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 + ) + + def edit_caption( + self, + caption: str, + parse_mode: str = "", + reply_markup: "pyrogram.InlineKeyboardMarkup" = None + ) -> Union["pyrogram.Message", bool]: + """Edit the caption of media messages attached to this callback query. + + Bound method *edit_message_caption* of :obj:`CallbackQuery`. + + 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. + """ + return self.edit_text(caption, parse_mode, reply_markup) + + def edit_media( + self, + media: "pyrogram.InputMedia", + reply_markup: "pyrogram.InlineKeyboardMarkup" = None + ) -> Union["pyrogram.Message", bool]: + """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`): + 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. + """ + 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 + ) + + def edit_reply_markup( + self, + reply_markup: "pyrogram.InlineKeyboardMarkup" = None + ) -> Union["pyrogram.Message", bool]: + """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`): + 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. + """ + 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 + ) 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 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``): 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 - ) diff --git a/pyrogram/client/types/messages_and_media/__init__.py b/pyrogram/client/types/messages_and_media/__init__.py index 17a6e36a..b9bcb460 100644 --- a/pyrogram/client/types/messages_and_media/__init__.py +++ b/pyrogram/client/types/messages_and_media/__init__.py @@ -24,11 +24,9 @@ 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 -from .profile_photos import ProfilePhotos from .sticker import Sticker from .stripped_thumbnail import StrippedThumbnail from .thumbnail import Thumbnail @@ -38,7 +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", "ProfilePhotos", "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 84fb28cc..0e074987 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 - async def reply( + async 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* :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``): @@ -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): ) async 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: @@ -2356,19 +2356,14 @@ class Message(Object, Update): progress_args=progress_args ) - async def edit( + async def edit_text( self, 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* :obj:`Message `. + """Bound method *edit_text* of :obj:`Message`. Use as a shortcut for: @@ -2383,7 +2378,7 @@ class Message(Object, Update): Example: .. code-block:: python - message.edit("hello") + message.edit_text("hello") Parameters: text (``str``): @@ -2418,14 +2413,9 @@ 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* :obj:`Message `. + """Bound method *edit_caption* of :obj:`Message`. Use as a shortcut for: @@ -2468,7 +2458,7 @@ class Message(Object, Update): ) async 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: @@ -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*): @@ -2506,7 +2496,7 @@ class Message(Object, Update): ) async 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 +2537,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: @@ -2617,7 +2607,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: @@ -2690,7 +2680,7 @@ class Message(Object, Update): ) async def delete(self, revoke: bool = True): - """Bound method *delete* :obj:`Message `. + """Bound method *delete* of :obj:`Message`. Use as a shortcut for: @@ -2726,7 +2716,7 @@ class Message(Object, Update): ) async 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 +2843,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 +2892,7 @@ class Message(Object, Update): ) async def pin(self, disable_notification: bool = None) -> "Message": - """Bound method *pin* :obj:`Message `. + """Bound method *pin* of :obj:`Message`. Use as a shortcut for: 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. 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 138a6648..00000000 --- a/pyrogram/client/types/messages_and_media/messages.py +++ /dev/null @@ -1,174 +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 - async 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 - ) - - # TODO: WTF! Py 3.5 doesn't support await inside comprehensions - parsed_messages = [] - - for message in messages.messages: - parsed_messages.append(await Message._parse(client, message, users, chats, replies=0)) - - 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 = (await 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 - ) 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 - ) diff --git a/pyrogram/client/types/messages_and_media/sticker.py b/pyrogram/client/types/messages_and_media/sticker.py index 50ea2ff8..6460cd81 100644 --- a/pyrogram/client/types/messages_and_media/sticker.py +++ b/pyrogram/client/types/messages_and_media/sticker.py @@ -95,7 +95,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 diff --git a/pyrogram/client/types/user_and_chats/__init__.py b/pyrogram/client/types/user_and_chats/__init__.py index 2059589a..922ac86a 100644 --- a/pyrogram/client/types/user_and_chats/__init__.py +++ b/pyrogram/client/types/user_and_chats/__init__.py @@ -18,16 +18,13 @@ 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 from .dialog import Dialog -from .dialogs import Dialogs 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", "User", "UserStatus" ] diff --git a/pyrogram/client/types/user_and_chats/chat.py b/pyrogram/client/types/user_and_chats/chat.py index ad416e4c..a383ae71 100644 --- a/pyrogram/client/types/user_and_chats/chat.py +++ b/pyrogram/client/types/user_and_chats/chat.py @@ -20,6 +20,7 @@ from typing import Union import pyrogram from pyrogram.api import types + from .chat_permissions import ChatPermissions from .chat_photo import ChatPhoto from ..object import Object @@ -36,14 +37,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 +96,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 +110,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 +132,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 +154,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, @@ -257,3 +267,49 @@ class Chat(Object): return Chat._parse_user_chat(client, chat) else: return Chat._parse_channel_chat(client, chat) + + async 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 await self._client.archive_chats(self.id) + + async 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 await self._client.unarchive_chats(self.id) 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 - ) 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 e5de58d1..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 - async 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] = await 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 - ) diff --git a/pyrogram/client/types/user_and_chats/user.py b/pyrogram/client/types/user_and_chats/user.py index 50dd8361..18527747 100644 --- a/pyrogram/client/types/user_and_chats/user.py +++ b/pyrogram/client/types/user_and_chats/user.py @@ -18,6 +18,7 @@ import pyrogram from pyrogram.api import types + from .chat_photo import ChatPhoto from .user_status import UserStatus from ..object import Object @@ -52,12 +53,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 +87,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 +103,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 +124,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 +149,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), @@ -160,3 +161,49 @@ class User(Object): restriction_reason=user.restriction_reason, client=client ) + + async 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 await self._client.archive_chats(self.id) + + async 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 await self._client.unarchive_chats(self.id) diff --git a/pyrogram/session/auth.py b/pyrogram/session/auth.py index 9134a856..135100e2 100644 --- a/pyrogram/session/auth.py +++ b/pyrogram/session/auth.py @@ -23,10 +23,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__) @@ -35,11 +37,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 eb5de2b8..ff65483c 100644 --- a/pyrogram/session/session.py +++ b/pyrogram/session/session.py @@ -30,6 +30,7 @@ from pyrogram.api.core import TLObject, MsgContainer, Int, Long, FutureSalt, Fut from pyrogram.connection import Connection from pyrogram.crypto import MTProto from pyrogram.errors import RPCError, InternalServerError, AuthKeyDuplicated + from .internals import MsgId, MsgFactory log = logging.getLogger(__name__) @@ -66,12 +67,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") @@ -110,7 +113,12 @@ class Session: async def start(self): while True: - self.connection = Connection(self.dc_id, self.client.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: await self.connection.connect() diff --git a/setup.py b/setup.py index 146dae9e..45c2871b 100644 --- a/setup.py +++ b/setup.py @@ -168,12 +168,13 @@ 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, extras_require={ - "fast": ["tgcrypto==1.1.1"] + "fast": ["tgcrypto==1.2.0"] }, cmdclass={ "clean": Clean,