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 diff --git a/compiler/docs/compiler.py b/compiler/docs/compiler.py index 212cf370..ffbaffb6 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 @@ -249,6 +249,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 @@ -349,6 +365,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 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 diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 5cb003b6..dd587dd6 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,70 +278,64 @@ class Client(Methods, BaseClient): self._proxy["enabled"] = bool(value.get("enabled", True)) self._proxy.update(value) - def start(self): - """Start the client. + def connect(self) -> bool: + """ + Connect the client to Telegram servers. - 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. + 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 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 connect an already connected client. """ - if self.is_started: - raise ConnectionError("Client has already been started") + 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 - 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() + self.is_connected = True - 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)) + return bool(self.storage.user_id) - now = time.time() + def disconnect(self): + """Disconnect the client from Telegram servers. - 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 + 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") + + self.session.stop() + self.storage.close() + self.is_connected = False + + def initialize(self): + """Initialize the client by starting up workers. + + 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 initialize a disconnected client or in case you try to initialize an + already initialized client. + """ + 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_plugins() for i in range(self.UPDATES_WORKERS): self.updates_workers_list.append( @@ -367,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()) @@ -426,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 @@ -437,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. @@ -462,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. @@ -525,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. @@ -628,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 @@ -656,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. @@ -1211,8 +1667,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) @@ -1444,37 +1900,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. @@ -1495,9 +1951,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: @@ -1678,7 +2136,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: @@ -1813,7 +2271,6 @@ class Client(Methods, BaseClient): if progress: progress( - self, min(offset, file_size) if file_size != 0 else offset, @@ -1896,7 +2353,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 + ] + ) 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__ 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)