diff --git a/MANIFEST.in b/MANIFEST.in index a1d19d94..f818e13a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ ## Include -include COPYING COPYING.lesser NOTICE requirements.txt requirements_extras.txt +include COPYING COPYING.lesser NOTICE requirements.txt recursive-include compiler *.py *.tl *.tsv *.txt ## Exclude diff --git a/README.rst b/README.rst index 67dbd3a5..0b9efb76 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ |header| -Pyrogram |twitter| -================== +Pyrogram +======== .. code-block:: python @@ -12,12 +12,10 @@ Pyrogram |twitter| @app.on_message(Filters.private) def hello(client, message): - client.send_message( - message.chat.id, "Hello {}".format(message.from_user.first_name)) + message.reply("Hello {}".format(message.from_user.first_name)) - app.start() - app.idle() + app.run() **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 that interact with the MTProto API as both User and Bot. @@ -28,8 +26,8 @@ Features - **Easy to use**: You can easily install Pyrogram using pip and start building your app right away. - **High-level**: The low-level details of MTProto are abstracted and automatically handled. - **Fast**: Crypto parts are boosted up by TgCrypto_, a high-performance library written in pure C. -- **Updated** to the latest Telegram API version, currently Layer 76 running on MTProto 2.0. -- **Documented**: Pyrogram API methods are documented and resemble the Telegram Bot API. +- **Updated** to the latest Telegram API version, currently Layer 81 on top of MTProto 2.0. +- **Documented**: The Pyrogram API is well documented and resembles the Telegram Bot API. - **Full API**, allowing to execute any advanced action an official client is able to do, and more. Requirements @@ -56,7 +54,7 @@ Getting Started Contributing ------------ -Pyrogram is brand new! **You are welcome to try it and help make it better** by either submitting pull +Pyrogram is brand new, and **you are welcome to try it and help make it even 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! @@ -102,28 +100,25 @@ Copyright & License

- Scheme Layer 76 + Scheme Layer - TgCrypto

-.. |twitter| image:: https://media.pyrogram.ml/images/twitter.svg - :target: https://twitter.com/intent/tweet?text=Build%20custom%20Telegram%20applications%20with%20Pyrogram&url=https://github.com/pyrogram/pyrogram&hashtags=Telegram,MTProto,Python - .. |logo| image:: https://pyrogram.ml/images/logo.png :target: https://pyrogram.ml :alt: Pyrogram .. |description| replace:: **Telegram MTProto API Client Library for Python** -.. |scheme| image:: https://www.pyrogram.ml/images/scheme.svg +.. |scheme| image:: "https://img.shields.io/badge/SCHEME-LAYER%2081-eda738.svg?longCache=true&style=for-the-badge&colorA=262b30" :target: compiler/api/source/main_api.tl - :alt: Scheme Layer 76 + :alt: Scheme Layer -.. |tgcrypto| image:: https://www.pyrogram.ml/images/tgcrypto.svg +.. |tgcrypto| image:: "https://img.shields.io/badge/TGCRYPTO-V1.0.4-eda738.svg?longCache=true&style=for-the-badge&colorA=262b30" :target: https://github.com/pyrogram/tgcrypto :alt: TgCrypto diff --git a/compiler/api/source/auth_key.tl b/compiler/api/source/auth_key.tl index e0af9dcd..dad176fe 100644 --- a/compiler/api/source/auth_key.tl +++ b/compiler/api/source/auth_key.tl @@ -7,7 +7,9 @@ resPQ#05162463 nonce:int128 server_nonce:int128 pq:bytes server_public_key_fingerprints:Vector = ResPQ; p_q_inner_data#83c95aec pq:bytes p:bytes q:bytes nonce:int128 server_nonce:int128 new_nonce:int256 = P_Q_inner_data; +p_q_inner_data_dc#a9f55f95 pq:bytes p:bytes q:bytes nonce:int128 server_nonce:int128 new_nonce:int256 dc:int = P_Q_inner_data; p_q_inner_data_temp#3c6a84d4 pq:bytes p:bytes q:bytes nonce:int128 server_nonce:int128 new_nonce:int256 expires_in:int = P_Q_inner_data; +p_q_inner_data_temp_dc#56fddf88 pq:bytes p:bytes q:bytes nonce:int128 server_nonce:int128 new_nonce:int256 dc:int expires_in:int = P_Q_inner_data; bind_auth_key_inner#75a3f765 nonce:long temp_auth_key_id:long perm_auth_key_id:long temp_session_id:long expires_at:int = BindAuthKeyInner; diff --git a/compiler/api/source/main_api.tl b/compiler/api/source/main_api.tl index 9ede3e28..d1f2fa33 100644 --- a/compiler/api/source/main_api.tl +++ b/compiler/api/source/main_api.tl @@ -60,6 +60,7 @@ inputPhoto#fb95c6c4 id:long access_hash:long = InputPhoto; inputFileLocation#14637196 volume_id:long local_id:int secret:long = InputFileLocation; inputEncryptedFileLocation#f5235d55 id:long access_hash:long = InputFileLocation; inputDocumentFileLocation#430f0724 id:long access_hash:long version:int = InputFileLocation; +inputSecureFileLocation#cbc7ee28 id:long access_hash:long = InputFileLocation; inputAppEvent#770656a8 time:double type:string peer:long data:string = InputAppEvent; @@ -97,7 +98,7 @@ userStatusLastMonth#77ebc742 = UserStatus; chatEmpty#9ba2d800 id:int = Chat; chat#d91cdd54 flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true admins_enabled:flags.3?true admin:flags.4?true deactivated:flags.5?true id:int title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel = Chat; chatForbidden#7328bdb id:int title:string = Chat; -channel#450b7115 flags:# creator:flags.0?true left:flags.2?true editor:flags.3?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true democracy:flags.10?true signatures:flags.11?true min:flags.12?true id:int access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int version:int restriction_reason:flags.9?string admin_rights:flags.14?ChannelAdminRights banned_rights:flags.15?ChannelBannedRights participants_count:flags.17?int = Chat; +channel#c88974ac flags:# creator:flags.0?true left:flags.2?true editor:flags.3?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true democracy:flags.10?true signatures:flags.11?true min:flags.12?true id:int access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int version:int restriction_reason:flags.9?string admin_rights:flags.14?ChannelAdminRights banned_rights:flags.15?ChannelBannedRights participants_count:flags.17?int = Chat; channelForbidden#289da732 flags:# broadcast:flags.5?true megagroup:flags.8?true id:int access_hash:long title:string until_date:flags.16?int = Chat; chatFull#2e02a614 id:int participants:ChatParticipants chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector = ChatFull; @@ -149,6 +150,8 @@ messageActionPhoneCall#80e11a7f flags:# call_id:long reason:flags.0?PhoneCallDis messageActionScreenshotTaken#4792929b = MessageAction; messageActionCustomAction#fae69f56 message:string = MessageAction; messageActionBotAllowed#abe9affe domain:string = MessageAction; +messageActionSecureValuesSentMe#1b287353 values:Vector credentials:SecureCredentialsEncrypted = MessageAction; +messageActionSecureValuesSent#d95c6154 types:Vector = 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; @@ -164,7 +167,7 @@ geoPoint#2049d70c long:double lat:double = GeoPoint; auth.checkedPhone#811ea28e phone_registered:Bool = auth.CheckedPhone; -auth.sentCode#5e002502 flags:# phone_registered:flags.0?true type:auth.SentCodeType phone_code_hash:string next_type:flags.1?auth.CodeType timeout:flags.2?int = auth.SentCode; +auth.sentCode#38faab5f flags:# phone_registered:flags.0?true type:auth.SentCodeType phone_code_hash:string next_type:flags.1?auth.CodeType timeout:flags.2?int terms_of_service:flags.3?help.TermsOfService = auth.SentCode; auth.authorization#cd050916 flags:# tmp_sessions:flags.0?int user:User = auth.Authorization; @@ -173,18 +176,10 @@ auth.exportedAuthorization#df969c2d id:int bytes:bytes = auth.ExportedAuthorizat inputNotifyPeer#b8bc5b0c peer:InputPeer = InputNotifyPeer; inputNotifyUsers#193b4417 = InputNotifyPeer; inputNotifyChats#4a95e84e = InputNotifyPeer; -inputNotifyAll#a429b886 = InputNotifyPeer; -inputPeerNotifyEventsEmpty#f03064d8 = InputPeerNotifyEvents; -inputPeerNotifyEventsAll#e86a2c74 = InputPeerNotifyEvents; +inputPeerNotifySettings#9c3d198e flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int sound:flags.3?string = InputPeerNotifySettings; -inputPeerNotifySettings#38935eb2 flags:# show_previews:flags.0?true silent:flags.1?true mute_until:int sound:string = InputPeerNotifySettings; - -peerNotifyEventsEmpty#add53cb3 = PeerNotifyEvents; -peerNotifyEventsAll#6d1ded88 = PeerNotifyEvents; - -peerNotifySettingsEmpty#70a68512 = PeerNotifySettings; -peerNotifySettings#9acda4c0 flags:# show_previews:flags.0?true silent:flags.1?true mute_until:int sound:string = PeerNotifySettings; +peerNotifySettings#af509d20 flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int sound:flags.3?string = PeerNotifySettings; peerSettings#818426cd flags:# report_spam:flags.0?true = PeerSettings; @@ -338,9 +333,9 @@ photos.photo#20212ca8 photo:Photo users:Vector = photos.Photo; upload.file#96a18d5 type:storage.FileType mtime:int bytes:bytes = 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; +dcOption#18b7a10d 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 secret:flags.10?bytes = DcOption; -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; +config#eb7bb160 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 blocked_mode:flags.8?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 autoupdate_url_prefix:flags.7?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; @@ -385,7 +380,6 @@ help.support#17c6b5f6 phone_number:string user:User = help.Support; notifyPeer#9fd40bd8 peer:Peer = NotifyPeer; notifyUsers#b4c83b4c = NotifyPeer; notifyChats#c007cec3 = NotifyPeer; -notifyAll#74d07c60 = NotifyPeer; sendMessageTypingAction#16bf744e = SendMessageAction; sendMessageCancelAction#fd5ec8f5 = SendMessageAction; @@ -438,7 +432,7 @@ documentAttributeFilename#15590068 file_name:string = DocumentAttribute; documentAttributeHasStickers#9801d2f7 = DocumentAttribute; messages.stickersNotModified#f1749a22 = messages.Stickers; -messages.stickers#8a8ecd32 hash:string stickers:Vector = messages.Stickers; +messages.stickers#e4599bbd hash:int stickers:Vector = messages.Stickers; stickerPack#12b299d4 emoticon:string documents:Vector = StickerPack; @@ -461,12 +455,12 @@ authorization#7bf2e6f6 hash:long flags:int device_model:string platform:string s account.authorizations#1250abde authorizations:Vector = account.Authorizations; -account.noPassword#96dabc18 new_salt:bytes email_unconfirmed_pattern:string = account.Password; -account.password#7c18141c current_salt:bytes new_salt:bytes hint:string has_recovery:Bool email_unconfirmed_pattern:string = account.Password; +account.noPassword#5ea182f6 new_salt:bytes new_secure_salt:bytes secure_random:bytes email_unconfirmed_pattern:string = account.Password; +account.password#ca39b447 flags:# has_recovery:flags.0?true has_secure_values:flags.1?true current_salt:bytes new_salt:bytes new_secure_salt:bytes secure_random:bytes hint:string email_unconfirmed_pattern:string = account.Password; -account.passwordSettings#b7b72ab3 email:string = account.PasswordSettings; +account.passwordSettings#7bd9c3f1 email:string secure_salt:bytes secure_secret:bytes secure_secret_id:long = account.PasswordSettings; -account.passwordInputSettings#86916deb flags:# new_salt:flags.0?bytes new_password_hash:flags.0?bytes hint:flags.0?string email:flags.1?string = account.PasswordInputSettings; +account.passwordInputSettings#21ffa60d flags:# new_salt:flags.0?bytes new_password_hash:flags.0?bytes hint:flags.0?string email:flags.1?string new_secure_salt:flags.2?bytes new_secure_secret:flags.2?bytes new_secure_secret_id:flags.2?long = account.PasswordInputSettings; auth.passwordRecovery#137948a5 email_pattern:string = auth.PasswordRecovery; @@ -554,7 +548,7 @@ channels.channelParticipantsNotModified#f0173fe9 = channels.ChannelParticipants; channels.channelParticipant#d0d9b163 participant:ChannelParticipant users:Vector = channels.ChannelParticipant; -help.termsOfService#f1ee3e90 text:string = help.TermsOfService; +help.termsOfService#780a0310 flags:# popup:flags.0?true id:DataJSON text:string entities:Vector min_age_confirm:flags.1?int = help.TermsOfService; foundGif#162ecc1f url:string thumb_url:string content_url:string content_type:string w:int h:int = FoundGif; foundGifCached#9c750409 url:string photo:Photo document:Document = FoundGif; @@ -567,7 +561,7 @@ messages.savedGifs#2e0709a5 hash:int gifs:Vector = messages.SavedGifs; inputBotInlineMessageMediaAuto#3380c786 flags:# message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageText#3dcd7a87 flags:# no_webpage:flags.0?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaGeo#c1b15d65 flags:# geo_point:InputGeoPoint period:int reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; -inputBotInlineMessageMediaVenue#aaafadc8 flags:# geo_point:InputGeoPoint title:string address:string provider:string venue_id:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; +inputBotInlineMessageMediaVenue#417bbf11 flags:# geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; 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; @@ -579,7 +573,7 @@ inputBotInlineResultGame#4fa417f2 id:string short_name:string send_message:Input botInlineMessageMediaAuto#764cf810 flags:# message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageText#8c7f65e2 flags:# no_webpage:flags.0?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaGeo#b722de65 flags:# geo:GeoPoint period:int reply_markup:flags.2?ReplyMarkup = BotInlineMessage; -botInlineMessageMediaVenue#4366232e flags:# geo:GeoPoint title:string address:string provider:string venue_id:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; +botInlineMessageMediaVenue#8a86659c flags:# geo:GeoPoint title:string address:string provider:string venue_id:string venue_type: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#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; @@ -718,6 +712,8 @@ webDocumentNoProxy#f9c8bcc6 url:string size:int mime_type:string attributes:Vect inputWebDocument#9bed434d url:string size:int mime_type:string attributes:Vector = InputWebDocument; inputWebFileLocation#c239d686 url:string access_hash:long = InputWebFileLocation; +inputWebFileGeoPointLocation#66275a62 geo_point:InputGeoPoint w:int h:int zoom:int scale:int = InputWebFileLocation; +inputWebFileGeoMessageLocation#553f32eb peer:InputPeer msg_id:int w:int h:int zoom:int scale:int = InputWebFileLocation; upload.webFile#21e753bc size:int mime_type:string file_type:storage.FileType mtime:int bytes:bytes = upload.WebFile; @@ -773,7 +769,7 @@ langPackDifference#f385c1f6 lang_code:string from_version:int version:int string langPackLanguage#117698f1 name:string native_name:string lang_code:string = LangPackLanguage; -channelAdminRights#5d7ceba5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true invite_link:flags.6?true pin_messages:flags.7?true add_admins:flags.9?true = ChannelAdminRights; +channelAdminRights#5d7ceba5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true invite_link:flags.6?true pin_messages:flags.7?true add_admins:flags.9?true manage_call:flags.10?true = ChannelAdminRights; channelBannedRights#58cf4249 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true until_date:int = ChannelBannedRights; @@ -832,15 +828,69 @@ messages.foundStickerSets#5108d648 hash:int sets:Vector = mes fileHash#6242c773 offset:int limit:int hash:bytes = FileHash; +inputClientProxy#75588b3f address:string port:int = InputClientProxy; + +help.proxyDataEmpty#e09e1fb8 expires:int = help.ProxyData; +help.proxyDataPromo#2bf7ee23 expires:int peer:Peer chats:Vector users:Vector = help.ProxyData; + +help.termsOfServiceUpdateEmpty#e3309f7f expires:int = help.TermsOfServiceUpdate; +help.termsOfServiceUpdate#28ecf961 expires:int terms_of_service:help.TermsOfService = help.TermsOfServiceUpdate; + +inputSecureFileUploaded#3334b0f0 id:long parts:int md5_checksum:string file_hash:bytes secret:bytes = InputSecureFile; +inputSecureFile#5367e5be id:long access_hash:long = InputSecureFile; + +secureFileEmpty#64199744 = SecureFile; +secureFile#e0277a62 id:long access_hash:long size:int dc_id:int date:int file_hash:bytes secret:bytes = SecureFile; + +secureData#8aeabec3 data:bytes data_hash:bytes secret:bytes = SecureData; + +securePlainPhone#7d6099dd phone:string = SecurePlainData; +securePlainEmail#21ec5a5f email:string = SecurePlainData; + +secureValueTypePersonalDetails#9d2a81e3 = SecureValueType; +secureValueTypePassport#3dac6a00 = SecureValueType; +secureValueTypeDriverLicense#6e425c4 = SecureValueType; +secureValueTypeIdentityCard#a0d0744b = SecureValueType; +secureValueTypeInternalPassport#99a48f23 = SecureValueType; +secureValueTypeAddress#cbe31e26 = SecureValueType; +secureValueTypeUtilityBill#fc36954e = SecureValueType; +secureValueTypeBankStatement#89137c0d = SecureValueType; +secureValueTypeRentalAgreement#8b883488 = SecureValueType; +secureValueTypePassportRegistration#99e3806a = SecureValueType; +secureValueTypeTemporaryRegistration#ea02ec33 = SecureValueType; +secureValueTypePhone#b320aadb = SecureValueType; +secureValueTypeEmail#8e3ca7ee = SecureValueType; + +secureValue#b4b4b699 flags:# type:SecureValueType data:flags.0?SecureData front_side:flags.1?SecureFile reverse_side:flags.2?SecureFile selfie:flags.3?SecureFile files:flags.4?Vector plain_data:flags.5?SecurePlainData hash:bytes = SecureValue; + +inputSecureValue#67872e8 flags:# type:SecureValueType data:flags.0?SecureData front_side:flags.1?InputSecureFile reverse_side:flags.2?InputSecureFile selfie:flags.3?InputSecureFile files:flags.4?Vector plain_data:flags.5?SecurePlainData = InputSecureValue; + +secureValueHash#ed1ecdb0 type:SecureValueType hash:bytes = SecureValueHash; + +secureValueErrorData#e8a40bd9 type:SecureValueType data_hash:bytes field:string text:string = SecureValueError; +secureValueErrorFrontSide#be3dfa type:SecureValueType file_hash:bytes text:string = SecureValueError; +secureValueErrorReverseSide#868a2aa5 type:SecureValueType file_hash:bytes text:string = SecureValueError; +secureValueErrorSelfie#e537ced6 type:SecureValueType file_hash:bytes text:string = SecureValueError; +secureValueErrorFile#7a700873 type:SecureValueType file_hash:bytes text:string = SecureValueError; +secureValueErrorFiles#666220e9 type:SecureValueType file_hash:Vector text:string = SecureValueError; + +secureCredentialsEncrypted#33f0ea47 data:bytes hash:bytes secret:bytes = SecureCredentialsEncrypted; + +account.authorizationForm#cb976d53 flags:# selfie_required:flags.1?true required_types:Vector values:Vector errors:Vector users:Vector privacy_policy_url:flags.0?string = account.AuthorizationForm; + +account.sentEmailCode#811f854f email_pattern:string length:int = account.SentEmailCode; + +help.deepLinkInfoEmpty#66afa166 = help.DeepLinkInfo; +help.deepLinkInfo#6a4ee832 flags:# update_app:flags.0?true message:string entities:flags.1?Vector = help.DeepLinkInfo; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; invokeAfterMsgs#3dc4b4f0 {X:Type} msg_ids:Vector query:!X = X; -initConnection#c7481da6 {X:Type} api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string query:!X = X; +initConnection#785188b8 {X:Type} flags:# api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string proxy:flags.0?InputClientProxy query:!X = X; invokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X; invokeWithoutUpdates#bf9459b7 {X:Type} query:!X = X; -auth.checkPhone#6fe51dfb phone_number:string = auth.CheckedPhone; auth.sendCode#86aef0ec flags:# allow_flashcall:flags.0?true phone_number:string current_number:flags.0?Bool api_id:int api_hash:string = auth.SentCode; auth.signUp#1b067634 phone_number:string phone_code_hash:string phone_code:string first_name:string last_name:string = auth.Authorization; auth.signIn#bcd51581 phone_number:string phone_code_hash:string phone_code:string = auth.Authorization; @@ -888,9 +938,20 @@ account.getTmpPassword#4a82327e password_hash:bytes period:int = account.TmpPass account.getWebAuthorizations#182e6d6f = account.WebAuthorizations; account.resetWebAuthorization#2d01b9ef hash:long = Bool; account.resetWebAuthorizations#682d2594 = Bool; +account.getAllSecureValues#b288bc7d = Vector; +account.getSecureValue#73665bc2 types:Vector = Vector; +account.saveSecureValue#899fe31d value:InputSecureValue secure_secret_id:long = SecureValue; +account.deleteSecureValue#b880bc4b types:Vector = Bool; +account.getAuthorizationForm#b86ba8e1 bot_id:int scope:string public_key:string = account.AuthorizationForm; +account.acceptAuthorization#e7027c94 bot_id:int scope:string public_key:string value_hashes:Vector credentials:SecureCredentialsEncrypted = Bool; +account.sendVerifyPhoneCode#823380b4 flags:# allow_flashcall:flags.0?true phone_number:string current_number:flags.0?Bool = auth.SentCode; +account.verifyPhone#4dd3a7f6 phone_number:string phone_code_hash:string phone_code:string = Bool; +account.sendVerifyEmailCode#7011509f email:string = account.SentEmailCode; +account.verifyEmail#ecba39db email:string code:string = Bool; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#ca30a5b1 id:InputUser = UserFull; +users.setSecureValueErrors#90c894b5 id:InputUser errors:Vector = Bool; contacts.getStatuses#c4a353ee = Vector; contacts.getContacts#c023849f hash:int = contacts.Contacts; @@ -943,7 +1004,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#85cb5182 flags:# exclude_featured:flags.0?true emoticon:string hash:string = messages.Stickers; +messages.getStickers#43d4f2c emoticon:string hash:int = 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; @@ -967,8 +1028,8 @@ messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_p messages.setInlineBotResults#eb5ea206 flags:# gallery:flags.0?true private:flags.1?true query_id:long results:Vector cache_time:int next_offset:flags.2?string switch_pm:flags.3?InlineBotSwitchPM = Bool; messages.sendInlineBotResult#b16e06fe flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int random_id:long query_id:long id:string = Updates; messages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData; -messages.editMessage#5d1b8dd flags:# no_webpage:flags.1?true stop_geo_live:flags.12?true peer:InputPeer id:int message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector geo_point:flags.13?InputGeoPoint = Updates; -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.editMessage#c000e4c8 flags:# no_webpage:flags.1?true stop_geo_live:flags.12?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector geo_point:flags.13?InputGeoPoint = Updates; +messages.editInlineBotMessage#adc3e828 flags:# no_webpage:flags.1?true stop_geo_live:flags.12?true id:InputBotInlineMessageID message:flags.11?string media:flags.14?InputMedia 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#e470bcfd peers:Vector = messages.PeerDialogs; @@ -1030,10 +1091,13 @@ help.saveAppLog#6f02f748 events:Vector = Bool; help.getInviteText#4d392343 = help.InviteText; help.getSupport#9cdf08cd = help.Support; help.getAppChangelog#9010ef6f prev_app_version:string = Updates; -help.getTermsOfService#350170f3 = help.TermsOfService; help.setBotUpdatesStatus#ec22cfcd pending_updates_count:int message:string = Bool; help.getCdnConfig#52029342 = CdnConfig; help.getRecentMeUrls#3dc0f114 referer:string = help.RecentMeUrls; +help.getProxyData#3d7758e1 = help.ProxyData; +help.getTermsOfServiceUpdate#2ca51fd1 = help.TermsOfServiceUpdate; +help.acceptTermsOfService#ee72f79a id:DataJSON = Bool; +help.getDeepLinkInfo#3fedc75f path:string = help.DeepLinkInfo; channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool; channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector = messages.AffectedMessages; @@ -1097,4 +1161,4 @@ langpack.getStrings#2e1ee318 lang_code:string keys:Vector = Vector; -// LAYER 76 +// LAYER 81 diff --git a/compiler/api/source/sys_msgs.tl b/compiler/api/source/sys_msgs.tl index cd00cd2d..067ab91e 100644 --- a/compiler/api/source/sys_msgs.tl +++ b/compiler/api/source/sys_msgs.tl @@ -42,9 +42,16 @@ new_session_created#9ec20908 first_msg_id:long unique_id:long server_salt:long = // msg_copy#e06046b2 orig_message:Message = MessageCopy; // Not used // gzip_packed#3072cfa1 packed_data:string = Object; // Parsed manually +http_wait#9299359f max_delay:int wait_after:int max_wait:int = HttpWait; + //// 0xd433ad73 = hex(crc32(b"ipPort ipv4:int port:int = IpPort")) +// ipPort ipv4:int port:int = IpPort; +// help.configSimple#d997c3c5 date:int expires:int dc_id:int ip_port_list:Vector = help.ConfigSimple; + ipPort#d433ad73 ipv4:int port:int = IpPort; -help.configSimple#d997c3c5 date:int expires:int dc_id:int ip_port_list:Vector = help.ConfigSimple; +ipPortSecret#37982646 ipv4:int port:int secret:bytes = IpPort; +accessPointRule#4679b65f phone_prefix_rules:string dc_id:int ips:vector = AccessPointRule; +help.configSimple#5a592a6c date:int expires:int rules:vector = help.ConfigSimple; ---functions--- @@ -58,5 +65,3 @@ ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong; destroy_session#e7512126 session_id:long = DestroySessionRes; contest.saveDeveloperInfo#9a5f6e95 vk_id:int name:string phone_number:string age:int city:string = Bool; - -http_wait#9299359f max_delay:int wait_after:int max_wait:int = HttpWait; diff --git a/compiler/docs/compiler.py b/compiler/docs/compiler.py index 494697de..73b5a578 100644 --- a/compiler/docs/compiler.py +++ b/compiler/docs/compiler.py @@ -77,7 +77,8 @@ def generate(source_path, base): build(source_path) - for k, v in all_entities.items(): + for k, v in sorted(all_entities.items()): + v = sorted(v) entities = [] for i in v: diff --git a/docs/Makefile b/docs/Makefile index c647eb13..c01e3d3d 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,7 @@ # You can set these variables from the command line. SPHINXOPTS = -SPHINXBUILD = sphinx-build +SPHINXBUILD = ~/PycharmProjects/pyrogram/venv3.6/bin/sphinx-build SPHINXPROJ = Pyrogram SOURCEDIR = source BUILDDIR = build diff --git a/docs/Makefile_ b/docs/Makefile_ new file mode 100644 index 00000000..c647eb13 --- /dev/null +++ b/docs/Makefile_ @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = Pyrogram +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index fced42bb..051b5af8 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -26,11 +26,11 @@ Welcome to Pyrogram

