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

- -
Pyrogram Icon
-
Pyrogram Label
+
+
Pyrogram Icon
+
Pyrogram Label

@@ -204,11 +181,11 @@ License

- Scheme Layer 75 + Scheme Layer 76 - TgCrypto

@@ -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() )