diff --git a/MANIFEST.in b/MANIFEST.in
index 84d50dd4..f818e13a 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,6 +1,6 @@
## Include
-include COPYING COPYING.lesser NOTICE
-recursive-include compiler *.tl *.tsv *.txt
+include COPYING COPYING.lesser NOTICE requirements.txt
+recursive-include compiler *.py *.tl *.tsv *.txt
## Exclude
prune pyrogram/api/errors/exceptions
diff --git a/README.rst b/README.rst
index 554ef7a1..ae6f084f 100644
--- a/README.rst
+++ b/README.rst
@@ -29,42 +29,33 @@ Table of Contents
About
=====
-**Pyrogram** is a fully functional Telegram Client Library written from the ground up in Python.
-It offers simple and complete access to the `Telegram Messenger API`_ and is designed for Python
-developers keen on building custom Telegram applications.
-
+**Pyrogram** is a brand new Telegram_ Client Library written from the ground up in Python and C. It can be used for building
+custom Telegram applications in Python that interact with the MTProto API as both User and Bot.
Features
--------
-- **Easy to setup**: Pyrogram can be easily installed and upgraded using **pip**, requires
- a minimal set of dependencies (which are also automatically managed) and very few lines
- of code to get started with.
+- **Easy to setup**: Pyrogram can be easily installed using pip and requires very few lines of code to get started with.
+
+- **Easy to use**: Pyrogram provides idiomatic, clean and readable Python code making the Telegram API simple to use.
-- **Easy to use**: Pyrogram provides idiomatic, developer-friendly, clean and readable
- Python code (either generated or hand-written) making the Telegram API simple to use.
+- **High-level**: Pyrogram automatically handles all the low-level details of communication with Telegram servers.
-- **High level**: Pyrogram automatically handles all the low-level details of
- communication with the Telegram servers by implementing the
- `MTProto Mobile Protocol v2.0`_ and the mechanisms needed for establishing
- a reliable connection.
+- **Updated**: Pyrogram makes use of the latest Telegram MTProto API version, currently Layer 76.
-- **Fast**: Pyrogram's speed is boosted up by `TgCrypto`_, a high-performance, easy-to-install
- Telegram Crypto Library written in C as a Python extension.
+- **Fast**: Pyrogram critical parts are boosted up by `TgCrypto`_, a high-performance Crypto Library written in pure C.
+
+- **Documented**: Pyrogram API methods are documented and resemble the well established Telegram Bot API,
+ thus offering a familiar look to Bot developers.
-- **Updated**: Pyrogram makes use of the latest Telegram API version, currently `Layer 75`_.
-
-- **Documented**: Pyrogram API public methods are documented and resemble the well
- established Telegram Bot API, thus offering a familiar look to Bot developers.
-
-- **Full API support**: Beside the simple, bot-like methods offered by the Pyrogram API,
- the library also provides a complete, low-level access to every single Telegram API method.
+- **Full API support**: Beside the simple Bot API-like methods, Pyrogram also provides an easy access to every single
+ Telegram MTProto API method allowing you to programmatically execute any action an official client is able to do, and more.
Requirements
------------
-- Python 3.3 or higher.
+- Python 3.4 or higher.
- A Telegram API key.
@@ -75,17 +66,11 @@ Getting Started
Installation
------------
-- You can easily install and upgrade the library using standard Python tools:
+- You can install and upgrade Pyrogram using pip:
.. code:: shell
- $ pip install --upgrade pyrogram
-
-- Or, with TgCrypto_:
-
- .. code:: shell
-
- $ pip install --upgrade pyrogram[tgcrypto]
+ $ pip3 install --upgrade pyrogram
Configuration
-------------
@@ -102,7 +87,7 @@ Configuration
Usage
-----
-- And here's how Pyrogram looks like:
+- And here is how Pyrogram looks like:
.. code:: python
@@ -112,26 +97,25 @@ Usage
client.start()
client.send_message("me", "Hi there! I'm using Pyrogram")
- client.send_photo("me", "/home/dan/pic.jpg", "Nice photo!")
client.stop()
That's all you need for getting started with Pyrogram. For more detailed information,
-please refer to the Documentation_.
+please refer to the Documentation_ and the Examples_ folder.
Documentation
=============
-- The entire Pyrogram's documentation resides at https://docs.pyrogram.ml.
+- The entire Pyrogram documentation resides at https://docs.pyrogram.ml.
Contribution
============
-**You are very welcome to contribute** by either submitting pull requests or
-reporting issues/bugs as well as suggesting best practices, ideas, enhancements
-on both code and documentation. Any help is appreciated!
+Pyrogram is brand new! **You are welcome to try it and help make it better** by either submitting pull
+requests or reporting issues/bugs as well as suggesting best practices, ideas, enhancements on both code
+and documentation. Any help is appreciated!
Feedback
@@ -140,7 +124,6 @@ Feedback
Means for getting in touch:
- `Community`_
-- `Telegram`_
- `GitHub`_
- `Email`_
@@ -154,17 +137,11 @@ License
`GNU Lesser General Public License v3 or later (LGPLv3+)`_
-.. _`Telegram Messenger API`: https://core.telegram.org/api#telegram-api
+.. _`Telegram`: https://telegram.org/
-.. _`MTProto Mobile Protocol v2.0`: https://core.telegram.org/mtproto
+.. _`your own`: https://docs.pyrogram.ml/start/ProjectSetup#api-keys
-.. _`Layer 75`: compiler/api/source/main_api.tl
-
-.. _`your own`: https://github.com/pyrogram/pyrogram/wiki/Getting-Started#api-keys
-
-.. _`Introduction`: https://github.com/pyrogram/pyrogram/wiki/Getting-Started
-
-.. _`Telegram`: https://t.me/haskell
+.. _`Examples`: https://github.com/pyrogram/pyrogram/blob/master/examples/README.md
.. _`Community`: https://t.me/PyrogramChat
@@ -181,9 +158,9 @@ License
.. |header| raw:: html
@@ -204,11 +181,11 @@ License
-
+
-
@@ -221,7 +198,7 @@ License
.. |scheme| image:: https://www.pyrogram.ml/images/scheme.svg
:target: compiler/api/source/main_api.tl
- :alt: Scheme Layer 75
+ :alt: Scheme Layer 76
.. |tgcrypto| image:: https://www.pyrogram.ml/images/tgcrypto.svg
:target: https://github.com/pyrogram/tgcrypto
diff --git a/compiler/api/source/main_api.tl b/compiler/api/source/main_api.tl
index b4d0e7a8..9ede3e28 100644
--- a/compiler/api/source/main_api.tl
+++ b/compiler/api/source/main_api.tl
@@ -148,6 +148,7 @@ messageActionPaymentSent#40699cd0 currency:string total_amount:long = MessageAct
messageActionPhoneCall#80e11a7f flags:# call_id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = MessageAction;
messageActionScreenshotTaken#4792929b = MessageAction;
messageActionCustomAction#fae69f56 message:string = MessageAction;
+messageActionBotAllowed#abe9affe domain:string = MessageAction;
dialog#e4def5db flags:# pinned:flags.2?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage = Dialog;
@@ -300,8 +301,8 @@ updateRecentStickers#9a422c20 = Update;
updateConfig#a229dd06 = Update;
updatePtsChanged#3354678f = Update;
updateChannelWebPage#40771900 channel_id:int webpage:WebPage pts:int pts_count:int = Update;
-updateDialogPinned#d711a2cc flags:# pinned:flags.0?true peer:Peer = Update;
-updatePinnedDialogs#d8caf68d flags:# order:flags.0?Vector = Update;
+updateDialogPinned#19d27f3c flags:# pinned:flags.0?true peer:DialogPeer = Update;
+updatePinnedDialogs#ea4cb65b flags:# order:flags.0?Vector = Update;
updateBotWebhookJSON#8317c0c3 data:DataJSON = Update;
updateBotWebhookJSONQuery#9b9240a6 query_id:long data:DataJSON timeout:int = Update;
updateBotShippingQuery#e0cdc940 query_id:long user_id:int payload:bytes shipping_address:PostAddress = Update;
@@ -335,11 +336,11 @@ photos.photosSlice#15051f54 count:int photos:Vector users:Vector =
photos.photo#20212ca8 photo:Photo users:Vector = photos.Photo;
upload.file#96a18d5 type:storage.FileType mtime:int bytes:bytes = upload.File;
-upload.fileCdnRedirect#ea52fe5a dc_id:int file_token:bytes encryption_key:bytes encryption_iv:bytes cdn_file_hashes:Vector = upload.File;
+upload.fileCdnRedirect#f18cda44 dc_id:int file_token:bytes encryption_key:bytes encryption_iv:bytes file_hashes:Vector = upload.File;
dcOption#5d8c6cc flags:# ipv6:flags.0?true media_only:flags.1?true tcpo_only:flags.2?true cdn:flags.3?true static:flags.4?true id:int ip_address:string port:int = DcOption;
-config#9c840964 flags:# phonecalls_enabled:flags.1?true default_p2p_contacts:flags.3?true date:int expires:int test_mode:Bool this_dc:int dc_options:Vector chat_size_max:int megagroup_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int chat_big_size:int push_chat_period_ms:int push_chat_limit:int saved_gifs_limit:int edit_time_limit:int rating_e_decay:int stickers_recent_limit:int stickers_faved_limit:int channels_read_media_period:int tmp_sessions:flags.0?int pinned_dialogs_count_max:int call_receive_timeout_ms:int call_ring_timeout_ms:int call_connect_timeout_ms:int call_packet_timeout_ms:int me_url_prefix:string suggested_lang_code:flags.2?string lang_pack_version:flags.2?int disabled_features:Vector = Config;
+config#86b5778e flags:# phonecalls_enabled:flags.1?true default_p2p_contacts:flags.3?true preload_featured_stickers:flags.4?true ignore_phone_entities:flags.5?true revoke_pm_inbox:flags.6?true date:int expires:int test_mode:Bool this_dc:int dc_options:Vector chat_size_max:int megagroup_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int push_chat_period_ms:int push_chat_limit:int saved_gifs_limit:int edit_time_limit:int revoke_time_limit:int revoke_pm_time_limit:int rating_e_decay:int stickers_recent_limit:int stickers_faved_limit:int channels_read_media_period:int tmp_sessions:flags.0?int pinned_dialogs_count_max:int call_receive_timeout_ms:int call_ring_timeout_ms:int call_connect_timeout_ms:int call_packet_timeout_ms:int me_url_prefix:string suggested_lang_code:flags.2?string lang_pack_version:flags.2?int = Config;
nearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc;
@@ -444,8 +445,6 @@ stickerPack#12b299d4 emoticon:string documents:Vector = StickerPack;
messages.allStickersNotModified#e86602c3 = messages.AllStickers;
messages.allStickers#edfd405f hash:int sets:Vector = messages.AllStickers;
-disabledFeature#ae636f24 feature:string description:string = DisabledFeature;
-
messages.affectedMessages#84d19185 pts:int pts_count:int = messages.AffectedMessages;
contactLinkUnknown#5f4f9247 = ContactLink;
@@ -483,7 +482,7 @@ inputStickerSetEmpty#ffb62b95 = InputStickerSet;
inputStickerSetID#9de7a269 id:long access_hash:long = InputStickerSet;
inputStickerSetShortName#861cc8a0 short_name:string = InputStickerSet;
-stickerSet#cd303b41 flags:# installed:flags.0?true archived:flags.1?true official:flags.2?true masks:flags.3?true id:long access_hash:long title:string short_name:string count:int hash:int = StickerSet;
+stickerSet#5585a139 flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string count:int hash:int = StickerSet;
messages.stickerSet#b60a24a6 set:StickerSet packs:Vector documents:Vector = messages.StickerSet;
@@ -520,6 +519,8 @@ messageEntityPre#73924be0 offset:int length:int language:string = MessageEntity;
messageEntityTextUrl#76a6d327 offset:int length:int url:string = MessageEntity;
messageEntityMentionName#352dca58 offset:int length:int user_id:int = MessageEntity;
inputMessageEntityMentionName#208e68c9 offset:int length:int user_id:InputUser = MessageEntity;
+messageEntityPhone#9b69e34b offset:int length:int = MessageEntity;
+messageEntityCashtag#4c4e743f offset:int length:int = MessageEntity;
inputChannelEmpty#ee8c1e86 = InputChannel;
inputChannel#afeb712e channel_id:int access_hash:long = InputChannel;
@@ -570,7 +571,7 @@ inputBotInlineMessageMediaVenue#aaafadc8 flags:# geo_point:InputGeoPoint title:s
inputBotInlineMessageMediaContact#2daf01a7 flags:# phone_number:string first_name:string last_name:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
inputBotInlineMessageGame#4b425864 flags:# reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
-inputBotInlineResult#2cbbe15a flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb_url:flags.4?string content_url:flags.5?string content_type:flags.5?string w:flags.6?int h:flags.6?int duration:flags.7?int send_message:InputBotInlineMessage = InputBotInlineResult;
+inputBotInlineResult#88bf9319 flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb:flags.4?InputWebDocument content:flags.5?InputWebDocument send_message:InputBotInlineMessage = InputBotInlineResult;
inputBotInlineResultPhoto#a8d864a7 id:string type:string photo:InputPhoto send_message:InputBotInlineMessage = InputBotInlineResult;
inputBotInlineResultDocument#fff8fdc4 flags:# id:string type:string title:flags.1?string description:flags.2?string document:InputDocument send_message:InputBotInlineMessage = InputBotInlineResult;
inputBotInlineResultGame#4fa417f2 id:string short_name:string send_message:InputBotInlineMessage = InputBotInlineResult;
@@ -581,7 +582,7 @@ botInlineMessageMediaGeo#b722de65 flags:# geo:GeoPoint period:int reply_markup:f
botInlineMessageMediaVenue#4366232e flags:# geo:GeoPoint title:string address:string provider:string venue_id:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage;
botInlineMessageMediaContact#35edb4d4 flags:# phone_number:string first_name:string last_name:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage;
-botInlineResult#9bebaeb9 flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb_url:flags.4?string content_url:flags.5?string content_type:flags.5?string w:flags.6?int h:flags.6?int duration:flags.7?int send_message:BotInlineMessage = BotInlineResult;
+botInlineResult#11965f3a flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb:flags.4?WebDocument content:flags.5?WebDocument send_message:BotInlineMessage = BotInlineResult;
botInlineMediaResult#17db940b flags:# id:string type:string photo:flags.0?Photo document:flags.1?Document title:flags.2?string description:flags.3?string send_message:BotInlineMessage = BotInlineResult;
messages.botResults#947ca848 flags:# gallery:flags.0?true query_id:long next_offset:flags.1?string switch_pm:flags.2?InlineBotSwitchPM results:Vector cache_time:int users:Vector = messages.BotResults;
@@ -630,7 +631,7 @@ messages.featuredStickersNotModified#4ede3cf = messages.FeaturedStickers;
messages.featuredStickers#f89d88e5 hash:int sets:Vector unread:Vector = messages.FeaturedStickers;
messages.recentStickersNotModified#b17f890 = messages.RecentStickers;
-messages.recentStickers#5ce20970 hash:int stickers:Vector = messages.RecentStickers;
+messages.recentStickers#22f3afb3 hash:int packs:Vector stickers:Vector dates:Vector = messages.RecentStickers;
messages.archivedStickers#4fcba9c8 count:int sets:Vector = messages.ArchivedStickers;
@@ -712,6 +713,7 @@ paymentRequestedInfo#909c3f94 flags:# name:flags.0?string phone:flags.1?string e
paymentSavedCredentialsCard#cdc27a1f id:string title:string = PaymentSavedCredentials;
webDocument#c61acbd8 url:string access_hash:long size:int mime_type:string attributes:Vector dc_id:int = WebDocument;
+webDocumentNoProxy#f9c8bcc6 url:string size:int mime_type:string attributes:Vector = WebDocument;
inputWebDocument#9bed434d url:string size:int mime_type:string attributes:Vector = InputWebDocument;
@@ -800,8 +802,6 @@ channelAdminLogEventsFilter#ea107ae4 flags:# join:flags.0?true leave:flags.1?tru
popularContact#5ce14175 client_id:long importers:int = PopularContact;
-cdnFileHash#77eec38f offset:int limit:int hash:bytes = CdnFileHash;
-
messages.favedStickersNotModified#9e8fa6d3 = messages.FavedStickers;
messages.favedStickers#f37f2f16 hash:int packs:Vector stickers:Vector = messages.FavedStickers;
@@ -823,6 +823,15 @@ inputMessageID#a676a322 id:int = InputMessage;
inputMessageReplyTo#bad88395 id:int = InputMessage;
inputMessagePinned#86872538 = InputMessage;
+inputDialogPeer#fcaafeb7 peer:InputPeer = InputDialogPeer;
+
+dialogPeer#e56dbf05 peer:Peer = DialogPeer;
+
+messages.foundStickerSetsNotModified#d54b65d = messages.FoundStickerSets;
+messages.foundStickerSets#5108d648 hash:int sets:Vector = messages.FoundStickerSets;
+
+fileHash#6242c773 offset:int limit:int hash:bytes = FileHash;
+
---functions---
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
@@ -849,7 +858,7 @@ auth.resendCode#3ef1a9bf phone_number:string phone_code_hash:string = auth.SentC
auth.cancelCode#1f040578 phone_number:string phone_code_hash:string = Bool;
auth.dropTempAuthKeys#8e48a188 except_auth_keys:Vector = Bool;
-account.registerDevice#1389cc token_type:int token:string app_sandbox:Bool other_uids:Vector = Bool;
+account.registerDevice#5cbea590 token_type:int token:string app_sandbox:Bool secret:bytes other_uids:Vector = Bool;
account.unregisterDevice#3076c4bf token_type:int token:string other_uids:Vector = Bool;
account.updateNotifySettings#84be5b93 peer:InputNotifyPeer settings:InputPeerNotifySettings = Bool;
account.getNotifySettings#12b3ad31 peer:InputNotifyPeer = PeerNotifySettings;
@@ -902,7 +911,7 @@ contacts.resetSaved#879537f1 = Bool;
messages.getMessages#63c66506 id:Vector = messages.Messages;
messages.getDialogs#191ba9c5 flags:# exclude_pinned:flags.0?true offset_date:int offset_id:int offset_peer:InputPeer limit:int = messages.Dialogs;
messages.getHistory#dcbb8260 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:int = messages.Messages;
-messages.search#39e9ea0 flags:# peer:InputPeer q:string from_id:flags.0?InputUser filter:MessagesFilter min_date:int max_date:int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;
+messages.search#8614ef68 flags:# peer:InputPeer q:string from_id:flags.0?InputUser filter:MessagesFilter min_date:int max_date:int offset_id:int add_offset:int limit:int max_id:int min_id:int hash:int = messages.Messages;
messages.readHistory#e306d3a peer:InputPeer max_id:int = messages.AffectedMessages;
messages.deleteHistory#1c015b09 flags:# just_clear:flags.0?true peer:InputPeer max_id:int = messages.AffectedHistory;
messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector = messages.AffectedMessages;
@@ -914,6 +923,7 @@ messages.forwardMessages#708e0195 flags:# silent:flags.5?true background:flags.6
messages.reportSpam#cf1592db peer:InputPeer = Bool;
messages.hideReportSpam#a8f1709b peer:InputPeer = Bool;
messages.getPeerSettings#3672e09c peer:InputPeer = PeerSettings;
+messages.report#bd82b658 peer:InputPeer id:Vector reason:ReportReason = Bool;
messages.getChats#3c6aa187 id:Vector = messages.Chats;
messages.getFullChat#3b831c66 chat_id:int = messages.ChatFull;
messages.editChatTitle#dc452855 chat_id:int title:string = Updates;
@@ -933,7 +943,7 @@ messages.sendEncryptedService#32d439a4 peer:InputEncryptedChat random_id:long da
messages.receivedQueue#55a5bb66 max_qts:int = Vector;
messages.reportEncryptedSpam#4b0c8c0f peer:InputEncryptedChat = Bool;
messages.readMessageContents#36a73f77 id:Vector = messages.AffectedMessages;
-messages.getStickers#ae22e045 emoticon:string hash:string = messages.Stickers;
+messages.getStickers#85cb5182 flags:# exclude_featured:flags.0?true emoticon:string hash:string = messages.Stickers;
messages.getAllStickers#1c9618b1 hash:int = messages.AllStickers;
messages.getWebPagePreview#8b68b0cc flags:# message:string entities:flags.3?Vector = MessageMedia;
messages.exportChatInvite#7d885289 chat_id:int = ExportedChatInvite;
@@ -961,7 +971,7 @@ messages.editMessage#5d1b8dd flags:# no_webpage:flags.1?true stop_geo_live:flags
messages.editInlineBotMessage#b0e08243 flags:# no_webpage:flags.1?true stop_geo_live:flags.12?true id:InputBotInlineMessageID message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector geo_point:flags.13?InputGeoPoint = Bool;
messages.getBotCallbackAnswer#810a9fec flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes = messages.BotCallbackAnswer;
messages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long message:flags.0?string url:flags.2?string cache_time:int = Bool;
-messages.getPeerDialogs#2d9776b9 peers:Vector = messages.PeerDialogs;
+messages.getPeerDialogs#e470bcfd peers:Vector = messages.PeerDialogs;
messages.saveDraft#bc39e14b flags:# no_webpage:flags.1?true reply_to_msg_id:flags.0?int peer:InputPeer message:string entities:flags.3?Vector = Bool;
messages.getAllDrafts#6a3f8d65 = Updates;
messages.getFeaturedStickers#2dacca4f hash:int = messages.FeaturedStickers;
@@ -979,8 +989,8 @@ messages.getInlineGameHighScores#f635e1b id:InputBotInlineMessageID user_id:Inpu
messages.getCommonChats#d0a48c4 user_id:InputUser max_id:int limit:int = messages.Chats;
messages.getAllChats#eba80ff0 except_ids:Vector = messages.Chats;
messages.getWebPage#32ca8f91 url:string hash:int = WebPage;
-messages.toggleDialogPin#3289be6a flags:# pinned:flags.0?true peer:InputPeer = Bool;
-messages.reorderPinnedDialogs#959ff644 flags:# force:flags.0?true order:Vector = Bool;
+messages.toggleDialogPin#a731e257 flags:# pinned:flags.0?true peer:InputDialogPeer = Bool;
+messages.reorderPinnedDialogs#5b51d63f flags:# force:flags.0?true order:Vector = Bool;
messages.getPinnedDialogs#e254d64e = messages.PeerDialogs;
messages.setBotShippingResults#e5f672fa flags:# query_id:long error:flags.0?string shipping_options:flags.1?Vector = Bool;
messages.setBotPrecheckoutResults#9c2dd95 flags:# success:flags.1?true query_id:long error:flags.0?string = Bool;
@@ -990,9 +1000,10 @@ messages.getFavedStickers#21ce0b0e hash:int = messages.FavedStickers;
messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool;
messages.getUnreadMentions#46578472 peer:InputPeer offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;
messages.readMentions#f0189d3 peer:InputPeer = messages.AffectedHistory;
-messages.getRecentLocations#249431e2 peer:InputPeer limit:int = messages.Messages;
+messages.getRecentLocations#bbc45b09 peer:InputPeer limit:int hash:int = messages.Messages;
messages.sendMultiMedia#2095512f flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int multi_media:Vector = Updates;
messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile;
+messages.searchStickerSets#c2b7d08b flags:# exclude_featured:flags.0?true q:string hash:int = messages.FoundStickerSets;
updates.getState#edd4882a = updates.State;
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
@@ -1008,8 +1019,9 @@ upload.getFile#e3a6cfb5 location:InputFileLocation offset:int limit:int = upload
upload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool;
upload.getWebFile#24e6818d location:InputWebFileLocation offset:int limit:int = upload.WebFile;
upload.getCdnFile#2000bcc3 file_token:bytes offset:int limit:int = upload.CdnFile;
-upload.reuploadCdnFile#1af91c09 file_token:bytes request_token:bytes = Vector;
-upload.getCdnFileHashes#f715c87b file_token:bytes offset:int = Vector;
+upload.reuploadCdnFile#9b2754a8 file_token:bytes request_token:bytes = Vector;
+upload.getCdnFileHashes#4da54231 file_token:bytes offset:int = Vector;
+upload.getFileHashes#c7025931 location:InputFileLocation offset:int = Vector;
help.getConfig#c4f9186b = Config;
help.getNearestDc#1fb33026 = NearestDc;
@@ -1085,4 +1097,4 @@ langpack.getStrings#2e1ee318 lang_code:string keys:Vector = Vector;
-// LAYER 75
\ No newline at end of file
+// LAYER 76
diff --git a/compiler/api/template/class.txt b/compiler/api/template/class.txt
index 2cba4bb9..d29caf05 100644
--- a/compiler/api/template/class.txt
+++ b/compiler/api/template/class.txt
@@ -11,7 +11,7 @@ class {class_name}(Object):
"""
ID = {object_id}
- def __init__(self{arguments}, **kwargs):
+ def __init__(self{arguments}):
{fields}
@staticmethod
diff --git a/examples/README.md b/examples/README.md
index 6f640ef4..66ca9405 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -2,12 +2,16 @@
This folder contains example scripts to show you how **Pyrogram** looks like.
You can start with [hello_world.py](https://github.com/pyrogram/pyrogram/blob/master/examples/hello_world.py) and continue
-with the more advanced examples. Every script is working right away, meaning you can simply copy-paste and run, the only things
-you have to change are the target chats (username, id) and file paths for sending media (photo, video, ...).
+with the more advanced examples.
+
+Every script is working right away (provided you correctly set up your credentials), meaning
+you can simply copy-paste and run, the only things you have to change are the target chats (username, id) and file paths for
+sending media (photo, video, ...).
- [**hello_world.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/hello_world.py)
- [**get_history.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/get_history.py)
- [**get_participants.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/get_participants.py)
+- [**get_participants2.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/get_participants2.py)
- [**inline_bots.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/inline_bots.py)
- [**updates.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/updates.py)
- [**simple_echo.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/simple_echo.py)
diff --git a/examples/get_participants2.py b/examples/get_participants2.py
new file mode 100644
index 00000000..23ed328f
--- /dev/null
+++ b/examples/get_participants2.py
@@ -0,0 +1,63 @@
+import time
+from string import ascii_lowercase
+
+from pyrogram import Client
+from pyrogram.api import functions, types
+from pyrogram.api.errors import FloodWait
+
+"""
+This is an improved version of get_participants.py
+
+Since Telegram will return at most 10.000 users for a single query, this script
+repeats the search using numbers ("0" to "9") and all the available ascii letters ("a" to "z").
+
+This can be further improved by also searching for non-ascii characters (e.g.: Japanese script),
+as some user names may not contain ascii letters at all.
+"""
+
+client = Client("example")
+client.start()
+
+target = "username" # Target channel/supergroup username or id
+users = {} # To ensure uniqueness, users will be stored in a dictionary with user_id as key
+limit = 200 # Amount of users to retrieve for each API call (200 is the maximum)
+
+# "" + "0123456789" + "abcdefghijklmnopqrstuvwxyz" (as list)
+queries = [""] + [str(i) for i in range(10)] + list(ascii_lowercase)
+
+for q in queries:
+ print("Searching for '{}'".format(q))
+ offset = 0 # For each query, offset restarts from 0
+
+ while True:
+ try:
+ participants = client.send(
+ functions.channels.GetParticipants(
+ channel=client.resolve_peer(target),
+ filter=types.ChannelParticipantsSearch(q),
+ offset=offset,
+ limit=limit,
+ hash=0
+ )
+ )
+ except FloodWait as e:
+ # Very large chats could trigger FloodWait.
+ # When happens, wait X seconds before continuing
+ print("Flood wait: {} seconds".format(e.x))
+ time.sleep(e.x)
+ continue
+
+ if not participants.participants:
+ print("Done searching for '{}'".format(q))
+ print()
+ break # No more participants left
+
+ # User information are stored in the participants.users list.
+ # Add those users to the dictionary
+ users.update({i.id: i for i in participants.users})
+
+ offset += len(participants.participants)
+
+ print("Total users: {}".format(len(users)))
+
+client.stop()
diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py
index b2b85d0c..469dd399 100644
--- a/pyrogram/__init__.py
+++ b/pyrogram/__init__.py
@@ -23,7 +23,7 @@ __copyright__ = "Copyright (C) 2017-2018 Dan Tès .
import base64
+import binascii
import json
import logging
import math
import mimetypes
import os
import re
+import shutil
+import struct
+import tempfile
import threading
import time
-from collections import namedtuple
from configparser import ConfigParser
from datetime import datetime
from hashlib import sha256, md5
@@ -41,7 +44,7 @@ from pyrogram.api.errors import (
PhoneCodeExpired, PhoneCodeEmpty, SessionPasswordNeeded,
PasswordHashInvalid, FloodWait, PeerIdInvalid, FilePartMissing,
ChatAdminRequired, FirstnameInvalid, PhoneNumberBanned,
- VolumeLocNotFound)
+ VolumeLocNotFound, UserMigrate)
from pyrogram.crypto import AES
from pyrogram.session import Auth, Session
from pyrogram.session.internals import MsgId
@@ -50,8 +53,20 @@ from .style import Markdown, HTML
log = logging.getLogger(__name__)
-ApiKey = namedtuple("ApiKey", ["api_id", "api_hash"])
-Proxy = namedtuple("Proxy", ["enabled", "hostname", "port", "username", "password"])
+
+class APIKey:
+ def __init__(self, api_id: int, api_hash: str):
+ self.api_id = api_id
+ self.api_hash = api_hash
+
+
+class Proxy:
+ def __init__(self, enabled: bool, hostname: str, port: int, username: str, password: str):
+ self.enabled = enabled
+ self.hostname = hostname
+ self.port = port
+ self.username = username
+ self.password = password
class Client:
@@ -60,61 +75,62 @@ class Client:
invoke every single Telegram API method available.
Args:
- session_name (:obj:`str`):
- Name to uniquely identify an authorized session. It will be used
- to save the session to a file named *.session* and to load
- it when you restart your script. As long as a valid session file exists,
- Pyrogram won't ask you again to input your phone number.
+ session_name (``str``):
+ Name to uniquely identify a session of either a User or a Bot.
+ For Users: pass a string of your choice, e.g.: "my_main_account".
+ For Bots: pass your Bot API token, e.g.: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
+ Note: as long as a valid User session file exists, Pyrogram won't ask you again to input your phone number.
- api_key (:obj:`tuple`, optional):
+ api_key (``tuple``, optional):
Your Telegram API Key as tuple: *(api_id, api_hash)*.
E.g.: *(12345, "0123456789abcdef0123456789abcdef")*. This is an alternative way to pass it if you
don't want to use the *config.ini* file.
- proxy (:obj:`dict`, optional):
- Your SOCKS5 Proxy settings as dict: *{hostname: str, port: int, username: str, password: str}*.
- E.g.: *dict(hostname="11.22.33.44", port=1080, username="user", password="pass")*.
+ proxy (``dict``, optional):
+ Your SOCKS5 Proxy settings as dict,
+ e.g.: *dict(hostname="11.22.33.44", port=1080, username="user", password="pass")*.
*username* and *password* can be omitted if your proxy doesn't require authorization.
This is an alternative way to setup a proxy if you don't want to use the *config.ini* file.
- test_mode (:obj:`bool`, optional):
+ test_mode (``bool``, optional):
Enable or disable log-in to testing servers. Defaults to False.
Only applicable for new sessions and will be ignored in case previously
created sessions are loaded.
- phone_number (:obj:`str`, optional):
+ phone_number (``str``, optional):
Pass your phone number (with your Country Code prefix included) to avoid
entering it manually. Only applicable for new sessions.
- phone_code (:obj:`str` | :obj:`callable`, optional):
+ phone_code (``str`` | ``callable``, optional):
Pass the phone code as string (for test numbers only), or pass a callback function
which must return the correct phone code as string (e.g., "12345").
Only applicable for new sessions.
- password (:obj:`str`, optional):
+ password (``str``, optional):
Pass your Two-Step Verification password (if you have one) to avoid entering it
manually. Only applicable for new sessions.
- first_name (:obj:`str`, optional):
+ first_name (``str``, optional):
Pass a First Name to avoid entering it manually. It will be used to automatically
create a new Telegram account in case the phone number you passed is not registered yet.
- last_name (:obj:`str`, optional):
+ last_name (``str``, optional):
Same purpose as *first_name*; pass a Last Name to avoid entering it manually. It can
be an empty string: ""
- workers (:obj:`int`, optional):
+ workers (``int``, optional):
Thread pool size for handling incoming updates. Defaults to 4.
"""
- INVITE_LINK_RE = re.compile(r"^(?:https?://)?t\.me/joinchat/(.+)$")
+ INVITE_LINK_RE = re.compile(r"^(?:https?://)?(?:t\.me/joinchat/)([\w-]+)$")
+ BOT_TOKEN_RE = re.compile(r"^\d+:[\w-]+$")
DIALOGS_AT_ONCE = 100
- UPDATES_WORKERS = 2
+ UPDATES_WORKERS = 1
DOWNLOAD_WORKERS = 1
def __init__(self,
session_name: str,
- api_key: tuple or ApiKey = None,
+ api_key: tuple or APIKey = None,
proxy: dict or Proxy = None,
test_mode: bool = False,
phone_number: str = None,
@@ -136,11 +152,13 @@ class Client:
self.workers = workers
+ self.token = None
+
self.dc_id = None
self.auth_key = None
self.user_id = None
- self.rnd_id = None
+ self.rnd_id = MsgId
self.peers_by_id = {}
self.peers_by_username = {}
@@ -153,6 +171,7 @@ class Client:
self.session = None
+ self.is_started = None
self.is_idle = None
self.updates_queue = Queue()
@@ -166,8 +185,12 @@ class Client:
Requires no parameters.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
+ if self.BOT_TOKEN_RE.match(self.session_name):
+ self.token = self.session_name
+ self.session_name = self.session_name.split(":")[0]
+
self.load_config()
self.load_session(self.session_name)
@@ -180,18 +203,22 @@ class Client:
client=self
)
- terms = self.session.start()
+ self.session.start()
+ self.is_started = True
if self.user_id is None:
- print("\n".join(terms.splitlines()), "\n")
+ if self.token is None:
+ self.authorize_user()
+ else:
+ self.authorize_bot()
- self.user_id = self.authorize()
- self.password = None
self.save_session()
- self.rnd_id = MsgId
- self.get_dialogs()
- self.get_contacts()
+ if self.token is None:
+ self.get_dialogs()
+ self.get_contacts()
+ else:
+ self.send(functions.updates.GetState())
for i in range(self.UPDATES_WORKERS):
Thread(target=self.updates_worker, name="UpdatesWorker#{}".format(i + 1)).start()
@@ -208,6 +235,7 @@ class Client:
"""Use this method to manually stop the Client.
Requires no parameters.
"""
+ self.is_started = False
self.session.stop()
for _ in range(self.UPDATES_WORKERS):
@@ -219,336 +247,37 @@ class Client:
for _ in range(self.DOWNLOAD_WORKERS):
self.download_queue.put(None)
- def fetch_peers(self, entities: list):
- for entity in entities:
- if isinstance(entity, types.User):
- user_id = entity.id
-
- if user_id in self.peers_by_id:
- continue
-
- access_hash = entity.access_hash
-
- if access_hash is None:
- continue
-
- username = entity.username
- phone = entity.phone
-
- input_peer = types.InputPeerUser(
- user_id=user_id,
- access_hash=access_hash
+ def authorize_bot(self):
+ try:
+ r = self.send(
+ functions.auth.ImportBotAuthorization(
+ flags=0,
+ api_id=self.api_key.api_id,
+ api_hash=self.api_key.api_hash,
+ bot_auth_token=self.token
)
-
- self.peers_by_id[user_id] = input_peer
-
- if username is not None:
- self.peers_by_username[username] = input_peer
-
- if phone is not None:
- self.peers_by_phone[phone] = input_peer
-
- if isinstance(entity, types.Chat):
- chat_id = entity.id
- peer_id = -chat_id
-
- if peer_id in self.peers_by_id:
- continue
-
- input_peer = types.InputPeerChat(
- chat_id=chat_id
- )
-
- self.peers_by_id[peer_id] = input_peer
-
- if isinstance(entity, types.Channel):
- channel_id = entity.id
- peer_id = int("-100" + str(channel_id))
-
- if peer_id in self.peers_by_id:
- continue
-
- access_hash = entity.access_hash
-
- if access_hash is None:
- continue
-
- username = entity.username
-
- 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] = input_peer
-
- def download_worker(self):
- name = threading.current_thread().name
- log.debug("{} started".format(name))
-
- while True:
- media = self.download_queue.get()
-
- if media is None:
- break
-
- try:
- media, file_name, done, progress, path = media
- tmp_file_name = None
-
- if isinstance(media, types.MessageMediaDocument):
- document = media.document
-
- if isinstance(document, types.Document):
- if not file_name:
- file_name = "doc_{}{}".format(
- datetime.fromtimestamp(document.date).strftime("%Y-%m-%d_%H-%M-%S"),
- ".txt" if document.mime_type == "text/plain" else
- mimetypes.guess_extension(document.mime_type) if document.mime_type else ".unknown"
- )
-
- for i in document.attributes:
- if isinstance(i, types.DocumentAttributeFilename):
- file_name = i.file_name
- break
- elif isinstance(i, types.DocumentAttributeSticker):
- file_name = file_name.replace("doc", "sticker")
- elif isinstance(i, types.DocumentAttributeAudio):
- file_name = file_name.replace("doc", "audio")
- elif isinstance(i, types.DocumentAttributeVideo):
- file_name = file_name.replace("doc", "video")
- elif isinstance(i, types.DocumentAttributeAnimated):
- file_name = file_name.replace("doc", "gif")
-
- tmp_file_name = self.get_file(
- dc_id=document.dc_id,
- id=document.id,
- access_hash=document.access_hash,
- version=document.version,
- size=document.size,
- progress=progress
- )
- elif isinstance(media, types.MessageMediaPhoto):
- photo = media.photo
-
- if isinstance(photo, types.Photo):
- if not file_name:
- file_name = "photo_{}.jpg".format(
- datetime.fromtimestamp(photo.date).strftime("%Y-%m-%d_%H-%M-%S")
- )
-
- photo_loc = photo.sizes[-1].location
-
- tmp_file_name = self.get_file(
- dc_id=photo_loc.dc_id,
- volume_id=photo_loc.volume_id,
- local_id=photo_loc.local_id,
- secret=photo_loc.secret,
- size=photo.sizes[-1].size,
- progress=progress
- )
-
- if file_name is not None:
- path[0] = "downloads/{}".format(file_name)
-
- try:
- os.remove("downloads/{}".format(file_name))
- except OSError:
- pass
- finally:
- try:
- os.renames("{}".format(tmp_file_name), "downloads/{}".format(file_name))
- except OSError:
- pass
- except Exception as e:
- log.error(e, exc_info=True)
- finally:
- done.set()
-
- try:
- os.remove("{}".format(tmp_file_name))
- except OSError:
- pass
-
- log.debug("{} stopped".format(name))
-
- def updates_worker(self):
- name = threading.current_thread().name
- log.debug("{} started".format(name))
-
- while True:
- updates = self.updates_queue.get()
-
- if updates is None:
- break
-
- try:
- if isinstance(updates, (types.Update, types.UpdatesCombined)):
- self.fetch_peers(updates.users)
- self.fetch_peers(updates.chats)
-
- for update in updates.updates:
- channel_id = getattr(
- getattr(
- getattr(
- update, "message", None
- ), "to_id", None
- ), "channel_id", None
- ) or getattr(update, "channel_id", None)
-
- pts = getattr(update, "pts", None)
-
- if channel_id and pts:
- if channel_id not in self.channels_pts:
- self.channels_pts[channel_id] = []
-
- if pts in self.channels_pts[channel_id]:
- continue
-
- self.channels_pts[channel_id].append(pts)
-
- if len(self.channels_pts[channel_id]) > 50:
- self.channels_pts[channel_id] = self.channels_pts[channel_id][25:]
-
- self.update_queue.put((update, updates.users, updates.chats))
- elif isinstance(updates, (types.UpdateShortMessage, types.UpdateShortChatMessage)):
- diff = self.send(
- functions.updates.GetDifference(
- pts=updates.pts - updates.pts_count,
- date=updates.date,
- qts=-1
- )
- )
-
- self.fetch_peers(diff.users)
- self.fetch_peers(diff.chats)
-
- self.update_queue.put((
- types.UpdateNewMessage(
- message=diff.new_messages[0],
- pts=updates.pts,
- pts_count=updates.pts_count
- ),
- diff.users,
- diff.chats
- ))
- elif isinstance(updates, types.UpdateShort):
- self.update_queue.put((updates.update, [], []))
- except Exception as e:
- log.error(e, exc_info=True)
-
- log.debug("{} stopped".format(name))
-
- def update_worker(self):
- name = threading.current_thread().name
- log.debug("{} started".format(name))
-
- while True:
- update = self.update_queue.get()
-
- if update is None:
- break
-
- try:
- if self.update_handler:
- self.update_handler(
- self,
- update[0],
- {i.id: i for i in update[1]},
- {i.id: i for i in update[2]}
- )
- except Exception as e:
- log.error(e, exc_info=True)
-
- log.debug("{} stopped".format(name))
-
- def signal_handler(self, *args):
- self.stop()
- self.is_idle = False
-
- def idle(self, stop_signals: tuple = (SIGINT, SIGTERM, SIGABRT)):
- """Blocks the program execution until one of the signals are received,
- then gently stop the Client by closing the underlying connection.
-
- Args:
- stop_signals (:obj:`tuple`, optional):
- Iterable containing signals the signal handler will listen to.
- Defaults to (SIGINT, SIGTERM, SIGABRT).
- """
- for s in stop_signals:
- signal(s, self.signal_handler)
-
- self.is_idle = True
-
- while self.is_idle:
- time.sleep(1)
-
- def set_update_handler(self, callback: callable):
- """Use this method to set the update handler.
-
- You must call this method *before* you *start()* the Client.
-
- Args:
- callback (:obj:`callable`):
- A function that will be called when a new update is received from the server. It takes
- :obj:`(client, update, users, chats)` as positional arguments (Look at the section below for
- a detailed description).
-
- Other Parameters:
- client (:obj:`pyrogram.Client`):
- The Client itself, useful when you want to call other API methods inside the update handler.
-
- update (:obj:`Update`):
- The received update, which can be one of the many single Updates listed in the *updates*
- field you see in the :obj:`types.Update ` type.
-
- users (:obj:`dict`):
- Dictionary of all :obj:`types.User ` mentioned in the update.
- You can access extra info about the user (such as *first_name*, *last_name*, etc...) by using
- the IDs you find in the *update* argument (e.g.: *users[1768841572]*).
-
- chats (:obj:`dict`):
- Dictionary of all :obj:`types.Chat ` and
- :obj:`types.Channel ` mentioned in the update.
- You can access extra info about the chat (such as *title*, *participants_count*, etc...)
- by using the IDs you find in the *update* argument (e.g.: *chats[1701277281]*).
-
- Note:
- The following Empty or Forbidden types may exist inside the *users* and *chats* dictionaries.
- They mean you have been blocked by the user or banned from the group/channel.
-
- - :obj:`types.UserEmpty `
- - :obj:`types.ChatEmpty `
- - :obj:`types.ChatForbidden `
- - :obj:`types.ChannelForbidden `
- """
- self.update_handler = callback
-
- def send(self, data: Object):
- """Use this method to send Raw Function queries.
-
- This method makes possible to manually call every single Telegram API method in a low-level manner.
- Available functions are listed in the :obj:`pyrogram.api.functions` package and may accept compound
- data types from :obj:`pyrogram.api.types` as well as bare types such as :obj:`int`, :obj:`str`, etc...
-
- Args:
- data (:obj:`Object`):
- The API Scheme function filled with proper arguments.
-
- Raises:
- :class:`pyrogram.Error`
- """
- r = self.session.send(data)
-
- self.fetch_peers(getattr(r, "users", []))
- self.fetch_peers(getattr(r, "chats", []))
-
- return r
-
- def authorize(self):
+ )
+ except UserMigrate as e:
+ self.session.stop()
+
+ self.dc_id = e.x
+ self.auth_key = Auth(self.dc_id, self.test_mode, self.proxy).create()
+
+ self.session = Session(
+ self.dc_id,
+ self.test_mode,
+ self.proxy,
+ self.auth_key,
+ self.api_key.api_id,
+ client=self
+ )
+
+ self.session.start()
+ self.authorize_bot()
+ else:
+ self.user_id = r.user.id
+
+ def authorize_user(self):
phone_number_invalid_raises = self.phone_number is not None
phone_code_invalid_raises = self.phone_code is not None
password_hash_invalid_raises = self.password is not None
@@ -566,6 +295,8 @@ class Client:
elif confirm in ("n", "2"):
self.phone_number = input("Enter phone number: ")
+ self.phone_number = self.phone_number.strip("+")
+
try:
r = self.send(
functions.auth.SendCode(
@@ -706,24 +437,392 @@ class Client:
else:
break
- return r.user.id
+ self.password = None
+ self.user_id = r.user.id
+
+ def fetch_peers(self, entities: list):
+ for entity in entities:
+ if isinstance(entity, types.User):
+ user_id = entity.id
+
+ if user_id in self.peers_by_id:
+ continue
+
+ access_hash = entity.access_hash
+
+ if access_hash is None:
+ 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
+
+ if phone is not None:
+ self.peers_by_phone[phone] = input_peer
+
+ if isinstance(entity, types.Chat):
+ chat_id = entity.id
+ peer_id = -chat_id
+
+ if peer_id in self.peers_by_id:
+ continue
+
+ input_peer = types.InputPeerChat(
+ chat_id=chat_id
+ )
+
+ self.peers_by_id[peer_id] = input_peer
+
+ if isinstance(entity, types.Channel):
+ channel_id = entity.id
+ peer_id = int("-100" + str(channel_id))
+
+ if peer_id in self.peers_by_id:
+ continue
+
+ access_hash = entity.access_hash
+
+ if access_hash is None:
+ continue
+
+ username = entity.username
+
+ 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
+
+ def download_worker(self):
+ name = threading.current_thread().name
+ log.debug("{} started".format(name))
+
+ while True:
+ media = self.download_queue.get()
+ temp_file_path = ""
+ final_file_path = ""
+
+ if media is None:
+ break
+
+ try:
+ media, file_name, done, progress, path = media
+
+ directory, file_name = os.path.split(file_name)
+ directory = directory or "downloads"
+
+ if isinstance(media, types.MessageMediaDocument):
+ document = media.document
+
+ if isinstance(document, types.Document):
+ if not file_name:
+ file_name = "doc_{}{}".format(
+ datetime.fromtimestamp(document.date).strftime("%Y-%m-%d_%H-%M-%S"),
+ ".txt" if document.mime_type == "text/plain" else
+ mimetypes.guess_extension(document.mime_type) if document.mime_type else ".unknown"
+ )
+
+ for i in document.attributes:
+ if isinstance(i, types.DocumentAttributeFilename):
+ file_name = i.file_name
+ break
+ elif isinstance(i, types.DocumentAttributeSticker):
+ file_name = file_name.replace("doc", "sticker")
+ elif isinstance(i, types.DocumentAttributeAudio):
+ file_name = file_name.replace("doc", "audio")
+ elif isinstance(i, types.DocumentAttributeVideo):
+ file_name = file_name.replace("doc", "video")
+ elif isinstance(i, types.DocumentAttributeAnimated):
+ file_name = file_name.replace("doc", "gif")
+
+ temp_file_path = self.get_file(
+ dc_id=document.dc_id,
+ id=document.id,
+ access_hash=document.access_hash,
+ version=document.version,
+ size=document.size,
+ progress=progress
+ )
+ elif isinstance(media, (types.MessageMediaPhoto, types.Photo)):
+ if isinstance(media, types.MessageMediaPhoto):
+ photo = media.photo
+ else:
+ photo = media
+
+ if isinstance(photo, types.Photo):
+ if not file_name:
+ file_name = "photo_{}_{}.jpg".format(
+ datetime.fromtimestamp(photo.date).strftime("%Y-%m-%d_%H-%M-%S"),
+ self.rnd_id()
+ )
+
+ photo_loc = photo.sizes[-1].location
+
+ temp_file_path = self.get_file(
+ dc_id=photo_loc.dc_id,
+ volume_id=photo_loc.volume_id,
+ local_id=photo_loc.local_id,
+ secret=photo_loc.secret,
+ size=photo.sizes[-1].size,
+ progress=progress
+ )
+
+ if temp_file_path:
+ final_file_path = os.path.abspath(re.sub("\\\\", "/", os.path.join(directory, file_name)))
+ os.makedirs(directory, exist_ok=True)
+ shutil.move(temp_file_path, final_file_path)
+ except Exception as e:
+ log.error(e, exc_info=True)
+
+ try:
+ os.remove(temp_file_path)
+ except OSError:
+ pass
+ else:
+ # TODO: "" or None for faulty download, which is better?
+ # os.path methods return "" in case something does not exist, I prefer this.
+ # For now let's keep None
+ path[0] = final_file_path or None
+ finally:
+ done.set()
+
+ log.debug("{} stopped".format(name))
+
+ def updates_worker(self):
+ name = threading.current_thread().name
+ log.debug("{} started".format(name))
+
+ while True:
+ updates = self.updates_queue.get()
+
+ if updates is None:
+ break
+
+ try:
+ if isinstance(updates, (types.Update, types.UpdatesCombined)):
+ self.fetch_peers(updates.users)
+ self.fetch_peers(updates.chats)
+
+ for update in updates.updates:
+ channel_id = getattr(
+ getattr(
+ getattr(
+ update, "message", None
+ ), "to_id", None
+ ), "channel_id", None
+ ) or getattr(update, "channel_id", None)
+
+ pts = getattr(update, "pts", None)
+ pts_count = getattr(update, "pts_count", None)
+
+ if isinstance(update, types.UpdateNewChannelMessage):
+ message = update.message
+
+ if not isinstance(message, types.MessageEmpty):
+ diff = self.send(
+ functions.updates.GetChannelDifference(
+ channel=self.resolve_peer(update.message.to_id.channel_id),
+ filter=types.ChannelMessagesFilter(
+ ranges=[types.MessageRange(
+ min_id=update.message.id,
+ max_id=update.message.id
+ )]
+ ),
+ pts=pts - pts_count,
+ limit=pts
+ )
+ )
+
+ if not isinstance(diff, types.updates.ChannelDifferenceEmpty):
+ updates.users += diff.users
+ updates.chats += diff.chats
+
+ if channel_id and pts:
+ if channel_id not in self.channels_pts:
+ self.channels_pts[channel_id] = []
+
+ if pts in self.channels_pts[channel_id]:
+ continue
+
+ self.channels_pts[channel_id].append(pts)
+
+ if len(self.channels_pts[channel_id]) > 50:
+ self.channels_pts[channel_id] = self.channels_pts[channel_id][25:]
+
+ self.update_queue.put((update, updates.users, updates.chats))
+ elif isinstance(updates, (types.UpdateShortMessage, types.UpdateShortChatMessage)):
+ diff = self.send(
+ functions.updates.GetDifference(
+ pts=updates.pts - updates.pts_count,
+ date=updates.date,
+ qts=-1
+ )
+ )
+
+ self.update_queue.put((
+ types.UpdateNewMessage(
+ message=diff.new_messages[0],
+ pts=updates.pts,
+ pts_count=updates.pts_count
+ ),
+ diff.users,
+ diff.chats
+ ))
+ elif isinstance(updates, types.UpdateShort):
+ self.update_queue.put((updates.update, [], []))
+ except Exception as e:
+ log.error(e, exc_info=True)
+
+ log.debug("{} stopped".format(name))
+
+ def update_worker(self):
+ name = threading.current_thread().name
+ log.debug("{} started".format(name))
+
+ while True:
+ update = self.update_queue.get()
+
+ if update is None:
+ break
+
+ try:
+ if self.update_handler:
+ self.update_handler(
+ self,
+ update[0],
+ {i.id: i for i in update[1]},
+ {i.id: i for i in update[2]}
+ )
+ except Exception as e:
+ log.error(e, exc_info=True)
+
+ log.debug("{} stopped".format(name))
+
+ def signal_handler(self, *args):
+ self.stop()
+ self.is_idle = False
+
+ def idle(self, stop_signals: tuple = (SIGINT, SIGTERM, SIGABRT)):
+ """Blocks the program execution until one of the signals are received,
+ then gently stop the Client by closing the underlying connection.
+
+ Args:
+ stop_signals (``tuple``, optional):
+ Iterable containing signals the signal handler will listen to.
+ Defaults to (SIGINT, SIGTERM, SIGABRT).
+ """
+ for s in stop_signals:
+ signal(s, self.signal_handler)
+
+ self.is_idle = True
+
+ while self.is_idle:
+ time.sleep(1)
+
+ def set_update_handler(self, callback: callable):
+ """Use this method to set the update handler.
+
+ You must call this method *before* you *start()* the Client.
+
+ Args:
+ callback (``callable``):
+ A function that will be called when a new update is received from the server. It takes
+ *(client, update, users, chats)* as positional arguments (Look at the section below for
+ a detailed description).
+
+ Other Parameters:
+ client (:class:`Client `):
+ The Client itself, useful when you want to call other API methods inside the update handler.
+
+ update (``Update``):
+ The received update, which can be one of the many single Updates listed in the *updates*
+ field you see in the :obj:`Update ` type.
+
+ users (``dict``):
+ Dictionary of all :obj:`User ` mentioned in the update.
+ You can access extra info about the user (such as *first_name*, *last_name*, etc...) by using
+ the IDs you find in the *update* argument (e.g.: *users[1768841572]*).
+
+ chats (``dict``):
+ Dictionary of all :obj:`Chat ` and
+ :obj:`Channel ` mentioned in the update.
+ You can access extra info about the chat (such as *title*, *participants_count*, etc...)
+ by using the IDs you find in the *update* argument (e.g.: *chats[1701277281]*).
+
+ Note:
+ The following Empty or Forbidden types may exist inside the *users* and *chats* dictionaries.
+ They mean you have been blocked by the user or banned from the group/channel.
+
+ - :obj:`UserEmpty `
+ - :obj:`ChatEmpty `
+ - :obj:`ChatForbidden `
+ - :obj:`ChannelForbidden `
+ """
+ self.update_handler = callback
+
+ def send(self, data: Object):
+ """Use this method to send Raw Function queries.
+
+ This method makes possible to manually call every single Telegram API method in a low-level manner.
+ Available functions are listed in the :obj:`functions ` package and may accept compound
+ data types from :obj:`types ` as well as bare types such as ``int``, ``str``, etc...
+
+ Args:
+ data (``Object``):
+ The API Scheme function filled with proper arguments.
+
+ Raises:
+ :class:`Error `
+ """
+ if self.is_started:
+ r = self.session.send(data)
+
+ self.fetch_peers(getattr(r, "users", []))
+ self.fetch_peers(getattr(r, "chats", []))
+
+ return r
+ else:
+ raise ConnectionError("client '{}' is not started".format(self.session_name))
def load_config(self):
parser = ConfigParser()
parser.read("config.ini")
- if parser.has_section("pyrogram"):
- self.api_key = ApiKey(
+ if self.api_key is not None:
+ self.api_key = APIKey(
+ api_id=int(self.api_key[0]),
+ api_hash=self.api_key[1]
+ )
+ elif parser.has_section("pyrogram"):
+ self.api_key = APIKey(
api_id=parser.getint("pyrogram", "api_id"),
api_hash=parser.get("pyrogram", "api_hash")
)
else:
- self.api_key = ApiKey(
- api_id=int(self.api_key[0]),
- api_hash=self.api_key[1]
- )
+ raise AttributeError("No API Key found")
- if parser.has_section("proxy"):
+ if self.proxy is not None:
+ self.proxy = Proxy(
+ enabled=True,
+ hostname=self.proxy["hostname"],
+ port=int(self.proxy["port"]),
+ username=self.proxy.get("username", None),
+ password=self.proxy.get("password", None)
+ )
+ elif parser.has_section("proxy"):
self.proxy = Proxy(
enabled=parser.getboolean("proxy", "enabled"),
hostname=parser.get("proxy", "hostname"),
@@ -731,15 +830,6 @@ class Client:
username=parser.get("proxy", "username", fallback=None) or None,
password=parser.get("proxy", "password", fallback=None) or None
)
- else:
- if self.proxy is not None:
- self.proxy = Proxy(
- enabled=True,
- hostname=self.proxy["hostname"],
- port=int(self.proxy["port"]),
- username=self.proxy.get("username", None),
- password=self.proxy.get("password", None)
- )
def load_session(self, session_name):
try:
@@ -809,35 +899,6 @@ class Client:
offset_date = parse_dialogs(dialogs)
log.info("Entities count: {}".format(len(self.peers_by_id)))
- def resolve_username(self, username: str):
- username = username.lower().strip("@")
-
- resolved_peer = self.send(
- functions.contacts.ResolveUsername(
- username=username
- )
- ) # type: types.contacts.ResolvedPeer
-
- if type(resolved_peer.peer) is types.PeerUser:
- input_peer = types.InputPeerUser(
- user_id=resolved_peer.users[0].id,
- access_hash=resolved_peer.users[0].access_hash
- )
- peer_id = input_peer.user_id
- elif type(resolved_peer.peer) is types.PeerChannel:
- input_peer = types.InputPeerChannel(
- channel_id=resolved_peer.chats[0].id,
- access_hash=resolved_peer.chats[0].access_hash
- )
- peer_id = int("-100" + str(input_peer.channel_id))
- else:
- raise PeerIdInvalid
-
- self.peers_by_username[username] = input_peer
- self.peers_by_id[peer_id] = input_peer
-
- return input_peer
-
def resolve_peer(self, peer_id: int or str):
"""Use this method to get the *InputPeer* of a known *peer_id*.
@@ -845,9 +906,9 @@ class Client:
not available yet in the Client class as an easy-to-use method).
Args:
- peer_id (:obj:`int` | :obj:`str` | :obj:`Peer`):
- The Peer ID you want to extract the InputPeer from. Can be one of these types: :obj:`int` (direct ID),
- :obj:`str` (@username), :obj:`PeerUser `,
+ peer_id (``int`` | ``str`` | ``Peer``):
+ The Peer ID you want to extract the InputPeer from. Can be one of these types: ``int`` (direct ID),
+ ``str`` (@username), :obj:`PeerUser `,
:obj:`PeerChat `, :obj:`PeerChannel `
Returns:
@@ -856,12 +917,20 @@ class Client:
:obj:`InputPeerChannel ` depending on the *peer_id*.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
if type(peer_id) is str:
if peer_id in ("self", "me"):
return types.InputPeerSelf()
+ match = self.INVITE_LINK_RE.match(peer_id)
+
+ try:
+ decoded = base64.b64decode(match.group(1) + "=" * (-len(match.group(1)) % 4), "-_")
+ return self.resolve_peer(struct.unpack(">2iq", decoded)[1])
+ except (AttributeError, binascii.Error, struct.error):
+ pass
+
peer_id = peer_id.lower().strip("@+")
try:
@@ -870,7 +939,8 @@ class Client:
try:
return self.peers_by_username[peer_id]
except KeyError:
- return self.resolve_username(peer_id)
+ self.send(functions.contacts.ResolveUsername(peer_id))
+ return self.peers_by_username[peer_id]
else:
try:
return self.peers_by_phone[peer_id]
@@ -903,7 +973,7 @@ class Client:
Full information about the user in form of a :obj:`UserFull ` object.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
return self.send(
functions.users.GetFullUser(
@@ -921,34 +991,35 @@ class Client:
"""Use this method to send text messages.
Args:
- chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ 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).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
- text (:obj:`str`):
+ text (``str``):
Text of the message to be sent.
- parse_mode (:obj:`str`):
- Use :obj:`pyrogram.ParseMode.MARKDOWN` or :obj:`pyrogram.ParseMode.HTML` if you want Telegram apps
- to show bold, italic, fixed-width text or inline URLs in your message.
+ parse_mode (``str``):
+ Use :obj:`MARKDOWN ` or :obj:`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 (:obj:`bool`, optional):
+ disable_web_page_preview (``bool``, optional):
Disables link previews for links in this message.
- disable_notification (:obj:`bool`, optional):
+ disable_notification (``bool``, optional):
Sends the message silently.
Users will receive a notification with no sound.
- reply_to_message_id (:obj:`bool`, optional):
+ reply_to_message_id (``bool``, optional):
If the message is a reply, ID of the original message.
Returns:
On success, the sent Message is returned.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
style = self.html if parse_mode.lower() == "html" else self.markdown
@@ -971,21 +1042,22 @@ class Client:
"""Use this method to forward messages of any kind.
Args:
- chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ 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).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
- from_chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the chat where the original message was sent
- (or channel/supergroup username in the format @username). For your personal cloud
- storage (Saved Messages) you can simply use "me" or "self".
- Phone numbers that exist in your Telegram address book are also supported.
+ from_chat_id (``int`` | ``str``):
+ Unique identifier (int) or username (str) of the source chat where the original message was sent.
+ 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).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
- message_ids (:obj:`list`):
+ message_ids (``list``):
A list of Message identifiers in the chat specified in *from_chat_id*.
- disable_notification (:obj:`bool`, optional):
+ disable_notification (``bool``, optional):
Sends the message silently.
Users will receive a notification with no sound.
@@ -993,7 +1065,7 @@ class Client:
On success, the sent Message is returned.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
return self.send(
functions.messages.ForwardMessages(
@@ -1017,51 +1089,52 @@ class Client:
"""Use this method to send photos.
Args:
- chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ 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).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
- photo (:obj:`str`):
+ photo (``str``):
Photo to send.
Pass a file path as string to send a photo that exists on your local machine.
- caption (:obj:`bool`, optional):
+ caption (``bool``, optional):
Photo caption, 0-200 characters.
- parse_mode (:obj:`str`):
- Use :obj:`pyrogram.ParseMode.MARKDOWN` or :obj:`pyrogram.ParseMode.HTML` if you want Telegram apps
- to show bold, italic, fixed-width text or inline URLs in your caption.
+ parse_mode (``str``):
+ Use :obj:`MARKDOWN ` or :obj:`HTML `
+ if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your caption.
Defaults to Markdown.
- ttl_seconds (:obj:`int`, optional):
+ ttl_seconds (``int``, optional):
Self-Destruct Timer.
- If you set a timer, the photo will self-destruct in :obj:`ttl_seconds`
+ If you set a timer, the photo will self-destruct in *ttl_seconds*
seconds after it was viewed.
- disable_notification (:obj:`bool`, optional):
+ disable_notification (``bool``, optional):
Sends the message silently.
Users will receive a notification with no sound.
- reply_to_message_id (:obj:`int`, optional):
+ reply_to_message_id (``int``, optional):
If the message is a reply, ID of the original message.
- progress (:obj:`callable`):
+ progress (``callable``):
Pass a callback function to view the upload progress.
The function must accept two arguments (current, total).
Other Parameters:
- current (:obj:`int`):
+ current (``int``):
The amount of bytes uploaded so far.
- total (:obj:`int`):
+ total (``int``):
The size of the file.
Returns:
On success, the sent Message is returned.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
style = self.html if parse_mode.lower() == "html" else self.markdown
file = self.save_file(photo, progress=progress)
@@ -1102,55 +1175,56 @@ class Client:
For sending voice messages, use the :obj:`send_voice` method instead.
Args:
- chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ 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).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
- audio (:obj:`str`):
+ audio (``str``):
Audio file to send.
Pass a file path as string to send an audio file that exists on your local machine.
- caption (:obj:`str`, optional):
+ caption (``str``, optional):
Audio caption, 0-200 characters.
- parse_mode (:obj:`str`):
- Use :obj:`pyrogram.ParseMode.MARKDOWN` or :obj:`pyrogram.ParseMode.HTML` if you want Telegram apps
- to show bold, italic, fixed-width text or inline URLs in your caption.
+ parse_mode (``str``):
+ Use :obj:`MARKDOWN ` or :obj:`HTML `
+ if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your caption.
Defaults to Markdown.
- duration (:obj:`int`, optional):
+ duration (``int``, optional):
Duration of the audio in seconds.
- performer (:obj:`str`, optional):
+ performer (``str``, optional):
Performer.
- title (:obj:`str`, optional):
+ title (``str``, optional):
Track name.
- disable_notification (:obj:`bool`, optional):
+ disable_notification (``bool``, optional):
Sends the message silently.
Users will receive a notification with no sound.
- reply_to_message_id (:obj:`int`, optional):
+ reply_to_message_id (``int``, optional):
If the message is a reply, ID of the original message.
- progress (:obj:`callable`):
+ progress (``callable``):
Pass a callback function to view the upload progress.
The function must accept two arguments (current, total).
Other Parameters:
- current (:obj:`int`):
+ current (``int``):
The amount of bytes uploaded so far.
- total (:obj:`int`):
+ total (``int``):
The size of the file.
Returns:
On success, the sent Message is returned.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
style = self.html if parse_mode.lower() == "html" else self.markdown
file = self.save_file(audio, progress=progress)
@@ -1194,46 +1268,47 @@ class Client:
"""Use this method to send general files.
Args:
- chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ 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).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
- document (:obj:`str`):
+ document (``str``):
File to send.
Pass a file path as string to send a file that exists on your local machine.
- caption (:obj:`str`, optional):
+ caption (``str``, optional):
Document caption, 0-200 characters.
- parse_mode (:obj:`str`):
- Use :obj:`pyrogram.ParseMode.MARKDOWN` or :obj:`pyrogram.ParseMode.HTML` if you want Telegram apps
- to show bold, italic, fixed-width text or inline URLs in your caption.
+ parse_mode (``str``):
+ Use :obj:`MARKDOWN ` or :obj:`HTML `
+ if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your caption.
Defaults to Markdown.
- disable_notification (:obj:`bool`, optional):
+ disable_notification (``bool``, optional):
Sends the message silently.
Users will receive a notification with no sound.
- reply_to_message_id (:obj:`int`, optional):
+ reply_to_message_id (``int``, optional):
If the message is a reply, ID of the original message.
- progress (:obj:`callable`):
+ progress (``callable``):
Pass a callback function to view the upload progress.
The function must accept two arguments (current, total).
Other Parameters:
- current (:obj:`int`):
+ current (``int``):
The amount of bytes uploaded so far.
- total (:obj:`int`):
+ total (``int``):
The size of the file.
Returns:
On success, the sent Message is returned.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
style = self.html if parse_mode.lower() == "html" else self.markdown
file = self.save_file(document, progress=progress)
@@ -1270,38 +1345,39 @@ class Client:
"""Use this method to send .webp stickers.
Args:
- chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ 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).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
- sticker (:obj:`str`):
+ sticker (``str``):
Sticker to send.
Pass a file path as string to send a sticker that exists on your local machine.
- disable_notification (:obj:`bool`, optional):
+ disable_notification (``bool``, optional):
Sends the message silently.
Users will receive a notification with no sound.
- reply_to_message_id (:obj:`int`, optional):
+ reply_to_message_id (``int``, optional):
If the message is a reply, ID of the original message.
- progress (:obj:`callable`):
+ progress (``callable``):
Pass a callback function to view the upload progress.
The function must accept two arguments (current, total).
Other Parameters:
- current (:obj:`int`):
+ current (``int``):
The amount of bytes uploaded so far.
- total (:obj:`int`):
+ total (``int``):
The size of the file.
Returns:
On success, the sent Message is returned.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
file = self.save_file(sticker, progress=progress)
@@ -1337,70 +1413,71 @@ class Client:
width: int = 0,
height: int = 0,
thumb: str = None,
- supports_streaming: bool = None,
+ supports_streaming: bool = True,
disable_notification: bool = None,
reply_to_message_id: int = None,
progress: callable = None):
"""Use this method to send video files.
Args:
- chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ 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).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
- video (:obj:`str`):
+ video (``str``):
Video to send.
Pass a file path as string to send a video that exists on your local machine.
- caption (:obj:`str`, optional):
+ caption (``str``, optional):
Video caption, 0-200 characters.
- parse_mode (:obj:`str`):
- Use :obj:`pyrogram.ParseMode.MARKDOWN` or :obj:`pyrogram.ParseMode.HTML` if you want Telegram apps
- to show bold, italic, fixed-width text or inline URLs in your caption.
+ parse_mode (``str``):
+ Use :obj:`MARKDOWN ` or :obj:`HTML `
+ if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your caption.
Defaults to Markdown.
- duration (:obj:`int`, optional):
+ duration (``int``, optional):
Duration of sent video in seconds.
- width (:obj:`int`, optional):
+ width (``int``, optional):
Video width.
- height (:obj:`int`, optional):
+ height (``int``, optional):
Video height.
- thumb (:obj:`str`, optional):
+ thumb (``str``, optional):
Video thumbnail.
Pass a file path as string to send an image that exists on your local machine.
Thumbnail should have 90 or less pixels of width and 90 or less pixels of height.
- supports_streaming (:obj:`bool`, optional):
+ supports_streaming (``bool``, optional):
Pass True, if the uploaded video is suitable for streaming.
- disable_notification (:obj:`bool`, optional):
+ disable_notification (``bool``, optional):
Sends the message silently.
Users will receive a notification with no sound.
- reply_to_message_id (:obj:`int`, optional):
+ reply_to_message_id (``int``, optional):
If the message is a reply, ID of the original message.
- progress (:obj:`callable`):
+ progress (``callable``):
Pass a callback function to view the upload progress.
The function must accept two arguments (current, total).
Other Parameters:
- current (:obj:`int`):
+ current (``int``):
The amount of bytes uploaded so far.
- total (:obj:`int`):
+ total (``int``):
The size of the file.
Returns:
On success, the sent Message is returned.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
style = self.html if parse_mode.lower() == "html" else self.markdown
file = self.save_file(video, progress=progress)
@@ -1417,7 +1494,7 @@ class Client:
thumb=file_thumb,
attributes=[
types.DocumentAttributeVideo(
- supports_streaming=supports_streaming,
+ supports_streaming=supports_streaming or None,
duration=duration,
w=width,
h=height
@@ -1448,49 +1525,50 @@ class Client:
"""Use this method to send audio files.
Args:
- chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ 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).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
- voice (:obj:`str`):
+ voice (``str``):
Audio file to send.
Pass a file path as string to send an audio file that exists on your local machine.
- caption (:obj:`str`, optional):
+ caption (``str``, optional):
Voice message caption, 0-200 characters.
- parse_mode (:obj:`str`):
- Use :obj:`pyrogram.ParseMode.MARKDOWN` or :obj:`pyrogram.ParseMode.HTML` if you want Telegram apps
- to show bold, italic, fixed-width text or inline URLs in your caption.
+ parse_mode (``str``):
+ Use :obj:`MARKDOWN ` or :obj:`HTML `
+ if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your caption.
Defaults to Markdown.
- duration (:obj:`int`, optional):
+ duration (``int``, optional):
Duration of the voice message in seconds.
- disable_notification (:obj:`bool`, optional):
+ disable_notification (``bool``, optional):
Sends the message silently.
Users will receive a notification with no sound.
- reply_to_message_id (:obj:`int`, optional):
+ reply_to_message_id (``int``, optional):
If the message is a reply, ID of the original message
- progress (:obj:`callable`):
+ progress (``callable``):
Pass a callback function to view the upload progress.
The function must accept two arguments (current, total).
Other Parameters:
- current (:obj:`int`):
+ current (``int``):
The amount of bytes uploaded so far.
- total (:obj:`int`):
+ total (``int``):
The size of the file.
Returns:
On success, the sent Message is returned.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
style = self.html if parse_mode.lower() == "html" else self.markdown
file = self.save_file(voice, progress=progress)
@@ -1532,44 +1610,45 @@ class Client:
"""Use this method to send video messages.
Args:
- chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ 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).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
- video_note (:obj:`str`):
+ video_note (``str``):
Video note to send.
Pass a file path as string to send a video note that exists on your local machine.
- duration (:obj:`int`, optional):
+ duration (``int``, optional):
Duration of sent video in seconds.
- length (:obj:`int`, optional):
+ length (``int``, optional):
Video width and height.
- disable_notification (:obj:`bool`, optional):
+ disable_notification (``bool``, optional):
Sends the message silently.
Users will receive a notification with no sound.
- reply_to_message_id (:obj:`int`, optional):
+ reply_to_message_id (``int``, optional):
If the message is a reply, ID of the original message
- progress (:obj:`callable`):
+ progress (``callable``):
Pass a callback function to view the upload progress.
The function must accept two arguments (current, total).
Other Parameters:
- current (:obj:`int`):
+ current (``int``):
The amount of bytes uploaded so far.
- total (:obj:`int`):
+ total (``int``):
The size of the file.
Returns:
On success, the sent Message is returned.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
file = self.save_file(video_note, progress=progress)
@@ -1611,20 +1690,21 @@ class Client:
On success, an Update containing the sent Messages is returned.
Args:
- chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ 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).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
- media (:obj:`list`):
+ media (``list``):
A list containing either :obj:`pyrogram.InputMedia.Photo` or :obj:`pyrogram.InputMedia.Video` objects
describing photos and videos to be sent, must include 2–10 items.
- disable_notification (:obj:`bool`, optional):
+ disable_notification (``bool``, optional):
Sends the message silently.
Users will receive a notification with no sound.
- reply_to_message_id (:obj:`int`, optional):
+ reply_to_message_id (``int``, optional):
If the message is a reply, ID of the original message.
"""
multi_media = []
@@ -1667,7 +1747,7 @@ class Client:
mime_type=mimetypes.types_map[".mp4"],
attributes=[
types.DocumentAttributeVideo(
- supports_streaming=i.supports_streaming,
+ supports_streaming=i.supports_streaming or None,
duration=i.duration,
w=i.width,
h=i.height
@@ -1709,29 +1789,30 @@ class Client:
"""Use this method to send points on the map.
Args:
- chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ 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).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
- latitude (:obj:`float`):
+ latitude (``float``):
Latitude of the location.
- longitude (:obj:`float`):
+ longitude (``float``):
Longitude of the location.
- disable_notification (:obj:`bool`, optional):
+ disable_notification (``bool``, optional):
Sends the message silently.
Users will receive a notification with no sound.
- reply_to_message_id (:obj:`int`, optional):
+ reply_to_message_id (``int``, optional):
If the message is a reply, ID of the original message
Returns:
On success, the sent Message is returned.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
return self.send(
functions.messages.SendMedia(
@@ -1761,38 +1842,39 @@ class Client:
"""Use this method to send information about a venue.
Args:
- chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ 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).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
- latitude (:obj:`float`):
+ latitude (``float``):
Latitude of the venue.
- longitude (:obj:`float`):
+ longitude (``float``):
Longitude of the venue.
- title (:obj:`str`):
+ title (``str``):
Name of the venue.
- address (:obj:`str`):
+ address (``str``):
Address of the venue.
- foursquare_id (:obj:`str`, optional):
+ foursquare_id (``str``, optional):
Foursquare identifier of the venue.
- disable_notification (:obj:`bool`, optional):
+ disable_notification (``bool``, optional):
Sends the message silently.
Users will receive a notification with no sound.
- reply_to_message_id (:obj:`int`, optional):
+ reply_to_message_id (``int``, optional):
If the message is a reply, ID of the original message
Returns:
On success, the sent Message is returned.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
return self.send(
functions.messages.SendMedia(
@@ -1825,32 +1907,33 @@ class Client:
"""Use this method to send phone contacts.
Args:
- chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ 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).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
- phone_number (:obj:`str`):
+ phone_number (``str``):
Contact's phone number.
- first_name (:obj:`str`):
+ first_name (``str``):
Contact's first name.
- last_name (:obj:`str`):
+ last_name (``str``):
Contact's last name.
- disable_notification (:obj:`bool`, optional):
+ disable_notification (``bool``, optional):
Sends the message silently.
Users will receive a notification with no sound.
- reply_to_message_id (:obj:`int`, optional):
+ reply_to_message_id (``int``, optional):
If the message is a reply, ID of the original message.
Returns:
On success, the sent Message is returned.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
return self.send(
functions.messages.SendMedia(
@@ -1874,26 +1957,32 @@ class Client:
"""Use this method when you need to tell the other party that something is happening on your side.
Args:
- chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ 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).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
- action (:obj:`callable`):
+ action (``callable``):
Type of action to broadcast.
- Choose one from the :class:`pyrogram.ChatAction` class,
+ Choose one from the :class:`ChatAction ` class,
depending on what the user is about to receive.
- progress (:obj:`int`, optional):
+ progress (``int``, optional):
Progress of the upload process.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
+ if "Upload" in action.__name__:
+ action = action(progress)
+ else:
+ action = action()
+
return self.send(
functions.messages.SetTyping(
peer=self.resolve_peer(chat_id),
- action=action(progress=progress)
+ action=action
)
)
@@ -1904,19 +1993,19 @@ class Client:
"""Use this method to get a list of profile pictures for a user.
Args:
- user_id (:obj:`int` | :obj:`str`):
+ user_id (``int`` | ``str``):
Unique identifier of the target user.
- offset (:obj:`int`, optional):
+ offset (``int``, optional):
Sequential number of the first photo to be returned.
By default, all photos are returned.
- limit (:obj:`int`, optional):
+ limit (``int``, optional):
Limits the number of photos to be retrieved.
Values between 1—100 are accepted. Defaults to 100.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
return self.send(
functions.photos.GetUserPhotos(
@@ -1936,27 +2025,28 @@ class Client:
"""Use this method to edit text messages.
Args:
- chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ 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).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
- message_id (:obj:`int`):
+ message_id (``int``):
Message identifier in the chat specified in chat_id.
- text (:obj:`str`):
+ text (``str``):
New text of the message.
- parse_mode (:obj:`str`):
- Use :obj:`pyrogram.ParseMode.MARKDOWN` or :obj:`pyrogram.ParseMode.HTML` if you want Telegram apps
- to show bold, italic, fixed-width text or inline URLs in your message.
+ parse_mode (``str``):
+ Use :obj:`MARKDOWN ` or :obj:`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 (:obj:`bool`, optional):
+ disable_web_page_preview (``bool``, optional):
Disables link previews for links in this message.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
style = self.html if parse_mode.lower() == "html" else self.markdown
@@ -1977,24 +2067,25 @@ class Client:
"""Use this method to edit captions of messages.
Args:
- chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ 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).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
- message_id (:obj:`int`):
+ message_id (``int``):
Message identifier in the chat specified in chat_id.
- caption (:obj:`str`):
+ caption (``str``):
New caption of the message.
- parse_mode (:obj:`str`):
- Use :obj:`pyrogram.ParseMode.MARKDOWN` or :obj:`pyrogram.ParseMode.HTML` if you want Telegram apps
- to show bold, italic, fixed-width text or inline URLs in your caption.
+ parse_mode (``str``):
+ Use :obj:`MARKDOWN ` or :obj:`HTML `
+ if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your caption.
Defaults to Markdown.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
style = self.html if parse_mode.lower() == "html" else self.markdown
@@ -2019,21 +2110,22 @@ class Client:
- If the user has *can_delete_messages* permission in a supergroup or a channel, it can delete any message there.
Args:
- chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ 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).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
- message_ids (:obj:`list`):
+ message_ids (``list``):
List of identifiers of the messages to delete.
- revoke (:obj:`bool`, optional):
+ revoke (``bool``, optional):
Deletes messages on both parts.
This is only for private cloud chats and normal groups, messages on
channels and supergroups are always revoked (i.e.: deleted for everyone).
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
peer = self.resolve_peer(chat_id)
@@ -2084,14 +2176,21 @@ class Client:
md5_sum = "".join([hex(i)[2:].zfill(2) for i in md5_sum.digest()])
break
- session.send(
- (functions.upload.SaveBigFilePart if is_big else functions.upload.SaveFilePart)(
+ if is_big:
+ rpc = functions.upload.SaveBigFilePart(
file_id=file_id,
file_part=file_part,
- bytes=chunk,
- file_total_parts=file_total_parts
+ file_total_parts=file_total_parts,
+ bytes=chunk
)
- )
+ else:
+ rpc = functions.upload.SaveFilePart(
+ file_id=file_id,
+ file_part=file_part,
+ bytes=chunk
+ )
+
+ assert self.send(rpc), "Couldn't upload file"
if is_missing_part:
return
@@ -2104,14 +2203,22 @@ class Client:
if progress:
progress(min(file_part * part_size, file_size), file_size)
except Exception as e:
- log.error(e)
+ log.error(e, exc_info=True)
else:
- return (types.InputFileBig if is_big else types.InputFile)(
- id=file_id,
- parts=file_total_parts,
- name=os.path.basename(path),
- md5_checksum=md5_sum
- )
+ if is_big:
+ return types.InputFileBig(
+ id=file_id,
+ parts=file_total_parts,
+ name=os.path.basename(path),
+
+ )
+ else:
+ return types.InputFile(
+ id=file_id,
+ parts=file_total_parts,
+ name=os.path.basename(path),
+ md5_checksum=md5_sum
+ )
finally:
session.stop()
@@ -2172,9 +2279,9 @@ class Client:
version=version
)
- file_name = "download_{}.temp".format(MsgId())
limit = 1024 * 1024
offset = 0
+ file_name = ""
try:
r = session.send(
@@ -2186,7 +2293,9 @@ class Client:
)
if isinstance(r, types.upload.File):
- with open(file_name, "wb") as f:
+ with tempfile.NamedTemporaryFile('wb', delete=False) as f:
+ file_name = f.name
+
while True:
chunk = r.bytes
@@ -2210,7 +2319,7 @@ class Client:
)
)
- if isinstance(r, types.upload.FileCdnRedirect):
+ elif isinstance(r, types.upload.FileCdnRedirect):
cdn_session = Session(
r.dc_id,
self.test_mode,
@@ -2223,11 +2332,12 @@ class Client:
cdn_session.start()
try:
- with open(file_name, "wb") as f:
+ with tempfile.NamedTemporaryFile('wb', delete=False) as f:
+ file_name = f.name
+
while True:
r2 = cdn_session.send(
functions.upload.GetCdnFile(
- location=location,
file_token=r.file_token,
offset=offset,
limit=limit
@@ -2281,11 +2391,18 @@ class Client:
if len(chunk) < limit:
break
except Exception as e:
- log.error(e)
+ raise e
finally:
cdn_session.stop()
except Exception as e:
- log.error(e)
+ log.error(e, exc_info=True)
+
+ try:
+ os.remove(file_name)
+ except OSError:
+ pass
+
+ return ""
else:
return file_name
finally:
@@ -2295,12 +2412,12 @@ class Client:
"""Use this method to join a group chat or channel.
Args:
- chat_id (:obj:`str`):
+ chat_id (``str``):
Unique identifier for the target chat in form of *t.me/joinchat/* links or username of the target
channel/supergroup (in the format @username).
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
match = self.INVITE_LINK_RE.match(chat_id)
@@ -2332,15 +2449,15 @@ class Client:
"""Use this method to leave a group chat or channel.
Args:
- chat_id (:obj:`int` | :obj:`str`):
+ chat_id (``int`` | ``str``):
Unique identifier for the target chat or username of the target channel/supergroup
(in the format @username).
- delete (:obj:`bool`, optional):
+ delete (``bool``, optional):
Deletes the group chat dialog after leaving (for simple group chats, not supergroups).
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
peer = self.resolve_peer(chat_id)
@@ -2374,11 +2491,11 @@ class Client:
The user must be an administrator in the chat for this to work and must have the appropriate admin rights.
Args:
- chat_id (:obj:`int` | :obj:`str`):
+ chat_id (``int`` | ``str``):
Unique identifier for the target chat or username of the target channel/supergroup
(in the format @username).
- new (:obj:`bool`):
+ new (``bool``):
The previous link will be deactivated and a new link will be generated.
This is also used to create the invite link in case it doesn't exist yet.
@@ -2386,7 +2503,7 @@ class Client:
On success, the exported invite link as string is returned.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
Note:
If the returned link is a new one it may take a while for it to be activated.
@@ -2436,20 +2553,20 @@ class Client:
This password will be asked when you log in on a new device in addition to the SMS code.
Args:
- password (:obj:`str`):
+ password (``str``):
Your password.
- hint (:obj:`str`, optional):
+ hint (``str``, optional):
A password hint.
- email (:obj:`str`, optional):
+ email (``str``, optional):
Recovery e-mail.
Returns:
True on success, False otherwise.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
r = self.send(functions.account.GetPassword())
@@ -2475,20 +2592,20 @@ class Client:
"""Use this method to change your Two-Step Verification password (Cloud Password) with a new one.
Args:
- current_password (:obj:`str`):
+ current_password (``str``):
Your current password.
- new_password (:obj:`str`):
+ new_password (``str``):
Your new password.
- new_hint (:obj:`str`, optional):
+ new_hint (``str``, optional):
A new password hint.
Returns:
True on success, False otherwise.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
r = self.send(functions.account.GetPassword())
@@ -2515,14 +2632,14 @@ class Client:
"""Use this method to turn off the Two-Step Verification security feature (Cloud Password) on your account.
Args:
- password (:obj:`str`):
+ password (``str``):
Your current password.
Returns:
True on success, False otherwise.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
r = self.send(functions.account.GetPassword())
@@ -2544,46 +2661,51 @@ class Client:
def download_media(self,
message: types.Message,
- file_name: str = None,
+ file_name: str = "",
block: bool = True,
progress: callable = None):
"""Use this method to download the media from a Message.
- Files are saved in the *downloads* folder.
-
Args:
message (:obj:`Message `):
The Message containing the media.
- file_name (:obj:`str`, optional):
- Specify a custom *file_name* to be used instead of the one provided by Telegram.
+ file_name (``str``, optional):
+ A custom *file_name* to be used instead of the one provided by Telegram.
+ By default, all files are downloaded in the *downloads* folder in your working directory.
+ You can also specify a path for downloading files in a custom location: paths that end with "/"
+ are considered directories. All non-existent folders will be created automatically.
- block (:obj:`bool`, optional):
+ block (``bool``, optional):
Blocks the code execution until the file has been downloaded.
Defaults to True.
- progress (:obj:`callable`):
+ progress (``callable``):
Pass a callback function to view the download progress.
The function must accept two arguments (current, total).
Other Parameters:
- current (:obj:`int`):
+ current (``int``):
The amount of bytes downloaded so far.
- total (:obj:`int`):
+ total (``int``):
The size of the file.
Returns:
- The relative path of the downloaded file.
+ On success, the absolute path of the downloaded file as string is returned, None otherwise.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
- if isinstance(message, types.Message):
+ if isinstance(message, (types.Message, types.Photo)):
done = Event()
- media = message.media
path = [None]
+ if isinstance(message, types.Message):
+ media = message.media
+ else:
+ media = message
+
if media is not None:
self.download_queue.put((media, file_name, done, progress, path))
else:
@@ -2594,18 +2716,61 @@ class Client:
return path[0]
+ def download_photo(self,
+ photo: types.Photo or types.UserProfilePhoto or types.ChatPhoto,
+ file_name: str = "",
+ block: bool = True):
+ """Use this method to download a photo not contained inside a Message.
+ For example, a photo of a User or a Chat/Channel.
+
+ Args:
+ photo (:obj:`Photo ` | :obj:`UserProfilePhoto ` | :obj:`ChatPhoto `):
+ The photo object.
+
+ file_name (``str``, optional):
+ A custom *file_name* to be used instead of the one provided by Telegram.
+ By default, all photos are downloaded in the *downloads* folder in your working directory.
+ You can also specify a path for downloading photos in a custom location: paths that end with "/"
+ are considered directories. All non-existent folders will be created automatically.
+
+ block (``bool``, optional):
+ Blocks the code execution until the photo has been downloaded.
+ Defaults to True.
+
+ Returns:
+ On success, the absolute path of the downloaded photo as string is returned, None otherwise.
+
+ Raises:
+ :class:`Error `
+ """
+ if isinstance(photo, (types.UserProfilePhoto, types.ChatPhoto)):
+ photo = types.Photo(
+ id=0,
+ access_hash=0,
+ date=int(time.time()),
+ sizes=[types.PhotoSize(
+ type="",
+ location=photo.photo_big,
+ w=0,
+ h=0,
+ size=0
+ )]
+ )
+
+ return self.download_media(photo, file_name, block)
+
def add_contacts(self, contacts: list):
"""Use this method to add contacts to your Telegram address book.
Args:
- contacts (:obj:`list`):
+ contacts (``list``):
A list of :obj:`InputPhoneContact `
Returns:
On success, the added contacts are returned.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
imported_contacts = self.send(
functions.contacts.ImportContacts(
@@ -2619,7 +2784,7 @@ class Client:
"""Use this method to delete contacts from your Telegram address book
Args:
- ids (:obj:`list`):
+ ids (``list``):
A list of unique identifiers for the target users.
Can be an ID (int), a username (string) or phone number (string).
@@ -2627,7 +2792,7 @@ class Client:
True on success.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
contacts = []
@@ -2669,25 +2834,25 @@ class Client:
You can then send a result using :obj:`send_inline_bot_result `
Args:
- bot (:obj:`int` | :obj:`str`):
+ bot (``int`` | ``str``):
Unique identifier of the inline bot you want to get results from. You can specify
a @username (str) or a bot ID (int).
- query (:obj:`str`):
+ query (``str``):
Text of the query (up to 512 characters).
- offset (:obj:`str`):
+ offset (``str``):
Offset of the results to be returned.
- location (:obj:`tuple`, optional):
+ location (``tuple``, optional):
Your location in tuple format (latitude, longitude), e.g.: (51.500729, -0.124583).
Useful for location-based results only.
Returns:
- On Success, `BotResults `_ is returned.
+ On Success, :obj:`BotResults ` is returned.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
return self.send(
functions.messages.GetInlineBotResults(
@@ -2712,29 +2877,30 @@ class Client:
Bot results can be retrieved using :obj:`get_inline_bot_results `
Args:
- chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ 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).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
- query_id (:obj:`int`):
+ query_id (``int``):
Unique identifier for the answered query.
- result_id (:obj:`str`):
+ result_id (``str``):
Unique identifier for the result that was chosen.
- disable_notification (:obj:`bool`, optional):
+ disable_notification (``bool``, optional):
Sends the message silently.
Users will receive a notification with no sound.
- reply_to_message_id (:obj:`bool`, optional):
+ reply_to_message_id (``bool``, optional):
If the message is a reply, ID of the original message.
Returns:
On success, the sent Message is returned.
Raises:
- :class:`pyrogram.Error`
+ :class:`Error `
"""
return self.send(
functions.messages.SendInlineBotResult(
@@ -2746,3 +2912,40 @@ class Client:
reply_to_msg_id=reply_to_message_id
)
)
+
+ def get_messages(self,
+ chat_id: int or str,
+ message_ids: list):
+ """Use this method to get messages that belong to a specific chat.
+ You can retrieve up to 200 messages at once.
+
+ Args:
+ chat_id (``int`` | ``str``):
+ Unique identifier (int) or username (str) of the target chat.
+ For your personal cloud (Saved Messages) you can simply use "me" or "self".
+ For a contact that exists in your Telegram address book you can use his phone number (str).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
+
+ message_ids (``list``):
+ A list of Message identifiers in the chat specified in *chat_id*.
+
+ Returns:
+ List of the requested messages
+
+ Raises:
+ :class:`Error `
+ """
+ peer = self.resolve_peer(chat_id)
+ message_ids = [types.InputMessageID(i) for i in message_ids]
+
+ if isinstance(peer, types.InputPeerChannel):
+ rpc = functions.channels.GetMessages(
+ channel=peer,
+ id=message_ids
+ )
+ else:
+ rpc = functions.messages.GetMessages(
+ id=message_ids
+ )
+
+ return self.send(rpc)
diff --git a/pyrogram/client/input_media.py b/pyrogram/client/input_media.py
index da270c2e..f4110f1b 100644
--- a/pyrogram/client/input_media.py
+++ b/pyrogram/client/input_media.py
@@ -81,7 +81,7 @@ class InputMedia:
width: int = 0,
height: int = 0,
duration: int = 0,
- supports_streaming: bool = None):
+ supports_streaming: bool = True):
self.media = media
self.caption = caption
self.parse_mode = parse_mode
diff --git a/pyrogram/client/style/markdown.py b/pyrogram/client/style/markdown.py
index e39ac876..d9e8c571 100644
--- a/pyrogram/client/style/markdown.py
+++ b/pyrogram/client/style/markdown.py
@@ -36,7 +36,7 @@ class Markdown:
CODE_DELIMITER = "`"
PRE_DELIMITER = "```"
- MARKDOWN_RE = re.compile(r"```([\w ]*)\n([\w\W]*)(?:\n|)```|\[([^[(]+)\]\(([^])]+)\)|({d})(.+?)\5".format(
+ MARKDOWN_RE = re.compile(r"```([\w ]*)\n([\w\W]*)(?:\n|)```|\[(.+?)\]\((.+?)\)|({d})(.+?)\5".format(
d="|".join(
["".join(i) for i in [
["\{}".format(j) for j in i]
diff --git a/pyrogram/connection/connection.py b/pyrogram/connection/connection.py
index aa958a7a..02f57efc 100644
--- a/pyrogram/connection/connection.py
+++ b/pyrogram/connection/connection.py
@@ -54,6 +54,7 @@ class Connection:
def close(self):
self.connection.close()
+ log.info("Disconnected")
def send(self, data: bytes):
with self.lock:
diff --git a/pyrogram/connection/transport/tcp/tcp.py b/pyrogram/connection/transport/tcp/tcp.py
index a6195586..22b953c1 100644
--- a/pyrogram/connection/transport/tcp/tcp.py
+++ b/pyrogram/connection/transport/tcp/tcp.py
@@ -20,7 +20,15 @@ import logging
import socket
from collections import namedtuple
-import socks
+try:
+ import socks
+except ImportError as e:
+ e.msg = (
+ "PySocks is missing and Pyrogram can't run without. "
+ "Please install it using \"pip3 install pysocks\"."
+ )
+
+ raise e
log = logging.getLogger(__name__)
diff --git a/pyrogram/crypto/aes.py b/pyrogram/crypto/aes.py
index c571fb2d..8ca72535 100644
--- a/pyrogram/crypto/aes.py
+++ b/pyrogram/crypto/aes.py
@@ -16,52 +16,33 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see .
-import logging
-
-log = logging.getLogger(__name__)
-
try:
import tgcrypto
-except ImportError:
- log.warning(
- "TgCrypto is missing! "
- "Pyrogram will work the same, but at a much slower speed. "
+except ImportError as e:
+ e.msg = (
+ "TgCrypto is missing and Pyrogram can't run without. "
+ "Please install it using \"pip3 install tgcrypto\". "
"More info: https://docs.pyrogram.ml/resources/TgCrypto"
)
- is_fast = False
- import pyaes
-else:
- log.info("Using TgCrypto")
- is_fast = True
+
+ raise e
-# TODO: Ugly IFs
class AES:
@classmethod
def ige_encrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes:
- if is_fast:
- return tgcrypto.ige_encrypt(data, key, iv)
- else:
- return cls.ige(data, key, iv, True)
+ return tgcrypto.ige_encrypt(data, key, iv)
@classmethod
def ige_decrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes:
- if is_fast:
- return tgcrypto.ige_decrypt(data, key, iv)
- else:
- return cls.ige(data, key, iv, False)
+ return tgcrypto.ige_decrypt(data, key, iv)
@staticmethod
def ctr_decrypt(data: bytes, key: bytes, iv: bytes, offset: int) -> bytes:
- replace = int.to_bytes(offset // 16, byteorder="big", length=4)
+ replace = int.to_bytes(offset // 16, 4, "big")
iv = iv[:-4] + replace
- if is_fast:
- return tgcrypto.ctr_decrypt(data, key, iv)
- else:
- ctr = pyaes.AESModeOfOperationCTR(key)
- ctr._counter._counter = list(iv)
- return ctr.decrypt(data)
+ return tgcrypto.ctr_decrypt(data, key, iv)
@staticmethod
def xor(a: bytes, b: bytes) -> bytes:
@@ -70,23 +51,3 @@ class AES:
len(a),
"big",
)
-
- @classmethod
- def ige(cls, data: bytes, key: bytes, iv: bytes, encrypt: bool) -> bytes:
- cipher = pyaes.AES(key)
-
- iv_1 = iv[:16]
- iv_2 = iv[16:]
-
- data = [data[i: i + 16] for i in range(0, len(data), 16)]
-
- if encrypt:
- for i, chunk in enumerate(data):
- iv_1 = data[i] = cls.xor(cipher.encrypt(cls.xor(chunk, iv_1)), iv_2)
- iv_2 = chunk
- else:
- for i, chunk in enumerate(data):
- iv_2 = data[i] = cls.xor(cipher.decrypt(cls.xor(chunk, iv_2)), iv_1)
- iv_1 = chunk
-
- return b"".join(data)
diff --git a/pyrogram/session/session.py b/pyrogram/session/session.py
index 3ea7ed15..46d722fc 100644
--- a/pyrogram/session/session.py
+++ b/pyrogram/session/session.py
@@ -61,7 +61,7 @@ class Session:
INITIAL_SALT = 0x616e67656c696361
NET_WORKERS = 1
- WAIT_TIMEOUT = 10
+ WAIT_TIMEOUT = 30
MAX_RETRIES = 5
ACKS_THRESHOLD = 8
PING_INTERVAL = 5
@@ -122,8 +122,6 @@ class Session:
self.is_connected = Event()
def start(self):
- terms = None
-
while True:
try:
self.connection.connect()
@@ -141,7 +139,7 @@ class Session:
self.next_salt_thread.start()
if not self.is_cdn:
- terms = self._send(
+ self._send(
functions.InvokeWithLayer(
layer,
functions.InitConnection(
@@ -150,10 +148,10 @@ class Session:
self.SYSTEM_VERSION,
self.APP_VERSION,
"en", "", "en",
- functions.help.GetTermsOfService(),
+ functions.help.GetConfig(),
)
)
- ).text
+ )
self.ping_thread = Thread(target=self.ping, name="PingThread")
self.ping_thread.start()
@@ -168,8 +166,6 @@ class Session:
log.debug("Session started")
- return terms
-
def stop(self):
self.is_connected.clear()
@@ -190,6 +186,9 @@ class Session:
for i in range(self.NET_WORKERS):
self.recv_queue.put(None)
+ for i in self.results.values():
+ i.event.set()
+
log.debug("Session stopped")
def restart(self):
@@ -401,7 +400,7 @@ class Session:
try:
return self._send(data)
except (OSError, TimeoutError):
- log.warning("Retrying {}".format(type(data)))
+ (log.warning if i > 0 else log.info)("{}: {} Retrying {}".format(i, datetime.now(), type(data)))
continue
else:
return None
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..21c697f1
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+pysocks
+tgcrypto
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 10294494..c82c1a9d 100644
--- a/setup.py
+++ b/setup.py
@@ -24,12 +24,15 @@ from setuptools import setup, find_packages
from compiler.api import compiler as api_compiler
from compiler.error import compiler as error_compiler
-# from compiler.docs import compiler as docs_compiler
+
+def requirements():
+ with open("requirements.txt", encoding="utf-8") as r:
+ return [i.strip() for i in r]
+
if len(argv) > 1 and argv[1] != "sdist":
api_compiler.start()
error_compiler.start()
- # docs_compiler.start()
with open("pyrogram/__init__.py", encoding="utf-8") as f:
version = re.findall(r"__version__ = \"(.+)\"", f.read())[0]
@@ -43,12 +46,12 @@ setup(
name="Pyrogram",
version=version,
description="Telegram MTProto API Client Library for Python",
- url="https://github.com/pyrogram/pyrogram",
+ long_description=readme,
+ url="https://github.com/pyrogram",
+ download_url="https://github.com/pyrogram/pyrogram/releases/latest",
author="Dan Tès",
author_email="admin@pyrogram.ml",
license="LGPLv3+",
- keywords="telegram mtproto api client library python",
- long_description=readme,
classifiers=[
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
@@ -56,25 +59,28 @@ setup(
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
+ "Programming Language :: Python :: Implementation",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Internet",
+ "Topic :: Communications",
"Topic :: Communications :: Chat",
"Topic :: Software Development :: Libraries",
- "Topic :: Software Development :: Libraries :: Python Modules"
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ "Topic :: Software Development :: Libraries :: Application Frameworks"
],
- packages=find_packages(),
- zip_safe=False,
- install_requires=[
- "pyaes",
- "pysocks"
- ],
- extras_require={
- "tgcrypto": [
- "tgcrypto"
- ]
+ keywords="telegram chat messenger mtproto api client library python",
+ project_urls={
+ "Tracker": "https://github.com/pyrogram/pyrogram/issues",
+ "Community": "https://t.me/PyrogramChat",
+ "Source": "https://github.com/pyrogram/pyrogram",
+ "Documentation": "https://docs.pyrogram.ml",
},
- include_package_data=True,
+ python_requires="~=3.4",
+ packages=find_packages(exclude=["compiler*"]),
+ zip_safe=False,
+ install_requires=requirements()
)