- Scheme Layer 75 + Scheme Layer - TgCrypto

@@ -44,12 +44,10 @@ Welcome to Pyrogram @app.on_message(Filters.private) def hello(client, message): - client.send_message( - message.chat.id, "Hello {}".format(message.from_user.first_name)) + message.reply("Hello {}".format(message.from_user.first_name)) - app.start() - app.idle() + app.run() Welcome to Pyrogram's Documentation! Here you can find resources for learning how to use the library. Contents are organized by topic and can be accessed from the sidebar, or by following them one by one using the Next @@ -58,7 +56,7 @@ button at the end of each page. But first, here's a brief overview of what is th About ----- -Pyrogram is a brand new Telegram_ Client Library written from the ground up in Python and C. It can be used for building +**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 that interact with the MTProto API as both User and Bot. Features @@ -67,29 +65,30 @@ Features - **Easy to use**: You can easily install Pyrogram using pip and start building your app right away. - **High-level**: The low-level details of MTProto are abstracted and automatically handled. - **Fast**: Crypto parts are boosted up by TgCrypto_, a high-performance library written in pure C. -- **Updated** to the latest Telegram API version, currently Layer 76 running on MTProto 2.0. -- **Documented**: Pyrogram API methods are documented and resemble the Telegram Bot API. +- **Updated** to the latest Telegram API version, currently Layer 81 on top of MTProto 2.0. +- **Documented**: The Pyrogram API is well documented and resembles the Telegram Bot API. - **Full API**, allowing to execute any advanced action an official client is able to do, and more. To get started, press the Next button. .. toctree:: :hidden: - :caption: Getting Started + :caption: Quick Start - start/QuickInstallation - start/ProjectSetup - start/BasicUsage + start/Installation + start/Setup + start/Usage .. toctree:: :hidden: :caption: Resources resources/UpdateHandling - resources/SOCKS5Proxy - resources/TgCrypto resources/AutoAuthorization + resources/CustomizeSessions + resources/TgCrypto resources/TextFormatting + resources/SOCKS5Proxy resources/BotsInteraction resources/ErrorHandling @@ -107,5 +106,4 @@ To get started, press the Next button. types/index .. _`Telegram`: https://telegram.org/ - -.. _TgCrypto: https://docs.pyrogram.ml/resources/TgCrypto/ +.. _TgCrypto: https://docs.pyrogram.ml/resources/TgCrypto/ \ No newline at end of file diff --git a/docs/source/pyrogram/ChatAction.rst b/docs/source/pyrogram/ChatAction.rst index a7d5975d..dfa56945 100644 --- a/docs/source/pyrogram/ChatAction.rst +++ b/docs/source/pyrogram/ChatAction.rst @@ -3,4 +3,3 @@ ChatAction .. autoclass:: pyrogram.ChatAction :members: - :undoc-members: diff --git a/docs/source/pyrogram/Client.rst b/docs/source/pyrogram/Client.rst index e879db71..0448c3cb 100644 --- a/docs/source/pyrogram/Client.rst +++ b/docs/source/pyrogram/Client.rst @@ -7,6 +7,8 @@ Client :inherited-members: :members: + .. _available-methods: + **Available methods** .. autosummary:: @@ -15,6 +17,7 @@ Client start stop idle + run on_message on_callback_query on_raw_update @@ -49,6 +52,10 @@ Client enable_cloud_password change_cloud_password remove_cloud_password + kick_chat_member + unban_chat_member + restrict_chat_member + promote_chat_member add_contacts get_contacts delete_contacts diff --git a/docs/source/pyrogram/handlers/DeletedMessagesHandler.rst b/docs/source/pyrogram/handlers/DeletedMessagesHandler.rst new file mode 100644 index 00000000..128bc656 --- /dev/null +++ b/docs/source/pyrogram/handlers/DeletedMessagesHandler.rst @@ -0,0 +1,6 @@ +DeletedMessagesHandler +====================== + +.. autoclass:: pyrogram.DeletedMessagesHandler + :members: + :undoc-members: diff --git a/docs/source/pyrogram/handlers/DisconnectHandler.rst b/docs/source/pyrogram/handlers/DisconnectHandler.rst new file mode 100644 index 00000000..594081f1 --- /dev/null +++ b/docs/source/pyrogram/handlers/DisconnectHandler.rst @@ -0,0 +1,6 @@ +DisconnectHandler +================= + +.. autoclass:: pyrogram.DisconnectHandler + :members: + :undoc-members: diff --git a/docs/source/pyrogram/handlers/index.rst b/docs/source/pyrogram/handlers/index.rst index bbd55542..272e529f 100644 --- a/docs/source/pyrogram/handlers/index.rst +++ b/docs/source/pyrogram/handlers/index.rst @@ -5,5 +5,7 @@ Handlers .. toctree:: MessageHandler + DeletedMessagesHandler CallbackQueryHandler - RawUpdateHandler \ No newline at end of file + DisconnectHandler + RawUpdateHandler diff --git a/docs/source/resources/AutoAuthorization.rst b/docs/source/resources/AutoAuthorization.rst index 46f0809d..d7a099fe 100644 --- a/docs/source/resources/AutoAuthorization.rst +++ b/docs/source/resources/AutoAuthorization.rst @@ -14,28 +14,33 @@ Log In To automate the **Log In** process, pass your ``phone_number`` and ``password`` (if you have one) in the Client parameters. If you want to retrieve the phone code programmatically, pass a callback function in the ``phone_code`` field — this -function must return the correct phone code as string (e.g., "12345") — otherwise, ignore this parameter, Pyrogram will -ask you to input the phone code manually. +function accepts a single positional argument (phone_number) and must return the correct phone code (e.g., "12345") +— otherwise, ignore this parameter, Pyrogram will ask you to input the phone code manually. + +Example: .. code-block:: python from pyrogram import Client - def phone_code_callback(): + def phone_code_callback(phone_number): code = ... # Get your code programmatically - return code # Must be string, e.g., "12345" + return code # e.g., "12345" app = Client( session_name="example", phone_number="39**********", - phone_code=phone_code_callback, + phone_code=phone_code_callback, # Note the missing parentheses password="password" # (if you have one) ) app.start() + print(app.get_me()) + app.stop() + Sign Up ------- @@ -43,22 +48,27 @@ To automate the **Sign Up** process (i.e., automatically create a new Telegram a ``first_name`` and ``last_name`` fields alongside the other parameters; they will be used to automatically create a new Telegram account in case the phone number you passed is not registered yet. +Example: + .. code-block:: python from pyrogram import Client - def phone_code_callback(): + def phone_code_callback(phone_number): code = ... # Get your code programmatically - return code # Must be string, e.g., "12345" + return code # e.g., "12345" app = Client( session_name="example", phone_number="39**********", - phone_code=phone_code_callback, + phone_code=phone_code_callback, # Note the missing parentheses first_name="Pyrogram", last_name="" # Can be an empty string ) app.start() - print(app.get_me()) \ No newline at end of file + + print(app.get_me()) + + app.stop() \ No newline at end of file diff --git a/docs/source/resources/BotsInteraction.rst b/docs/source/resources/BotsInteraction.rst index cbbe23c1..de7925a2 100644 --- a/docs/source/resources/BotsInteraction.rst +++ b/docs/source/resources/BotsInteraction.rst @@ -28,8 +28,12 @@ Inline Bots .. code-block:: python - # Send the first result (bot_results.results[0]) to your own chat (Saved Messages) - app.send_inline_bot_result("me", bot_results.query_id, bot_results.results[0].id) + # Send the first result to your own chat + app.send_inline_bot_result( + "me", + bot_results.query_id, + bot_results.results[0].id + ) .. figure:: https://i.imgur.com/wwxr7B7.png :width: 90% diff --git a/docs/source/resources/CustomizeSessions.rst b/docs/source/resources/CustomizeSessions.rst new file mode 100644 index 00000000..e98792b7 --- /dev/null +++ b/docs/source/resources/CustomizeSessions.rst @@ -0,0 +1,66 @@ +Customize Sessions +================== + +As you may probably know, Telegram allows Users (and Bots) having more than one session (authorizations) registered +in the system at the same time. + +Briefly explaining, sessions are simply new logins in your account. They can be reviewed in the settings of an official +app (or by invoking `GetAuthorizations <../functions/account/GetAuthorizations.html>`_ with Pyrogram) and store some useful +information about the client who generated them. + + +.. figure:: https://i.imgur.com/lzGPCdZ.png + :width: 70% + :align: center + + A Pyrogram session running on Linux, Python 3.6. + +That's how a session looks like on the Android app, showing the three main pieces of information. + +- ``app_version``: **Pyrogram 🔥 0.7.5** +- ``device_model``: **CPython 3.6.5** +- ``system_version``: **Linux 4.15.0-23-generic** + +Set Custom Values +----------------- + +To set custom values, you can either make use of the ``config.ini`` file, this way: + +.. code-block:: ini + + [pyrogram] + app_version = 1.2.3 + device_model = PC + system_version = Linux + +Or, pass the arguments directly in the Client's constructor. + +.. code-block:: python + + app = Client( + "my_account", + app_version="1.2.3", + device_model="PC", + system_version="Linux" + ) + +Set Custom Languages +-------------------- + +To tell Telegram in which language should speak to you (terms of service, bots, service messages, ...) you can +set ``lang_code`` in `ISO 639-1 `_ standard (defaults to "en", +English). + +With the following code we make Telegram know we want it to speak in Italian (it): + +.. code-block:: ini + + [pyrogram] + lang_code = it + +.. code-block:: python + + app = Client( + "my_account", + lang_code="it", + ) \ No newline at end of file diff --git a/docs/source/resources/UpdateHandling.rst b/docs/source/resources/UpdateHandling.rst index 4373433b..0aa6457f 100644 --- a/docs/source/resources/UpdateHandling.rst +++ b/docs/source/resources/UpdateHandling.rst @@ -6,8 +6,10 @@ and are handled by registering one or more callback functions with an Handler. T from, one for each kind of update: - `MessageHandler <../pyrogram/handlers/MessageHandler.html>`_ +- `DeletedMessagesHandler <../pyrogram/handlers/DeletedMessagesHandler.html>`_ - `CallbackQueryHandler <../pyrogram/handlers/CallbackQueryHandler.html>`_ - `RawUpdateHandler <../pyrogram/handlers/RawUpdateHandler.html>`_ +- `DisconnectHandler <../pyrogram/handlers/DisconnectHandler.html>`_ Registering an Handler ---------------------- @@ -31,8 +33,7 @@ We shall examine the :obj:`MessageHandler `, which will print(message) - app.start() - app.idle() + app.run() - If you prefer not to use decorators, there is an alternative way for registering Handlers. This is useful, for example, when you want to keep your callback functions in separate files. @@ -50,8 +51,7 @@ We shall examine the :obj:`MessageHandler `, which will app.add_handler(MessageHandler(my_handler)) - app.start() - app.idle() + app.run() Using Filters ------------- diff --git a/docs/source/start/BasicUsage.rst b/docs/source/start/BasicUsage.rst deleted file mode 100644 index 94dfd00c..00000000 --- a/docs/source/start/BasicUsage.rst +++ /dev/null @@ -1,97 +0,0 @@ -Basic Usage -=========== - -.. note:: - - All the snippets below assume you have successfully created and started a :class:`Client ` - instance. You also must be authorized, that is, a valid *.session file does exist in your working directory. - -Simple API Access ------------------ - -The easiest way to interact with the Telegram API is via the :class:`Client ` class, which -exposes `Bot API-like`_ methods: - -- Get information about the authorized user: - - .. code-block:: python - - print(app.get_me()) - -- Send a message to yourself (Saved Messages): - - .. code-block:: python - - app.send_message("me", "Hi there! I'm using Pyrogram") - -- Upload a new photo (with caption): - - .. code-block:: python - - app.send_photo("me", "/home/dan/perla.jpg", "Cute!") - -.. seealso:: For a complete list of the available methods and an exhaustive description for each of them, have a look - at the :class:`Client ` class. - -.. _using-raw-functions: - -Using Raw Functions -------------------- - -If you want **complete**, low-level access to the Telegram API you have to use the raw -:mod:`functions ` and :mod:`types ` exposed by the ``pyrogram.api`` -package and call any Telegram API method you wish using the :meth:`send() ` method provided by -the Client class. - -Here some examples: - -- Update first name, last name and bio: - - .. code-block:: python - - from pyrogram.api import functions - - ... - - app.send( - functions.account.UpdateProfile( - first_name="Dan", last_name="Tès", - about="Bio written from Pyrogram" - ) - ) - -- Share your Last Seen time only with your contacts: - - .. code-block:: python - - from pyrogram.api import functions, types - - ... - - app.send( - functions.account.SetPrivacy( - key=types.InputPrivacyKeyStatusTimestamp(), - rules=[types.InputPrivacyValueAllowContacts()] - ) - ) - -- Invite users to your channel/supergroup: - - .. code-block:: python - - from pyrogram.api import functions, types - - ... - - app.send( - functions.channels.InviteToChannel( - channel=app.resolve_peer(123456789), # ID or Username - users=[ # The users you want to invite - app.resolve_peer(23456789), # By ID - app.resolve_peer("username"), # By username - app.resolve_peer("393281234567"), # By phone number - ] - ) - ) - -.. _`Bot API-like`: https://core.telegram.org/bots/api#available-methods \ No newline at end of file diff --git a/docs/source/start/Installation.rst b/docs/source/start/Installation.rst new file mode 100644 index 00000000..8aea16de --- /dev/null +++ b/docs/source/start/Installation.rst @@ -0,0 +1,50 @@ +Installation +============ + +Being a Python library, Pyrogram requires Python to be installed in your system. +We recommend using the latest version of Python 3 and pip. + +Get Python 3 from https://www.python.org/downloads/ or with your package manager and pip +by following the instructions at https://pip.pypa.io/en/latest/installing/. + +Pyrogram supports Python 3 only, starting from version 3.4 and PyPy. + +Install Pyrogram +---------------- + +- The easiest way to install and upgrade Pyrogram is by using **pip**: + + .. code-block:: bash + + $ pip3 install --upgrade pyrogram + +- or, with TgCrypto_ (recommended): + + .. code-block:: bash + + $ pip3 install --upgrade pyrogram[tgcrypto] + +Bleeding Edge +------------- + +If you want the latest development version of Pyrogram, you can install it straight from the develop_ +branch using this command: + +.. code-block:: bash + + $ pip3 install --upgrade git+https://github.com/pyrogram/pyrogram.git + +Verifying +--------- + +To verify that Pyrogram is correctly installed, open a Python shell and import it. +If no error shows up you are good to go. + +.. code-block:: bash + + >>> import pyrogram + >>> pyrogram.__version__ + '0.7.5' + +.. _TgCrypto: https://docs.pyrogram.ml/resources/TgCrypto +.. _develop: http://github.com/pyrogram/pyrogram \ No newline at end of file diff --git a/docs/source/start/QuickInstallation.rst b/docs/source/start/QuickInstallation.rst deleted file mode 100644 index f0400207..00000000 --- a/docs/source/start/QuickInstallation.rst +++ /dev/null @@ -1,37 +0,0 @@ -Quick Installation -================== - -- The easiest way to install and upgrade Pyrogram is by using **pip**: - - .. code-block:: bash - - $ pip3 install --upgrade pyrogram - -- or, with TgCrypto_ (recommended): - - .. code-block:: bash - - $ pip3 install --upgrade pyrogram[tgcrypto] - -Bleeding Edge -------------- - -If you want the latest development version of the library, you can install it with: - -.. code-block:: bash - - $ pip3 install --upgrade git+https://github.com/pyrogram/pyrogram.git - -Verifying ---------- - -To verify that Pyrogram is correctly installed, open a Python shell and import it. -If no error shows up you are good to go. - -.. code-block:: bash - - >>> import pyrogram - >>> pyrogram.__version__ - '0.7.4' - -.. _TgCrypto: https://docs.pyrogram.ml/resources/TgCrypto \ No newline at end of file diff --git a/docs/source/start/ProjectSetup.rst b/docs/source/start/Setup.rst similarity index 81% rename from docs/source/start/ProjectSetup.rst rename to docs/source/start/Setup.rst index 05d46810..417d62d8 100644 --- a/docs/source/start/ProjectSetup.rst +++ b/docs/source/start/Setup.rst @@ -1,8 +1,9 @@ -Project Setup -============= +Setup +===== -This section provides all the information you need to setup your project with Pyrogram. -There are a few steps you have to follow before you can actually use the library to make API calls. +Once you successfully `installed Pyrogram`_, you will still have to follow a few steps before you can actually use +the library to make API calls. This section provides all the information you need in order to set up a project +with Pyrogram. API Keys -------- @@ -39,7 +40,7 @@ There are two ways to configure a Pyrogram application project, and you can choo from pyrogram import Client app = Client( - session_name="my_account", + "my_account", api_id=12345, api_hash="0123456789abcdef0123456789abcdef" ) @@ -53,14 +54,14 @@ User Authorization In order to use the API, Telegram requires that Users be authorized via their phone numbers. Pyrogram automatically manages this access, all you need to do is create an instance of the :class:`Client ` class by passing to it a ``session_name`` of your choice -(e.g.: "my_account") and call the :meth:`start() ` method: +(e.g.: "my_account") and call the :meth:`run() ` method: .. code-block:: python from pyrogram import Client app = Client("my_account") - app.start() + app.run() This starts an interactive shell asking you to input your **phone number** (including your `Country Code`_) and the **phone code** you will receive: @@ -75,14 +76,14 @@ After successfully authorizing yourself, a new file called ``my_account.session` Pyrogram executing API calls with your identity. This file will be loaded again when you restart your app, and as long as you keep the session alive, Pyrogram won't ask you again to enter your phone number. -.. important:: Your *.session file(s) must be kept secret. +.. important:: Your ``*.session`` file(s) must be kept secret. Bot Authorization ----------------- Being written entirely from the ground up, Pyrogram is also able to authorize Bots. -Bots are a special kind of users which also make use of MTProto. This means that you can use Pyrogram to -execute API calls with a Bot identity. +Bots are a special kind of users which also make use of MTProto, the underlying Telegram protocol. +This means that you can use Pyrogram to execute API calls with a Bot identity. Instead of phone numbers, Bots are authorized via their tokens which are created by BotFather_: @@ -91,10 +92,11 @@ Instead of phone numbers, Bots are authorized via their tokens which are created from pyrogram import Client app = Client("123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11") - app.start() + app.run() That's all, no further action is needed. The session file will be named after the Bot user_id, which is ``123456.session`` for the example above. +.. _installed Pyrogram: Installation.html .. _`Country Code`: https://en.wikipedia.org/wiki/List_of_country_calling_codes .. _BotFather: https://t.me/botfather \ No newline at end of file diff --git a/docs/source/start/Usage.rst b/docs/source/start/Usage.rst new file mode 100644 index 00000000..6c20decd --- /dev/null +++ b/docs/source/start/Usage.rst @@ -0,0 +1,118 @@ +Usage +===== + +Having your `project set up`_ and your account authorized_, it's time to play with the API. +In this section, you'll be shown two ways of communicating with Telegram using Pyrogram. Let's start! + +High-level API +-------------- + +The easiest and recommended way to interact with Telegram is via the high-level Pyrogram methods_ and types_, which are +named after the `Telegram Bot API`_. + +Examples (more on `GitHub `_): + +- Get information about the authorized user: + + .. code-block:: python + + print(app.get_me()) + +- Send a message to yourself (Saved Messages): + + .. code-block:: python + + app.send_message("me", "Hi there! I'm using Pyrogram") + +- Upload a new photo (with caption): + + .. code-block:: python + + app.send_photo("me", "/home/dan/perla.jpg", "Cute!") + +Raw Functions +------------- + +If you can't find a high-level method for your needs or if want complete, low-level access to the whole Telegram API, +you have to use the raw :mod:`functions ` and :mod:`types ` exposed by the +``pyrogram.api`` package and call any Telegram API method you wish using the :meth:`send() ` +method provided by the Client class. + +.. hint:: Every high-level method mentioned in the section above is built on top of these raw functions. + + Nothing stops you from using the raw functions only, but they are rather complex and `plenty of them`_ are already + re-implemented by providing a much simpler and cleaner interface which is very similar to the Bot API. + + If you think a raw function should be wrapped and added as a high-level method, feel free to ask in our Community_! + +Examples (more on `GitHub `_): + +- Update first name, last name and bio: + + .. code-block:: python + + from pyrogram import Client + from pyrogram.api import functions + + app = Client("my_account") + app.start() + + app.send( + functions.account.UpdateProfile( + first_name="Dan", last_name="Tès", + about="Bio written from Pyrogram" + ) + ) + + app.stop() + +- Share your Last Seen time only with your contacts: + + .. code-block:: python + + from pyrogram import Client + from pyrogram.api import functions, types + + app = Client("my_account") + app.start() + + app.send( + functions.account.SetPrivacy( + key=types.InputPrivacyKeyStatusTimestamp(), + rules=[types.InputPrivacyValueAllowContacts()] + ) + ) + + app.stop() + +- Invite users to your channel/supergroup: + + .. code-block:: python + + from pyrogram import Client + from pyrogram.api import functions, types + + app = Client("my_account") + app.start() + + app.send( + functions.channels.InviteToChannel( + channel=app.resolve_peer(123456789), # ID or Username + users=[ # The users you want to invite + app.resolve_peer(23456789), # By ID + app.resolve_peer("username"), # By username + app.resolve_peer("393281234567"), # By phone number + ] + ) + ) + + app.stop() + +.. _methods: ../pyrogram/Client.html#available-methods +.. _plenty of them: ../pyrogram/Client.html#available-methods +.. _types: ../pyrogram/types/index.html +.. _Raw Functions: Usage.html#using-raw-functions +.. _Community: https://t.me/PyrogramChat +.. _project set up: Setup.html +.. _authorized: Setup.html#user-authorization +.. _Telegram Bot API: https://core.telegram.org/bots/api \ No newline at end of file diff --git a/examples/callback_query_handler.py b/examples/callback_query_handler.py index 6b76edd8..999c2686 100644 --- a/examples/callback_query_handler.py +++ b/examples/callback_query_handler.py @@ -34,5 +34,4 @@ def answer(client, callback_query): ) -app.start() -app.idle() +app.run() # Automatically start() and idle() diff --git a/examples/echo_bot.py b/examples/echo_bot.py index ed2d4df5..adda52c7 100644 --- a/examples/echo_bot.py +++ b/examples/echo_bot.py @@ -36,5 +36,4 @@ def echo(client, message): ) -app.start() -app.idle() +app.run() # Automatically start() and idle() diff --git a/examples/get_history.py b/examples/get_history.py index d53bed96..433d127c 100644 --- a/examples/get_history.py +++ b/examples/get_history.py @@ -24,12 +24,12 @@ from pyrogram.api.errors import FloodWait """This example shows how to retrieve the full message history of a chat""" app = Client("my_account") -app.start() - target = "me" # "me" refers to your own chat (Saved Messages) messages = [] # List that will contain all the messages of the target chat offset_id = 0 # ID of the last message of the chunk +app.start() + while True: try: m = app.get_history(target, offset_id=offset_id) diff --git a/examples/get_participants.py b/examples/get_participants.py index d10710ba..fd5257fb 100644 --- a/examples/get_participants.py +++ b/examples/get_participants.py @@ -28,13 +28,13 @@ Refer to get_participants2.py for more than 10.000 users. """ app = Client("my_account") -app.start() - target = "pyrogramchat" # Target channel/supergroup users = [] # List that will contain all the users of the target chat limit = 200 # Amount of users to retrieve for each API call offset = 0 # Offset starts at 0 +app.start() + while True: try: participants = app.send( diff --git a/examples/get_participants2.py b/examples/get_participants2.py index dfad315b..a70afb74 100644 --- a/examples/get_participants2.py +++ b/examples/get_participants2.py @@ -33,15 +33,14 @@ as some user names may not contain ascii letters at all. """ app = Client("my_account") -app.start() - target = "pyrogramchat" # 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) +app.start() + for q in queries: print("Searching for '{}'".format(q)) offset = 0 # For each query, offset restarts from 0 diff --git a/examples/raw_update_handler.py b/examples/raw_update_handler.py index eb417c24..c7195761 100644 --- a/examples/raw_update_handler.py +++ b/examples/raw_update_handler.py @@ -28,5 +28,4 @@ def raw(client, update, users, chats): print(update) -app.start() -app.idle() +app.run() # Automatically start() and idle() diff --git a/examples/welcome_bot.py b/examples/welcome_bot.py index 8e087728..5fd93293 100644 --- a/examples/welcome_bot.py +++ b/examples/welcome_bot.py @@ -49,5 +49,4 @@ def welcome(client, message): ) -app.start() -app.idle() +app.run() # Automatically start() and idle() diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index 44edb3cb..96fb4a76 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -23,7 +23,7 @@ __copyright__ = "Copyright (C) 2017-2018 Dan Tès ` """ @@ -180,12 +208,9 @@ class Client(Methods, BaseClient): self.load_session() self.session = Session( + self, self.dc_id, - self.test_mode, - self.proxy, - self.auth_key, - self.api_id, - client=self + self.auth_key ) self.session.start() @@ -273,6 +298,39 @@ class Client(Methods, BaseClient): self.is_started = False self.session.stop() + 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). + """ + + def signal_handler(*args): + self.is_idle = False + + for s in stop_signals: + signal(s, signal_handler) + + self.is_idle = True + + while self.is_idle: + time.sleep(1) + + self.stop() + + def run(self): + """Use this method to automatically start and idle a Client. + Requires no parameters. + + Raises: + :class:`Error ` + """ + self.start() + self.idle() + def add_handler(self, handler, group: int = 0): """Use this method to register an update handler. @@ -290,7 +348,10 @@ class Client(Methods, BaseClient): Returns: A tuple of (handler, group) """ - self.dispatcher.add_handler(handler, group) + if isinstance(handler, DisconnectHandler): + self.disconnect_handler = handler.callback + else: + self.dispatcher.add_handler(handler, group) return handler, group @@ -308,7 +369,10 @@ class Client(Methods, BaseClient): group (``int``, *optional*): The group identifier, defaults to 0. """ - self.dispatcher.remove_handler(handler, group) + if isinstance(handler, DisconnectHandler): + self.disconnect_handler = None + else: + self.dispatcher.remove_handler(handler, group) def authorize_bot(self): try: @@ -324,15 +388,12 @@ class Client(Methods, BaseClient): self.session.stop() self.dc_id = e.x - self.auth_key = Auth(self.dc_id, self.test_mode, self.proxy).create() + self.auth_key = Auth(self.dc_id, self.test_mode, self._proxy).create() self.session = Session( + self, self.dc_id, - self.test_mode, - self.proxy, - self.auth_key, - self.api_id, - client=self + self.auth_key ) self.session.start() @@ -372,15 +433,12 @@ class Client(Methods, BaseClient): self.session.stop() self.dc_id = e.x - self.auth_key = Auth(self.dc_id, self.test_mode, self.proxy).create() + self.auth_key = Auth(self.dc_id, self.test_mode, self._proxy).create() self.session = Session( + self, self.dc_id, - self.test_mode, - self.proxy, - self.auth_key, - self.api_id, - client=self + self.auth_key ) self.session.start() @@ -399,8 +457,11 @@ class Client(Methods, BaseClient): print(e.MESSAGE) self.phone_number = None except FloodWait as e: - print(e.MESSAGE.format(x=e.x)) - time.sleep(e.x) + if phone_number_invalid_raises: + raise + else: + print(e.MESSAGE.format(x=e.x)) + time.sleep(e.x) except Exception as e: log.error(e, exc_info=True) else: @@ -408,6 +469,10 @@ class Client(Methods, BaseClient): phone_registered = r.phone_registered phone_code_hash = r.phone_code_hash + terms_of_service = r.terms_of_service + + if terms_of_service: + print("\n" + terms_of_service.text + "\n") if self.force_sms: self.send( @@ -421,7 +486,7 @@ class Client(Methods, BaseClient): self.phone_code = ( input("Enter phone code: ") if self.phone_code is None else self.phone_code if type(self.phone_code) is str - else str(self.phone_code()) + else str(self.phone_code(self.phone_number)) ) try: @@ -478,7 +543,7 @@ class Client(Methods, BaseClient): if self.password is None: print("Hint: {}".format(r.hint)) - self.password = input("Enter password: ") # TODO: Use getpass + self.password = getpass.getpass("Enter password: ") if type(self.password) is str: self.password = r.current_salt + self.password.encode() + r.current_salt @@ -493,24 +558,35 @@ class Client(Methods, BaseClient): print(e.MESSAGE) self.password = None except FloodWait as e: - print(e.MESSAGE.format(x=e.x)) - time.sleep(e.x) + if password_hash_invalid_raises: + raise + else: + print(e.MESSAGE.format(x=e.x)) + time.sleep(e.x) except Exception as e: log.error(e, exc_info=True) else: break break except FloodWait as e: - print(e.MESSAGE.format(x=e.x)) - time.sleep(e.x) + if phone_code_invalid_raises or first_name_invalid_raises: + raise + else: + print(e.MESSAGE.format(x=e.x)) + time.sleep(e.x) except Exception as e: log.error(e, exc_info=True) else: break + if terms_of_service: + assert self.send(functions.help.AcceptTermsOfService(terms_of_service.id)) + self.password = None self.user_id = r.user.id + print("Login successful") + def fetch_peers(self, entities: list): for entity in entities: if isinstance(entity, types.User): @@ -763,29 +839,7 @@ class Client(Methods, BaseClient): log.debug("{} stopped".format(name)) - def signal_handler(self, *args): - 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) - - self.stop() - - def send(self, data: Object): + def send(self, data: Object, retries: int = Session.MAX_RETRIES, timeout: float = Session.WAIT_TIMEOUT): """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. @@ -796,13 +850,19 @@ class Client(Methods, BaseClient): data (``Object``): The API Scheme function filled with proper arguments. + retries (``int``): + Number of retries. + + timeout (``float``): + Timeout in seconds. + Raises: :class:`Error ` """ if not self.is_started: raise ConnectionError("Client has not been started") - r = self.session.send(data) + r = self.session.send(data, retries, timeout) self.fetch_peers(getattr(r, "users", [])) self.fetch_peers(getattr(r, "chats", [])) @@ -825,19 +885,42 @@ class Client(Methods, BaseClient): "More info: https://docs.pyrogram.ml/start/ProjectSetup#configuration" ) - if self.proxy: - self.proxy["enabled"] = True - self.proxy["username"] = self.proxy.get("username", None) - self.proxy["password"] = self.proxy.get("password", None) + for option in {"app_version", "device_model", "system_version", "lang_code"}: + if getattr(self, option): + pass + else: + setattr(self, option, Client.APP_VERSION) + + if parser.has_section("pyrogram"): + setattr(self, option, parser.get( + "pyrogram", + option, + fallback=getattr(Client, option.upper()) + )) + + if self.lang_code: + pass else: - self.proxy = {} + self.lang_code = Client.LANG_CODE + + if parser.has_section("pyrogram"): + self.lang_code = parser.get( + "pyrogram", + "lang_code", + fallback=Client.LANG_CODE + ) + + if self._proxy: + self._proxy["enabled"] = True + else: + self._proxy = {} if parser.has_section("proxy"): - self.proxy["enabled"] = parser.getboolean("proxy", "enabled") - self.proxy["hostname"] = parser.get("proxy", "hostname") - self.proxy["port"] = parser.getint("proxy", "port") - self.proxy["username"] = parser.get("proxy", "username", fallback=None) or None - self.proxy["password"] = parser.get("proxy", "password", fallback=None) or None + self._proxy["enabled"] = parser.getboolean("proxy", "enabled") + self._proxy["hostname"] = parser.get("proxy", "hostname") + self._proxy["port"] = parser.getint("proxy", "port") + self._proxy["username"] = parser.get("proxy", "username", fallback=None) or None + self._proxy["password"] = parser.get("proxy", "password", fallback=None) or None def load_session(self): try: @@ -846,7 +929,7 @@ class Client(Methods, BaseClient): except FileNotFoundError: self.dc_id = 1 self.date = 0 - self.auth_key = Auth(self.dc_id, self.test_mode, self.proxy).create() + self.auth_key = Auth(self.dc_id, self.test_mode, self._proxy).create() else: self.dc_id = s["dc_id"] self.test_mode = s["test_mode"] @@ -983,7 +1066,6 @@ class Client(Methods, BaseClient): except (KeyError, ValueError): raise PeerIdInvalid - # TODO: Improvements for the new API def save_file(self, path: str, file_id: int = None, @@ -998,7 +1080,7 @@ class Client(Methods, BaseClient): file_id = file_id or self.rnd_id() md5_sum = md5() if not is_big and not is_missing_part else None - session = Session(self.dc_id, self.test_mode, self.proxy, self.auth_key, self.api_id) + session = Session(self, self.dc_id, self.auth_key, is_media=True) session.start() try: @@ -1027,7 +1109,7 @@ class Client(Methods, BaseClient): bytes=chunk ) - assert self.send(rpc), "Couldn't upload file" + assert session.send(rpc), "Couldn't upload file" if is_missing_part: return @@ -1059,7 +1141,6 @@ class Client(Methods, BaseClient): finally: session.stop() - # TODO: Improvements for the new API def get_file(self, dc_id: int, id: int = None, @@ -1083,11 +1164,10 @@ class Client(Methods, BaseClient): ) session = Session( + self, dc_id, - self.test_mode, - self.proxy, - Auth(dc_id, self.test_mode, self.proxy).create(), - self.api_id + Auth(dc_id, self.test_mode, self._proxy).create(), + is_media=True ) session.start() @@ -1102,11 +1182,10 @@ class Client(Methods, BaseClient): ) else: session = Session( + self, dc_id, - self.test_mode, - self.proxy, self.auth_key, - self.api_id + is_media=True ) session.start() @@ -1172,11 +1251,10 @@ class Client(Methods, BaseClient): if cdn_session is None: cdn_session = Session( + self, r.dc_id, - self.test_mode, - self.proxy, - Auth(r.dc_id, self.test_mode, self.proxy).create(), - self.api_id, + Auth(r.dc_id, self.test_mode, self._proxy).create(), + is_media=True, is_cdn=True ) @@ -1213,11 +1291,13 @@ class Client(Methods, BaseClient): chunk = r2.bytes # https://core.telegram.org/cdn#decrypting-files - decrypted_chunk = AES.ctr_decrypt( + decrypted_chunk = AES.ctr256_decrypt( chunk, r.encryption_key, - r.encryption_iv, - offset + bytearray( + r.encryption_iv[:-4] + + (offset // 16).to_bytes(4, "big") + ) ) hashes = session.send( @@ -1245,9 +1325,6 @@ class Client(Methods, BaseClient): break except Exception as e: raise e - finally: - pass # Don't stop sessions, they are now cached and kept online - # cdn_session.stop() TODO: Remove this branch except Exception as e: log.error(e, exc_info=True) @@ -1259,6 +1336,3 @@ class Client(Methods, BaseClient): return "" else: return file_name - finally: - pass # Don't stop sessions, they are now cached and kept online - # session.stop() TODO: Remove this branch diff --git a/pyrogram/client/dispatcher/dispatcher.py b/pyrogram/client/dispatcher/dispatcher.py index 5558a2fd..ffff16bd 100644 --- a/pyrogram/client/dispatcher/dispatcher.py +++ b/pyrogram/client/dispatcher/dispatcher.py @@ -25,7 +25,7 @@ from threading import Thread import pyrogram from pyrogram.api import types from ..ext import utils -from ..handlers import RawUpdateHandler, CallbackQueryHandler, MessageHandler +from ..handlers import RawUpdateHandler, CallbackQueryHandler, MessageHandler, DeletedMessagesHandler log = logging.getLogger(__name__) @@ -41,6 +41,11 @@ class Dispatcher: types.UpdateEditChannelMessage ) + DELETE_MESSAGE_UPDATES = ( + types.UpdateDeleteMessages, + types.UpdateDeleteChannelMessages + ) + MESSAGE_UPDATES = NEW_MESSAGE_UPDATES + EDIT_MESSAGE_UPDATES def __init__(self, client, workers): @@ -50,8 +55,6 @@ class Dispatcher: self.updates = Queue() self.groups = OrderedDict() - self._handler_lock = threading.Lock() - def start(self): for i in range(self.workers): self.workers_list.append( @@ -73,52 +76,57 @@ class Dispatcher: self.workers_list.clear() def add_handler(self, handler, group: int): - with self._handler_lock: - if group not in self.groups: - self.groups[group] = [] - self.groups = OrderedDict(sorted(self.groups.items())) + if group not in self.groups: + self.groups[group] = [] + self.groups = OrderedDict(sorted(self.groups.items())) - self.groups[group].append(handler) + self.groups[group].append(handler) def remove_handler(self, handler, group: int): - with self._handler_lock: - if group not in self.groups: - raise ValueError("Group {} does not exist. " - "Handler was not removed.".format(group)) - self.groups[group].remove(handler) + if group not in self.groups: + raise ValueError("Group {} does not exist. " + "Handler was not removed.".format(group)) + self.groups[group].remove(handler) def dispatch(self, update, users: dict = None, chats: dict = None, is_raw: bool = False): - with self._handler_lock: - for group in self.groups.values(): - for handler in group: - if is_raw: - if not isinstance(handler, RawUpdateHandler): + for group in self.groups.values(): + for handler in group: + if is_raw: + if not isinstance(handler, RawUpdateHandler): + continue + + args = (self.client, update, users, chats) + else: + message = (update.message + or update.channel_post + or update.edited_message + or update.edited_channel_post) + + deleted_messages = (update.deleted_channel_posts + or update.deleted_messages) + + callback_query = update.callback_query + + if message and isinstance(handler, MessageHandler): + if not handler.check(message): continue - args = (self.client, update, users, chats) + args = (self.client, message) + elif deleted_messages and isinstance(handler, DeletedMessagesHandler): + if not handler.check(deleted_messages): + continue + + args = (self.client, deleted_messages) + elif callback_query and isinstance(handler, CallbackQueryHandler): + if not handler.check(callback_query): + continue + + args = (self.client, callback_query) else: - message = (update.message - or update.channel_post - or update.edited_message - or update.edited_channel_post) + continue - callback_query = update.callback_query - - if message and isinstance(handler, MessageHandler): - if not handler.check(message): - continue - - args = (self.client, message) - elif callback_query and isinstance(handler, CallbackQueryHandler): - if not handler.check(callback_query): - continue - - args = (self.client, callback_query) - else: - continue - - handler.callback(*args) - break + handler.callback(*args) + break def update_worker(self): name = threading.current_thread().name @@ -166,6 +174,22 @@ class Dispatcher: else None) ) ) + + elif isinstance(update, Dispatcher.DELETE_MESSAGE_UPDATES): + is_channel = hasattr(update, 'channel_id') + + messages = utils.parse_deleted_messages( + update.messages, + (update.channel_id if is_channel else None) + ) + + self.dispatch( + pyrogram.Update( + deleted_messages=(messages if not is_channel else None), + deleted_channel_posts=(messages if is_channel else None) + ) + ) + elif isinstance(update, types.UpdateBotCallbackQuery): self.dispatch( pyrogram.Update( @@ -174,6 +198,14 @@ class Dispatcher: ) ) ) + elif isinstance(update, types.UpdateInlineBotCallbackQuery): + self.dispatch( + pyrogram.Update( + callback_query=utils.parse_inline_callback_query( + update, users + ) + ) + ) else: continue except Exception as e: diff --git a/pyrogram/client/ext/base_client.py b/pyrogram/client/ext/base_client.py index ce3d5591..cd54570d 100644 --- a/pyrogram/client/ext/base_client.py +++ b/pyrogram/client/ext/base_client.py @@ -16,17 +16,35 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . +import platform import re from queue import Queue from threading import Lock +from pyrogram import __version__ from ..style import Markdown, HTML from ...api.core import Object +from ...session import Session from ...session.internals import MsgId class BaseClient: - INVITE_LINK_RE = re.compile(r"^(?:https?://)?(?:t\.me/joinchat/)([\w-]+)$") + APP_VERSION = "Pyrogram \U0001f525 {}".format(__version__) + + DEVICE_MODEL = "{} {}".format( + platform.python_implementation(), + platform.python_version() + ) + + SYSTEM_VERSION = "{} {}".format( + platform.system(), + platform.release() + ) + + SYSTEM_LANG_CODE = "en" + LANG_CODE = "en" + + INVITE_LINK_RE = re.compile(r"^(?:https?://)?(?:www\.)?(?:t(?:elegram)?\.(?:org|me|dog)/joinchat/)([\w-]+)$") BOT_TOKEN_RE = re.compile(r"^\d+:[\w-]+$") DIALOGS_AT_ONCE = 100 UPDATES_WORKERS = 1 @@ -75,7 +93,9 @@ class BaseClient: self.download_queue = Queue() self.download_workers_list = [] - def send(self, data: Object): + self.disconnect_handler = None + + def send(self, data: Object, retries: int = Session.MAX_RETRIES, timeout: float = Session.WAIT_TIMEOUT): pass def resolve_peer(self, peer_id: int or str): diff --git a/pyrogram/client/ext/utils.py b/pyrogram/client/ext/utils.py index 54515eb2..2913c38b 100644 --- a/pyrogram/client/ext/utils.py +++ b/pyrogram/client/ext/utils.py @@ -33,26 +33,28 @@ log = logging.getLogger(__name__) # TODO: Organize the code better? class Str(str): - def __init__(self, value): - str.__init__(value) - self._markdown = None - self._html = None + __slots__ = "_client", "_entities" + + def __init__(self, *args): + super().__init__() + self._client = None + self._entities = None + + def init(self, client, entities): + self._client = client + self._entities = entities + + @property + def text(self): + return self @property def markdown(self): - return self._markdown - - @markdown.setter - def markdown(self, value): - self._markdown = value + return self._client.markdown.unparse(self, self._entities) @property def html(self): - return self._html - - @html.setter - def html(self, value): - self._html = value + return self._client.html.unparse(self, self._entities) ENTITIES = { @@ -413,7 +415,7 @@ def parse_messages( date=doc.date ) elif types.DocumentAttributeAnimated in attributes: - video_attributes = attributes[types.DocumentAttributeVideo] + video_attributes = attributes.get(types.DocumentAttributeVideo, None) gif = pyrogram_types.GIF( file_id=encode( @@ -425,9 +427,9 @@ def parse_messages( doc.access_hash ) ), - width=video_attributes.w, - height=video_attributes.h, - duration=video_attributes.duration, + width=getattr(video_attributes, "w", 0), + height=getattr(video_attributes, "h", 0), + duration=getattr(video_attributes, "duration", 0), thumb=parse_thumb(doc.thumb), mime_type=doc.mime_type, file_size=doc.size, @@ -581,16 +583,10 @@ def parse_messages( ) if m.text: - args = (m.text, m.entities or []) - - m.text.markdown = client.markdown.unparse(*args) - m.text.html = client.html.unparse(*args) + m.text.init(m._client, m.entities or []) if m.caption: - args = (m.caption, m.caption_entities or []) - - m.caption.markdown = client.markdown.unparse(*args) - m.caption.html = client.html.unparse(*args) + m.caption.init(m._client, m.caption_entities or []) if message.reply_to_msg_id and replies: while True: @@ -715,6 +711,25 @@ def parse_messages( return parsed_messages if is_list else parsed_messages[0] +def parse_deleted_messages( + messages: list, + channel_id: int +) -> pyrogram_types.Messages: + parsed_messages = [] + + for message in messages: + parsed_messages.append( + pyrogram_types.Message( + message_id=message, + chat=(pyrogram_types.Chat(id=int("-100" + str(channel_id)), type="channel") + if channel_id is not None + else None) + ) + ) + + return pyrogram_types.Messages(len(parsed_messages), parsed_messages) + + def get_peer_id(input_peer) -> int: return ( input_peer.user_id if isinstance(input_peer, types.InputPeerUser) @@ -805,13 +820,30 @@ def parse_callback_query(client, callback_query, users): peer_id = int("-100" + str(peer.channel_id)) return pyrogram_types.CallbackQuery( - id=callback_query.query_id, + id=str(callback_query.query_id), from_user=parse_user(users[callback_query.user_id]), message=client.get_messages(peer_id, callback_query.msg_id), chat_instance=str(callback_query.chat_instance), data=callback_query.data.decode(), game_short_name=callback_query.game_short_name - # TODO: add inline_message_id + ) + + +def parse_inline_callback_query(callback_query, users): + return pyrogram_types.CallbackQuery( + id=str(callback_query.query_id), + from_user=parse_user(users[callback_query.user_id]), + chat_instance=str(callback_query.chat_instance), + inline_message_id=b64encode( + pack( + "` objects.""" - private = build("Private", lambda _, m: bool(m.chat.type == "private")) + private = build("Private", lambda _, m: bool(m.chat and m.chat.type == "private")) """Filter messages sent in private chats.""" - group = build("Group", lambda _, m: bool(m.chat.type in {"group", "supergroup"})) + group = build("Group", lambda _, m: bool(m.chat and m.chat.type in {"group", "supergroup"})) """Filter messages sent in group or supergroup chats.""" - channel = build("Channel", lambda _, m: bool(m.chat.type == "channel")) + channel = build("Channel", lambda _, m: bool(m.chat and m.chat.type == "channel")) """Filter messages sent in channels.""" new_chat_members = build("NewChatMembers", lambda _, m: bool(m.new_chat_members)) @@ -131,10 +132,17 @@ class Filters: pinned_message = build("PinnedMessage", lambda _, m: bool(m.pinned_message)) """Filter service messages for pinned messages.""" - # TODO: Add filters for reply markups + reply_keyboard = build("ReplyKeyboard", lambda _, m: isinstance(m.reply_markup, ReplyKeyboardMarkup)) + """Filter messages containing reply keyboard markups""" + + inline_keyboard = build("InlineKeyboard", lambda _, m: isinstance(m.reply_markup, InlineKeyboardMarkup)) + """Filter messages containing inline keyboard markups""" @staticmethod - def command(command: str or list, prefix: str = "/", separator: str = " "): + def command(command: str or list, + prefix: str = "/", + separator: str = " ", + case_sensitive: bool = False): """Filter commands, i.e.: text messages starting with "/" or any other custom prefix. Args: @@ -144,25 +152,40 @@ class Filters: a command arrives, the command itself and its arguments will be stored in the *command* field of the :class:`Message `. - prefix (``str``): - The command prefix. Defaults to "/". + prefix (``str``, *optional*): + The command prefix. Defaults to "/" (slash). Examples: /start, .help, !settings. - separator (``str``): + separator (``str``, *optional*): The command arguments separator. Defaults to " " (white space). Examples: /start first second, /start-first-second, /start.first.second. + + case_sensitive (``bool``, *optional*): + Pass True if you want your command(s) to be case sensitive. Defaults to False. + Examples: when True, command="Start" would trigger /Start but not /start. """ def f(_, m): if m.text and m.text.startswith(_.p): - c = m.text[1:].split(_.s)[0] - m.command = ([c] + m.text.split(_.s)[1:]) if c in _.c else None + t = m.text.split(_.s) + c, a = t[0][len(_.p):], t[1:] + c = c if _.cs else c.lower() + m.command = ([c] + a) if c in _.c else None return bool(m.command) return build( - "Command", f, c={command} if not isinstance(command, list) else {c for c in command}, - p=prefix, s=separator + "Command", + f, + c={command if case_sensitive + else command.lower()} + if not isinstance(command, list) + else {c if case_sensitive + else c.lower() + for c in command}, + p=prefix, + s=separator, + cs=case_sensitive ) @staticmethod @@ -253,6 +276,7 @@ class Filters: or Filters.photo(m) or Filters.sticker(m) or Filters.video(m) + or Filters.gif(m) or Filters.voice(m) or Filters.video_note(m) or Filters.contact(m) diff --git a/pyrogram/client/handlers/__init__.py b/pyrogram/client/handlers/__init__.py index ed330b21..d06b2a76 100644 --- a/pyrogram/client/handlers/__init__.py +++ b/pyrogram/client/handlers/__init__.py @@ -16,4 +16,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from .handlers import MessageHandler, CallbackQueryHandler, RawUpdateHandler +from .callback_query_handler import CallbackQueryHandler +from .disconnect_handler import DisconnectHandler +from .message_handler import MessageHandler +from .deleted_messages_handler import DeletedMessagesHandler +from .raw_update_handler import RawUpdateHandler diff --git a/pyrogram/client/handlers/callback_query_handler.py b/pyrogram/client/handlers/callback_query_handler.py new file mode 100644 index 00000000..c5346519 --- /dev/null +++ b/pyrogram/client/handlers/callback_query_handler.py @@ -0,0 +1,54 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from .handler import Handler + + +class CallbackQueryHandler(Handler): + """The CallbackQuery handler class. Used to handle callback queries coming from inline buttons. + It is intended to be used with :meth:`add_handler() ` + + For a nicer way to register this handler, have a look at the + :meth:`on_callback_query() ` decorator. + + Args: + callback (``callable``): + Pass a function that will be called when a new CallbackQuery arrives. It takes *(client, callback_query)* + as positional arguments (look at the section below for a detailed description). + + filters (:obj:`Filters `): + Pass one or more filters to allow only a subset of callback queries to be passed + in your callback function. + + Other parameters: + client (:obj:`Client `): + The Client itself, useful when you want to call other API methods inside the message handler. + + callback_query (:obj:`CallbackQuery `): + The received callback query. + """ + + def __init__(self, callback: callable, filters=None): + super().__init__(callback, filters) + + def check(self, callback_query): + return ( + self.filters(callback_query) + if self.filters + else True + ) diff --git a/pyrogram/client/handlers/deleted_messages_handler.py b/pyrogram/client/handlers/deleted_messages_handler.py new file mode 100644 index 00000000..55d5715f --- /dev/null +++ b/pyrogram/client/handlers/deleted_messages_handler.py @@ -0,0 +1,55 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from .handler import Handler + + +class DeletedMessagesHandler(Handler): + """The deleted Messages handler class. Used to handle deleted messages coming from any chat + (private, group, channel). It is intended to be used with + :meth:`add_handler() ` + + For a nicer way to register this handler, have a look at the + :meth:`on_deleted_messages() ` decorator. + + Args: + callback (``callable``): + Pass a function that will be called when one or more Messages have been deleted. + It takes *(client, messages)* as positional arguments (look at the section below for a detailed description). + + filters (:obj:`Filters `): + Pass one or more filters to allow only a subset of messages to be passed + in your callback function. + + Other parameters: + client (:obj:`Client `): + The Client itself, useful when you want to call other API methods inside the message handler. + + messages (:obj:`Messages `): + The deleted messages. + """ + + def __init__(self, callback: callable, filters=None): + super().__init__(callback, filters) + + def check(self, messages): + return ( + self.filters(messages.messages[0]) + if self.filters + else True + ) diff --git a/pyrogram/client/handlers/disconnect_handler.py b/pyrogram/client/handlers/disconnect_handler.py new file mode 100644 index 00000000..a8b800a8 --- /dev/null +++ b/pyrogram/client/handlers/disconnect_handler.py @@ -0,0 +1,41 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from .handler import Handler + + +class DisconnectHandler(Handler): + """The Disconnect handler class. Used to handle disconnections. It is intended to be used with + :meth:`add_handler() ` + + For a nicer way to register this handler, have a look at the + :meth:`on_disconnect() ` decorator. + + Args: + callback (``callable``): + Pass a function that will be called when a disconnection occurs. It takes *(client)* + as positional argument (look at the section below for a detailed description). + + Other parameters: + client (:obj:`Client `): + The Client itself. Useful, for example, when you want to change the proxy before a new connection + is established. + """ + + def __init__(self, callback: callable): + super().__init__(callback) diff --git a/pyrogram/client/handlers/message_handler.py b/pyrogram/client/handlers/message_handler.py new file mode 100644 index 00000000..1b4770b3 --- /dev/null +++ b/pyrogram/client/handlers/message_handler.py @@ -0,0 +1,55 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from .handler import Handler + + +class MessageHandler(Handler): + """The Message handler class. Used to handle text, media and service messages coming from + any chat (private, group, channel). It is intended to be used with + :meth:`add_handler() ` + + For a nicer way to register this handler, have a look at the + :meth:`on_message() ` decorator. + + Args: + callback (``callable``): + Pass a function that will be called when a new Message arrives. It takes *(client, message)* + as positional arguments (look at the section below for a detailed description). + + filters (:obj:`Filters `): + Pass one or more filters to allow only a subset of messages to be passed + in your callback function. + + Other parameters: + client (:obj:`Client `): + The Client itself, useful when you want to call other API methods inside the message handler. + + message (:obj:`Message `): + The received message. + """ + + def __init__(self, callback: callable, filters=None): + super().__init__(callback, filters) + + def check(self, message): + return ( + self.filters(message) + if self.filters + else True + ) diff --git a/pyrogram/client/handlers/handlers.py b/pyrogram/client/handlers/raw_update_handler.py similarity index 54% rename from pyrogram/client/handlers/handlers.py rename to pyrogram/client/handlers/raw_update_handler.py index c50e6725..5a8913b6 100644 --- a/pyrogram/client/handlers/handlers.py +++ b/pyrogram/client/handlers/raw_update_handler.py @@ -19,75 +19,13 @@ from .handler import Handler -class MessageHandler(Handler): - """The Message handler class. Used to handle text, media and service messages coming from - any chat (private, group, channel). It is intended to be used with - :meth:`add_handler() ` - - Args: - callback (``callable``): - Pass a function that will be called when a new Message arrives. It takes *(client, message)* - as positional arguments (look at the section below for a detailed description). - - filters (:obj:`Filters `): - Pass one or more filters to allow only a subset of messages to be passed - in your callback function. - - Other parameters: - client (:obj:`Client `): - The Client itself, useful when you want to call other API methods inside the message handler. - - message (:obj:`Message `): - The received message. - """ - - def __init__(self, callback: callable, filters=None): - super().__init__(callback, filters) - - def check(self, message): - return ( - self.filters(message) - if self.filters - else True - ) - - -class CallbackQueryHandler(Handler): - """The CallbackQuery handler class. Used to handle callback queries coming from inline buttons. - It is intended to be used with :meth:`add_handler() ` - - Args: - callback (``callable``): - Pass a function that will be called when a new CallbackQuery arrives. It takes *(client, callback_query)* - as positional arguments (look at the section below for a detailed description). - - filters (:obj:`Filters `): - Pass one or more filters to allow only a subset of callback queries to be passed - in your callback function. - - Other parameters: - client (:obj:`Client `): - The Client itself, useful when you want to call other API methods inside the message handler. - - callback_query (:obj:`CallbackQuery `): - The received callback query. - """ - - def __init__(self, callback: callable, filters=None): - super().__init__(callback, filters) - - def check(self, callback_query): - return ( - self.filters(callback_query) - if self.filters - else True - ) - - class RawUpdateHandler(Handler): """The Raw Update handler class. Used to handle raw updates. It is intended to be used with :meth:`add_handler() ` + For a nicer way to register this handler, have a look at the + :meth:`on_raw_update() ` decorator. + Args: callback (``callable``): A function that will be called when a new update is received from the server. It takes diff --git a/pyrogram/client/methods/bots/__init__.py b/pyrogram/client/methods/bots/__init__.py index 62e0eb61..2d89c2fb 100644 --- a/pyrogram/client/methods/bots/__init__.py +++ b/pyrogram/client/methods/bots/__init__.py @@ -16,12 +16,16 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from .callback_query import CallbackQuery -from .inline import Inline +from .answer_callback_query import AnswerCallbackQuery +from .get_inline_bot_results import GetInlineBotResults +from .request_callback_answer import RequestCallbackAnswer +from .send_inline_bot_result import SendInlineBotResult class Bots( - CallbackQuery, - Inline + AnswerCallbackQuery, + GetInlineBotResults, + RequestCallbackAnswer, + SendInlineBotResult ): pass diff --git a/pyrogram/client/methods/bots/callback_query/answer_callback_query.py b/pyrogram/client/methods/bots/answer_callback_query.py similarity index 98% rename from pyrogram/client/methods/bots/callback_query/answer_callback_query.py rename to pyrogram/client/methods/bots/answer_callback_query.py index a4baa166..64951692 100644 --- a/pyrogram/client/methods/bots/callback_query/answer_callback_query.py +++ b/pyrogram/client/methods/bots/answer_callback_query.py @@ -17,7 +17,7 @@ # along with Pyrogram. If not, see . from pyrogram.api import functions -from ....ext import BaseClient +from pyrogram.client.ext import BaseClient class AnswerCallbackQuery(BaseClient): diff --git a/pyrogram/client/methods/bots/inline/get_inline_bot_results.py b/pyrogram/client/methods/bots/get_inline_bot_results.py similarity index 98% rename from pyrogram/client/methods/bots/inline/get_inline_bot_results.py rename to pyrogram/client/methods/bots/get_inline_bot_results.py index 52c3b005..a43eb6c1 100644 --- a/pyrogram/client/methods/bots/inline/get_inline_bot_results.py +++ b/pyrogram/client/methods/bots/get_inline_bot_results.py @@ -18,7 +18,7 @@ from pyrogram.api import functions, types from pyrogram.api.errors import UnknownError -from ....ext import BaseClient +from pyrogram.client.ext import BaseClient class GetInlineBotResults(BaseClient): diff --git a/pyrogram/client/methods/bots/inline/__init__.py b/pyrogram/client/methods/bots/inline/__init__.py deleted file mode 100644 index af88c57e..00000000 --- a/pyrogram/client/methods/bots/inline/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# Pyrogram - Telegram MTProto API Client Library for Python -# Copyright (C) 2017-2018 Dan Tès -# -# This file is part of Pyrogram. -# -# Pyrogram is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Pyrogram is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Pyrogram. If not, see . - -from .get_inline_bot_results import GetInlineBotResults -from .send_inline_bot_result import SendInlineBotResult - - -class Inline( - SendInlineBotResult, - GetInlineBotResults -): - pass diff --git a/pyrogram/client/methods/bots/request_callback_answer.py b/pyrogram/client/methods/bots/request_callback_answer.py new file mode 100644 index 00000000..52dab58c --- /dev/null +++ b/pyrogram/client/methods/bots/request_callback_answer.py @@ -0,0 +1,60 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from pyrogram.api import functions +from pyrogram.client.ext import BaseClient + + +class RequestCallbackAnswer(BaseClient): + def request_callback_answer(self, + chat_id: int or str, + message_id: int, + callback_data: str): + """Use this method to request a callback answer from bots. This is the equivalent of clicking an + inline button containing callback data. + + 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_id (``int``): + The message id the inline keyboard is attached on. + + callback_data (``str``): + Callback data associated with the inline button you want to get the answer from. + + Returns: + The answer containing info useful for clients to display a notification at the top of the chat screen + or as an alert. + + Raises: + :class:`Error ` + ``TimeoutError``: If the bot fails to answer within 10 seconds + """ + return self.send( + functions.messages.GetBotCallbackAnswer( + peer=self.resolve_peer(chat_id), + msg_id=message_id, + data=callback_data.encode() + ), + retries=0, + timeout=10 + ) diff --git a/pyrogram/client/methods/bots/inline/send_inline_bot_result.py b/pyrogram/client/methods/bots/send_inline_bot_result.py similarity index 98% rename from pyrogram/client/methods/bots/inline/send_inline_bot_result.py rename to pyrogram/client/methods/bots/send_inline_bot_result.py index 947433cd..c194298a 100644 --- a/pyrogram/client/methods/bots/inline/send_inline_bot_result.py +++ b/pyrogram/client/methods/bots/send_inline_bot_result.py @@ -17,7 +17,7 @@ # along with Pyrogram. If not, see . from pyrogram.api import functions -from ....ext import BaseClient +from pyrogram.client.ext import BaseClient class SendInlineBotResult(BaseClient): diff --git a/pyrogram/client/methods/chats/__init__.py b/pyrogram/client/methods/chats/__init__.py index f32e474e..9c887ef5 100644 --- a/pyrogram/client/methods/chats/__init__.py +++ b/pyrogram/client/methods/chats/__init__.py @@ -19,13 +19,21 @@ from .export_chat_invite_link import ExportChatInviteLink from .get_chat import GetChat from .join_chat import JoinChat +from .kick_chat_member import KickChatMember from .leave_chat import LeaveChat +from .promote_chat_member import PromoteChatMember +from .restrict_chat_member import RestrictChatMember +from .unban_chat_member import UnbanChatMember class Chats( GetChat, ExportChatInviteLink, LeaveChat, - JoinChat + JoinChat, + KickChatMember, + UnbanChatMember, + RestrictChatMember, + PromoteChatMember ): pass diff --git a/pyrogram/client/methods/chats/kick_chat_member.py b/pyrogram/client/methods/chats/kick_chat_member.py new file mode 100644 index 00000000..6275718c --- /dev/null +++ b/pyrogram/client/methods/chats/kick_chat_member.py @@ -0,0 +1,87 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from pyrogram.api import functions, types +from ...ext import BaseClient + + +class KickChatMember(BaseClient): + def kick_chat_member(self, + chat_id: int or str, + user_id: int or str, + until_date: int = 0): + """Use this method to kick a user from a group, a supergroup or a channel. + In the case of supergroups and channels, the user will not be able to return to the group on their own using + invite links, etc., unless unbanned first. You must be an administrator in the chat for this to work and must + have the appropriate admin rights. + + Note: + In regular groups (non-supergroups), this method will only work if the "All Members Are Admins" setting is + off in the target group. Otherwise members may only be removed by the group's creator or by the member + that added them. + + Args: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + For a private channel/supergroup you can use its *t.me/joinchat/* link. + + user_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target user. + For a contact that exists in your Telegram address book you can use his phone number (str). + + until_date (``int``, *optional*): + Date when the user will be unbanned, unix time. + If user is banned for more than 366 days or less than 30 seconds from the current time they are + considered to be banned forever. Defaults to 0 (ban forever). + + Returns: + True on success. + + Raises: + :class:`Error ` + """ + chat_peer = self.resolve_peer(chat_id) + user_peer = self.resolve_peer(user_id) + + if isinstance(chat_peer, types.InputPeerChannel): + self.send( + functions.channels.EditBanned( + channel=chat_peer, + user_id=user_peer, + banned_rights=types.ChannelBannedRights( + until_date=until_date, + view_messages=True, + send_messages=True, + send_media=True, + send_stickers=True, + send_gifs=True, + send_games=True, + send_inline=True, + embed_links=True + ) + ) + ) + else: + self.send( + functions.messages.DeleteChatUser( + chat_id=abs(chat_id), + user_id=user_peer + ) + ) + + return True diff --git a/pyrogram/client/methods/chats/promote_chat_member.py b/pyrogram/client/methods/chats/promote_chat_member.py new file mode 100644 index 00000000..eb70578a --- /dev/null +++ b/pyrogram/client/methods/chats/promote_chat_member.py @@ -0,0 +1,99 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from pyrogram.api import functions, types +from ...ext import BaseClient + + +class PromoteChatMember(BaseClient): + def promote_chat_member(self, + chat_id: int or str, + user_id: int or str, + can_change_info: bool = True, + can_post_messages: bool = True, + can_edit_messages: bool = True, + can_delete_messages: bool = True, + can_invite_users: bool = True, + can_restrict_members: bool = True, + can_pin_messages: bool = True, + can_promote_members: bool = False): + """Use this method to promote or demote a user in a supergroup or a channel. + You must be an administrator in the chat for this to work and must have the appropriate admin rights. + Pass False for all boolean parameters to demote a user. + + Args: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + For a private channel/supergroup you can use its *t.me/joinchat/* link. + + user_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target user. + For a contact that exists in your Telegram address book you can use his phone number (str). + + can_change_info (``bool``, *optional*): + Pass True, if the administrator can change chat title, photo and other settings. + + can_post_messages (``bool``, *optional*): + Pass True, if the administrator can create channel posts, channels only. + + can_edit_messages (``bool``, *optional*): + Pass True, if the administrator can edit messages of other users and can pin messages, channels only. + + can_delete_messages (``bool``, *optional*): + Pass True, if the administrator can delete messages of other users. + + can_invite_users (``bool``, *optional*): + Pass True, if the administrator can invite new users to the chat. + + can_restrict_members (``bool``, *optional*): + Pass True, if the administrator can restrict, ban or unban chat members. + + can_pin_messages (``bool``, *optional*): + Pass True, if the administrator can pin messages, supergroups only. + + can_promote_members (``bool``, *optional*): + Pass True, if the administrator can add new administrators with a subset of his own privileges or + demote administrators that he has promoted, directly or indirectly (promoted by administrators that + were appointed by him). + + Returns: + True on success. + + Raises: + :class:`Error ` + """ + self.send( + functions.channels.EditAdmin( + channel=self.resolve_peer(chat_id), + user_id=self.resolve_peer(user_id), + admin_rights=types.ChannelAdminRights( + change_info=can_change_info or None, + post_messages=can_post_messages or None, + edit_messages=can_edit_messages or None, + delete_messages=can_delete_messages or None, + ban_users=can_restrict_members or None, + invite_users=can_invite_users or None, + invite_link=can_invite_users or None, + pin_messages=can_pin_messages or None, + add_admins=can_promote_members or None, + manage_call=None + ) + ) + ) + + return True diff --git a/pyrogram/client/methods/chats/restrict_chat_member.py b/pyrogram/client/methods/chats/restrict_chat_member.py new file mode 100644 index 00000000..ae1e4d9c --- /dev/null +++ b/pyrogram/client/methods/chats/restrict_chat_member.py @@ -0,0 +1,113 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from pyrogram.api import functions, types +from ...ext import BaseClient + + +class RestrictChatMember(BaseClient): + def restrict_chat_member(self, + chat_id: int or str, + user_id: int or str, + until_date: int = 0, + can_send_messages: bool = False, + can_send_media_messages: bool = False, + can_send_other_messages: bool = False, + can_add_web_page_previews: bool = False): + """Use this method to restrict a user in a supergroup. The bot must be an administrator in the supergroup for + this to work and must have the appropriate admin rights. Pass True for all boolean parameters to lift + restrictions from a user. + + Args: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + For a private channel/supergroup you can use its *t.me/joinchat/* link. + + user_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target user. + For a contact that exists in your Telegram address book you can use his phone number (str). + + until_date (``int``, *optional*): + Date when the user will be unbanned, unix time. + If user is banned for more than 366 days or less than 30 seconds from the current time they are + considered to be banned forever. Defaults to 0 (ban forever). + + can_send_messages (``bool``, *optional*): + Pass True, if the user can send text messages, contacts, locations and venues. + + can_send_media_messages (``bool``, *optional*): + Pass True, if the user can send audios, documents, photos, videos, video notes and voice notes, + implies can_send_messages. + + can_send_other_messages (``bool``, *optional*): + Pass True, if the user can send animations, games, stickers and use inline bots, + implies can_send_media_messages. + + can_add_web_page_previews (``bool``, *optional*): + Pass True, if the user may add web page previews to their messages, implies can_send_media_messages + + Returns: + True on success. + + Raises: + :class:`Error ` + """ + send_messages = True + send_media = True + send_stickers = True + send_gifs = True + send_games = True + send_inline = True + embed_links = True + + if can_send_messages: + send_messages = None + + if can_send_media_messages: + send_messages = None + send_media = None + + if can_send_other_messages: + send_media = None + send_stickers = None + send_gifs = None + send_games = None + send_inline = None + + if can_add_web_page_previews: + send_media = None + embed_links = None + + self.send( + functions.channels.EditBanned( + channel=self.resolve_peer(chat_id), + user_id=self.resolve_peer(user_id), + banned_rights=types.ChannelBannedRights( + until_date=until_date, + send_messages=send_messages, + send_media=send_media, + send_stickers=send_stickers, + send_gifs=send_gifs, + send_games=send_games, + send_inline=send_inline, + embed_links=embed_links + ) + ) + ) + + return True diff --git a/pyrogram/client/methods/chats/unban_chat_member.py b/pyrogram/client/methods/chats/unban_chat_member.py new file mode 100644 index 00000000..b0916eb4 --- /dev/null +++ b/pyrogram/client/methods/chats/unban_chat_member.py @@ -0,0 +1,56 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from pyrogram.api import functions, types +from ...ext import BaseClient + + +class UnbanChatMember(BaseClient): + def unban_chat_member(self, + chat_id: int or str, + user_id: int or str): + """Use this method to unban a previously kicked user in a supergroup or channel. + The user will **not** return to the group or channel automatically, but will be able to join via link, etc. + You must be an administrator for this to work. + + Args: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + For a private channel/supergroup you can use its *t.me/joinchat/* link. + + user_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target user. + For a contact that exists in your Telegram address book you can use his phone number (str). + + Returns: + True on success. + + Raises: + :class:`Error ` + """ + self.send( + functions.channels.EditBanned( + channel=self.resolve_peer(chat_id), + user_id=self.resolve_peer(user_id), + banned_rights=types.ChannelBannedRights( + until_date=0 + ) + ) + ) + + return True diff --git a/pyrogram/client/methods/decorators/__init__.py b/pyrogram/client/methods/decorators/__init__.py index 89645906..f84a922c 100644 --- a/pyrogram/client/methods/decorators/__init__.py +++ b/pyrogram/client/methods/decorators/__init__.py @@ -17,9 +17,11 @@ # along with Pyrogram. If not, see . from .on_callback_query import OnCallbackQuery +from .on_disconnect import OnDisconnect from .on_message import OnMessage +from .on_deleted_messages import OnDeletedMessages from .on_raw_update import OnRawUpdate -class Decorators(OnMessage, OnCallbackQuery, OnRawUpdate): +class Decorators(OnMessage, OnDeletedMessages, OnCallbackQuery, OnRawUpdate, OnDisconnect): pass diff --git a/pyrogram/client/methods/decorators/on_callback_query.py b/pyrogram/client/methods/decorators/on_callback_query.py index 3bafc94d..5f22fc92 100644 --- a/pyrogram/client/methods/decorators/on_callback_query.py +++ b/pyrogram/client/methods/decorators/on_callback_query.py @@ -24,7 +24,7 @@ class OnCallbackQuery(BaseClient): def on_callback_query(self, filters=None, group: int = 0): """Use this decorator to automatically register a function for handling callback queries. This does the same thing as :meth:`add_handler` using the - CallbackQueryHandler. + :class:`CallbackQueryHandler`. Args: filters (:obj:`Filters `): diff --git a/pyrogram/client/methods/decorators/on_deleted_messages.py b/pyrogram/client/methods/decorators/on_deleted_messages.py new file mode 100644 index 00000000..3f603c41 --- /dev/null +++ b/pyrogram/client/methods/decorators/on_deleted_messages.py @@ -0,0 +1,42 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +import pyrogram +from ...ext import BaseClient + + +class OnDeletedMessages(BaseClient): + def on_deleted_messages(self, filters=None, group: int = 0): + """Use this decorator to automatically register a function for handling + deleted messages. This does the same thing as :meth:`add_handler` using the + :class:`DeletedMessagesHandler`. + + Args: + filters (:obj:`Filters `): + Pass one or more filters to allow only a subset of messages to be passed + in your function. + + group (``int``, *optional*): + The group identifier, defaults to 0. + """ + + def decorator(func): + self.add_handler(pyrogram.DeletedMessagesHandler(func, filters), group) + return func + + return decorator diff --git a/pyrogram/client/methods/bots/callback_query/__init__.py b/pyrogram/client/methods/decorators/on_disconnect.py similarity index 63% rename from pyrogram/client/methods/bots/callback_query/__init__.py rename to pyrogram/client/methods/decorators/on_disconnect.py index 76575724..4bc593e3 100644 --- a/pyrogram/client/methods/bots/callback_query/__init__.py +++ b/pyrogram/client/methods/decorators/on_disconnect.py @@ -16,10 +16,19 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from .answer_callback_query import AnswerCallbackQuery +import pyrogram +from ...ext import BaseClient -class CallbackQuery( - AnswerCallbackQuery -): - pass +class OnDisconnect(BaseClient): + def on_disconnect(self): + """Use this decorator to automatically register a function for handling + disconnections. This does the same thing as :meth:`add_handler` using the + :class:`DisconnectHandler`. + """ + + def decorator(func): + self.add_handler(pyrogram.DisconnectHandler(func)) + return func + + return decorator diff --git a/pyrogram/client/methods/decorators/on_message.py b/pyrogram/client/methods/decorators/on_message.py index 43c6f9f8..0011e083 100644 --- a/pyrogram/client/methods/decorators/on_message.py +++ b/pyrogram/client/methods/decorators/on_message.py @@ -24,7 +24,7 @@ class OnMessage(BaseClient): def on_message(self, filters=None, group: int = 0): """Use this decorator to automatically register a function for handling messages. This does the same thing as :meth:`add_handler` using the - MessageHandler. + :class:`MessageHandler`. Args: filters (:obj:`Filters `): diff --git a/pyrogram/client/methods/decorators/on_raw_update.py b/pyrogram/client/methods/decorators/on_raw_update.py index ca1c9d9b..902d9854 100644 --- a/pyrogram/client/methods/decorators/on_raw_update.py +++ b/pyrogram/client/methods/decorators/on_raw_update.py @@ -24,7 +24,7 @@ class OnRawUpdate(BaseClient): def on_raw_update(self, group: int = 0): """Use this decorator to automatically register a function for handling raw updates. This does the same thing as :meth:`add_handler` using the - RawUpdateHandler. + :class:`RawUpdateHandler`. Args: group (``int``, *optional*): diff --git a/pyrogram/client/methods/messages/__init__.py b/pyrogram/client/methods/messages/__init__.py index c2ff2400..e2c2462e 100644 --- a/pyrogram/client/methods/messages/__init__.py +++ b/pyrogram/client/methods/messages/__init__.py @@ -16,22 +16,50 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from .action import Action +from .delete_messages import DeleteMessages +from .edit_message_caption import EditMessageCaption +from .edit_message_reply_markup import EditMessageReplyMarkup +from .edit_message_text import EditMessageText from .forward_messages import ForwardMessages from .get_history import GetHistory from .get_messages import GetMessages -from .media import Media +from .send_audio import SendAudio +from .send_chat_action import SendChatAction +from .send_contact import SendContact +from .send_document import SendDocument +from .send_gif import SendGIF +from .send_location import SendLocation +from .send_media_group import SendMediaGroup from .send_message import SendMessage -from .update import Update +from .send_photo import SendPhoto +from .send_sticker import SendSticker +from .send_venue import SendVenue +from .send_video import SendVideo +from .send_video_note import SendVideoNote +from .send_voice import SendVoice class Messages( + DeleteMessages, + EditMessageCaption, + EditMessageReplyMarkup, + EditMessageText, + ForwardMessages, GetHistory, GetMessages, - Action, - Media, - Update, - ForwardMessages, - SendMessage + SendAudio, + SendChatAction, + SendContact, + SendDocument, + SendGIF, + SendLocation, + SendMediaGroup, + SendMessage, + SendPhoto, + SendSticker, + SendVenue, + SendVideo, + SendVideoNote, + SendVoice ): pass diff --git a/pyrogram/client/methods/messages/action/__init__.py b/pyrogram/client/methods/messages/action/__init__.py deleted file mode 100644 index 639405f2..00000000 --- a/pyrogram/client/methods/messages/action/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# Pyrogram - Telegram MTProto API Client Library for Python -# Copyright (C) 2017-2018 Dan Tès -# -# This file is part of Pyrogram. -# -# Pyrogram is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Pyrogram is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Pyrogram. If not, see . - -from .send_chat_action import SendChatAction - - -class Action( - SendChatAction -): - pass diff --git a/pyrogram/client/methods/messages/update/delete_messages.py b/pyrogram/client/methods/messages/delete_messages.py similarity index 98% rename from pyrogram/client/methods/messages/update/delete_messages.py rename to pyrogram/client/methods/messages/delete_messages.py index 3d29bf55..a4c97c76 100644 --- a/pyrogram/client/methods/messages/update/delete_messages.py +++ b/pyrogram/client/methods/messages/delete_messages.py @@ -17,7 +17,7 @@ # along with Pyrogram. If not, see . from pyrogram.api import functions, types -from ....ext import BaseClient +from pyrogram.client.ext import BaseClient class DeleteMessages(BaseClient): diff --git a/pyrogram/client/methods/messages/update/edit_message_caption.py b/pyrogram/client/methods/messages/edit_message_caption.py similarity index 98% rename from pyrogram/client/methods/messages/update/edit_message_caption.py rename to pyrogram/client/methods/messages/edit_message_caption.py index 90bf26f7..25276dc2 100644 --- a/pyrogram/client/methods/messages/update/edit_message_caption.py +++ b/pyrogram/client/methods/messages/edit_message_caption.py @@ -17,7 +17,7 @@ # along with Pyrogram. If not, see . from pyrogram.api import functions, types -from ....ext import BaseClient, utils +from pyrogram.client.ext import BaseClient, utils class EditMessageCaption(BaseClient): diff --git a/pyrogram/client/methods/messages/update/edit_message_reply_markup.py b/pyrogram/client/methods/messages/edit_message_reply_markup.py similarity index 98% rename from pyrogram/client/methods/messages/update/edit_message_reply_markup.py rename to pyrogram/client/methods/messages/edit_message_reply_markup.py index 295eb258..b840adab 100644 --- a/pyrogram/client/methods/messages/update/edit_message_reply_markup.py +++ b/pyrogram/client/methods/messages/edit_message_reply_markup.py @@ -17,7 +17,7 @@ # along with Pyrogram. If not, see . from pyrogram.api import functions, types -from ....ext import BaseClient, utils +from pyrogram.client.ext import BaseClient, utils class EditMessageReplyMarkup(BaseClient): diff --git a/pyrogram/client/methods/messages/update/edit_message_text.py b/pyrogram/client/methods/messages/edit_message_text.py similarity index 98% rename from pyrogram/client/methods/messages/update/edit_message_text.py rename to pyrogram/client/methods/messages/edit_message_text.py index be7b380c..741c0890 100644 --- a/pyrogram/client/methods/messages/update/edit_message_text.py +++ b/pyrogram/client/methods/messages/edit_message_text.py @@ -17,7 +17,7 @@ # along with Pyrogram. If not, see . from pyrogram.api import functions, types -from ....ext import BaseClient, utils +from pyrogram.client.ext import BaseClient, utils class EditMessageText(BaseClient): diff --git a/pyrogram/client/methods/messages/media/__init__.py b/pyrogram/client/methods/messages/media/__init__.py deleted file mode 100644 index b0e287c5..00000000 --- a/pyrogram/client/methods/messages/media/__init__.py +++ /dev/null @@ -1,47 +0,0 @@ -# Pyrogram - Telegram MTProto API Client Library for Python -# Copyright (C) 2017-2018 Dan Tès -# -# This file is part of Pyrogram. -# -# Pyrogram is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Pyrogram is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Pyrogram. If not, see . - -from .send_audio import SendAudio -from .send_contact import SendContact -from .send_document import SendDocument -from .send_gif import SendGIF -from .send_location import SendLocation -from .send_media_group import SendMediaGroup -from .send_photo import SendPhoto -from .send_sticker import SendSticker -from .send_venue import SendVenue -from .send_video import SendVideo -from .send_video_note import SendVideoNote -from .send_voice import SendVoice - - -class Media( - SendContact, - SendVenue, - SendLocation, - SendMediaGroup, - SendVideoNote, - SendVoice, - SendVideo, - SendGIF, - SendSticker, - SendDocument, - SendAudio, - SendPhoto -): - pass diff --git a/pyrogram/client/methods/messages/media/send_audio.py b/pyrogram/client/methods/messages/send_audio.py similarity index 99% rename from pyrogram/client/methods/messages/media/send_audio.py rename to pyrogram/client/methods/messages/send_audio.py index 41f4457f..7d590b79 100644 --- a/pyrogram/client/methods/messages/media/send_audio.py +++ b/pyrogram/client/methods/messages/send_audio.py @@ -23,7 +23,7 @@ import struct from pyrogram.api import functions, types from pyrogram.api.errors import FileIdInvalid, FilePartMissing -from ....ext import BaseClient, utils +from pyrogram.client.ext import BaseClient, utils class SendAudio(BaseClient): diff --git a/pyrogram/client/methods/messages/action/send_chat_action.py b/pyrogram/client/methods/messages/send_chat_action.py similarity index 98% rename from pyrogram/client/methods/messages/action/send_chat_action.py rename to pyrogram/client/methods/messages/send_chat_action.py index 4b34dd40..49625b48 100644 --- a/pyrogram/client/methods/messages/action/send_chat_action.py +++ b/pyrogram/client/methods/messages/send_chat_action.py @@ -17,7 +17,7 @@ # along with Pyrogram. If not, see . from pyrogram.api import functions -from ....ext import BaseClient, ChatAction +from pyrogram.client.ext import BaseClient, ChatAction class SendChatAction(BaseClient): diff --git a/pyrogram/client/methods/messages/media/send_contact.py b/pyrogram/client/methods/messages/send_contact.py similarity index 98% rename from pyrogram/client/methods/messages/media/send_contact.py rename to pyrogram/client/methods/messages/send_contact.py index eb1bb6c4..fc0abdd5 100644 --- a/pyrogram/client/methods/messages/media/send_contact.py +++ b/pyrogram/client/methods/messages/send_contact.py @@ -17,7 +17,7 @@ # along with Pyrogram. If not, see . from pyrogram.api import functions, types -from ....ext import BaseClient, utils +from pyrogram.client.ext import BaseClient, utils class SendContact(BaseClient): diff --git a/pyrogram/client/methods/messages/media/send_document.py b/pyrogram/client/methods/messages/send_document.py similarity index 99% rename from pyrogram/client/methods/messages/media/send_document.py rename to pyrogram/client/methods/messages/send_document.py index 1092147f..b2b5c532 100644 --- a/pyrogram/client/methods/messages/media/send_document.py +++ b/pyrogram/client/methods/messages/send_document.py @@ -23,7 +23,7 @@ import struct from pyrogram.api import functions, types from pyrogram.api.errors import FileIdInvalid, FilePartMissing -from ....ext import BaseClient, utils +from pyrogram.client.ext import BaseClient, utils class SendDocument(BaseClient): diff --git a/pyrogram/client/methods/messages/media/send_gif.py b/pyrogram/client/methods/messages/send_gif.py similarity index 99% rename from pyrogram/client/methods/messages/media/send_gif.py rename to pyrogram/client/methods/messages/send_gif.py index 0d4bb4b9..a4a14d84 100644 --- a/pyrogram/client/methods/messages/media/send_gif.py +++ b/pyrogram/client/methods/messages/send_gif.py @@ -23,7 +23,7 @@ import struct from pyrogram.api import functions, types from pyrogram.api.errors import FileIdInvalid, FilePartMissing -from ....ext import BaseClient, utils +from pyrogram.client.ext import BaseClient, utils class SendGIF(BaseClient): diff --git a/pyrogram/client/methods/messages/media/send_location.py b/pyrogram/client/methods/messages/send_location.py similarity index 98% rename from pyrogram/client/methods/messages/media/send_location.py rename to pyrogram/client/methods/messages/send_location.py index 08dac02b..a3db2561 100644 --- a/pyrogram/client/methods/messages/media/send_location.py +++ b/pyrogram/client/methods/messages/send_location.py @@ -17,7 +17,7 @@ # along with Pyrogram. If not, see . from pyrogram.api import functions, types -from ....ext import BaseClient, utils +from pyrogram.client.ext import BaseClient, utils class SendLocation(BaseClient): diff --git a/pyrogram/client/methods/messages/media/send_media_group.py b/pyrogram/client/methods/messages/send_media_group.py similarity index 99% rename from pyrogram/client/methods/messages/media/send_media_group.py rename to pyrogram/client/methods/messages/send_media_group.py index 6d004d9f..297d8f83 100644 --- a/pyrogram/client/methods/messages/media/send_media_group.py +++ b/pyrogram/client/methods/messages/send_media_group.py @@ -24,7 +24,7 @@ import struct from pyrogram.api import functions, types from pyrogram.api.errors import FileIdInvalid from pyrogram.client import types as pyrogram_types -from ....ext import BaseClient, utils +from pyrogram.client.ext import BaseClient, utils class SendMediaGroup(BaseClient): diff --git a/pyrogram/client/methods/messages/media/send_photo.py b/pyrogram/client/methods/messages/send_photo.py similarity index 99% rename from pyrogram/client/methods/messages/media/send_photo.py rename to pyrogram/client/methods/messages/send_photo.py index 52e98ff1..a6264bf3 100644 --- a/pyrogram/client/methods/messages/media/send_photo.py +++ b/pyrogram/client/methods/messages/send_photo.py @@ -22,7 +22,7 @@ import struct from pyrogram.api import functions, types from pyrogram.api.errors import FileIdInvalid, FilePartMissing -from ....ext import BaseClient, utils +from pyrogram.client.ext import BaseClient, utils class SendPhoto(BaseClient): diff --git a/pyrogram/client/methods/messages/media/send_sticker.py b/pyrogram/client/methods/messages/send_sticker.py similarity index 99% rename from pyrogram/client/methods/messages/media/send_sticker.py rename to pyrogram/client/methods/messages/send_sticker.py index 639e3600..fbf7a205 100644 --- a/pyrogram/client/methods/messages/media/send_sticker.py +++ b/pyrogram/client/methods/messages/send_sticker.py @@ -22,7 +22,7 @@ import struct from pyrogram.api import functions, types from pyrogram.api.errors import FileIdInvalid, FilePartMissing -from ....ext import BaseClient, utils +from pyrogram.client.ext import BaseClient, utils class SendSticker(BaseClient): diff --git a/pyrogram/client/methods/messages/media/send_venue.py b/pyrogram/client/methods/messages/send_venue.py similarity index 98% rename from pyrogram/client/methods/messages/media/send_venue.py rename to pyrogram/client/methods/messages/send_venue.py index d65ea43b..50946e86 100644 --- a/pyrogram/client/methods/messages/media/send_venue.py +++ b/pyrogram/client/methods/messages/send_venue.py @@ -17,7 +17,7 @@ # along with Pyrogram. If not, see . from pyrogram.api import functions, types -from ....ext import BaseClient, utils +from pyrogram.client.ext import BaseClient, utils class SendVenue(BaseClient): diff --git a/pyrogram/client/methods/messages/media/send_video.py b/pyrogram/client/methods/messages/send_video.py similarity index 99% rename from pyrogram/client/methods/messages/media/send_video.py rename to pyrogram/client/methods/messages/send_video.py index a4cc0309..b86b4702 100644 --- a/pyrogram/client/methods/messages/media/send_video.py +++ b/pyrogram/client/methods/messages/send_video.py @@ -23,7 +23,7 @@ import struct from pyrogram.api import functions, types from pyrogram.api.errors import FileIdInvalid, FilePartMissing -from ....ext import BaseClient, utils +from pyrogram.client.ext import BaseClient, utils class SendVideo(BaseClient): diff --git a/pyrogram/client/methods/messages/media/send_video_note.py b/pyrogram/client/methods/messages/send_video_note.py similarity index 99% rename from pyrogram/client/methods/messages/media/send_video_note.py rename to pyrogram/client/methods/messages/send_video_note.py index d7b417d5..a266e5dd 100644 --- a/pyrogram/client/methods/messages/media/send_video_note.py +++ b/pyrogram/client/methods/messages/send_video_note.py @@ -23,7 +23,7 @@ import struct from pyrogram.api import functions, types from pyrogram.api.errors import FileIdInvalid, FilePartMissing -from ....ext import BaseClient, utils +from pyrogram.client.ext import BaseClient, utils class SendVideoNote(BaseClient): diff --git a/pyrogram/client/methods/messages/media/send_voice.py b/pyrogram/client/methods/messages/send_voice.py similarity index 99% rename from pyrogram/client/methods/messages/media/send_voice.py rename to pyrogram/client/methods/messages/send_voice.py index ae21de6d..f4fe4229 100644 --- a/pyrogram/client/methods/messages/media/send_voice.py +++ b/pyrogram/client/methods/messages/send_voice.py @@ -23,7 +23,7 @@ import struct from pyrogram.api import functions, types from pyrogram.api.errors import FileIdInvalid, FilePartMissing -from ....ext import BaseClient, utils +from pyrogram.client.ext import BaseClient, utils class SendVoice(BaseClient): diff --git a/pyrogram/client/methods/messages/update/__init__.py b/pyrogram/client/methods/messages/update/__init__.py deleted file mode 100644 index cc913e23..00000000 --- a/pyrogram/client/methods/messages/update/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -# Pyrogram - Telegram MTProto API Client Library for Python -# Copyright (C) 2017-2018 Dan Tès -# -# This file is part of Pyrogram. -# -# Pyrogram is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Pyrogram is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Pyrogram. If not, see . - -from .delete_messages import DeleteMessages -from .edit_message_caption import EditMessageCaption -from .edit_message_reply_markup import EditMessageReplyMarkup -from .edit_message_text import EditMessageText - - -class Update( - DeleteMessages, - EditMessageReplyMarkup, - EditMessageCaption, - EditMessageText -): - pass diff --git a/pyrogram/client/types/__init__.py b/pyrogram/client/types/__init__.py index 5431b89e..acd001dd 100644 --- a/pyrogram/client/types/__init__.py +++ b/pyrogram/client/types/__init__.py @@ -30,6 +30,7 @@ from .input_phone_contact import InputPhoneContact from .location import Location from .message import Message from .message_entity import MessageEntity +from .messages import Messages from .photo_size import PhotoSize from .reply_markup import ( ForceReply, InlineKeyboardButton, InlineKeyboardMarkup, @@ -43,4 +44,3 @@ from .venue import Venue from .video import Video from .video_note import VideoNote from .voice import Voice -from .messages import Messages diff --git a/pyrogram/client/types/message.py b/pyrogram/client/types/message.py index ca095f1d..b969865c 100644 --- a/pyrogram/client/types/message.py +++ b/pyrogram/client/types/message.py @@ -17,6 +17,7 @@ # along with Pyrogram. If not, see . from pyrogram.api.core import Object +from .reply_markup import InlineKeyboardMarkup, ReplyKeyboardMarkup class Message(Object): @@ -260,7 +261,7 @@ class Message(Object): reply_markup=None, ): self.message_id = message_id # int - self.client = client + self._client = client self.date = date # int self.chat = chat # Chat self.from_user = from_user # flags.0?User @@ -309,3 +310,267 @@ class Message(Object): self.matches = matches self.command = command self.reply_markup = reply_markup + + def reply(self, + text: str, + quote: bool = None, + parse_mode: str = "", + disable_web_page_preview: bool = None, + disable_notification: bool = None, + reply_to_message_id: int = None, + reply_markup=None): + """Use this method as a shortcut for: + + .. code-block:: python + + client.send_message( + chat_id=message.chat.id, + text="hello", + reply_to_message_id=message.message_id + ) + + Example: + .. code-block:: python + + message.reply("hello", quote=True) + + Args: + text (``str``): + Text of the message to be sent. + + quote (``bool``, *optional*): + If ``True``, the message will be sent as a reply to this message. + If *reply_to_message_id* is passed, this parameter will be ignored. + Defaults to ``True`` in group chats and ``False`` in private chats. + + parse_mode (``str``, *optional*): + 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 (``bool``, *optional*): + Disables link previews for links in this message. + + disable_notification (``bool``, *optional*): + Sends the message silently. + Users will receive a notification with no sound. + + reply_to_message_id (``int``, *optional*): + If the message is a reply, ID of the original message. + + reply_markup (:obj:`InlineKeyboardMarkup` | :obj:`ReplyKeyboardMarkup` | :obj:`ReplyKeyboardRemove` | :obj:`ForceReply`, *optional*): + Additional interface options. An object for an inline keyboard, custom reply keyboard, + instructions to remove reply keyboard or to force a reply from the user. + + Returns: + On success, the sent Message is returned. + + Raises: + :class:`Error ` + """ + if quote is None: + quote = self.chat.type != "private" + + if reply_to_message_id is None and quote: + reply_to_message_id = self.message_id + + return self._client.send_message( + chat_id=self.chat.id, + text=text, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup + ) + + def forward(self, + chat_id: int or str, + disable_notification: bool = None): + """Use this method as a shortcut for: + + .. code-block:: python + + client.forward_messages( + chat_id=chat_id, + from_chat_id=message.chat.id, + message_ids=message.message_id, + ) + + Example: + .. code-block:: python + + message.forward(chat_id) + + 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. + + disable_notification (``bool``, *optional*): + Sends the message silently. + Users will receive a notification with no sound. + + Returns: + On success, the forwarded Message is returned. + + Raises: + :class:`Error ` + """ + return self._client.forward_messages( + chat_id=chat_id, + from_chat_id=self.chat.id, + message_ids=self.message_id, + disable_notification=disable_notification + ) + + def delete(self, revoke: bool = True): + """Use this method as a shortcut for: + + .. code-block:: python + + client.delete_messages( + chat_id=chat_id, + message_ids=message.message_id + ) + + Example: + .. code-block:: python + + message.delete() + + Args: + 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). + Defaults to True. + + Returns: + True on success. + + Raises: + :class:`Error ` + """ + self._client.delete_messages( + chat_id=self.chat.id, + message_ids=self.message_id, + revoke=revoke + ) + + return True + + def click(self, x: int or str, y: int = None, quote: bool = None): + """Use this method to click a button attached to the message. + It's a shortcut for: + + - Clicking inline buttons: + + .. code-block:: python + + client.request_callback_answer( + chat_id=message.chat.id, + message_id=message.message_id, + callback_data=message.reply_markup[i][j].callback_data + ) + + - Clicking normal buttons: + + .. code-block:: python + + client.send_message( + chat_id=message.chat.id, + text=message.reply_markup[i][j].text + ) + + This method can be used in three different ways: + + 1. Pass one integer argument only (e.g.: ``.click(2)``, to click a button at index 2). + Buttons are counted left to right, starting from the top. + + 2. Pass two integer arguments (e.g.: ``.click(1, 0)``, to click a button at position (1, 0)). + The origin (0, 0) is top-left. + + 3. Pass one string argument only (e.g.: ``.click("Settings")``, to click a button by using its label). + Only the first matching button will be pressed. + + Args: + x (``int`` | ``str``): + Used as integer index, integer abscissa (in pair with y) or as string label. + + y (``int``, *optional*): + Used as ordinate only (in pair with x). + + quote (``bool``, *optional*): + Useful for normal buttons only, where pressing it will result in a new message sent. + If ``True``, the message will be sent as a reply to this message. + Defaults to ``True`` in group chats and ``False`` in private chats. + + Returns: + - The result of *request_callback_answer()* in case of inline callback button clicks. + - The result of *reply()* in case of normal button clicks. + - A string in case the inline button is an URL, switch_inline_query or switch_inline_query_current_chat + button. + + Raises: + :class:`Error ` + ``ValueError``: If the provided index or position is out of range or the button label was not found + ``TimeoutError``: If, after clicking an inline button, the bot fails to answer within 10 seconds + """ + if isinstance(self.reply_markup, ReplyKeyboardMarkup): + if quote is None: + quote = self.chat.type != "private" + + return self.reply(x, quote=quote) + elif isinstance(self.reply_markup, InlineKeyboardMarkup): + if isinstance(x, int) and y is None: + try: + button = [ + button + for row in self.reply_markup.inline_keyboard + for button in row + ][x] + except IndexError: + raise ValueError("The button at index {} doesn't exist".format(x)) from None + elif isinstance(x, int) and isinstance(y, int): + try: + button = self.reply_markup.inline_keyboard[y][x] + except IndexError: + raise ValueError("The button at position ({}, {}) doesn't exist".format(x, y)) from None + elif isinstance(x, str): + x = x.encode("utf-16", "surrogatepass").decode("utf-16") + + try: + button = [ + button + for row in self.reply_markup.inline_keyboard + for button in row + if x == button.text + ][0] + except IndexError: + raise ValueError( + "The button with label '{}' doesn't exists".format( + x.encode("unicode_escape").decode() + ) + ) from None + else: + raise ValueError("Invalid arguments") + + if button.callback_data: + return self._client.request_callback_answer( + chat_id=self.chat.id, + message_id=self.message_id, + callback_data=button.callback_data + ) + elif button.url: + return button.url + elif button.switch_inline_query: + return button.switch_inline_query + elif button.switch_inline_query_current_chat: + return button.switch_inline_query_current_chat + else: + raise ValueError("This button is not supported yet") + else: + raise ValueError("The message doesn't contain any keyboard") diff --git a/pyrogram/client/types/update.py b/pyrogram/client/types/update.py index edd5d85c..c8959708 100644 --- a/pyrogram/client/types/update.py +++ b/pyrogram/client/types/update.py @@ -30,12 +30,18 @@ class Update(Object): edited_message (:obj:`Message `, *optional*): New version of a message that is known to the bot and was edited. + deleted_messages (:obj:`Messages `, *optional*): + Deleted messages. + channel_post (:obj:`Message `, *optional*): New incoming channel post of any kind — text, photo, sticker, etc. edited_channel_post (:obj:`Message `, *optional*): New version of a channel post that is known to the bot and was edited. + deleted_channel_posts (:obj:`Messages `, *optional*): + Deleted channel posts. + inline_query (:obj:`InlineQuery `, *optional*): New incoming inline query. @@ -60,8 +66,10 @@ class Update(Object): self, message=None, edited_message=None, + deleted_messages=None, channel_post=None, edited_channel_post=None, + deleted_channel_posts=None, inline_query=None, chosen_inline_result=None, callback_query=None, @@ -70,8 +78,10 @@ class Update(Object): ): self.message = message self.edited_message = edited_message + self.deleted_messages = deleted_messages self.channel_post = channel_post self.edited_channel_post = edited_channel_post + self.deleted_channel_posts = deleted_channel_posts self.inline_query = inline_query self.chosen_inline_result = chosen_inline_result self.callback_query = callback_query diff --git a/pyrogram/client/types/video_note.py b/pyrogram/client/types/video_note.py index 2421436b..7f0b6736 100644 --- a/pyrogram/client/types/video_note.py +++ b/pyrogram/client/types/video_note.py @@ -35,9 +35,6 @@ class VideoNote(Object): thumb (:obj:`PhotoSize `, *optional*): Video thumbnail. - file_size (``int``, *optional*): - File size. - file_name (``str``, *optional*): Video note file name. diff --git a/pyrogram/connection/connection.py b/pyrogram/connection/connection.py index 118f0d83..a53295ce 100644 --- a/pyrogram/connection/connection.py +++ b/pyrogram/connection/connection.py @@ -26,10 +26,14 @@ log = logging.getLogger(__name__) class Connection: + MAX_RETRIES = 3 + MODES = { 0: TCPFull, 1: TCPAbridged, - 2: TCPIntermediate + 2: TCPIntermediate, + 3: TCPAbridgedO, + 4: TCPIntermediateO } def __init__(self, address: tuple, proxy: dict, mode: int = 1): @@ -40,7 +44,7 @@ class Connection: self.connection = None def connect(self): - while True: + for i in range(Connection.MAX_RETRIES): self.connection = self.mode(self.proxy) try: @@ -51,6 +55,8 @@ class Connection: time.sleep(1) else: break + else: + raise TimeoutError def close(self): self.connection.close() diff --git a/pyrogram/connection/transport/tcp/__init__.py b/pyrogram/connection/transport/tcp/__init__.py index d1daff63..ce662e61 100644 --- a/pyrogram/connection/transport/tcp/__init__.py +++ b/pyrogram/connection/transport/tcp/__init__.py @@ -17,5 +17,7 @@ # along with Pyrogram. If not, see . from .tcp_abridged import TCPAbridged +from .tcp_abridged_o import TCPAbridgedO from .tcp_full import TCPFull from .tcp_intermediate import TCPIntermediate +from .tcp_intermediate_o import TCPIntermediateO diff --git a/pyrogram/connection/transport/tcp/tcp.py b/pyrogram/connection/transport/tcp/tcp.py index 38005e57..5df8aacb 100644 --- a/pyrogram/connection/transport/tcp/tcp.py +++ b/pyrogram/connection/transport/tcp/tcp.py @@ -41,15 +41,15 @@ class TCP(socks.socksocket): if proxy and self.proxy_enabled: self.set_proxy( proxy_type=socks.SOCKS5, - addr=proxy["hostname"], - port=proxy["port"], - username=proxy["username"], - password=proxy["password"] + addr=proxy.get("hostname", None), + port=proxy.get("port", None), + username=proxy.get("username", None), + password=proxy.get("password", None) ) log.info("Using proxy {}:{}".format( - proxy["hostname"], - proxy["port"] + proxy.get("hostname", None), + proxy.get("port", None) )) def close(self): diff --git a/pyrogram/connection/transport/tcp/tcp_abridged.py b/pyrogram/connection/transport/tcp/tcp_abridged.py index ad682fed..472f4799 100644 --- a/pyrogram/connection/transport/tcp/tcp_abridged.py +++ b/pyrogram/connection/transport/tcp/tcp_abridged.py @@ -26,28 +26,23 @@ log = logging.getLogger(__name__) class TCPAbridged(TCP): def __init__(self, proxy: dict): super().__init__(proxy) - self.is_first_packet = None def connect(self, address: tuple): super().connect(address) - self.is_first_packet = True + super().sendall(b"\xef") + log.info("Connected{}!".format(" with proxy" if self.proxy_enabled else "")) def sendall(self, data: bytes, *args): length = len(data) // 4 - data = ( - bytes([length]) + data - if length <= 126 - else b"\x7f" + int.to_bytes(length, 3, "little") + data + super().sendall( + (bytes([length]) + if length <= 126 + else b"\x7f" + length.to_bytes(3, "little")) + + data ) - if self.is_first_packet: - data = b"\xef" + data - self.is_first_packet = False - - super().sendall(data) - def recvall(self, length: int = 0) -> bytes or None: length = super().recvall(1) diff --git a/pyrogram/connection/transport/tcp/tcp_abridged_o.py b/pyrogram/connection/transport/tcp/tcp_abridged_o.py new file mode 100644 index 00000000..bba88e34 --- /dev/null +++ b/pyrogram/connection/transport/tcp/tcp_abridged_o.py @@ -0,0 +1,93 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +import logging +import os + +from .tcp import TCP +from ....crypto.aes import AES + +log = logging.getLogger(__name__) + + +class TCPAbridgedO(TCP): + RESERVED = (b"HEAD", b"POST", b"GET ", b"OPTI", b"\xee" * 4) + + def __init__(self, proxy: dict): + super().__init__(proxy) + self.encrypt = None + self.decrypt = None + + def connect(self, address: tuple): + super().connect(address) + + while True: + nonce = bytearray(os.urandom(64)) + + if (nonce[0] != b"\xef" + and nonce[:4] not in self.RESERVED + and nonce[4:4] != b"\x00" * 4): + nonce[56] = nonce[57] = nonce[58] = nonce[59] = 0xef + break + + temp = bytearray(nonce[55:7:-1]) + + self.encrypt = (nonce[8:40], nonce[40:56], bytearray(1)) + self.decrypt = (temp[0:32], temp[32:48], bytearray(1)) + + nonce[56:64] = AES.ctr256_encrypt(nonce, *self.encrypt)[56:64] + + super().sendall(nonce) + + log.info("Connected{}!".format(" with proxy" if self.proxy_enabled else "")) + + def sendall(self, data: bytes, *args): + length = len(data) // 4 + + super().sendall( + AES.ctr256_encrypt( + (bytes([length]) + if length <= 126 + else b"\x7f" + length.to_bytes(3, "little")) + + data, + *self.encrypt + ) + ) + + def recvall(self, length: int = 0) -> bytes or None: + length = super().recvall(1) + + if length is None: + return None + + length = AES.ctr256_decrypt(length, *self.decrypt) + + if length == b"\x7f": + length = super().recvall(3) + + if length is None: + return None + + length = AES.ctr256_decrypt(length, *self.decrypt) + + data = super().recvall(int.from_bytes(length, "little") * 4) + + if data is None: + return None + + return AES.ctr256_decrypt(data, *self.decrypt) diff --git a/pyrogram/connection/transport/tcp/tcp_intermediate.py b/pyrogram/connection/transport/tcp/tcp_intermediate.py index 55a7d071..4b2e2596 100644 --- a/pyrogram/connection/transport/tcp/tcp_intermediate.py +++ b/pyrogram/connection/transport/tcp/tcp_intermediate.py @@ -19,7 +19,7 @@ import logging from struct import pack, unpack -from .tcp_abridged import TCP +from .tcp import TCP log = logging.getLogger(__name__) @@ -27,22 +27,15 @@ log = logging.getLogger(__name__) class TCPIntermediate(TCP): def __init__(self, proxy: dict): super().__init__(proxy) - self.is_first_packet = None def connect(self, address: tuple): super().connect(address) - self.is_first_packet = True + super().sendall(b"\xee" * 4) + log.info("Connected{}!".format(" with proxy" if self.proxy_enabled else "")) def sendall(self, data: bytes, *args): - length = len(data) - data = pack(" bytes or None: length = super().recvall(4) @@ -50,4 +43,4 @@ class TCPIntermediate(TCP): if length is None: return None - return super().recvall(unpack(" +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +import logging +import os +from struct import pack, unpack + +from .tcp import TCP +from ....crypto.aes import AES + +log = logging.getLogger(__name__) + + +class TCPIntermediateO(TCP): + RESERVED = (b"HEAD", b"POST", b"GET ", b"OPTI", b"\xee" * 4) + + def __init__(self, proxy: dict): + super().__init__(proxy) + self.encrypt = None + self.decrypt = None + + def connect(self, address: tuple): + super().connect(address) + + while True: + nonce = bytearray(os.urandom(64)) + + if (nonce[0] != b"\xef" + and nonce[:4] not in self.RESERVED + and nonce[4:4] != b"\x00" * 4): + nonce[56] = nonce[57] = nonce[58] = nonce[59] = 0xee + break + + temp = bytearray(nonce[55:7:-1]) + + self.encrypt = (nonce[8:40], nonce[40:56], bytearray(1)) + self.decrypt = (temp[0:32], temp[32:48], bytearray(1)) + + nonce[56:64] = AES.ctr256_encrypt(nonce, *self.encrypt)[56:64] + + super().sendall(nonce) + + log.info("Connected{}!".format(" with proxy" if self.proxy_enabled else "")) + + def sendall(self, data: bytes, *args): + super().sendall( + AES.ctr256_encrypt( + pack(" bytes or None: + length = super().recvall(4) + + if length is None: + return None + + length = AES.ctr256_decrypt(length, *self.decrypt) + + data = super().recvall(unpack(" bytes: + return tgcrypto.ige_encrypt(data, key, iv) + + @classmethod + def ige256_decrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes: + return tgcrypto.ige_decrypt(data, key, iv) + + @staticmethod + def ctr256_encrypt(data: bytes, key: bytes, iv: bytearray, state: bytearray = None) -> bytes: + return tgcrypto.ctr_encrypt(data, key, iv, state or bytearray(1)) + + @staticmethod + def ctr256_decrypt(data: bytes, key: bytes, iv: bytearray, state: bytearray = None) -> bytes: + return tgcrypto.ctr_decrypt(data, key, iv, state or bytearray(1)) + + @staticmethod + def xor(a: bytes, b: bytes) -> bytes: + return int.to_bytes( + int.from_bytes(a, "big") ^ int.from_bytes(b, "big"), + len(a), + "big", + ) except ImportError: + import pyaes + log.warning( "TgCrypto is missing! " "Pyrogram will work the same, but at a much slower speed. " "More info: https://docs.pyrogram.ml/resources/TgCrypto" ) - is_fast = False - import pyaes -else: - log.info("Using TgCrypto") - is_fast = True -# 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: + class AES: + @classmethod + def ige256_encrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes: return cls.ige(data, key, iv, True) - @classmethod - def ige_decrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes: - if is_fast: - return tgcrypto.ige_decrypt(data, key, iv) - else: + @classmethod + def ige256_decrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes: return cls.ige(data, key, iv, False) - @staticmethod - def ctr_decrypt(data: bytes, key: bytes, iv: bytes, offset: int) -> bytes: - replace = int.to_bytes(offset // 16, 4, "big") - iv = iv[:-4] + replace + @classmethod + def ctr256_encrypt(cls, data: bytes, key: bytes, iv: bytearray, state: bytearray = None) -> bytes: + return cls.ctr(data, key, iv, state or bytearray(1)) - if is_fast: - return tgcrypto.ctr_decrypt(data, key, iv) - else: - ctr = pyaes.AESModeOfOperationCTR(key) - ctr._counter._counter = list(iv) - return ctr.decrypt(data) + @classmethod + def ctr256_decrypt(cls, data: bytes, key: bytes, iv: bytearray, state: bytearray = None) -> bytes: + return cls.ctr(data, key, iv, state or bytearray(1)) - @staticmethod - def xor(a: bytes, b: bytes) -> bytes: - return int.to_bytes( - int.from_bytes(a, "big") ^ int.from_bytes(b, "big"), - len(a), - "big", - ) + @staticmethod + def xor(a: bytes, b: bytes) -> bytes: + return int.to_bytes( + int.from_bytes(a, "big") ^ int.from_bytes(b, "big"), + len(a), + "big", + ) - @classmethod - def ige(cls, data: bytes, key: bytes, iv: bytes, encrypt: bool) -> bytes: - cipher = pyaes.AES(key) + @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:] + iv_1 = iv[:16] + iv_2 = iv[16:] - data = [data[i: i + 16] for i in range(0, len(data), 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 + 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) + return b"".join(data) + + @classmethod + def ctr(cls, data: bytes, key: bytes, iv: bytearray, state: bytearray) -> bytes: + cipher = pyaes.AES(key) + + out = bytearray(data) + chunk = cipher.encrypt(iv) + + for i in range(0, len(data), 16): + for j in range(0, min(len(data) - i, 16)): + out[i + j] ^= chunk[state[0]] + + state[0] += 1 + + if state[0] >= 16: + state[0] = 0 + + if state[0] == 0: + for k in range(15, -1, -1): + try: + iv[k] += 1 + break + except ValueError: + iv[k] = 0 + + chunk = cipher.encrypt(iv) + + return out diff --git a/pyrogram/session/auth.py b/pyrogram/session/auth.py index 449524b3..80956187 100644 --- a/pyrogram/session/auth.py +++ b/pyrogram/session/auth.py @@ -49,8 +49,9 @@ class Auth: def __init__(self, dc_id: int, test_mode: bool, proxy: dict): self.dc_id = dc_id self.test_mode = test_mode + self.proxy = proxy - self.connection = Connection(DataCenter(dc_id, test_mode), proxy) + self.connection = None @staticmethod def pack(data: Object) -> bytes: @@ -83,6 +84,8 @@ class Auth: # The server may close the connection at any time, causing the auth key creation to fail. # If that happens, just try again up to MAX_RETRIES times. while True: + self.connection = Connection(DataCenter(self.dc_id, self.test_mode), self.proxy) + try: log.info("Start creating a new auth key on DC{}".format(self.dc_id)) @@ -163,7 +166,7 @@ class Auth: server_nonce = int.from_bytes(server_nonce, "little", signed=True) - answer_with_hash = AES.ige_decrypt(encrypted_answer, tmp_aes_key, tmp_aes_iv) + answer_with_hash = AES.ige256_decrypt(encrypted_answer, tmp_aes_key, tmp_aes_iv) answer = answer_with_hash[20:] server_dh_inner_data = Object.read(BytesIO(answer)) @@ -192,7 +195,7 @@ class Auth: sha = sha1(data).digest() padding = urandom(- (len(data) + len(sha)) % 16) data_with_hash = sha + data + padding - encrypted_data = AES.ige_encrypt(data_with_hash, tmp_aes_key, tmp_aes_iv) + encrypted_data = AES.ige256_encrypt(data_with_hash, tmp_aes_key, tmp_aes_iv) log.debug("Send set_client_DH_params") set_client_dh_params_answer = self.send( diff --git a/pyrogram/session/internals/msg_factory.py b/pyrogram/session/internals/msg_factory.py index f599cd6f..76a35458 100644 --- a/pyrogram/session/internals/msg_factory.py +++ b/pyrogram/session/internals/msg_factory.py @@ -17,8 +17,8 @@ # along with Pyrogram. If not, see . from pyrogram.api.core import Message, MsgContainer, Object -from pyrogram.api.functions import Ping, HttpWait -from pyrogram.api.types import MsgsAck +from pyrogram.api.functions import Ping +from pyrogram.api.types import MsgsAck, HttpWait from .msg_id import MsgId from .seq_no import SeqNo diff --git a/pyrogram/session/session.py b/pyrogram/session/session.py index f8cdcc19..ef7b565c 100644 --- a/pyrogram/session/session.py +++ b/pyrogram/session/session.py @@ -17,7 +17,6 @@ # along with Pyrogram. If not, see . import logging -import platform import threading import time from datetime import timedelta, datetime @@ -47,19 +46,6 @@ class Result: class Session: - VERSION = __version__ - APP_VERSION = "Pyrogram \U0001f525 {}".format(VERSION) - - DEVICE_MODEL = "{} {}".format( - platform.python_implementation(), - platform.python_version() - ) - - SYSTEM_VERSION = "{} {}".format( - platform.system(), - platform.release() - ) - INITIAL_SALT = 0x616e67656c696361 NET_WORKERS = 1 WAIT_TIMEOUT = 15 @@ -84,24 +70,24 @@ class Session: } def __init__(self, + client: pyrogram, dc_id: int, - test_mode: bool, - proxy: dict, auth_key: bytes, - api_id: int, - is_cdn: bool = False, - client: pyrogram = None): + is_media: bool = False, + is_cdn: bool = False): if not Session.notice_displayed: print("Pyrogram v{}, {}".format(__version__, __copyright__)) print("Licensed under the terms of the " + __license__, end="\n\n") Session.notice_displayed = True - self.connection = Connection(DataCenter(dc_id, test_mode), proxy) - self.api_id = api_id - self.is_cdn = is_cdn self.client = client - + self.dc_id = dc_id self.auth_key = auth_key + self.is_media = is_media + self.is_cdn = is_cdn + + self.connection = None + self.auth_key_id = sha1(auth_key).digest()[-8:] self.session_id = Long(MsgId()) @@ -126,6 +112,8 @@ class Session: def start(self): while True: + self.connection = Connection(DataCenter(self.dc_id, self.client.test_mode), self.client.proxy) + try: self.connection.connect() @@ -153,12 +141,14 @@ class Session: functions.InvokeWithLayer( layer, functions.InitConnection( - self.api_id, - self.DEVICE_MODEL, - self.SYSTEM_VERSION, - self.APP_VERSION, - "en", "", "en", - functions.help.GetConfig(), + api_id=self.client.api_id, + app_version=self.client.app_version, + device_model=self.client.device_model, + system_version=self.client.system_version, + system_lang_code=self.client.lang_code, + lang_code=self.client.lang_code, + lang_pack="", + query=functions.help.GetConfig(), ) ) ) @@ -207,6 +197,12 @@ class Session: for i in self.results.values(): i.event.set() + if not self.is_media and callable(self.client.disconnect_handler): + try: + self.client.disconnect_handler(self.client) + except Exception as e: + log.error(e, exc_info=True) + log.debug("Session stopped") def restart(self): @@ -222,14 +218,14 @@ class Session: msg_key = msg_key_large[8:24] aes_key, aes_iv = KDF(self.auth_key, msg_key, True) - return self.auth_key_id + msg_key + AES.ige_encrypt(data + padding, aes_key, aes_iv) + return self.auth_key_id + msg_key + AES.ige256_encrypt(data + padding, aes_key, aes_iv) def unpack(self, b: BytesIO) -> Message: assert b.read(8) == self.auth_key_id, b.getvalue() msg_key = b.read(16) aes_key, aes_iv = KDF(self.auth_key, msg_key, False) - data = BytesIO(AES.ige_decrypt(b.read(), aes_key, aes_iv)) + data = BytesIO(AES.ige256_decrypt(b.read(), aes_key, aes_iv)) data.read(8) # https://core.telegram.org/mtproto/security_guidelines#checking-session-id @@ -379,7 +375,7 @@ class Session: log.debug("RecvThread stopped") - def _send(self, data: Object, wait_response: bool = True): + def _send(self, data: Object, wait_response: bool = True, timeout: float = WAIT_TIMEOUT): message = self.msg_factory(data) msg_id = message.msg_id @@ -395,7 +391,7 @@ class Session: raise e if wait_response: - self.results[msg_id].event.wait(self.WAIT_TIMEOUT) + self.results[msg_id].event.wait(timeout) result = self.results.pop(msg_id).value if result is None: @@ -410,11 +406,11 @@ class Session: else: return result - def send(self, data: Object, retries: int = MAX_RETRIES): + def send(self, data: Object, retries: int = MAX_RETRIES, timeout: float = WAIT_TIMEOUT): self.is_connected.wait(self.WAIT_TIMEOUT) try: - return self._send(data) + return self._send(data, timeout=timeout) except (OSError, TimeoutError, InternalServerError) as e: if retries == 0: raise e from None @@ -425,4 +421,4 @@ class Session: datetime.now(), type(data))) time.sleep(0.5) - return self.send(data, retries - 1) + return self.send(data, retries - 1, timeout) diff --git a/requirements_extras.txt b/requirements_extras.txt deleted file mode 100644 index 1d101a7e..00000000 --- a/requirements_extras.txt +++ /dev/null @@ -1 +0,0 @@ -tgcrypto>=1.0.4 \ No newline at end of file diff --git a/setup.py b/setup.py index ef703729..00f9be63 100644 --- a/setup.py +++ b/setup.py @@ -85,5 +85,5 @@ setup( packages=find_packages(exclude=["compiler*"]), zip_safe=False, install_requires=read("requirements.txt"), - extras_require={"tgcrypto": read("requirements_extras.txt")} + extras_require={"tgcrypto": ["tgcrypto>=1.0.4"]} )