From 50d62259a8a32e0f44169a0efac2410c1ce344b0 Mon Sep 17 00:00:00 2001 From: Elliot Manson Date: Sun, 14 Jul 2019 13:28:49 +0400 Subject: [PATCH 01/21] start by steps --- pyrogram/client/client.py | 372 +++++++++++++++++++++++++++++++++++++- 1 file changed, 366 insertions(+), 6 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 9c19e49a..5219240b 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -268,6 +268,368 @@ class Client(Methods, BaseClient): self._proxy["enabled"] = bool(value.get("enabled", True)) self._proxy.update(value) + def load_setting(self): + """Loading settings""" + + if self.is_started: + raise ConnectionError("Client has already been started") + + self.load_config() + self.load_session() + self.load_plugins() + + self.session = Session(self, self.storage.dc_id, self.storage.auth_key) + + self.session.start() + self.is_started = True + + + def end_session(self): + self.is_started = False + self.session.stop() + + def update(self): + for i in range(self.UPDATES_WORKERS): + self.updates_workers_list.append( + Thread( + target=self.updates_worker, + name="UpdatesWorker#{}".format(i + 1) + ) + ) + + self.updates_workers_list[-1].start() + + def download(self): + for i in range(self.DOWNLOAD_WORKERS): + self.download_workers_list.append( + Thread( + target=self.download_worker, + name="DownloadWorker#{}".format(i + 1) + ) + ) + + self.download_workers_list[-1].start() + + def get_hint(self): + r = self.send(functions.account.GetPassword()) + self.end_session() + return r.hint + + def get_recovery_code(self): + self.load_setting() + + try: + r = self.send(functions.auth.RequestPasswordRecovery()) + except Exception as e: + self.end_session() + raise e + + self.end_session() + + return r.email_pattern + + def get_phone_code_hash(self): + """Generating a phone code hash""" + self.load_setting() + + phone_number_invalid_raises = self.phone_number is not None + + def default_phone_number_callback(): + while True: + phone_number = input("Enter phone number: ") + confirm = input("Is \"{}\" correct? (y/n): ".format(phone_number)) + + if confirm in ("y", "1"): + return phone_number + elif confirm in ("n", "2"): + continue + + while True: + self.phone_number = ( + default_phone_number_callback() if self.phone_number is None + else str(self.phone_number()) if callable(self.phone_number) + else str(self.phone_number) + ) + + self.phone_number = self.phone_number.strip("+") + + try: + r = self.send( + functions.auth.SendCode( + phone_number=self.phone_number, + api_id=self.api_id, + api_hash=self.api_hash, + settings=types.CodeSettings() + ) + ) + except (PhoneMigrate, NetworkMigrate) as e: + self.session.stop() + + self.storage.dc_id = e.x + self.storage.auth_key = Auth(self, self.storage.dc_id).create() + + self.session = Session(self, self.storage.dc_id, self.storage.auth_key) + + self.session.start() + except (PhoneNumberInvalid, PhoneNumberBanned) as e: + if phone_number_invalid_raises: + self.end_session() + raise + else: + print(e.MESSAGE) + self.phone_number = None + except FloodWait as e: + if phone_number_invalid_raises: + self.end_session() + raise + else: + print(e.MESSAGE.format(x=e.x)) + time.sleep(e.x) + except Exception as e: + log.error(e, exc_info=True) + self.end_session() + raise + else: + break + + phone_registered = r.phone_registered + phone_code_hash = r.phone_code_hash + terms_of_service = r.terms_of_service + + if terms_of_service and not Client.terms_of_service_displayed: + print("\n" + terms_of_service.text + "\n") + Client.terms_of_service_displayed = True + + if self.force_sms: + self.send( + functions.auth.ResendCode( + phone_number=self.phone_number, + phone_code_hash=phone_code_hash + ) + ) + + self.end_session() + + return {'phone_registered': r.phone_registered, + 'phone_code_hash': r.phone_code_hash} + + def send_phone_code(self, phone_code_hash, phone_registered): + """Send phone code""" + + self.load_setting() + + phone_code_invalid_raises = self.phone_code is not None + first_name_invalid_raises = self.first_name is not None + + + while True: + if not phone_registered: + self.first_name = ( + input("First name: ") if self.first_name is None + else str(self.first_name()) if callable(self.first_name) + else str(self.first_name) + ) + + self.last_name = ( + input("Last name: ") if self.last_name is None + else str(self.last_name()) if callable(self.last_name) + else str(self.last_name) + ) + + self.phone_code = ( + input("Enter phone code: ") if self.phone_code is None + else str(self.phone_code(self.phone_number)) if callable(self.phone_code) + else str(self.phone_code) + ) + + try: + if phone_registered: + try: + r = self.send( + functions.auth.SignIn( + phone_number=self.phone_number, + phone_code_hash=phone_code_hash, + phone_code=self.phone_code + ) + ) + except PhoneNumberUnoccupied: + log.warning("Phone number unregistered") + phone_registered = False + continue + else: + try: + r = self.send( + functions.auth.SignUp( + phone_number=self.phone_number, + phone_code_hash=phone_code_hash, + phone_code=self.phone_code, + first_name=self.first_name, + last_name=self.last_name + ) + ) + except PhoneNumberOccupied: + log.warning("Phone number already registered") + phone_registered = True + continue + except (PhoneCodeInvalid, PhoneCodeEmpty, PhoneCodeExpired, PhoneCodeHashEmpty) as e: + if phone_code_invalid_raises: + self.end_session() + raise + else: + print(e.MESSAGE) + self.phone_code = None + except FirstnameInvalid as e: + if first_name_invalid_raises: + self.end_session() + raise + else: + print(e.MESSAGE) + self.first_name = None + except SessionPasswordNeeded as e: + print(e.MESSAGE) + raise e + except FloodWait as e: + if phone_code_invalid_raises or first_name_invalid_raises: + self.end_session() + raise + else: + print(e.MESSAGE.format(x=e.x)) + time.sleep(e.x) + except Exception as e: + log.error(e, exc_info=True) + self.end_session() + raise + else: + break + + self.user_id = r.user.id + self.dispatcher.start() + self.update() + self.download() + + mimetypes.init() + Syncer.add(self) + return self + + def send_password(self): + """Send cloud password""" + + self.load_setting() + + password_invalid_raises = self.password is not None + + def default_password_callback(password_hint: str) -> str: + print("Hint: {}".format(password_hint)) + return input("Enter password (empty to recover): ") + + while True: + try: + r = self.send(functions.account.GetPassword()) + + self.password = ( + default_password_callback(r.hint) if self.password is None + else str(self.password(r.hint) or "") if callable(self.password) + else str(self.password) + ) + + r = self.send( + functions.auth.CheckPassword( + password=compute_check(r, self.password) + ) + ) + except (PasswordEmpty, PasswordRecoveryNa, PasswordHashInvalid) as e: + if password_invalid_raises: + self.end_session() + raise + else: + print(e.MESSAGE) + self.password = None + self.recovery_code = None + except FloodWait as e: + if password_invalid_raises: + self.end_session() + raise + else: + print(e.MESSAGE.format(x=e.x)) + time.sleep(e.x) + self.password = None + self.recovery_code = None + except Exception as e: + log.error(e, exc_info=True) + self.end_session() + raise + else: + break + + self.password = None + self.user_id = r.user.id + self.dispatcher.start() + self.update() + self.download() + + mimetypes.init() + Syncer.add(self) + return self + + def send_recovery_code(self): + + self.load_setting() + + password_invalid_raises = self.password is not None + + def default_recovery_callback(email_pattern: str) -> str: + print("An e-mail containing the recovery code has been sent to {}".format(email_pattern)) + return input("Enter password recovery code: ") + + while True: + try: + r = self.send(functions.auth.RequestPasswordRecovery()) + + self.recovery_code = ( + default_recovery_callback(r.email_pattern) if self.recovery_code is None + else str(self.recovery_code(r.email_pattern)) if callable(self.recovery_code) + else str(self.recovery_code) + ) + + r = self.send( + functions.auth.RecoverPassword( + code=self.recovery_code + ) + ) + except (PasswordEmpty, PasswordRecoveryNa, PasswordHashInvalid) as e: + if password_invalid_raises: + self.end_session() + raise + else: + print(e.MESSAGE) + self.password = None + self.recovery_code = None + except FloodWait as e: + if password_invalid_raises: + self.end_session() + raise + else: + print(e.MESSAGE.format(x=e.x)) + time.sleep(e.x) + self.password = None + self.recovery_code = None + except Exception as e: + log.error(e, exc_info=True) + self.end_session() + raise + else: + break + + self.password = None + self.user_id = r.user.id + self.dispatcher.start() + self.update() + self.download() + + mimetypes.init() + Syncer.add(self) + return self + def start(self): """Start the Client. @@ -395,10 +757,8 @@ class Client(Methods, BaseClient): self.start() def idle(self, stop_signals: tuple = (SIGINT, SIGTERM, SIGABRT)): - """Block the main script execution until a signal is received. - - Once the signal is received (e.g.: from CTRL+C), the client will automatically stop and the main script will - continue its execution. + """Block the main script execution until a signal (e.g.: from CTRL+C) is received. + Once the signal is received, the client will automatically stop and the main script will continue its execution. This is used after starting one or more clients and is useful for event-driven applications only, that are, applications which react upon incoming Telegram updates through handlers, rather than executing a set of methods @@ -1067,8 +1427,8 @@ class Client(Methods, BaseClient): session_empty = any([ self.storage.test_mode is None, self.storage.auth_key is None, - self.storage.user_id is None, - self.storage.is_bot is None + # self.storage.user_id is None, + # self.storage.is_bot is None ]) if session_empty: From 52902d4d433844eb8819c14cc8c68cce1e1d6668 Mon Sep 17 00:00:00 2001 From: Elliot Manson Date: Sun, 14 Jul 2019 22:23:23 +0400 Subject: [PATCH 02/21] bug fix --- pyrogram/client/client.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 5219240b..de21cce3 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -577,20 +577,8 @@ class Client(Methods, BaseClient): password_invalid_raises = self.password is not None - def default_recovery_callback(email_pattern: str) -> str: - print("An e-mail containing the recovery code has been sent to {}".format(email_pattern)) - return input("Enter password recovery code: ") - while True: try: - r = self.send(functions.auth.RequestPasswordRecovery()) - - self.recovery_code = ( - default_recovery_callback(r.email_pattern) if self.recovery_code is None - else str(self.recovery_code(r.email_pattern)) if callable(self.recovery_code) - else str(self.recovery_code) - ) - r = self.send( functions.auth.RecoverPassword( code=self.recovery_code From ec00c332c23120f0e8dc5b57ccf21f7cc5e17397 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 2 Aug 2019 02:26:42 +0200 Subject: [PATCH 03/21] Update Photo file_id format --- pyrogram/client/types/messages_and_media/photo.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyrogram/client/types/messages_and_media/photo.py b/pyrogram/client/types/messages_and_media/photo.py index 8ccaaf19..ce117a25 100644 --- a/pyrogram/client/types/messages_and_media/photo.py +++ b/pyrogram/client/types/messages_and_media/photo.py @@ -77,10 +77,10 @@ class Photo(Object): return Photo( file_id=encode( pack( - " Date: Fri, 2 Aug 2019 02:27:29 +0200 Subject: [PATCH 04/21] Update Thumbnail (ex PhotoSize) file_id format --- pyrogram/client/types/messages_and_media/thumbnail.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyrogram/client/types/messages_and_media/thumbnail.py b/pyrogram/client/types/messages_and_media/thumbnail.py index 936241c6..57f369c3 100644 --- a/pyrogram/client/types/messages_and_media/thumbnail.py +++ b/pyrogram/client/types/messages_and_media/thumbnail.py @@ -66,7 +66,7 @@ class Thumbnail(Object): ) -> Union[List[Union[StrippedThumbnail, "Thumbnail"]], None]: if isinstance(media, types.Photo): raw_thumbnails = media.sizes[:-1] - media_type = 0 + media_type = 2 elif isinstance(media, types.Document): raw_thumbnails = media.thumbs media_type = 14 @@ -87,10 +87,10 @@ class Thumbnail(Object): Thumbnail( file_id=encode( pack( - " Date: Fri, 2 Aug 2019 02:30:19 +0200 Subject: [PATCH 05/21] Update ChatPhoto file_id format --- .../client/types/user_and_chats/chat_photo.py | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/pyrogram/client/types/user_and_chats/chat_photo.py b/pyrogram/client/types/user_and_chats/chat_photo.py index 70e114af..7378e3ce 100644 --- a/pyrogram/client/types/user_and_chats/chat_photo.py +++ b/pyrogram/client/types/user_and_chats/chat_photo.py @@ -52,20 +52,40 @@ class ChatPhoto(Object): if not isinstance(chat_photo, (types.UserProfilePhoto, types.ChatPhoto)): return None + photo_id = getattr(chat_photo, "photo_id", 0) loc_small = chat_photo.photo_small loc_big = chat_photo.photo_big + peer = client.resolve_peer(peer_id) + + if isinstance(peer, types.InputPeerUser): + peer_id = peer.user_id + peer_access_hash = peer.access_hash + x = 0 + elif isinstance(peer, types.InputPeerChat): + peer_id = -peer.chat_id + peer_access_hash = 0 + x = -1 + else: + peer_id += 1000727379968 + peer_access_hash = peer.access_hash + x = -234 + return ChatPhoto( small_file_id=encode( pack( - " Date: Fri, 2 Aug 2019 02:33:52 +0200 Subject: [PATCH 06/21] Update download_media and util methods to work with new file_id formats --- pyrogram/client/client.py | 7 ++++++- pyrogram/client/ext/file_data.py | 6 ++++-- pyrogram/client/ext/utils.py | 4 ++-- pyrogram/client/methods/messages/download_media.py | 13 +++++++------ 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 511dbf2a..6643e247 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -1063,6 +1063,7 @@ class Client(Methods, BaseClient): access_hash=data.access_hash, thumb_size=data.thumb_size, peer_id=data.peer_id, + peer_access_hash=data.peer_access_hash, volume_id=data.volume_id, local_id=data.local_id, file_size=data.file_size, @@ -1702,6 +1703,7 @@ class Client(Methods, BaseClient): access_hash: int, thumb_size: str, peer_id: int, + peer_access_hash: int, volume_id: int, local_id: int, file_size: int, @@ -1741,7 +1743,10 @@ class Client(Methods, BaseClient): if media_type == 1: location = types.InputPeerPhotoFileLocation( - peer=self.resolve_peer(peer_id), + peer=types.InputPeerUser( + user_id=peer_id, + access_hash=peer_access_hash + ), volume_id=volume_id, local_id=local_id, big=is_big or None diff --git a/pyrogram/client/ext/file_data.py b/pyrogram/client/ext/file_data.py index 9a19cd5d..ad1da9b6 100644 --- a/pyrogram/client/ext/file_data.py +++ b/pyrogram/client/ext/file_data.py @@ -20,8 +20,9 @@ class FileData: def __init__( self, *, media_type: int = None, dc_id: int = None, document_id: int = None, access_hash: int = None, - thumb_size: str = None, peer_id: int = None, volume_id: int = None, local_id: int = None, is_big: bool = None, - file_size: int = None, mime_type: str = None, file_name: str = None, date: int = None + thumb_size: str = None, peer_id: int = None, peer_access_hash: int = None, volume_id: int = None, + local_id: int = None, is_big: bool = None, file_size: int = None, mime_type: str = None, file_name: str = None, + date: int = None ): self.media_type = media_type self.dc_id = dc_id @@ -29,6 +30,7 @@ class FileData: self.access_hash = access_hash self.thumb_size = thumb_size self.peer_id = peer_id + self.peer_access_hash = peer_access_hash self.volume_id = volume_id self.local_id = local_id self.is_big = is_big diff --git a/pyrogram/client/ext/utils.py b/pyrogram/client/ext/utils.py index cdc0684c..39031adf 100644 --- a/pyrogram/client/ext/utils.py +++ b/pyrogram/client/ext/utils.py @@ -104,8 +104,8 @@ def get_input_media_from_file_id( raise ValueError("This file_id can only be used for download: {}".format(file_id_str)) if media_type == 2: - unpacked = struct.unpack(" Date: Mon, 12 Aug 2019 13:21:19 +0200 Subject: [PATCH 07/21] Update API schema to Layer 104 --- compiler/api/source/main_api.tl | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/compiler/api/source/main_api.tl b/compiler/api/source/main_api.tl index fa2c7af8..f649abf4 100644 --- a/compiler/api/source/main_api.tl +++ b/compiler/api/source/main_api.tl @@ -1,4 +1,4 @@ -// https://raw.githubusercontent.com/telegramdesktop/tdesktop/dev/Telegram/Resources/scheme.tl +// https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/Resources/tl/api.tl /////////////////////////////// ///////// Main application API @@ -101,11 +101,11 @@ userStatusLastMonth#77ebc742 = UserStatus; chatEmpty#9ba2d800 id:int = Chat; chat#3bda1bde flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true deactivated:flags.5?true id:int title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; chatForbidden#7328bdb id:int title:string = Chat; -channel#4df30834 flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?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?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int = Chat; +channel#4df30834 flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?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?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights 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#1b7c9db3 flags:# can_set_username:flags.7?true id:int about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int = ChatFull; -channelFull#10916653 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_view_stats:flags.12?true can_set_location:flags.16?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?int location:flags.15?ChannelLocation pts:int = ChatFull; +channelFull#2d895c74 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_view_stats:flags.12?true can_set_location:flags.16?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?int location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int pts:int = ChatFull; chatParticipant#c8d7493e user_id:int inviter_id:int date:int = ChatParticipant; chatParticipantCreator#da13538a user_id:int = ChatParticipant; @@ -172,11 +172,10 @@ photoStrippedSize#e0b0bc2e type:string bytes:bytes = PhotoSize; geoPointEmpty#1117dd5f = GeoPoint; geoPoint#296f104 long:double lat:double access_hash:long = GeoPoint; -auth.checkedPhone#811ea28e phone_registered:Bool = auth.CheckedPhone; - -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.sentCode#5e002502 flags:# type:auth.SentCodeType phone_code_hash:string next_type:flags.1?auth.CodeType timeout:flags.2?int = auth.SentCode; auth.authorization#cd050916 flags:# tmp_sessions:flags.0?int user:User = auth.Authorization; +auth.authorizationSignUpRequired#44747e9a flags:# terms_of_service:flags.0?help.TermsOfService = auth.Authorization; auth.exportedAuthorization#df969c2d id:int bytes:bytes = auth.ExportedAuthorization; @@ -353,7 +352,7 @@ config#330b4067 flags:# phonecalls_enabled:flags.1?true default_p2p_contacts:fla nearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc; -help.appUpdate#1da7158f flags:# popup:flags.0?true id:int version:string text:string entities:Vector document:flags.1?Document url:flags.2?string = help.AppUpdate; +help.appUpdate#1da7158f flags:# can_not_skip:flags.0?true id:int version:string text:string entities:Vector document:flags.1?Document url:flags.2?string = help.AppUpdate; help.noAppUpdate#c45a6536 = help.AppUpdate; help.inviteText#18cb9f78 message:string = help.InviteText; @@ -496,6 +495,7 @@ chatInvite#dfc2f58e flags:# channel:flags.0?true broadcast:flags.1?true public:f inputStickerSetEmpty#ffb62b95 = InputStickerSet; inputStickerSetID#9de7a269 id:long access_hash:long = InputStickerSet; inputStickerSetShortName#861cc8a0 short_name:string = InputStickerSet; +inputStickerSetAnimatedEmoji#28703c8 = InputStickerSet; stickerSet#eeb46f27 flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumb:flags.4?PhotoSize thumb_dc_id:flags.4?int count:int hash:int = StickerSet; @@ -559,8 +559,8 @@ channelMessagesFilter#cd77d957 flags:# exclude_new_messages:flags.1?true ranges: channelParticipant#15ebac1d user_id:int date:int = ChannelParticipant; channelParticipantSelf#a3289a6d user_id:int inviter_id:int date:int = ChannelParticipant; -channelParticipantCreator#e3e2e1f9 user_id:int = ChannelParticipant; -channelParticipantAdmin#5daa6e23 flags:# can_edit:flags.0?true self:flags.1?true user_id:int inviter_id:flags.1?int promoted_by:int date:int admin_rights:ChatAdminRights = ChannelParticipant; +channelParticipantCreator#808d15a4 flags:# user_id:int rank:flags.0?string = ChannelParticipant; +channelParticipantAdmin#ccbebbaf flags:# can_edit:flags.0?true self:flags.1?true user_id:int inviter_id:flags.1?int promoted_by:int date:int admin_rights:ChatAdminRights rank:flags.2?string = ChannelParticipant; channelParticipantBanned#1c0facaf flags:# left:flags.0?true user_id:int kicked_by:int date:int banned_rights:ChatBannedRights = ChannelParticipant; channelParticipantsRecent#de3f3c79 = ChannelParticipantsFilter; @@ -761,7 +761,7 @@ payments.paymentForm#3f56aea3 flags:# can_save_credentials:flags.2?true password payments.validatedRequestedInfo#d1451883 flags:# id:flags.0?string shipping_options:flags.1?Vector = payments.ValidatedRequestedInfo; payments.paymentResult#4e5f810d updates:Updates = payments.PaymentResult; -payments.paymentVerficationNeeded#6b56b921 url:string = payments.PaymentResult; +payments.paymentVerificationNeeded#d8411139 url:string = payments.PaymentResult; payments.paymentReceipt#500911e1 flags:# date:int bot_id:int invoice:Invoice provider_id:int info:flags.0?PaymentRequestedInfo shipping:flags.1?ShippingOption currency:string total_amount:long credentials_title:string users:Vector = payments.PaymentReceipt; @@ -828,6 +828,7 @@ channelAdminLogEventActionDefaultBannedRights#2df5fc0a prev_banned_rights:ChatBa channelAdminLogEventActionStopPoll#8f079643 message:Message = ChannelAdminLogEventAction; channelAdminLogEventActionChangeLinkedChat#a26f881b prev_value:int new_value:int = ChannelAdminLogEventAction; channelAdminLogEventActionChangeLocation#e6b76ae prev_value:ChannelLocation new_value:ChannelLocation = ChannelAdminLogEventAction; +channelAdminLogEventActionToggleSlowMode#53909779 prev_value:int new_value:int = ChannelAdminLogEventAction; channelAdminLogEvent#3b5a3e40 id:long date:int user_id:int action:ChannelAdminLogEventAction = ChannelAdminLogEvent; @@ -1003,7 +1004,7 @@ inputWallPaperSlug#72091c80 slug:string = InputWallPaper; account.wallPapersNotModified#1c199183 = account.WallPapers; account.wallPapers#702b65a9 hash:int wallpapers:Vector = account.WallPapers; -codeSettings#302f59f3 flags:# allow_flashcall:flags.0?true current_number:flags.1?true app_hash_persistent:flags.2?true app_hash:flags.3?string = CodeSettings; +codeSettings#debebe83 flags:# allow_flashcall:flags.0?true current_number:flags.1?true allow_app_hash:flags.4?true = CodeSettings; wallPaperSettings#a12f40b8 flags:# blur:flags.1?true motion:flags.2?true background_color:flags.0?int intensity:flags.3?int = WallPaperSettings; @@ -1050,7 +1051,7 @@ invokeWithMessagesRange#365275f2 {X:Type} range:MessageRange query:!X = X; invokeWithTakeout#aca9fd2e {X:Type} takeout_id:long query:!X = X; auth.sendCode#a677244f phone_number:string api_id:int api_hash:string settings:CodeSettings = auth.SentCode; -auth.signUp#1b067634 phone_number:string phone_code_hash:string phone_code:string first_name:string last_name:string = auth.Authorization; +auth.signUp#80eee427 phone_number:string phone_code_hash:string first_name:string last_name:string = auth.Authorization; auth.signIn#bcd51581 phone_number:string phone_code_hash:string phone_code:string = auth.Authorization; auth.logOut#5717da40 = Bool; auth.resetAuthorizations#9fab0d1a = Bool; @@ -1269,7 +1270,7 @@ photos.deletePhotos#87cf7f2f id:Vector = Vector; photos.getUserPhotos#91cd32a8 user_id:InputUser offset:int max_id:long limit:int = photos.Photos; upload.saveFilePart#b304a621 file_id:long file_part:int bytes:bytes = Bool; -upload.getFile#e3a6cfb5 location:InputFileLocation offset:int limit:int = upload.File; +upload.getFile#b15a9afc flags:# precise:flags.0?true location:InputFileLocation offset:int limit:int = upload.File; upload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool; upload.getWebFile#24e6818d location:InputWebFileLocation offset:int limit:int = upload.WebFile; upload.getCdnFile#2000bcc3 file_token:bytes offset:int limit:int = upload.CdnFile; @@ -1307,7 +1308,7 @@ channels.getParticipant#546dd7a6 channel:InputChannel user_id:InputUser = channe channels.getChannels#a7f6bbb id:Vector = messages.Chats; channels.getFullChannel#8736a09 channel:InputChannel = messages.ChatFull; channels.createChannel#3d5fb10f flags:# broadcast:flags.0?true megagroup:flags.1?true title:string about:string geo_point:flags.2?InputGeoPoint address:flags.2?string = Updates; -channels.editAdmin#70f893ba channel:InputChannel user_id:InputUser admin_rights:ChatAdminRights = Updates; +channels.editAdmin#d33c8902 channel:InputChannel user_id:InputUser admin_rights:ChatAdminRights rank:string = Updates; channels.editTitle#566decd0 channel:InputChannel title:string = Updates; channels.editPhoto#f12e57c9 channel:InputChannel photo:InputChatPhoto = Updates; channels.checkUsername#10e6bd2c channel:InputChannel username:string = Bool; @@ -1330,6 +1331,7 @@ channels.getGroupsForDiscussion#f5dad378 = messages.Chats; channels.setDiscussionGroup#40582bb2 broadcast:InputChannel group:InputChannel = Bool; channels.editCreator#8f38cd1f channel:InputChannel user_id:InputUser password:InputCheckPasswordSRP = Updates; channels.editLocation#58e63f6d channel:InputChannel geo_point:InputGeoPoint address:string = Bool; +channels.toggleSlowMode#edd49ef0 channel:InputChannel seconds:int = Updates; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; @@ -1364,4 +1366,4 @@ langpack.getLanguage#6a596502 lang_pack:string lang_code:string = LangPackLangua folders.editPeerFolders#6847d0ab folder_peers:Vector = Updates; folders.deleteFolder#1c295881 folder_id:int = Updates; -// LAYER 103 \ No newline at end of file +// LAYER 104 \ No newline at end of file From 7daf51af9b4b10fec459e544369332ae5269c90f Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 13 Aug 2019 13:15:58 +0200 Subject: [PATCH 08/21] Split start() into reusable methods This allows custom authorization processes to be implemented much more easily. Refactors #281 --- pyrogram/client/client.py | 1052 +++++++++-------- pyrogram/client/ext/base_client.py | 4 +- pyrogram/client/types/__init__.py | 1 + .../client/types/authorization/__init__.py | 22 + .../client/types/authorization/sent_code.py | 86 ++ .../types/authorization/terms_of_service.py | 56 + 6 files changed, 746 insertions(+), 475 deletions(-) create mode 100644 pyrogram/client/types/authorization/__init__.py create mode 100644 pyrogram/client/types/authorization/sent_code.py create mode 100644 pyrogram/client/types/authorization/terms_of_service.py diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index bbd939e8..8cdecefb 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -18,7 +18,6 @@ import logging import math -import mimetypes import os import re import shutil @@ -45,12 +44,13 @@ from pyrogram.errors import ( PhoneCodeExpired, PhoneCodeEmpty, SessionPasswordNeeded, PasswordHashInvalid, FloodWait, PeerIdInvalid, FirstnameInvalid, PhoneNumberBanned, VolumeLocNotFound, UserMigrate, ChannelPrivate, PhoneNumberOccupied, - PasswordRecoveryNa, PasswordEmpty, AuthBytesInvalid -) + PasswordRecoveryNa, PasswordEmpty, AuthBytesInvalid, + BadRequest) from pyrogram.session import Auth, Session from .ext import utils, Syncer, BaseClient, Dispatcher from .methods import Methods from .storage import Storage, FileStorage, MemoryStorage +from .types import User, SentCode, TermsOfService log = logging.getLogger(__name__) @@ -188,8 +188,6 @@ class Client(Methods, BaseClient): """ - terms_of_service_displayed = False - def __init__( self, session_name: Union[str, Storage], @@ -280,421 +278,65 @@ class Client(Methods, BaseClient): self._proxy["enabled"] = bool(value.get("enabled", True)) self._proxy.update(value) - def load_setting(self): - """Loading settings""" + def connect(self) -> bool: + """ + Connect the client to Telegram servers. - if self.is_started: - raise ConnectionError("Client has already been started") + Returns: + ``bool``: On success, in case the passed-in session is authorized, True is returned. Otherwise, in case + the session needs to be authorized, False is returned. + + Raises: + ConnectionError: In case you try to connect an already connected client. + """ + if self.is_connected: + raise ConnectionError("Client is already connected") self.load_config() self.load_session() - self.load_plugins() self.session = Session(self, self.storage.dc_id, self.storage.auth_key) - self.session.start() - self.is_started = True + self.is_connected = True + + return bool(self.storage.user_id) + + def disconnect(self): + """Disconnect the client from Telegram servers. + + Raises: + ConnectionError: In case you try to disconnect an already disconnected client or in case you try to + disconnect a client that needs to be terminated first. + """ + if not self.is_connected: + raise ConnectionError("Client is already disconnected") + + if self.is_initialized: + raise ConnectionError("Can't disconnect an initialized client") - def end_session(self): - self.is_started = False self.session.stop() + self.storage.close() + self.is_connected = False - def update(self): - for i in range(self.UPDATES_WORKERS): - self.updates_workers_list.append( - Thread( - target=self.updates_worker, - name="UpdatesWorker#{}".format(i + 1) - ) - ) + def initialize(self): + """Initialize the client by starting up workers. - self.updates_workers_list[-1].start() - - def download(self): - for i in range(self.DOWNLOAD_WORKERS): - self.download_workers_list.append( - Thread( - target=self.download_worker, - name="DownloadWorker#{}".format(i + 1) - ) - ) - - self.download_workers_list[-1].start() - - def get_hint(self): - r = self.send(functions.account.GetPassword()) - self.end_session() - return r.hint - - def get_recovery_code(self): - self.load_setting() - - try: - r = self.send(functions.auth.RequestPasswordRecovery()) - except Exception as e: - self.end_session() - raise e - - self.end_session() - - return r.email_pattern - - def get_phone_code_hash(self): - """Generating a phone code hash""" - self.load_setting() - - phone_number_invalid_raises = self.phone_number is not None - - def default_phone_number_callback(): - while True: - phone_number = input("Enter phone number: ") - confirm = input("Is \"{}\" correct? (y/n): ".format(phone_number)) - - if confirm in ("y", "1"): - return phone_number - elif confirm in ("n", "2"): - continue - - while True: - self.phone_number = ( - default_phone_number_callback() if self.phone_number is None - else str(self.phone_number()) if callable(self.phone_number) - else str(self.phone_number) - ) - - self.phone_number = self.phone_number.strip("+") - - try: - r = self.send( - functions.auth.SendCode( - phone_number=self.phone_number, - api_id=self.api_id, - api_hash=self.api_hash, - settings=types.CodeSettings() - ) - ) - except (PhoneMigrate, NetworkMigrate) as e: - self.session.stop() - - self.storage.dc_id = e.x - self.storage.auth_key = Auth(self, self.storage.dc_id).create() - - self.session = Session(self, self.storage.dc_id, self.storage.auth_key) - - self.session.start() - except (PhoneNumberInvalid, PhoneNumberBanned) as e: - if phone_number_invalid_raises: - self.end_session() - raise - else: - print(e.MESSAGE) - self.phone_number = None - except FloodWait as e: - if phone_number_invalid_raises: - self.end_session() - raise - else: - print(e.MESSAGE.format(x=e.x)) - time.sleep(e.x) - except Exception as e: - log.error(e, exc_info=True) - self.end_session() - raise - else: - break - - phone_registered = r.phone_registered - phone_code_hash = r.phone_code_hash - terms_of_service = r.terms_of_service - - if terms_of_service and not Client.terms_of_service_displayed: - print("\n" + terms_of_service.text + "\n") - Client.terms_of_service_displayed = True - - if self.force_sms: - self.send( - functions.auth.ResendCode( - phone_number=self.phone_number, - phone_code_hash=phone_code_hash - ) - ) - - self.end_session() - - return {'phone_registered': r.phone_registered, - 'phone_code_hash': r.phone_code_hash} - - def send_phone_code(self, phone_code_hash, phone_registered): - """Send phone code""" - - self.load_setting() - - phone_code_invalid_raises = self.phone_code is not None - first_name_invalid_raises = self.first_name is not None - - - while True: - if not phone_registered: - self.first_name = ( - input("First name: ") if self.first_name is None - else str(self.first_name()) if callable(self.first_name) - else str(self.first_name) - ) - - self.last_name = ( - input("Last name: ") if self.last_name is None - else str(self.last_name()) if callable(self.last_name) - else str(self.last_name) - ) - - self.phone_code = ( - input("Enter phone code: ") if self.phone_code is None - else str(self.phone_code(self.phone_number)) if callable(self.phone_code) - else str(self.phone_code) - ) - - try: - if phone_registered: - try: - r = self.send( - functions.auth.SignIn( - phone_number=self.phone_number, - phone_code_hash=phone_code_hash, - phone_code=self.phone_code - ) - ) - except PhoneNumberUnoccupied: - log.warning("Phone number unregistered") - phone_registered = False - continue - else: - try: - r = self.send( - functions.auth.SignUp( - phone_number=self.phone_number, - phone_code_hash=phone_code_hash, - phone_code=self.phone_code, - first_name=self.first_name, - last_name=self.last_name - ) - ) - except PhoneNumberOccupied: - log.warning("Phone number already registered") - phone_registered = True - continue - except (PhoneCodeInvalid, PhoneCodeEmpty, PhoneCodeExpired, PhoneCodeHashEmpty) as e: - if phone_code_invalid_raises: - self.end_session() - raise - else: - print(e.MESSAGE) - self.phone_code = None - except FirstnameInvalid as e: - if first_name_invalid_raises: - self.end_session() - raise - else: - print(e.MESSAGE) - self.first_name = None - except SessionPasswordNeeded as e: - print(e.MESSAGE) - raise e - except FloodWait as e: - if phone_code_invalid_raises or first_name_invalid_raises: - self.end_session() - raise - else: - print(e.MESSAGE.format(x=e.x)) - time.sleep(e.x) - except Exception as e: - log.error(e, exc_info=True) - self.end_session() - raise - else: - break - - self.user_id = r.user.id - self.dispatcher.start() - self.update() - self.download() - - mimetypes.init() - Syncer.add(self) - return self - - def send_password(self): - """Send cloud password""" - - self.load_setting() - - password_invalid_raises = self.password is not None - - def default_password_callback(password_hint: str) -> str: - print("Hint: {}".format(password_hint)) - return input("Enter password (empty to recover): ") - - while True: - try: - r = self.send(functions.account.GetPassword()) - - self.password = ( - default_password_callback(r.hint) if self.password is None - else str(self.password(r.hint) or "") if callable(self.password) - else str(self.password) - ) - - r = self.send( - functions.auth.CheckPassword( - password=compute_check(r, self.password) - ) - ) - except (PasswordEmpty, PasswordRecoveryNa, PasswordHashInvalid) as e: - if password_invalid_raises: - self.end_session() - raise - else: - print(e.MESSAGE) - self.password = None - self.recovery_code = None - except FloodWait as e: - if password_invalid_raises: - self.end_session() - raise - else: - print(e.MESSAGE.format(x=e.x)) - time.sleep(e.x) - self.password = None - self.recovery_code = None - except Exception as e: - log.error(e, exc_info=True) - self.end_session() - raise - else: - break - - self.password = None - self.user_id = r.user.id - self.dispatcher.start() - self.update() - self.download() - - mimetypes.init() - Syncer.add(self) - return self - - def send_recovery_code(self): - - self.load_setting() - - password_invalid_raises = self.password is not None - - while True: - try: - r = self.send( - functions.auth.RecoverPassword( - code=self.recovery_code - ) - ) - except (PasswordEmpty, PasswordRecoveryNa, PasswordHashInvalid) as e: - if password_invalid_raises: - self.end_session() - raise - else: - print(e.MESSAGE) - self.password = None - self.recovery_code = None - except FloodWait as e: - if password_invalid_raises: - self.end_session() - raise - else: - print(e.MESSAGE.format(x=e.x)) - time.sleep(e.x) - self.password = None - self.recovery_code = None - except Exception as e: - log.error(e, exc_info=True) - self.end_session() - raise - else: - break - - self.password = None - self.user_id = r.user.id - self.dispatcher.start() - self.update() - self.download() - - mimetypes.init() - Syncer.add(self) - return self - - def start(self): - """Start the client. - - This method connects the client to Telegram and, in case of new sessions, automatically manages the full login - process using an interactive prompt (by default). - - Has no parameters. + This method will start updates and download workers. + It will also load plugins and start the internal dispatcher. Raises: - ConnectionError: In case you try to start an already started client. - - Example: - .. code-block:: python - :emphasize-lines: 4 - - from pyrogram import Client - - app = Client("my_account") - app.start() - - ... # Call API methods - - app.stop() + ConnectionError: In case you try to initialize a disconnected client or in case you try to initialize an + already initialized client. """ - if self.is_started: - raise ConnectionError("Client has already been started") + if not self.is_connected: + raise ConnectionError("Can't initialize a disconnected client") + + if self.is_initialized: + raise ConnectionError("Client is already initialized") - self.load_config() - self.load_session() self.load_plugins() - self.session = Session(self, self.storage.dc_id, self.storage.auth_key) - - self.session.start() - self.is_started = True - - try: - if self.storage.user_id is None: - if self.bot_token is None: - self.storage.is_bot = False - self.authorize_user() - else: - self.storage.is_bot = True - self.authorize_bot() - - if not self.storage.is_bot: - if self.takeout: - self.takeout_id = self.send(functions.account.InitTakeoutSession()).id - log.warning("Takeout session {} initiated".format(self.takeout_id)) - - now = time.time() - - if abs(now - self.storage.date) > Client.OFFLINE_SLEEP: - self.get_initial_dialogs() - self.get_contacts() - else: - self.send(functions.messages.GetPinnedDialogs(folder_id=0)) - self.get_initial_dialogs_chunk() - else: - self.send(functions.updates.GetState()) - except Exception as e: - self.is_started = False - self.session.stop() - raise e - for i in range(self.UPDATES_WORKERS): self.updates_workers_list.append( Thread( @@ -717,36 +359,21 @@ class Client(Methods, BaseClient): self.dispatcher.start() - mimetypes.init() Syncer.add(self) - return self + self.is_initialized = True - def stop(self): - """Stop the Client. + def terminate(self): + """Terminate the client by shutting down workers. - This method disconnects the client from Telegram and stops the underlying tasks. - - Has no parameters. + This method does the opposite of :meth:`~Client.initialize`. + It will stop the dispatcher and shut down updates and download workers. Raises: - ConnectionError: In case you try to stop an already stopped client. - - Example: - .. code-block:: python - :emphasize-lines: 8 - - from pyrogram import Client - - app = Client("my_account") - app.start() - - ... # Call API methods - - app.stop() + ConnectionError: In case you try to terminate a client that is already terminated. """ - if not self.is_started: - raise ConnectionError("Client is already stopped") + if not self.is_initialized: + raise ConnectionError("Client is already terminated") if self.takeout_id: self.send(functions.account.FinishTakeoutSession()) @@ -776,8 +403,490 @@ class Client(Methods, BaseClient): self.media_sessions.clear() - self.is_started = False - self.session.stop() + self.is_initialized = False + + def send_code(self, phone_number: str) -> SentCode: + """Send the confirmation code to the given phone number. + + Parameters: + phone_number (``str``): + Phone number in international format (includes the country prefix). + + Returns: + :obj:`SentCode`: On success, an object containing information on the sent confirmation code is returned. + + Raises: + BadRequest: In case the phone number is invalid. + """ + phone_number = phone_number.strip(" +") + + while True: + try: + r = self.send( + functions.auth.SendCode( + phone_number=phone_number, + api_id=self.api_id, + api_hash=self.api_hash, + settings=types.CodeSettings() + ) + ) + except (PhoneMigrate, NetworkMigrate) as e: + self.session.stop() + + self.storage.dc_id = e.x + self.storage.auth_key = Auth(self, self.storage.dc_id).create() + self.session = Session(self, self.storage.dc_id, self.storage.auth_key) + + self.session.start() + else: + return SentCode._parse(r) + + def resend_code(self, phone_number: str, phone_code_hash: str) -> SentCode: + """Re-send the confirmation code using a different type. + + The type of the code to be re-sent is specified in the *next_type* attribute of the :obj:`SentCode` object + returned by :meth:`send_code`. + + Parameters: + phone_number (``str``): + Phone number in international format (includes the country prefix). + + phone_code_hash (``str``): + Confirmation code identifier. + + Returns: + :obj:`SentCode`: On success, an object containing information on the re-sent confirmation code is returned. + + Raises: + BadRequest: In case the arguments are invalid. + """ + phone_number = phone_number.strip(" +") + + r = self.send( + functions.auth.ResendCode( + phone_number=phone_number, + phone_code_hash=phone_code_hash + ) + ) + + return SentCode._parse(r) + + def sign_in(self, phone_number: str, phone_code_hash: str, phone_code: str) -> Union[User, TermsOfService, bool]: + """Authorize a user in Telegram with a valid confirmation code. + + Parameters: + phone_number (``str``): + Phone number in international format (includes the country prefix). + + phone_code_hash (``str``): + Code identifier taken from the result of :meth:`~Client.send_code`. + + phone_code (``str``): + The valid confirmation code you received (either as Telegram message or as SMS in your phone number). + + Returns: + :obj:`User` | :obj:`TermsOfService` | bool: On success, in case the authorization completed, the user is + returned. In case the phone number needs to be registered first AND the terms of services accepted (with + :meth:`~Client.accept_terms_of_service`), an object containing them is returned. In case the phone number + needs to be registered, but the terms of services don't need to be accepted, False is returned instead. + + Raises: + BadRequest: In case the arguments are invalid. + SessionPasswordNeeded: In case a password is needed to sign in. + """ + phone_number = phone_number.strip(" +") + + r = self.send( + functions.auth.SignIn( + phone_number=phone_number, + phone_code_hash=phone_code_hash, + phone_code=phone_code + ) + ) + + if isinstance(r, types.auth.AuthorizationSignUpRequired): + if r.terms_of_service: + return TermsOfService._parse(terms_of_service=r.terms_of_service) + + return False + else: + self.storage.user_id = r.user.id + self.storage.is_bot = False + + return User._parse(self, r.user) + + def sign_up(self, phone_number: str, phone_code_hash: str, first_name: str, last_name: str = "") -> User: + """Register a new user in Telegram. + + Parameters: + phone_number (``str``): + Phone number in international format (includes the country prefix). + + phone_code_hash (``str``): + Code identifier taken from the result of :meth:`~Client.send_code`. + + first_name (``str``): + New user first name. + + last_name (``str``, *optional*): + New user last name. Defaults to "" (empty string). + + Returns: + :obj:`User`: On success, the new registered user is returned. + + Raises: + BadRequest: In case the arguments are invalid. + """ + phone_number = phone_number.strip(" +") + + r = self.send( + functions.auth.SignUp( + phone_number=phone_number, + first_name=first_name, + last_name=last_name, + phone_code_hash=phone_code_hash + ) + ) + + self.storage.user_id = r.user.id + self.storage.is_bot = False + + return User._parse(self, r.user) + + def sign_in_bot(self, bot_token: str) -> User: + """Authorize a bot using its bot token generated by BotFather. + + Parameters: + bot_token (``str``): + The bot token generated by BotFather + + Returns: + :obj:`User`: On success, the bot identity is return in form of a user object. + + Raises: + BadRequest: In case the bot token is invalid. + """ + while True: + try: + r = self.send( + functions.auth.ImportBotAuthorization( + flags=0, + api_id=self.api_id, + api_hash=self.api_hash, + bot_auth_token=bot_token + ) + ) + except UserMigrate as e: + self.session.stop() + + self.storage.dc_id = e.x + self.storage.auth_key = Auth(self, self.storage.dc_id).create() + self.session = Session(self, self.storage.dc_id, self.storage.auth_key) + + self.session.start() + else: + self.storage.user_id = r.user.id + self.storage.is_bot = True + + return User._parse(self, r.user) + + def get_password_hint(self) -> str: + """Get your Two-Step Verification password hint. + + Returns: + ``str``: On success, the password hint as string is returned. + """ + return self.send(functions.account.GetPassword()).hint + + def check_password(self, password: str) -> User: + """Check your Two-Step Verification password and log in. + + Parameters: + password (``str``): + Your Two-Step Verification password. + + Returns: + :obj:`User`: On success, the authorized user is returned. + + Raises: + BadRequest: In case the password is invalid. + """ + r = self.send( + functions.auth.CheckPassword( + password=compute_check( + self.send(functions.account.GetPassword()), + password + ) + ) + ) + + self.storage.user_id = r.user.id + self.storage.is_bot = False + + return User._parse(self, r.user) + + def send_recovery_code(self) -> str: + """Send a code to your email to recover your password. + + Returns: + ``str``: On success, the hidden email pattern is returned and a recovery code is sent to that email. + + Raises: + BadRequest: In case no recovery email was set up. + """ + return self.send( + functions.auth.RequestPasswordRecovery() + ).email_pattern + + def recover_password(self, recovery_code: str) -> User: + """Recover your password with a recovery code and log in. + + Parameters: + recovery_code (``str``): + The recovery code sent via email. + + Returns: + :obj:`User`: On success, the authorized user is returned and the Two-Step Verification password reset. + + Raises: + BadRequest: In case the recovery code is invalid. + """ + r = self.send( + functions.auth.RecoverPassword( + code=recovery_code + ) + ) + + self.storage.user_id = r.user.id + self.storage.is_bot = False + + return User._parse(self, r.user) + + def accept_terms_of_service(self, terms_of_service_id: str) -> bool: + """Accept the given terms of service. + + Parameters: + terms_of_service_id (``str``): + The terms of service identifier. + """ + r = self.send( + functions.help.AcceptTermsOfService( + id=types.DataJSON( + data=terms_of_service_id + ) + ) + ) + + assert r + + return True + + def authorize(self) -> User: + if self.bot_token is not None: + return self.sign_in_bot(self.bot_token) + + while True: + if self.phone_number is None: + while True: + value = input("Enter phone number or bot token: ") + confirm = input("Is \"{}\" correct? (y/n): ".format(value)) + + if confirm in ("y", "1"): + break + elif confirm in ("n", "2"): + continue + + if ":" in value: + self.bot_token = value + return self.sign_in_bot(value) + else: + self.phone_number = value + + try: + sent_code = self.send_code(self.phone_number) + except BadRequest as e: + print(e.MESSAGE) + self.phone_number = None + except FloodWait as e: + print(e.MESSAGE.format(x=e.x)) + time.sleep(e.x) + except Exception as e: + log.error(e, exc_info=True) + else: + break + + if self.force_sms: + sent_code = self.resend_code(self.phone_number, sent_code.phone_code_hash) + + print("The confirmation code has been sent via {}".format( + { + "app": "Telegram app", + "sms": "SMS", + "call": "phone call", + "flash_call": "phone flash call" + }[sent_code.type] + )) + + while True: + if self.phone_code is None: + self.phone_code = input("Enter confirmation code: ") + + try: + signed_in = self.sign_in(self.phone_number, sent_code.phone_code_hash, self.phone_code) + except BadRequest as e: + print(e.MESSAGE) + self.phone_code = None + except SessionPasswordNeeded as e: + print(e.MESSAGE) + + while True: + print("Password hint: {}".format(self.get_password_hint())) + + if self.password is None: + self.password = input("Enter password (empty to recover): ") + + try: + if self.password == "": + confirm = input("Confirm password recovery (y/n): ") + + if confirm in ("y", "1"): + email_pattern = self.send_recovery_code() + print("The recovery code has been sent to {}".format(email_pattern)) + + while True: + recovery_code = input("Enter recovery code: ") + + try: + return self.recover_password(recovery_code) + except BadRequest as e: + print(e.MESSAGE) + except FloodWait as e: + print(e.MESSAGE.format(x=e.x)) + time.sleep(e.x) + except Exception as e: + log.error(e, exc_info=True) + raise + + elif confirm in ("n", "2"): + self.password = None + else: + return self.check_password(self.password) + except BadRequest as e: + print(e.MESSAGE) + self.password = None + except FloodWait as e: + print(e.MESSAGE.format(x=e.x)) + time.sleep(e.x) + except Exception as e: + log.error(e, exc_info=True) + raise + except FloodWait as e: + print(e.MESSAGE.format(x=e.x)) + time.sleep(e.x) + except Exception as e: + log.error(e, exc_info=True) + else: + break + + if isinstance(signed_in, User): + return signed_in + + while True: + self.first_name = input("Enter first name: ") + self.last_name = input("Enter last name (empty to skip): ") + + try: + signed_up = self.sign_up( + self.phone_number, + sent_code.phone_code_hash, + self.first_name, + self.last_name + ) + except BadRequest as e: + print(e.MESSAGE) + self.first_name = None + self.last_name = None + except FloodWait as e: + print(e.MESSAGE.format(x=e.x)) + time.sleep(e.x) + else: + break + + if isinstance(signed_in, TermsOfService): + print("\n" + signed_in.text + "\n") + self.accept_terms_of_service(signed_in.id) + + return signed_up + + def start(self): + """Start the client. + + This method connects the client to Telegram and, in case of new sessions, automatically manages the full + authorization process using an interactive prompt. + + Returns: + :obj:`Client`: The started client itself. + + Raises: + ConnectionError: In case you try to start an already started client. + + Example: + .. code-block:: python + :emphasize-lines: 4 + + from pyrogram import Client + + app = Client("my_account") + app.start() + + ... # Call API methods + + app.stop() + """ + is_authorized = self.connect() + + try: + if not is_authorized: + self.authorize() + + if not self.storage.is_bot and self.takeout: + self.takeout_id = self.send(functions.account.InitTakeoutSession()).id + log.warning("Takeout session {} initiated".format(self.takeout_id)) + + self.send(functions.updates.GetState()) + except Exception as e: + self.disconnect() + raise e + else: + self.initialize() + return self + + def stop(self): + """Stop the Client. + + This method disconnects the client from Telegram and stops the underlying tasks. + + Returns: + :obj:`Client`: The stopped client itself. + + Raises: + ConnectionError: In case you try to stop an already stopped client. + + Example: + .. code-block:: python + :emphasize-lines: 8 + + from pyrogram import Client + + app = Client("my_account") + app.start() + + ... # Call API methods + + app.stop() + """ + self.terminate() + self.disconnect() return self @@ -787,7 +896,8 @@ class Client(Methods, BaseClient): This method will first call :meth:`~Client.stop` and then :meth:`~Client.start` in a row in order to restart a client using a single method. - Has no parameters. + Returns: + :obj:`Client`: The restarted client itself. Raises: ConnectionError: In case you try to restart a stopped Client. @@ -812,6 +922,8 @@ class Client(Methods, BaseClient): self.stop() self.start() + return self + @staticmethod def idle(stop_signals: tuple = (SIGINT, SIGTERM, SIGABRT)): """Block the main script execution until a signal is received. @@ -875,8 +987,6 @@ class Client(Methods, BaseClient): sequence. It makes running a client less verbose, but is not suitable in case you want to run more than one client in a single main script, since idle() will block after starting the own client. - Has no parameters. - Raises: ConnectionError: In case you try to run an already started client. @@ -978,8 +1088,6 @@ class Client(Methods, BaseClient): This method must be called inside a progress callback function in order to stop the transmission at the desired time. The progress callback is called every time a file chunk is uploaded/downloaded. - Has no parameters. - Example: .. code-block:: python :emphasize-lines: 9 @@ -1006,8 +1114,6 @@ class Client(Methods, BaseClient): More detailed information about session strings can be found at the dedicated page of :doc:`Storage Engines <../../topics/storage-engines>`. - Has no parameters. - Returns: ``str``: The session serialized into a printable, url-safe string. @@ -1560,8 +1666,8 @@ class Client(Methods, BaseClient): Raises: RPCError: In case of a Telegram RPC error. """ - if not self.is_started: - raise ConnectionError("Client has not been started") + if not self.is_connected: + raise ConnectionError("Client has not been started yet") if self.no_updates: data = functions.InvokeWithoutUpdates(query=data) @@ -1658,8 +1764,8 @@ class Client(Methods, BaseClient): session_empty = any([ self.storage.test_mode is None, self.storage.auth_key is None, - # self.storage.user_id is None, - # self.storage.is_bot is None + self.storage.user_id is None, + self.storage.is_bot is None ]) if session_empty: @@ -1793,37 +1899,37 @@ class Client(Methods, BaseClient): log.warning('[{}] No plugin loaded from "{}"'.format( self.session_name, root)) - def get_initial_dialogs_chunk(self, offset_date: int = 0): - while True: - try: - r = self.send( - functions.messages.GetDialogs( - offset_date=offset_date, - offset_id=0, - offset_peer=types.InputPeerEmpty(), - limit=self.DIALOGS_AT_ONCE, - hash=0, - exclude_pinned=True - ) - ) - except FloodWait as e: - log.warning("get_dialogs flood: waiting {} seconds".format(e.x)) - time.sleep(e.x) - else: - log.info("Total peers: {}".format(self.storage.peers_count)) - return r - - def get_initial_dialogs(self): - self.send(functions.messages.GetPinnedDialogs(folder_id=0)) - - dialogs = self.get_initial_dialogs_chunk() - offset_date = utils.get_offset_date(dialogs) - - while len(dialogs.dialogs) == self.DIALOGS_AT_ONCE: - dialogs = self.get_initial_dialogs_chunk(offset_date) - offset_date = utils.get_offset_date(dialogs) - - self.get_initial_dialogs_chunk() + # def get_initial_dialogs_chunk(self, offset_date: int = 0): + # while True: + # try: + # r = self.send( + # functions.messages.GetDialogs( + # offset_date=offset_date, + # offset_id=0, + # offset_peer=types.InputPeerEmpty(), + # limit=self.DIALOGS_AT_ONCE, + # hash=0, + # exclude_pinned=True + # ) + # ) + # except FloodWait as e: + # log.warning("get_dialogs flood: waiting {} seconds".format(e.x)) + # time.sleep(e.x) + # else: + # log.info("Total peers: {}".format(self.storage.peers_count)) + # return r + # + # def get_initial_dialogs(self): + # self.send(functions.messages.GetPinnedDialogs(folder_id=0)) + # + # dialogs = self.get_initial_dialogs_chunk() + # offset_date = utils.get_offset_date(dialogs) + # + # while len(dialogs.dialogs) == self.DIALOGS_AT_ONCE: + # dialogs = self.get_initial_dialogs_chunk(offset_date) + # offset_date = utils.get_offset_date(dialogs) + # + # self.get_initial_dialogs_chunk() def resolve_peer(self, peer_id: Union[int, str]): """Get the InputPeer of a known peer id. @@ -1844,9 +1950,11 @@ class Client(Methods, BaseClient): ``InputPeer``: On success, the resolved peer id is returned in form of an InputPeer object. Raises: - RPCError: In case of a Telegram RPC error. KeyError: In case the peer doesn't exist in the internal database. """ + if not self.is_connected: + raise ConnectionError("Client has not been started yet") + try: return self.storage.get_peer_by_id(peer_id) except KeyError: @@ -2027,7 +2135,7 @@ class Client(Methods, BaseClient): file_part += 1 if progress: - progress(self, min(file_part * part_size, file_size), file_size, *progress_args) + progress(min(file_part * part_size, file_size), file_size, *progress_args) except Client.StopTransmission: raise except Exception as e: @@ -2158,7 +2266,6 @@ class Client(Methods, BaseClient): if progress: progress( - self, min(offset, file_size) if file_size != 0 else offset, @@ -2241,7 +2348,6 @@ class Client(Methods, BaseClient): if progress: progress( - self, min(offset, file_size) if file_size != 0 else offset, diff --git a/pyrogram/client/ext/base_client.py b/pyrogram/client/ext/base_client.py index ce736e87..260d58fc 100644 --- a/pyrogram/client/ext/base_client.py +++ b/pyrogram/client/ext/base_client.py @@ -50,7 +50,6 @@ class BaseClient: PARENT_DIR = Path(sys.argv[0]).parent INVITE_LINK_RE = re.compile(r"^(?:https?://)?(?:www\.)?(?:t(?:elegram)?\.(?:org|me|dog)/joinchat/)([\w-]+)$") - BOT_TOKEN_RE = re.compile(r"^\d+:[\w-]+$") DIALOGS_AT_ONCE = 100 UPDATES_WORKERS = 1 DOWNLOAD_WORKERS = 1 @@ -103,7 +102,8 @@ class BaseClient: self.media_sessions = {} self.media_sessions_lock = Lock() - self.is_started = None + self.is_connected = None + self.is_initialized = None self.takeout_id = None diff --git a/pyrogram/client/types/__init__.py b/pyrogram/client/types/__init__.py index 8fa55482..a9ef2d84 100644 --- a/pyrogram/client/types/__init__.py +++ b/pyrogram/client/types/__init__.py @@ -23,5 +23,6 @@ from .input_message_content import * from .list import List from .messages_and_media import * from .object import Object +from .authorization import * from .update import * from .user_and_chats import * diff --git a/pyrogram/client/types/authorization/__init__.py b/pyrogram/client/types/authorization/__init__.py new file mode 100644 index 00000000..a4e96273 --- /dev/null +++ b/pyrogram/client/types/authorization/__init__.py @@ -0,0 +1,22 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2019 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from .terms_of_service import TermsOfService +from .sent_code import SentCode + +__all__ = ["TermsOfService", "SentCode"] diff --git a/pyrogram/client/types/authorization/sent_code.py b/pyrogram/client/types/authorization/sent_code.py new file mode 100644 index 00000000..b534df00 --- /dev/null +++ b/pyrogram/client/types/authorization/sent_code.py @@ -0,0 +1,86 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2019 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from pyrogram.api import types +from ..object import Object + + +class SentCode(Object): + """Contains info on a sent confirmation code. + + Parameters: + type (``str``): + Type of the current sent code. + Can be *"app"* (code sent via Telegram), *"sms"* (code sent via SMS), *"call"* (code sent via voice call) or + *"flash_call"* (code is in the last 5 digits of the caller's phone number). + + phone_code_hash (``str``): + Confirmation code identifier useful for the next authorization steps (either :meth:`~Client.sign_in` or + :meth:`~Client.sign_up`). + + next_type (``str``): + Type of the next code to be sent with :meth:`~Client.resend_code`. + Can be *"sms"* (code will be sent via SMS), *"call"* (code will be sent via voice call) or *"flash_call"* + (code will be in the last 5 digits of caller's phone number). + + timeout (``int``): + Delay in seconds before calling :meth:`~Client.resend_code`. + """ + + def __init__( + self, *, + type: str, + phone_code_hash: str, + next_type: str = None, + timeout: int = None + ): + super().__init__() + + self.type = type + self.phone_code_hash = phone_code_hash + self.next_type = next_type + self.timeout = timeout + + @staticmethod + def _parse(sent_code: types.auth.SentCode) -> "SentCode": + type = sent_code.type + + if isinstance(type, types.auth.SentCodeTypeApp): + type = "app" + elif isinstance(type, types.auth.SentCodeTypeSms): + type = "sms" + elif isinstance(type, types.auth.SentCodeTypeCall): + type = "call" + elif isinstance(type, types.auth.SentCodeTypeFlashCall): + type = "flash_call" + + next_type = sent_code.next_type + + if isinstance(next_type, types.auth.CodeTypeSms): + next_type = "sms" + elif isinstance(next_type, types.auth.CodeTypeCall): + next_type = "call" + elif isinstance(next_type, types.auth.CodeTypeFlashCall): + next_type = "flash_call" + + return SentCode( + type=type, + phone_code_hash=sent_code.phone_code_hash, + next_type=next_type, + timeout=sent_code.timeout + ) diff --git a/pyrogram/client/types/authorization/terms_of_service.py b/pyrogram/client/types/authorization/terms_of_service.py new file mode 100644 index 00000000..dfc322d3 --- /dev/null +++ b/pyrogram/client/types/authorization/terms_of_service.py @@ -0,0 +1,56 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2019 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from typing import List + +from pyrogram.api import types +from ..messages_and_media import MessageEntity +from ..object import Object + + +class TermsOfService(Object): + """Telegram's Terms of Service returned by :meth:`~Client.sign_in`. + + Parameters: + id (``str``): + Terms of Service identifier. + + text (``str``): + Terms of Service text. + + entities (List of :obj:`MessageEntity`): + Special entities like URLs that appear in the text. + """ + + def __init__(self, *, id: str, text: str, entities: List[MessageEntity]): + super().__init__() + + self.id = id + self.text = text + self.entities = entities + + @staticmethod + def _parse(terms_of_service: types.help.TermsOfService) -> "TermsOfService": + return TermsOfService( + id=terms_of_service.id.data, + text=terms_of_service.text, + entities=[ + MessageEntity._parse(None, entity, {}) + for entity in terms_of_service.entities + ] + ) From 6982c436a88e88ef78124d5165e06560c834cec2 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 13 Aug 2019 13:18:01 +0200 Subject: [PATCH 09/21] Add the new auth-related methods and types to docs --- compiler/docs/compiler.py | 25 +++++++++++++++++++++++-- compiler/docs/template/methods.rst | 16 +++++++++++++++- compiler/docs/template/types.rst | 15 ++++++++++++++- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/compiler/docs/compiler.py b/compiler/docs/compiler.py index e515dc77..bbd85d90 100644 --- a/compiler/docs/compiler.py +++ b/compiler/docs/compiler.py @@ -128,10 +128,10 @@ def pyrogram_api(): utilities=""" Utilities start - stop - restart idle + stop run + restart add_handler remove_handler stop_transmission @@ -247,6 +247,22 @@ def pyrogram_api(): set_game_score get_game_high_scores """, + authorization=""" + Authorization + connect + disconnect + initialize + terminate + send_code + resend_code + sign_in + sign_up + get_password_hint + check_password + send_recovery_code + recover_password + accept_terms_of_service + """, advanced=""" Advanced send @@ -347,6 +363,11 @@ def pyrogram_api(): InputMessageContent InputMessageContent InputTextMessageContent + """, + authorization=""" + Authorization + SentCode + TermsOfService """ ) diff --git a/compiler/docs/template/methods.rst b/compiler/docs/template/methods.rst index 0de7ee87..a0c6df92 100644 --- a/compiler/docs/template/methods.rst +++ b/compiler/docs/template/methods.rst @@ -106,10 +106,24 @@ Bots {bots} +Authorization +------------- + +.. autosummary:: + :nosignatures: + + {authorization} + +.. toctree:: + :hidden: + + {authorization} + Advanced -------- -Learn more about these methods at :doc:`Advanced Usage <../../topics/advanced-usage>`. +Methods used only when dealing with the raw Telegram API. +Learn more about how to use the raw API at :doc:`Advanced Usage <../../topics/advanced-usage>`. .. autosummary:: :nosignatures: diff --git a/compiler/docs/template/types.rst b/compiler/docs/template/types.rst index 635a81d3..5c91e68f 100644 --- a/compiler/docs/template/types.rst +++ b/compiler/docs/template/types.rst @@ -92,4 +92,17 @@ InputMessageContent .. toctree:: :hidden: - {input_message_content} \ No newline at end of file + {input_message_content} + +Authorization +------------- + +.. autosummary:: + :nosignatures: + + {authorization} + +.. toctree:: + :hidden: + + {authorization} \ No newline at end of file From 4de203a81c02068ba3579372cb3378f4281b581b Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 13 Aug 2019 13:18:51 +0200 Subject: [PATCH 10/21] Add information about test/production servers when logging connections --- pyrogram/connection/connection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyrogram/connection/connection.py b/pyrogram/connection/connection.py index 0c325fae..cc1d4e03 100644 --- a/pyrogram/connection/connection.py +++ b/pyrogram/connection/connection.py @@ -39,6 +39,7 @@ class Connection: def __init__(self, dc_id: int, test_mode: bool, ipv6: bool, proxy: dict, mode: int = 3): self.dc_id = dc_id + self.test_mode = test_mode self.ipv6 = ipv6 self.proxy = proxy self.address = DataCenter(dc_id, test_mode, ipv6) @@ -59,7 +60,8 @@ class Connection: self.connection.close() time.sleep(1) else: - log.info("Connected! DC{} - IPv{} - {}".format( + log.info("Connected! {} DC{} - IPv{} - {}".format( + "Test" if self.test_mode else "Production", self.dc_id, "6" if self.ipv6 else "4", self.mode.__name__ From 7a2bddc301e3041edbb312308cd7c60cb7bb4692 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 13 Aug 2019 13:19:02 +0200 Subject: [PATCH 11/21] Add MEMBER_OCCUPY_PRIMARY_LOC_FAILED error --- compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv b/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv index 4bbea8ea..a2fd91cb 100644 --- a/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv +++ b/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv @@ -10,4 +10,5 @@ WORKER_BUSY_TOO_LONG_RETRY Telegram is having internal problems. Please try agai INTERDC_X_CALL_ERROR Telegram is having internal problems at DC{x}. Please try again later INTERDC_X_CALL_RICH_ERROR Telegram is having internal problems at DC{x}. Please try again later FOLDER_DEAC_AUTOFIX_ALL Telegram is having internal problems. Please try again later -MSGID_DECREASE_RETRY Telegram is having internal problems. Please try again later \ No newline at end of file +MSGID_DECREASE_RETRY Telegram is having internal problems. Please try again later +MEMBER_OCCUPY_PRIMARY_LOC_FAILED Telegram is having internal problems. Please try again later \ No newline at end of file From 4b914662ef4a67580beb5d5c03e1e57e95e2cab6 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 13 Aug 2019 13:20:21 +0200 Subject: [PATCH 12/21] Log the reason why a query ought to be re-tried --- pyrogram/session/session.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyrogram/session/session.py b/pyrogram/session/session.py index 689fe584..673bbcc9 100644 --- a/pyrogram/session/session.py +++ b/pyrogram/session/session.py @@ -26,11 +26,10 @@ from os import urandom from queue import Queue from threading import Event, Thread -from pyrogram.api.all import layer - import pyrogram from pyrogram import __copyright__, __license__, __version__ from pyrogram.api import functions, types, core +from pyrogram.api.all import layer from pyrogram.api.core import Message, TLObject, MsgContainer, Long, FutureSalt, Int from pyrogram.connection import Connection from pyrogram.crypto import AES, KDF @@ -440,9 +439,9 @@ class Session: raise e from None (log.warning if retries < 2 else log.info)( - "{}: {} Retrying {}".format( + "[{}] Retrying {} due to {}".format( Session.MAX_RETRIES - retries + 1, - datetime.now(), type(data))) + data.QUALNAME, e)) time.sleep(0.5) return self.send(data, retries - 1, timeout) From 55f5a1a36c85ce61937ae463b050ae2a17bc6e6c Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 17 Aug 2019 22:30:34 +0200 Subject: [PATCH 13/21] Fix IndexError when parsing empty nearby chats --- pyrogram/client/methods/chats/get_nearby_chats.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyrogram/client/methods/chats/get_nearby_chats.py b/pyrogram/client/methods/chats/get_nearby_chats.py index 7f9e3614..dc0910e8 100644 --- a/pyrogram/client/methods/chats/get_nearby_chats.py +++ b/pyrogram/client/methods/chats/get_nearby_chats.py @@ -57,6 +57,9 @@ class GetNearbyChats(BaseClient): ) ) + if not r.updates: + return [] + chats = pyrogram.List([pyrogram.Chat._parse_chat(self, chat) for chat in r.chats]) peers = r.updates[0].peers From b74a8eba5e02390f58375590c7a73dbc234b07f3 Mon Sep 17 00:00:00 2001 From: ColinShark Date: Tue, 20 Aug 2019 10:12:55 +0200 Subject: [PATCH 14/21] Add bound method for get_common_chats (#305) --- pyrogram/client/types/user_and_chats/user.py | 51 ++++++++++++++------ 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/pyrogram/client/types/user_and_chats/user.py b/pyrogram/client/types/user_and_chats/user.py index 783c0566..2be1f561 100644 --- a/pyrogram/client/types/user_and_chats/user.py +++ b/pyrogram/client/types/user_and_chats/user.py @@ -279,21 +279,21 @@ class User(Object, Update): def block(self): """Bound method *block* of :obj:`User`. - + Use as a shortcut for: - + .. code-block:: python - + client.block_user(123456789) - + Example: .. code-block:: python - + user.block() - + Returns: True on success. - + Raises: RPCError: In case of a Telegram RPC error. """ @@ -302,23 +302,46 @@ class User(Object, Update): def unblock(self): """Bound method *unblock* of :obj:`User`. - + Use as a shortcut for: - + .. code-block:: python - + client.unblock_user(123456789) - + Example: .. code-block:: python - + user.unblock() - + Returns: True on success. - + Raises: RPCError: In case of a Telegram RPC error. """ return self._client.unblock_user(self.id) + + def get_common_chats(self): + """Bound method *get_common_chats* of :obj:`User`. + + Use as a shortcut for: + + .. code-block:: python + + client.get_common_chats(123456789) + + Example: + .. code-block:: python + + user.get_common_chats() + + Returns: + True on success. + + Raises: + RPCError: In case of a Telegram RPC error. + """ + + return self._client.get_common_chats(self.id) From 4e119b27bd359490ae0872daa3fdb6eb6acbad42 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 21 Aug 2019 09:32:58 +0200 Subject: [PATCH 15/21] Don't stick navigation sidebar of documentation --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 47d0107e..2544800d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -58,7 +58,7 @@ html_show_copyright = False html_theme_options = { "canonical_url": "https://docs.pyrogram.org/", "collapse_navigation": True, - "sticky_navigation": True, + "sticky_navigation": False, "logo_only": True, "display_version": True, "style_external_links": True From 0011d12be50ce3e086971be28e65a15bb567ed9b Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 21 Aug 2019 09:56:33 +0200 Subject: [PATCH 16/21] Fix set_chat_photo not working because of new file ids --- pyrogram/client/methods/chats/set_chat_photo.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/pyrogram/client/methods/chats/set_chat_photo.py b/pyrogram/client/methods/chats/set_chat_photo.py index 71cd6590..c97f7a3c 100644 --- a/pyrogram/client/methods/chats/set_chat_photo.py +++ b/pyrogram/client/methods/chats/set_chat_photo.py @@ -17,7 +17,6 @@ # along with Pyrogram. If not, see . import os -from struct import unpack from typing import Union from pyrogram.api import functions, types @@ -62,15 +61,8 @@ class SetChatPhoto(BaseClient): if os.path.exists(photo): photo = types.InputChatUploadedPhoto(file=self.save_file(photo)) else: - unpacked = unpack(" Date: Wed, 28 Aug 2019 13:55:11 +0200 Subject: [PATCH 17/21] Fix Message.click default parameters --- pyrogram/client/types/messages_and_media/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/client/types/messages_and_media/message.py b/pyrogram/client/types/messages_and_media/message.py index 7616a6ec..0052e86d 100644 --- a/pyrogram/client/types/messages_and_media/message.py +++ b/pyrogram/client/types/messages_and_media/message.py @@ -2743,7 +2743,7 @@ class Message(Object, Update): revoke=revoke ) - def click(self, x: int or str, y: int = 0, quote: bool = None, timeout: int = 10): + def click(self, x: int or str = 0, y: int = None, quote: bool = None, timeout: int = 10): """Bound method *click* of :obj:`Message`. Use as a shortcut for clicking a button attached to the message instead of: From d15d38ba526d8b8d4d64ab46a21e52775b24a2bf Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 3 Sep 2019 15:45:47 +0200 Subject: [PATCH 18/21] Don't pass the client to progress callbacks anymore --- pyrogram/client/client.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 8b8f58f3..2d313229 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -1677,7 +1677,7 @@ class Client(Methods, BaseClient): file_part += 1 if progress: - progress(self, min(file_part * part_size, file_size), file_size, *progress_args) + progress(min(file_part * part_size, file_size), file_size, *progress_args) except Client.StopTransmission: raise except Exception as e: @@ -1808,7 +1808,6 @@ class Client(Methods, BaseClient): if progress: progress( - self, min(offset, file_size) if file_size != 0 else offset, @@ -1891,7 +1890,6 @@ class Client(Methods, BaseClient): if progress: progress( - self, min(offset, file_size) if file_size != 0 else offset, From f3b33ef1d50d78862573cc5a6312ddacc6103658 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 7 Sep 2019 12:49:35 +0200 Subject: [PATCH 19/21] Revert "Enhance Parser when dealing with leading and trailing whitespaces" This reverts commit 8cdcf90b --- pyrogram/client/parser/html.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyrogram/client/parser/html.py b/pyrogram/client/parser/html.py index 82499cb3..5ec43874 100644 --- a/pyrogram/client/parser/html.py +++ b/pyrogram/client/parser/html.py @@ -86,8 +86,7 @@ class Parser(HTMLParser): for entities in self.tag_entities.values(): for entity in entities: - entity.offset += len(data) - len(data.lstrip()) # Ignore left whitespaces for offsets - entity.length += len(data.strip()) # Ignore all whitespaces (left + right) for lengths + entity.length += len(data) self.text += data From 8db3d90c520f1044974585d414f0b9a6dee9ca67 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 7 Sep 2019 13:23:58 +0200 Subject: [PATCH 20/21] Update API schema to Layer 105 --- compiler/api/source/main_api.tl | 55 ++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/compiler/api/source/main_api.tl b/compiler/api/source/main_api.tl index f649abf4..d8c3eae1 100644 --- a/compiler/api/source/main_api.tl +++ b/compiler/api/source/main_api.tl @@ -86,7 +86,7 @@ storage.fileMp4#b3cea0e4 = storage.FileType; storage.fileWebp#1081464c = storage.FileType; userEmpty#200250ba id:int = User; -user#2e13f4c3 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true id:int access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?string bot_inline_placeholder:flags.19?string lang_code:flags.22?string = User; +user#938458c1 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true id:int access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string = User; userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto; userProfilePhoto#ecd75d8c photo_id:long photo_small:FileLocation photo_big:FileLocation dc_id:int = UserProfilePhoto; @@ -101,11 +101,11 @@ userStatusLastMonth#77ebc742 = UserStatus; chatEmpty#9ba2d800 id:int = Chat; chat#3bda1bde flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true deactivated:flags.5?true id:int title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; chatForbidden#7328bdb id:int title:string = Chat; -channel#4df30834 flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?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?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int = Chat; +channel#d31a961e flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true id:int access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int version:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights 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#1b7c9db3 flags:# can_set_username:flags.7?true id:int about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int = ChatFull; -channelFull#2d895c74 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_view_stats:flags.12?true can_set_location:flags.16?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?int location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int pts:int = ChatFull; +chatFull#1b7c9db3 flags:# can_set_username:flags.7?true has_scheduled:flags.8?true id:int about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int = ChatFull; +channelFull#2d895c74 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_view_stats:flags.12?true can_set_location:flags.16?true has_scheduled:flags.19?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?int location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int pts:int = ChatFull; chatParticipant#c8d7493e user_id:int inviter_id:int date:int = ChatParticipant; chatParticipantCreator#da13538a user_id:int = ChatParticipant; @@ -118,7 +118,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#475cdbd5 photo_small:FileLocation photo_big:FileLocation dc_id:int = ChatPhoto; messageEmpty#83e5de54 id:int = Message; -message#44f9b43d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long = Message; +message#452c0e65 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long restriction_reason:flags.22?Vector = Message; messageService#9e19a1f6 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?int to_id:Peer reply_to_msg_id:flags.3?int date:int action:MessageAction = Message; messageMediaEmpty#3ded6320 = MessageMedia; @@ -200,7 +200,7 @@ inputReportReasonOther#e1746d0a text:string = ReportReason; inputReportReasonCopyright#9b89f93a = ReportReason; inputReportReasonGeoIrrelevant#dbd4feed = ReportReason; -userFull#edf17c12 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true user:User about:flags.1?string settings:PeerSettings profile_photo:flags.2?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int = UserFull; +userFull#edf17c12 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true user:User about:flags.1?string settings:PeerSettings profile_photo:flags.2?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int = UserFull; contact#f911c994 user_id:int mutual:Bool = Contact; @@ -322,6 +322,9 @@ updateChatDefaultBannedRights#54c01850 peer:Peer default_banned_rights:ChatBanne updateFolderPeers#19360dc0 folder_peers:Vector pts:int pts_count:int = Update; updatePeerSettings#6a7e7366 peer:Peer settings:PeerSettings = Update; updatePeerLocated#b4afcfb0 peers:Vector = Update; +updateNewScheduledMessage#39a51dfb message:Message = Update; +updateDeleteScheduledMessages#90866cee peer:Peer messages:Vector = Update; +updateTheme#8216fba3 theme:Theme = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -418,6 +421,7 @@ inputPrivacyKeyPhoneP2P#db9e70d2 = InputPrivacyKey; inputPrivacyKeyForwards#a4dd4c08 = InputPrivacyKey; inputPrivacyKeyProfilePhoto#5719bacc = InputPrivacyKey; inputPrivacyKeyPhoneNumber#352dafa = InputPrivacyKey; +inputPrivacyKeyAddedByPhone#d1219bdd = InputPrivacyKey; privacyKeyStatusTimestamp#bc2eab30 = PrivacyKey; privacyKeyChatInvite#500e6dfa = PrivacyKey; @@ -426,6 +430,7 @@ privacyKeyPhoneP2P#39491cc8 = PrivacyKey; privacyKeyForwards#69ec56a3 = PrivacyKey; privacyKeyProfilePhoto#96151fed = PrivacyKey; privacyKeyPhoneNumber#d19ae46d = PrivacyKey; +privacyKeyAddedByPhone#42ffd42b = PrivacyKey; inputPrivacyValueAllowContacts#d09e07b = InputPrivacyRule; inputPrivacyValueAllowAll#184b35ce = InputPrivacyRule; @@ -469,7 +474,7 @@ messages.affectedMessages#84d19185 pts:int pts_count:int = messages.AffectedMess webPageEmpty#eb1477e8 id:long = WebPage; webPagePending#c586da1c id:long date:int = WebPage; -webPage#5f07b4bc flags:# id:long url:string display_url:string hash:int type:flags.0?string site_name:flags.1?string title:flags.2?string description:flags.3?string photo:flags.4?Photo embed_url:flags.5?string embed_type:flags.5?string embed_width:flags.6?int embed_height:flags.6?int duration:flags.7?int author:flags.8?string document:flags.9?Document cached_page:flags.10?Page = WebPage; +webPage#fa64e172 flags:# id:long url:string display_url:string hash:int type:flags.0?string site_name:flags.1?string title:flags.2?string description:flags.3?string photo:flags.4?Photo embed_url:flags.5?string embed_type:flags.5?string embed_width:flags.6?int embed_height:flags.6?int duration:flags.7?int author:flags.8?string document:flags.9?Document documents:flags.11?Vector cached_page:flags.10?Page = WebPage; webPageNotModified#85849473 = WebPage; authorization#ad01d61d flags:# current:flags.0?true official_app:flags.1?true password_pending:flags.2?true hash:long device_model:string platform:string system_version:string api_id:int app_name:string app_version:string date_created:int date_active:int ip:string country:string region:string = Authorization; @@ -1040,6 +1045,17 @@ channelLocation#209b82db geo_point:GeoPoint address:string = ChannelLocation; peerLocated#ca461b5d peer:Peer expires:int distance:int = PeerLocated; +restrictionReason#d072acb4 platform:string reason:string text:string = RestrictionReason; + +inputTheme#3c5693e9 id:long access_hash:long = InputTheme; +inputThemeSlug#f5890df1 slug:string = InputTheme; + +themeDocumentNotModified#483d270c = Theme; +theme#f7d90ce0 flags:# creator:flags.0?true default:flags.1?true id:long access_hash:long slug:string title:string document:flags.2?Document installs_count:int = Theme; + +account.themesNotModified#f41eb622 = account.Themes; +account.themes#7f676421 hash:int themes:Vector = account.Themes; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1121,6 +1137,13 @@ account.installWallPaper#feed5769 wallpaper:InputWallPaper settings:WallPaperSet account.resetWallPapers#bb3b9804 = Bool; account.getAutoDownloadSettings#56da0b3f = account.AutoDownloadSettings; account.saveAutoDownloadSettings#76f36233 flags:# low:flags.0?true high:flags.1?true settings:AutoDownloadSettings = Bool; +account.uploadTheme#1c3db333 flags:# file:InputFile thumb:flags.0?InputFile file_name:string mime_type:string = Document; +account.createTheme#2b7ffd7f slug:string title:string document:InputDocument = Theme; +account.updateTheme#3b8ea202 flags:# format:string theme:InputTheme slug:flags.0?string title:flags.1?string document:flags.2?InputDocument = Theme; +account.saveTheme#f257106c theme:InputTheme unsave:Bool = Bool; +account.installTheme#7ae43737 flags:# dark:flags.0?true format:flags.1?string theme:flags.1?InputTheme = Bool; +account.getTheme#8d9d742b format:string theme:InputTheme document_id:long = Theme; +account.getThemes#285946f8 format:string hash:int = account.Themes; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#ca30a5b1 id:InputUser = UserFull; @@ -1155,9 +1178,9 @@ messages.deleteHistory#1c015b09 flags:# just_clear:flags.0?true revoke:flags.1?t messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector = messages.AffectedMessages; messages.receivedMessages#5a954c0 max_id:int = Vector; messages.setTyping#a3825e50 peer:InputPeer action:SendMessageAction = Bool; -messages.sendMessage#fa88427a flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Updates; -messages.sendMedia#b8d1262b flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Updates; -messages.forwardMessages#708e0195 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true grouped:flags.9?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer = Updates; +messages.sendMessage#520c3870 flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int = Updates; +messages.sendMedia#3491eba9 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int = Updates; +messages.forwardMessages#d9fee60e flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true grouped:flags.9?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer schedule_date:flags.10?int = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.getPeerSettings#3672e09c peer:InputPeer = PeerSettings; messages.report#bd82b658 peer:InputPeer id:Vector reason:ReportReason = Bool; @@ -1201,9 +1224,9 @@ messages.getSavedGifs#83bf3d52 hash:int = messages.SavedGifs; messages.saveGif#327a30cb id:InputDocument unsave:Bool = Bool; messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults; 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 hide_via:flags.11?true peer:InputPeer reply_to_msg_id:flags.0?int random_id:long query_id:long id:string = Updates; +messages.sendInlineBotResult#220815b0 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to_msg_id:flags.0?int random_id:long query_id:long id:string schedule_date:flags.10?int = Updates; messages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData; -messages.editMessage#d116f31e flags:# no_webpage:flags.1?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Updates; +messages.editMessage#48f71778 flags:# no_webpage:flags.1?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.15?int = Updates; messages.editInlineBotMessage#83557dba flags:# no_webpage:flags.1?true id:InputBotInlineMessageID message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = 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; @@ -1237,7 +1260,7 @@ messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; messages.getUnreadMentions#46578472 peer:InputPeer offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.readMentions#f0189d3 peer:InputPeer = messages.AffectedHistory; messages.getRecentLocations#bbc45b09 peer:InputPeer limit:int hash:int = messages.Messages; -messages.sendMultiMedia#2095512f flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int multi_media:Vector = Updates; +messages.sendMultiMedia#cc0110cb flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int multi_media:Vector schedule_date:flags.10?int = Updates; messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile; messages.searchStickerSets#c2b7d08b flags:# exclude_featured:flags.0?true q:string hash:int = messages.FoundStickerSets; messages.getSplitRanges#1cff7e08 = Vector; @@ -1259,6 +1282,10 @@ messages.getSearchCounters#732eef00 peer:InputPeer filters:Vector = messages.Messages; +messages.sendScheduledMessages#bd38850a peer:InputPeer id:Vector = Updates; +messages.deleteScheduledMessages#59ae2b16 peer:InputPeer id:Vector = Updates; updates.getState#edd4882a = updates.State; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; @@ -1366,4 +1393,4 @@ langpack.getLanguage#6a596502 lang_pack:string lang_code:string = LangPackLangua folders.editPeerFolders#6847d0ab folder_peers:Vector = Updates; folders.deleteFolder#1c295881 folder_id:int = Updates; -// LAYER 104 \ No newline at end of file +// LAYER 105 \ No newline at end of file From 5ce62bd79c3503272e01341f1252382a592a26fc Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 7 Sep 2019 13:28:05 +0200 Subject: [PATCH 21/21] Add new Restriction object and make User and Chat objects use it --- compiler/docs/compiler.py | 1 + pyrogram/client/types/user_and_chats/chat.py | 16 +++--- .../types/user_and_chats/restriction.py | 50 +++++++++++++++++++ pyrogram/client/types/user_and_chats/user.py | 12 +++-- 4 files changed, 67 insertions(+), 12 deletions(-) create mode 100644 pyrogram/client/types/user_and_chats/restriction.py diff --git a/compiler/docs/compiler.py b/compiler/docs/compiler.py index ffbaffb6..34db202f 100644 --- a/compiler/docs/compiler.py +++ b/compiler/docs/compiler.py @@ -309,6 +309,7 @@ def pyrogram_api(): ChatMember ChatPermissions Dialog + Restriction """, messages_media=""" Messages & Media diff --git a/pyrogram/client/types/user_and_chats/chat.py b/pyrogram/client/types/user_and_chats/chat.py index 64507cb5..642c1ea2 100644 --- a/pyrogram/client/types/user_and_chats/chat.py +++ b/pyrogram/client/types/user_and_chats/chat.py @@ -16,12 +16,13 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from typing import Union +from typing import Union, List import pyrogram from pyrogram.api import types from .chat_permissions import ChatPermissions from .chat_photo import ChatPhoto +from .restriction import Restriction from ..object import Object from ...ext import utils @@ -87,8 +88,8 @@ class Chat(Object): members_count (``int``, *optional*): Chat members count, for groups, supergroups and channels only. - restriction_reason (``str``, *optional*): - The reason why this chat might be unavailable to some users. + restrictions (List of :obj:`Restriction`, *optional*): + The list of reasons why this chat might be unavailable to some users. This field is available only in case *is_restricted* is True. permissions (:obj:`ChatPermissions` *optional*): @@ -120,7 +121,7 @@ class Chat(Object): sticker_set_name: str = None, can_set_sticker_set: bool = None, members_count: int = None, - restriction_reason: str = None, + restrictions: List[Restriction] = None, permissions: "pyrogram.ChatPermissions" = None, distance: int = None ): @@ -143,7 +144,7 @@ class Chat(Object): self.sticker_set_name = sticker_set_name self.can_set_sticker_set = can_set_sticker_set self.members_count = members_count - self.restriction_reason = restriction_reason + self.restrictions = restrictions self.permissions = permissions self.distance = distance @@ -162,7 +163,7 @@ class Chat(Object): first_name=user.first_name, last_name=user.last_name, photo=ChatPhoto._parse(client, user.photo, peer_id), - restriction_reason=user.restriction_reason, + restrictions=pyrogram.List([Restriction._parse(r) for r in user.restriction_reason]) or None, client=client ) @@ -183,6 +184,7 @@ class Chat(Object): @staticmethod def _parse_channel_chat(client, channel: types.Channel) -> "Chat": peer_id = utils.get_channel_id(channel.id) + restriction_reason = getattr(channel, "restriction_reason", []) return Chat( id=peer_id, @@ -193,7 +195,7 @@ class Chat(Object): title=channel.title, username=getattr(channel, "username", None), photo=ChatPhoto._parse(client, getattr(channel, "photo", None), peer_id), - restriction_reason=getattr(channel, "restriction_reason", None), + restrictions=pyrogram.List([Restriction._parse(r) for r in restriction_reason]) or None, permissions=ChatPermissions._parse(getattr(channel, "default_banned_rights", None)), members_count=getattr(channel, "participants_count", None), client=client diff --git a/pyrogram/client/types/user_and_chats/restriction.py b/pyrogram/client/types/user_and_chats/restriction.py new file mode 100644 index 00000000..b9678985 --- /dev/null +++ b/pyrogram/client/types/user_and_chats/restriction.py @@ -0,0 +1,50 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2019 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from pyrogram.api import types +from ..object import Object + + +class Restriction(Object): + """A restriction applied to bots or chats. + + Parameters: + platform (``str``): + The platform the restriction is applied to, e.g. "ios", "android" + + reason (``str``): + The restriction reason, e.g. "porn", "copyright". + + text (``str``): + The restriction text. + """ + + def __init__(self, *, platform: str, reason: str, text: str): + super().__init__(None) + + self.platform = platform + self.reason = reason + self.text = text + + @staticmethod + def _parse(restriction: types.RestrictionReason) -> "Restriction": + return Restriction( + platform=restriction.platform, + reason=restriction.reason, + text=restriction.text + ) diff --git a/pyrogram/client/types/user_and_chats/user.py b/pyrogram/client/types/user_and_chats/user.py index 2be1f561..878a2084 100644 --- a/pyrogram/client/types/user_and_chats/user.py +++ b/pyrogram/client/types/user_and_chats/user.py @@ -17,10 +17,12 @@ # along with Pyrogram. If not, see . import html +from typing import List import pyrogram from pyrogram.api import types from .chat_photo import ChatPhoto +from .restriction import Restriction from ..object import Object from ..update import Update @@ -101,8 +103,8 @@ class User(Object, Update): photo (:obj:`ChatPhoto `, *optional*): User's or bot's current profile photo. Suitable for downloads only. - restriction_reason (``str``, *optional*): - The reason why this bot might be unavailable to some users. + restrictions (List of :obj:`Restriction`, *optional*): + The list of reasons why this bot might be unavailable to some users. This field is available only in case *is_restricted* is True. """ @@ -130,7 +132,7 @@ class User(Object, Update): dc_id: int = None, phone_number: str = None, photo: ChatPhoto = None, - restriction_reason: str = None + restrictions: List[Restriction] = None ): super().__init__(client) @@ -154,7 +156,7 @@ class User(Object, Update): self.dc_id = dc_id self.phone_number = phone_number self.photo = photo - self.restriction_reason = restriction_reason + self.restrictions = restrictions def __format__(self, format_spec): if format_spec == "mention": @@ -186,7 +188,7 @@ class User(Object, Update): dc_id=getattr(user.photo, "dc_id", None), phone_number=user.phone, photo=ChatPhoto._parse(client, user.photo, user.id), - restriction_reason=user.restriction_reason, + restrictions=pyrogram.List([Restriction._parse(r) for r in user.restriction_reason]) or None, client=client )