class TelegramThinClient(BaseClient): def __init__(self): super().__init__() self.api_id = 1 self.app_version = '1' self.device_model = '1' self.system_version = '1' self.lang_code = 'en' self.ipv6 = False self.proxy = {} self.storage = MemoryStorage(':memory:') def __enter__(self): try: self.storage.open() self.storage.auth_key(Auth(self, self.storage.dc_id()).create()) Session.notice_displayed = True self.session = Session(self, self.storage.dc_id(), self.storage.auth_key()) self.session.start() except (Exception, KeyboardInterrupt): self.disconnect() raise else: return self def __exit__(self, *args): self.session.stop() self.storage.close() def send(self, data: TLObject, retries: int = Session.MAX_RETRIES, timeout: float = Session.WAIT_TIMEOUT): return self.session.send(data, retries, timeout)
def authorize_bot(self): try: r = self.send( functions.auth.ImportBotAuthorization( flags=0, api_id=self.api_id, api_hash=self.api_hash, bot_auth_token=self.bot_token ) ) except UserMigrate as e: self.session.stop() self.dc_id = e.x self.auth_key = Auth(self.dc_id, self.test_mode, self.ipv6, self._proxy).create() self.session = Session( self, self.dc_id, self.auth_key ) self.session.start() self.authorize_bot() else: self.user_id = r.user.id
async def generate_media_session(self, client: Client, file_id: FileId) -> Session: """ Generates the media session for the DC that contains the media file. This is required for getting the bytes from Telegram servers. """ media_session = client.media_sessions.get(file_id.dc_id, None) if media_session is None: if file_id.dc_id != await client.storage.dc_id(): media_session = Session( client, file_id.dc_id, await Auth( client, file_id.dc_id, await client.storage.test_mode() ).create(), await client.storage.test_mode(), is_media=True, ) await media_session.start() for _ in range(6): exported_auth = await client.send( raw.functions.auth.ExportAuthorization(dc_id=file_id.dc_id) ) try: await media_session.send( raw.functions.auth.ImportAuthorization( id=exported_auth.id, bytes=exported_auth.bytes ) ) break except AuthBytesInvalid: logging.debug( f"Invalid authorization bytes for DC {file_id.dc_id}" ) continue else: await media_session.stop() raise AuthBytesInvalid else: media_session = Session( client, file_id.dc_id, await client.storage.auth_key(), await client.storage.test_mode(), is_media=True, ) await media_session.start() logging.debug(f"Created media session for DC {file_id.dc_id}") client.media_sessions[file_id.dc_id] = media_session else: logging.debug(f"Using cached media session for DC {file_id.dc_id}") return media_session
def __enter__(self): try: self.storage.open() self.storage.auth_key(Auth(self, self.storage.dc_id()).create()) Session.notice_displayed = True self.session = Session(self, self.storage.dc_id(), self.storage.auth_key()) self.session.start() except (Exception, KeyboardInterrupt): self.disconnect() raise else: return self
async def get_session(client: "pyrogram.Client", dc_id: int): if dc_id == await client.storage.dc_id(): return client async with lock: global session if session is not None: return session session = Session(client, dc_id, await Auth(client, dc_id, False).create(), False, is_media=True) await session.start() for _ in range(3): exported_auth = await client.send( raw.functions.auth.ExportAuthorization(dc_id=dc_id)) try: await session.send( raw.functions.auth.ImportAuthorization( id=exported_auth.id, bytes=exported_auth.bytes)) except AuthBytesInvalid: continue else: break else: await session.stop() raise AuthBytesInvalid return session
async def connect(self) -> bool: """ Connect the client to Telegram servers. 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() await self.load_session() self.session = Session(self, await self.storage.dc_id(), await self.storage.auth_key(), await self.storage.test_mode()) await self.session.start() self.is_connected = True return bool(await self.storage.user_id())
async def generate_media_session(self, client: Client, msg: Message): data = await self.generate_file_properties(msg) media_session = client.media_sessions.get(data.dc_id, None) if media_session is None: if data.dc_id != await client.storage.dc_id(): media_session = Session( client, data.dc_id, await Auth(client, data.dc_id, await client.storage.test_mode()).create(), await client.storage.test_mode(), is_media=True ) await media_session.start() for _ in range(3): exported_auth = await client.send( raw.functions.auth.ExportAuthorization( dc_id=data.dc_id ) ) try: await media_session.send( raw.functions.auth.ImportAuthorization( id=exported_auth.id, bytes=exported_auth.bytes ) ) except AuthBytesInvalid: continue else: break else: await media_session.stop() raise AuthBytesInvalid else: media_session = Session( client, data.dc_id, await client.storage.auth_key(), await client.storage.test_mode(), is_media=True ) await media_session.start() client.media_sessions[data.dc_id] = media_session return media_session
async def get_dc_session(self, dc_id: int) -> Session: async with self.media_sessions_lock: session = self.media_sessions.get(dc_id, None) if session is None: if dc_id != await self.storage.dc_id(): session = Session( self, dc_id, await Auth(self, dc_id, await self.storage.test_mode()).create(), await self.storage.test_mode(), is_media=True ) await session.start() for _ in range(3): exported_auth = await self.send( raw.functions.auth.ExportAuthorization( dc_id=dc_id ) ) try: await session.send( raw.functions.auth.ImportAuthorization( id=exported_auth.id, bytes=exported_auth.bytes ) ) except AuthBytesInvalid: continue else: break else: await session.stop() raise AuthBytesInvalid else: session = Session( self, dc_id, await self.storage.auth_key(), await self.storage.test_mode(), is_media=True ) await session.start() self.media_sessions[dc_id] = session return session
async def sign_in_bot(self, bot_token: str) -> "types.User": """Authorize a bot using its bot token generated by BotFather. Parameters: bot_token (``str``): The bot token generated by BotFather Returns: :obj:`~pyrogram.types.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 = await self.send( raw.functions.auth.ImportBotAuthorization( flags=0, api_id=self.api_id, api_hash=self.api_hash, bot_auth_token=bot_token ) ) except UserMigrate as e: await self.session.stop() await self.storage.dc_id(e.x) await self.storage.auth_key( await Auth( self, await self.storage.dc_id(), await self.storage.test_mode() ).create() ) self.session = Session( self, await self.storage.dc_id(), await self.storage.auth_key(), await self.storage.test_mode() ) await self.session.start() else: await self.storage.user_id(r.user.id) await self.storage.is_bot(True) return types.User._parse(self, r.user)
async def send_code(self, phone_number: str) -> "types.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:`~pyrogram.types.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 = await self.send( raw.functions.auth.SendCode( phone_number=phone_number, api_id=self.api_id, api_hash=self.api_hash, settings=raw.types.CodeSettings() ) ) except (PhoneMigrate, NetworkMigrate) as e: await self.session.stop() await self.storage.dc_id(e.x) await self.storage.auth_key( await Auth( self, await self.storage.dc_id(), await self.storage.test_mode() ).create() ) self.session = Session( self, await self.storage.dc_id(), await self.storage.auth_key(), await self.storage.test_mode() ) await self.session.start() else: return types.SentCode._parse(r)
def start(self): """Use this method to start the Client after creating it. Requires no parameters. Raises: :class:`Error <pyrogram.Error>` """ if self.is_started: raise ConnectionError("Client has already been started") if self.BOT_TOKEN_RE.match(self.session_name): self.bot_token = self.session_name self.session_name = self.session_name.split(":")[0] self.load_config() self.load_session() self.session = Session( self, self.dc_id, self.auth_key ) self.session.start() self.is_started = True try: if self.user_id is None: if self.bot_token is None: self.authorize_user() else: self.authorize_bot() self.save_session() if self.bot_token is None: now = time.time() if abs(now - self.date) > Client.OFFLINE_SLEEP: self.peers_by_username = {} self.peers_by_phone = {} self.get_initial_dialogs() self.get_contacts() else: self.send(functions.messages.GetPinnedDialogs()) 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( target=self.updates_worker, name="UpdatesWorker#{}".format(i + 1) ) ) self.updates_workers_list[-1].start() 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() self.dispatcher.start() mimetypes.init() Syncer.add(self)
def get_file(self, dc_id: int, id: int = None, access_hash: int = None, volume_id: int = None, local_id: int = None, secret: int = None, version: int = 0, size: int = None, progress: callable = None, progress_args: tuple = None) -> str: with self.media_sessions_lock: session = self.media_sessions.get(dc_id, None) if session is None: if dc_id != self.dc_id: exported_auth = self.send( functions.auth.ExportAuthorization( dc_id=dc_id ) ) session = Session( self, dc_id, Auth(dc_id, self.test_mode, self.ipv6, self._proxy).create(), is_media=True ) session.start() self.media_sessions[dc_id] = session session.send( functions.auth.ImportAuthorization( id=exported_auth.id, bytes=exported_auth.bytes ) ) else: session = Session( self, dc_id, self.auth_key, is_media=True ) session.start() self.media_sessions[dc_id] = session if volume_id: # Photos are accessed by volume_id, local_id, secret location = types.InputFileLocation( volume_id=volume_id, local_id=local_id, secret=secret ) else: # Any other file can be more easily accessed by id and access_hash location = types.InputDocumentFileLocation( id=id, access_hash=access_hash, version=version ) limit = 1024 * 1024 offset = 0 file_name = "" try: r = session.send( functions.upload.GetFile( location=location, offset=offset, limit=limit ) ) if isinstance(r, types.upload.File): with tempfile.NamedTemporaryFile("wb", delete=False) as f: file_name = f.name while True: chunk = r.bytes if not chunk: break f.write(chunk) offset += limit if progress: progress(self, min(offset, size), size, *progress_args) r = session.send( functions.upload.GetFile( location=location, offset=offset, limit=limit ) ) elif isinstance(r, types.upload.FileCdnRedirect): with self.media_sessions_lock: cdn_session = self.media_sessions.get(r.dc_id, None) if cdn_session is None: cdn_session = Session( self, r.dc_id, Auth(r.dc_id, self.test_mode, self.ipv6, self._proxy).create(), is_media=True, is_cdn=True ) cdn_session.start() self.media_sessions[r.dc_id] = cdn_session try: with tempfile.NamedTemporaryFile("wb", delete=False) as f: file_name = f.name while True: r2 = cdn_session.send( functions.upload.GetCdnFile( file_token=r.file_token, offset=offset, limit=limit ) ) if isinstance(r2, types.upload.CdnFileReuploadNeeded): try: session.send( functions.upload.ReuploadCdnFile( file_token=r.file_token, request_token=r2.request_token ) ) except VolumeLocNotFound: break else: continue chunk = r2.bytes # https://core.telegram.org/cdn#decrypting-files decrypted_chunk = AES.ctr256_decrypt( chunk, r.encryption_key, bytearray( r.encryption_iv[:-4] + (offset // 16).to_bytes(4, "big") ) ) hashes = session.send( functions.upload.GetCdnFileHashes( r.file_token, offset ) ) # https://core.telegram.org/cdn#verifying-files for i, h in enumerate(hashes): cdn_chunk = decrypted_chunk[h.limit * i: h.limit * (i + 1)] assert h.hash == sha256(cdn_chunk).digest(), "Invalid CDN hash part {}".format(i) f.write(decrypted_chunk) offset += limit if progress: progress(self, min(offset, size), size, *progress_args) if len(chunk) < limit: break except Exception as e: raise e except Exception as e: log.error(e, exc_info=True) try: os.remove(file_name) except OSError: pass return "" else: return file_name
def save_file(self, path: str, file_id: int = None, file_part: int = 0, progress: callable = None, progress_args: tuple = ()): part_size = 512 * 1024 file_size = os.path.getsize(path) file_total_parts = int(math.ceil(file_size / part_size)) is_big = True if file_size > 10 * 1024 * 1024 else False is_missing_part = True if file_id is not None else False file_id = file_id or self.rnd_id() md5_sum = md5() if not is_big and not is_missing_part else None session = Session(self, self.dc_id, self.auth_key, is_media=True) session.start() try: with open(path, "rb") as f: f.seek(part_size * file_part) while True: chunk = f.read(part_size) if not chunk: if not is_big: md5_sum = "".join([hex(i)[2:].zfill(2) for i in md5_sum.digest()]) break if is_big: rpc = functions.upload.SaveBigFilePart( file_id=file_id, file_part=file_part, file_total_parts=file_total_parts, bytes=chunk ) else: rpc = functions.upload.SaveFilePart( file_id=file_id, file_part=file_part, bytes=chunk ) assert session.send(rpc), "Couldn't upload file" if is_missing_part: return if not is_big: md5_sum.update(chunk) file_part += 1 if progress: progress(self, min(file_part * part_size, file_size), file_size, *progress_args) except Exception as e: log.error(e, exc_info=True) else: if is_big: return types.InputFileBig( id=file_id, parts=file_total_parts, name=os.path.basename(path), ) else: return types.InputFile( id=file_id, parts=file_total_parts, name=os.path.basename(path), md5_checksum=md5_sum ) finally: session.stop()
async def save_file(self, path: Union[str, BinaryIO], file_id: int = None, file_part: int = 0, progress: callable = None, progress_args: tuple = ()): """Upload a file onto Telegram servers, without actually sending the message to anyone. Useful whenever an InputFile type is required. .. note:: This is a utility method intended to be used **only** when working with raw :obj:`functions <pyrogram.api.functions>` (i.e: a Telegram API method you wish to use which is not available yet in the Client class as an easy-to-use method). Parameters: path (``str`` | ``BinaryIO``): The path of the file you want to upload that exists on your local machine or a binary file-like object with its attribute ".name" set for in-memory uploads. file_id (``int``, *optional*): In case a file part expired, pass the file_id and the file_part to retry uploading that specific chunk. file_part (``int``, *optional*): In case a file part expired, pass the file_id and the file_part to retry uploading that specific chunk. progress (``callable``, *optional*): Pass a callback function to view the file transmission progress. The function must take *(current, total)* as positional arguments (look at Other Parameters below for a detailed description) and will be called back each time a new file chunk has been successfully transmitted. progress_args (``tuple``, *optional*): Extra custom arguments for the progress callback function. You can pass anything you need to be available in the progress callback scope; for example, a Message object or a Client instance in order to edit the message with the updated progress status. Other Parameters: current (``int``): The amount of bytes transmitted so far. total (``int``): The total size of the file. *args (``tuple``, *optional*): Extra custom arguments as defined in the ``progress_args`` parameter. You can either keep ``*args`` or add every single extra argument in your function signature. Returns: ``InputFile``: On success, the uploaded file is returned in form of an InputFile object. Raises: RPCError: In case of a Telegram RPC error. """ if path is None: return None async def worker(session): while True: data = await queue.get() if data is None: return try: await self.loop.create_task(session.send(data)) except Exception as e: log.error(e) part_size = 512 * 1024 if isinstance(path, (str, PurePath)): fp = open(path, "rb") elif isinstance(path, io.IOBase): fp = path else: raise ValueError( "Invalid file. Expected a file path as string or a binary (not text) file pointer" ) file_name = fp.name fp.seek(0, os.SEEK_END) file_size = fp.tell() fp.seek(0) if file_size == 0: raise ValueError("File size equals to 0 B") if file_size > 2000 * 1024 * 1024: raise ValueError( "Telegram doesn't support uploading files bigger than 2000 MiB" ) file_total_parts = int(math.ceil(file_size / part_size)) is_big = file_size > 10 * 1024 * 1024 pool_size = 3 if is_big else 1 workers_count = 4 if is_big else 1 is_missing_part = file_id is not None file_id = file_id or self.rnd_id() md5_sum = md5() if not is_big and not is_missing_part else None pool = [ Session(self, await self.storage.dc_id(), await self.storage.auth_key(), await self.storage.test_mode(), is_media=True) for _ in range(pool_size) ] workers = [ self.loop.create_task(worker(session)) for session in pool for _ in range(workers_count) ] queue = asyncio.Queue(16) try: for session in pool: await session.start() with fp: fp.seek(part_size * file_part) while True: chunk = fp.read(part_size) if not chunk: if not is_big and not is_missing_part: md5_sum = "".join([ hex(i)[2:].zfill(2) for i in md5_sum.digest() ]) break if is_big: rpc = raw.functions.upload.SaveBigFilePart( file_id=file_id, file_part=file_part, file_total_parts=file_total_parts, bytes=chunk) else: rpc = raw.functions.upload.SaveFilePart( file_id=file_id, file_part=file_part, bytes=chunk) await queue.put(rpc) if is_missing_part: return if not is_big and not is_missing_part: md5_sum.update(chunk) file_part += 1 if progress: func = functools.partial( progress, min(file_part * part_size, file_size), file_size, *progress_args) if inspect.iscoroutinefunction(progress): await func() else: await self.loop.run_in_executor( self.executor, func) except StopTransmission: raise except Exception as e: log.error(e, exc_info=True) else: if is_big: return raw.types.InputFileBig( id=file_id, parts=file_total_parts, name=file_name, ) else: return raw.types.InputFile(id=file_id, parts=file_total_parts, name=file_name, md5_checksum=md5_sum) finally: for _ in workers: await queue.put(None) await asyncio.gather(*workers) for session in pool: await session.stop()
def authorize_user(self): phone_number_invalid_raises = self.phone_number is not None phone_code_invalid_raises = self.phone_code is not None password_hash_invalid_raises = self.password is not None first_name_invalid_raises = self.first_name is not None while True: if self.phone_number is None: self.phone_number = input("Enter phone number: ") while True: confirm = input("Is \"{}\" correct? (y/n): ".format(self.phone_number)) if confirm in ("y", "1"): break elif confirm in ("n", "2"): self.phone_number = input("Enter phone number: ") self.phone_number = self.phone_number.strip("+") try: r = self.send( functions.auth.SendCode( self.phone_number, self.api_id, self.api_hash ) ) except (PhoneMigrate, NetworkMigrate) as e: self.session.stop() self.dc_id = e.x self.auth_key = Auth(self.dc_id, self.test_mode, self.ipv6, self._proxy).create() self.session = Session( self, self.dc_id, self.auth_key ) self.session.start() r = self.send( functions.auth.SendCode( self.phone_number, self.api_id, self.api_hash ) ) break except (PhoneNumberInvalid, PhoneNumberBanned) as e: if phone_number_invalid_raises: raise else: print(e.MESSAGE) self.phone_number = None except FloodWait as e: if phone_number_invalid_raises: raise else: print(e.MESSAGE.format(x=e.x)) time.sleep(e.x) except Exception as e: log.error(e, exc_info=True) else: break phone_registered = r.phone_registered phone_code_hash = r.phone_code_hash terms_of_service = r.terms_of_service if terms_of_service: print("\n" + terms_of_service.text + "\n") if self.force_sms: self.send( functions.auth.ResendCode( phone_number=self.phone_number, phone_code_hash=phone_code_hash ) ) while True: self.phone_code = ( input("Enter phone code: ") if self.phone_code is None else self.phone_code if type(self.phone_code) is str else str(self.phone_code(self.phone_number)) ) try: if phone_registered: r = self.send( functions.auth.SignIn( self.phone_number, phone_code_hash, self.phone_code ) ) else: try: self.send( functions.auth.SignIn( self.phone_number, phone_code_hash, self.phone_code ) ) except PhoneNumberUnoccupied: pass self.first_name = self.first_name if self.first_name is not None else input("First name: ") self.last_name = self.last_name if self.last_name is not None else input("Last name: ") r = self.send( functions.auth.SignUp( self.phone_number, phone_code_hash, self.phone_code, self.first_name, self.last_name ) ) except (PhoneCodeInvalid, PhoneCodeEmpty, PhoneCodeExpired, PhoneCodeHashEmpty) as e: if phone_code_invalid_raises: raise else: print(e.MESSAGE) self.phone_code = None except FirstnameInvalid as e: if first_name_invalid_raises: raise else: print(e.MESSAGE) self.first_name = None except SessionPasswordNeeded as e: print(e.MESSAGE) r = self.send(functions.account.GetPassword()) while True: try: if self.password is None: print("Hint: {}".format(r.hint)) self.password = getpass.getpass("Enter password: "******"Login successful")
async def get_streaming_file( self, file_id: FileId, file_size: int, progress: callable, progress_args: tuple = () ) -> AsyncGenerator: dc_id = file_id.dc_id session = await self.get_dc_session(dc_id) location = await self.get_file_location(file_id) limit = 1024 * 1024 offset = 0 try: r = await session.send( raw.functions.upload.GetFile( location=location, offset=offset, limit=limit ), sleep_threshold=30 ) if isinstance(r, raw.types.upload.File): while True: chunk = r.bytes if not chunk: return yield chunk offset += limit if progress: func = functools.partial( progress, min(offset, file_size) if file_size != 0 else offset, file_size, *progress_args ) if inspect.iscoroutinefunction(progress): await func() else: await self.loop.run_in_executor(self.executor, func) r = await session.send( raw.functions.upload.GetFile( location=location, offset=offset, limit=limit ), sleep_threshold=30 ) elif isinstance(r, raw.types.upload.FileCdnRedirect): async with self.media_sessions_lock: cdn_session = self.media_sessions.get(r.dc_id, None) if cdn_session is None: cdn_session = Session( self, r.dc_id, await Auth(self, r.dc_id, await self.storage.test_mode()).create(), await self.storage.test_mode(), is_media=True, is_cdn=True ) await cdn_session.start() self.media_sessions[r.dc_id] = cdn_session while True: r2 = await cdn_session.send( raw.functions.upload.GetCdnFile( file_token=r.file_token, offset=offset, limit=limit ) ) if isinstance(r2, raw.types.upload.CdnFileReuploadNeeded): try: await session.send( raw.functions.upload.ReuploadCdnFile( file_token=r.file_token, request_token=r2.request_token ) ) except VolumeLocNotFound: return else: continue chunk = r2.bytes # https://core.telegram.org/cdn#decrypting-files decrypted_chunk = aes.ctr256_decrypt( chunk, r.encryption_key, bytearray( r.encryption_iv[:-4] + (offset // 16).to_bytes(4, "big") ) ) hashes = await session.send( raw.functions.upload.GetCdnFileHashes( file_token=r.file_token, offset=offset ) ) # https://core.telegram.org/cdn#verifying-files for i, h in enumerate(hashes): cdn_chunk = decrypted_chunk[h.limit * i: h.limit * (i + 1)] assert h.hash == sha256(cdn_chunk).digest(), f"Invalid CDN hash part {i}" yield decrypted_chunk offset += limit if progress: func = functools.partial( progress, min(offset, file_size) if file_size != 0 else offset, file_size, *progress_args ) if inspect.iscoroutinefunction(progress): await func() else: await self.loop.run_in_executor(self.executor, func) if len(chunk) < limit: return except Exception as e: if not isinstance(e, pyrogram.StopTransmission): log.error(e, exc_info=True) return
def change_dc(self, dc_id: int): with self.app.media_sessions_lock: session = self.app.media_sessions.get(dc_id, None) if session is None: if dc_id != self.app.storage.dc_id(): session = Session(self.app, dc_id, Auth(self.app, dc_id).create(), is_media=True) session.start() for _ in range(3): exported_auth = self.app.send( functions.auth.ExportAuthorization(dc_id=dc_id)) try: session.send( functions.auth.ImportAuthorization( id=exported_auth.id, bytes=exported_auth.bytes)) except AuthBytesInvalid: continue else: break else: session.stop() raise AuthBytesInvalid else: session = Session(self.app, dc_id, self.app.storage.auth_key(), is_media=True) session.start() self.app.media_sessions[dc_id] = session return session
async def get_file( self, file_id: FileId, filename: str, ) -> str: dc_id = file_id.dc_id async with self.client.media_sessions_lock: session = self.client.media_sessions.get(dc_id, None) if session is None: if dc_id != await self.client.storage.dc_id(): session = Session( self.client, dc_id, await Auth(self.client, dc_id, await self.client.storage.test_mode()).create(), await self.client.storage.test_mode(), is_media=True) await session.start() for _ in range(3): exported_auth = await self.client.send( raw.functions.auth.ExportAuthorization(dc_id=dc_id) ) try: await session.send( raw.functions.auth.ImportAuthorization( id=exported_auth.id, bytes=exported_auth.bytes)) except AuthBytesInvalid: continue else: break else: await session.stop() raise AuthBytesInvalid else: session = Session(self.client, dc_id, await self.client.storage.auth_key(), await self.client.storage.test_mode(), is_media=True) await session.start() self.client.media_sessions[dc_id] = session file_type = file_id.file_type if file_type == FileType.CHAT_PHOTO: if file_id.chat_id > 0: peer = raw.types.InputPeerUser( user_id=file_id.chat_id, access_hash=file_id.chat_access_hash) else: if file_id.chat_access_hash == 0: peer = raw.types.InputPeerChat(chat_id=-file_id.chat_id) else: peer = raw.types.InputPeerChannel( channel_id=utils.get_channel_id(file_id.chat_id), access_hash=file_id.chat_access_hash) location = raw.types.InputPeerPhotoFileLocation( peer=peer, volume_id=file_id.volume_id, local_id=file_id.local_id, big=file_id.thumbnail_source == ThumbnailSource.CHAT_PHOTO_BIG) elif file_type == FileType.PHOTO: location = raw.types.InputPhotoFileLocation( id=file_id.media_id, access_hash=file_id.access_hash, file_reference=file_id.file_reference, thumb_size=file_id.thumbnail_size) else: location = raw.types.InputDocumentFileLocation( id=file_id.media_id, access_hash=file_id.access_hash, file_reference=file_id.file_reference, thumb_size=file_id.thumbnail_size) limit = 1024 * 1024 offset = 0 file_name = "" try: r = await session.send(raw.functions.upload.GetFile( location=location, offset=offset, limit=limit), sleep_threshold=30) if isinstance(r, raw.types.upload.File): #with tempfile.NamedTemporaryFile("wb", delete=False) as f: with open(filename, 'wb') as f: file_name = filename while True: chunk = r.bytes if not chunk: break f.write(chunk) offset += limit r = await session.send(raw.functions.upload.GetFile( location=location, offset=offset, limit=limit), sleep_threshold=30) elif isinstance(r, raw.types.upload.FileCdnRedirect): async with self.client.media_sessions_lock: cdn_session = self.client.media_sessions.get(r.dc_id, None) if cdn_session is None: cdn_session = Session( self.client, r.dc_id, await Auth(self.client, r.dc_id, await self.client.storage.test_mode()).create(), await self.client.storage.test_mode(), is_media=True, is_cdn=True) await cdn_session.start() self.client.media_sessions[r.dc_id] = cdn_session try: with open(filename, 'wb') as f: file_name = f while True: r2 = await cdn_session.send( raw.functions.upload.GetCdnFile( file_token=r.file_token, offset=offset, limit=limit)) if isinstance( r2, raw.types.upload.CdnFileReuploadNeeded): try: await session.send( raw.functions.upload.ReuploadCdnFile( file_token=r.file_token, request_token=r2.request_token)) except VolumeLocNotFound: break else: continue chunk = r2.bytes # https://core.telegram.org/cdn#decrypting-files decrypted_chunk = aes.ctr256_decrypt( chunk, r.encryption_key, bytearray(r.encryption_iv[:-4] + (offset // 16).to_bytes(4, "big"))) hashes = await session.send( raw.functions.upload.GetCdnFileHashes( file_token=r.file_token, offset=offset)) # https://core.telegram.org/cdn#verifying-files for i, h in enumerate(hashes): cdn_chunk = decrypted_chunk[h.limit * i:h.limit * (i + 1)] assert h.hash == sha256(cdn_chunk).digest( ), f"Invalid CDN hash part {i}" f.write(decrypted_chunk) offset += limit if len(chunk) < limit: break except Exception as e: LOGGER.error(e, exc_info=True) raise e except Exception as e: if not isinstance(e, pyrogram.StopTransmission): LOGGER.error(str(e), exc_info=True) try: os.remove(file_name) except OSError: pass return "" else: return file_name
class Client(Methods, BaseClient): """This class represents a Client, the main mean for interacting with Telegram. It exposes bot-like methods for an easy access to the API as well as a simple way to invoke every single Telegram API method available. Args: session_name (``str``): Name to uniquely identify a session of either a User or a Bot. For Users: pass a string of your choice, e.g.: "my_main_account". For Bots: pass your Bot API token, e.g.: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" Note: as long as a valid User session file exists, Pyrogram won't ask you again to input your phone number. api_id (``int``, *optional*): The *api_id* part of your Telegram API Key, as integer. E.g.: 12345 This is an alternative way to pass it if you don't want to use the *config.ini* file. api_hash (``str``, *optional*): The *api_hash* part of your Telegram API Key, as string. E.g.: "0123456789abcdef0123456789abcdef" This is an alternative way to pass it if you don't want to use the *config.ini* file. app_version (``str``, *optional*): Application version. Defaults to "Pyrogram \U0001f525 vX.Y.Z" This is an alternative way to set it if you don't want to use the *config.ini* file. device_model (``str``, *optional*): Device model. Defaults to *platform.python_implementation() + " " + platform.python_version()* This is an alternative way to set it if you don't want to use the *config.ini* file. system_version (``str``, *optional*): Operating System version. Defaults to *platform.system() + " " + platform.release()* This is an alternative way to set it if you don't want to use the *config.ini* file. lang_code (``str``, *optional*): Code of the language used on the client, in ISO 639-1 standard. Defaults to "en". This is an alternative way to set it if you don't want to use the *config.ini* file. proxy (``dict``, *optional*): Your SOCKS5 Proxy settings as dict, e.g.: *dict(hostname="11.22.33.44", port=1080, username="******", password="******")*. *username* and *password* can be omitted if your proxy doesn't require authorization. This is an alternative way to setup a proxy if you don't want to use the *config.ini* file. test_mode (``bool``, *optional*): Enable or disable log-in to testing servers. Defaults to False. Only applicable for new sessions and will be ignored in case previously created sessions are loaded. phone_number (``str``, *optional*): Pass your phone number (with your Country Code prefix included) to avoid entering it manually. Only applicable for new sessions. phone_code (``str`` | ``callable``, *optional*): Pass the phone code as string (for test numbers only), or pass a callback function which accepts a single positional argument *(phone_number)* and must return the correct phone code (e.g., "12345"). Only applicable for new sessions. password (``str``, *optional*): Pass your Two-Step Verification password (if you have one) to avoid entering it manually. Only applicable for new sessions. force_sms (``str``, *optional*): Pass True to force Telegram sending the authorization code via SMS. Only applicable for new sessions. first_name (``str``, *optional*): Pass a First Name to avoid entering it manually. It will be used to automatically create a new Telegram account in case the phone number you passed is not registered yet. Only applicable for new sessions. last_name (``str``, *optional*): Same purpose as *first_name*; pass a Last Name to avoid entering it manually. It can be an empty string: "". Only applicable for new sessions. workers (``int``, *optional*): Thread pool size for handling incoming updates. Defaults to 4. workdir (``str``, *optional*): Define a custom working directory. The working directory is the location in your filesystem where Pyrogram will store your session files. Defaults to "." (current directory). config_file (``str``, *optional*): Path of the configuration file. Defaults to ./config.ini """ def __init__(self, session_name: str, api_id: int or str = None, api_hash: str = None, app_version: str = None, device_model: str = None, system_version: str = None, lang_code: str = None, ipv6: bool = False, proxy: dict = None, test_mode: bool = False, phone_number: str = None, phone_code: str or callable = None, password: str = None, force_sms: bool = False, first_name: str = None, last_name: str = None, workers: int = 4, workdir: str = ".", config_file: str = "./config.ini"): super().__init__() self.session_name = session_name self.api_id = int(api_id) if api_id else None self.api_hash = api_hash self.app_version = app_version self.device_model = device_model self.system_version = system_version self.lang_code = lang_code self.ipv6 = ipv6 # TODO: Make code consistent, use underscore for private/protected fields self._proxy = proxy self.test_mode = test_mode self.phone_number = phone_number self.phone_code = phone_code self.password = password self.force_sms = force_sms self.first_name = first_name self.last_name = last_name self.workers = workers self.workdir = workdir self.config_file = config_file self.dispatcher = Dispatcher(self, workers) def __enter__(self): self.start() return self def __exit__(self, exc_type, exc_val, exc_tb): self.stop() @property def proxy(self): return self._proxy @proxy.setter def proxy(self, value): self._proxy["enabled"] = True self._proxy.update(value) def start(self): """Use this method to start the Client after creating it. Requires no parameters. Raises: :class:`Error <pyrogram.Error>` """ if self.is_started: raise ConnectionError("Client has already been started") if self.BOT_TOKEN_RE.match(self.session_name): self.bot_token = self.session_name self.session_name = self.session_name.split(":")[0] self.load_config() self.load_session() self.session = Session( self, self.dc_id, self.auth_key ) self.session.start() self.is_started = True try: if self.user_id is None: if self.bot_token is None: self.authorize_user() else: self.authorize_bot() self.save_session() if self.bot_token is None: now = time.time() if abs(now - self.date) > Client.OFFLINE_SLEEP: self.peers_by_username = {} self.peers_by_phone = {} self.get_initial_dialogs() self.get_contacts() else: self.send(functions.messages.GetPinnedDialogs()) 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( target=self.updates_worker, name="UpdatesWorker#{}".format(i + 1) ) ) self.updates_workers_list[-1].start() 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() self.dispatcher.start() mimetypes.init() Syncer.add(self) def stop(self): """Use this method to manually stop the Client. Requires no parameters. """ if not self.is_started: raise ConnectionError("Client is already stopped") Syncer.remove(self) self.dispatcher.stop() for _ in range(self.DOWNLOAD_WORKERS): self.download_queue.put(None) for i in self.download_workers_list: i.join() self.download_workers_list.clear() for _ in range(self.UPDATES_WORKERS): self.updates_queue.put(None) for i in self.updates_workers_list: i.join() self.updates_workers_list.clear() for i in self.media_sessions.values(): i.stop() self.media_sessions.clear() self.is_started = False self.session.stop() def idle(self, stop_signals: tuple = (SIGINT, SIGTERM, SIGABRT)): """Blocks the program execution until one of the signals are received, then gently stop the Client by closing the underlying connection. Args: stop_signals (``tuple``, *optional*): Iterable containing signals the signal handler will listen to. Defaults to (SIGINT, SIGTERM, SIGABRT). """ def signal_handler(*args): self.is_idle = False for s in stop_signals: signal(s, signal_handler) self.is_idle = True while self.is_idle: time.sleep(1) self.stop() def run(self): """Use this method to automatically start and idle a Client. Requires no parameters. Raises: :class:`Error <pyrogram.Error>` """ self.start() self.idle() def add_handler(self, handler, group: int = 0): """Use this method to register an update handler. You can register multiple handlers, but at most one handler within a group will be used for a single update. To handle the same update more than once, register your handler using a different group id (lower group id == higher priority). Args: handler (``Handler``): The handler to be registered. group (``int``, *optional*): The group identifier, defaults to 0. Returns: A tuple of (handler, group) """ if isinstance(handler, DisconnectHandler): self.disconnect_handler = handler.callback else: self.dispatcher.add_handler(handler, group) return handler, group def remove_handler(self, handler, group: int = 0): """Removes a previously-added update handler. Make sure to provide the right group that the handler was added in. You can use the return value of the :meth:`add_handler` method, a tuple of (handler, group), and pass it directly. Args: handler (``Handler``): The handler to be removed. group (``int``, *optional*): The group identifier, defaults to 0. """ if isinstance(handler, DisconnectHandler): self.disconnect_handler = None else: self.dispatcher.remove_handler(handler, group) def authorize_bot(self): try: r = self.send( functions.auth.ImportBotAuthorization( flags=0, api_id=self.api_id, api_hash=self.api_hash, bot_auth_token=self.bot_token ) ) except UserMigrate as e: self.session.stop() self.dc_id = e.x self.auth_key = Auth(self.dc_id, self.test_mode, self.ipv6, self._proxy).create() self.session = Session( self, self.dc_id, self.auth_key ) self.session.start() self.authorize_bot() else: self.user_id = r.user.id def authorize_user(self): phone_number_invalid_raises = self.phone_number is not None phone_code_invalid_raises = self.phone_code is not None password_hash_invalid_raises = self.password is not None first_name_invalid_raises = self.first_name is not None while True: if self.phone_number is None: self.phone_number = input("Enter phone number: ") while True: confirm = input("Is \"{}\" correct? (y/n): ".format(self.phone_number)) if confirm in ("y", "1"): break elif confirm in ("n", "2"): self.phone_number = input("Enter phone number: ") self.phone_number = self.phone_number.strip("+") try: r = self.send( functions.auth.SendCode( self.phone_number, self.api_id, self.api_hash ) ) except (PhoneMigrate, NetworkMigrate) as e: self.session.stop() self.dc_id = e.x self.auth_key = Auth(self.dc_id, self.test_mode, self.ipv6, self._proxy).create() self.session = Session( self, self.dc_id, self.auth_key ) self.session.start() r = self.send( functions.auth.SendCode( self.phone_number, self.api_id, self.api_hash ) ) break except (PhoneNumberInvalid, PhoneNumberBanned) as e: if phone_number_invalid_raises: raise else: print(e.MESSAGE) self.phone_number = None except FloodWait as e: if phone_number_invalid_raises: raise else: print(e.MESSAGE.format(x=e.x)) time.sleep(e.x) except Exception as e: log.error(e, exc_info=True) else: break phone_registered = r.phone_registered phone_code_hash = r.phone_code_hash terms_of_service = r.terms_of_service if terms_of_service: print("\n" + terms_of_service.text + "\n") if self.force_sms: self.send( functions.auth.ResendCode( phone_number=self.phone_number, phone_code_hash=phone_code_hash ) ) while True: self.phone_code = ( input("Enter phone code: ") if self.phone_code is None else self.phone_code if type(self.phone_code) is str else str(self.phone_code(self.phone_number)) ) try: if phone_registered: r = self.send( functions.auth.SignIn( self.phone_number, phone_code_hash, self.phone_code ) ) else: try: self.send( functions.auth.SignIn( self.phone_number, phone_code_hash, self.phone_code ) ) except PhoneNumberUnoccupied: pass self.first_name = self.first_name if self.first_name is not None else input("First name: ") self.last_name = self.last_name if self.last_name is not None else input("Last name: ") r = self.send( functions.auth.SignUp( self.phone_number, phone_code_hash, self.phone_code, self.first_name, self.last_name ) ) except (PhoneCodeInvalid, PhoneCodeEmpty, PhoneCodeExpired, PhoneCodeHashEmpty) as e: if phone_code_invalid_raises: raise else: print(e.MESSAGE) self.phone_code = None except FirstnameInvalid as e: if first_name_invalid_raises: raise else: print(e.MESSAGE) self.first_name = None except SessionPasswordNeeded as e: print(e.MESSAGE) r = self.send(functions.account.GetPassword()) while True: try: if self.password is None: print("Hint: {}".format(r.hint)) self.password = getpass.getpass("Enter password: "******"Login successful") def fetch_peers(self, entities: list): for entity in entities: if isinstance(entity, types.User): user_id = entity.id access_hash = entity.access_hash if access_hash is None: continue username = entity.username phone = entity.phone input_peer = types.InputPeerUser( user_id=user_id, access_hash=access_hash ) self.peers_by_id[user_id] = input_peer if username is not None: self.peers_by_username[username.lower()] = input_peer if phone is not None: self.peers_by_phone[phone] = input_peer if isinstance(entity, (types.Chat, types.ChatForbidden)): chat_id = entity.id peer_id = -chat_id input_peer = types.InputPeerChat( chat_id=chat_id ) self.peers_by_id[peer_id] = input_peer if isinstance(entity, (types.Channel, types.ChannelForbidden)): channel_id = entity.id peer_id = int("-100" + str(channel_id)) access_hash = entity.access_hash if access_hash is None: continue username = getattr(entity, "username", None) input_peer = types.InputPeerChannel( channel_id=channel_id, access_hash=access_hash ) self.peers_by_id[peer_id] = input_peer if username is not None: self.peers_by_username[username.lower()] = input_peer def download_worker(self): name = threading.current_thread().name log.debug("{} started".format(name)) while True: media = self.download_queue.get() if media is None: break temp_file_path = "" final_file_path = "" try: media, file_name, done, progress, progress_args, path = media file_id = media.file_id size = media.file_size directory, file_name = os.path.split(file_name) directory = directory or "downloads" try: decoded = utils.decode(file_id) fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq" unpacked = struct.unpack(fmt, decoded) except (AssertionError, binascii.Error, struct.error): raise FileIdInvalid from None else: media_type = unpacked[0] dc_id = unpacked[1] id = unpacked[2] access_hash = unpacked[3] volume_id = None secret = None local_id = None if len(decoded) > 24: volume_id = unpacked[4] secret = unpacked[5] local_id = unpacked[6] media_type_str = Client.MEDIA_TYPE_ID.get(media_type, None) if media_type_str is None: raise FileIdInvalid("Unknown media type: {}".format(unpacked[0])) file_name = file_name or getattr(media, "file_name", None) if not file_name: if media_type == 3: extension = ".ogg" elif media_type in (4, 10, 13): extension = mimetypes.guess_extension(media.mime_type) or ".mp4" elif media_type == 5: extension = mimetypes.guess_extension(media.mime_type) or ".unknown" elif media_type == 8: extension = ".webp" elif media_type == 9: extension = mimetypes.guess_extension(media.mime_type) or ".mp3" elif media_type in (0, 1, 2): extension = ".jpg" else: continue file_name = "{}_{}_{}{}".format( media_type_str, datetime.fromtimestamp( getattr(media, "date", None) or time.time() ).strftime("%Y-%m-%d_%H-%M-%S"), self.rnd_id(), extension ) temp_file_path = self.get_file( dc_id=dc_id, id=id, access_hash=access_hash, volume_id=volume_id, local_id=local_id, secret=secret, size=size, progress=progress, progress_args=progress_args ) if temp_file_path: final_file_path = os.path.abspath(re.sub("\\\\", "/", os.path.join(directory, file_name))) os.makedirs(directory, exist_ok=True) shutil.move(temp_file_path, final_file_path) except Exception as e: log.error(e, exc_info=True) try: os.remove(temp_file_path) except OSError: pass else: # TODO: "" or None for faulty download, which is better? # os.path methods return "" in case something does not exist, I prefer this. # For now let's keep None path[0] = final_file_path or None finally: done.set() log.debug("{} stopped".format(name)) def updates_worker(self): name = threading.current_thread().name log.debug("{} started".format(name)) while True: updates = self.updates_queue.get() if updates is None: break try: if isinstance(updates, (types.Update, types.UpdatesCombined)): self.fetch_peers(updates.users) self.fetch_peers(updates.chats) for update in updates.updates: channel_id = getattr( getattr( getattr( update, "message", None ), "to_id", None ), "channel_id", None ) or getattr(update, "channel_id", None) pts = getattr(update, "pts", None) pts_count = getattr(update, "pts_count", None) if isinstance(update, types.UpdateChannelTooLong): log.warning(update) if isinstance(update, types.UpdateNewChannelMessage): message = update.message if not isinstance(message, types.MessageEmpty): diff = self.send( functions.updates.GetChannelDifference( channel=self.resolve_peer(int("-100" + str(channel_id))), filter=types.ChannelMessagesFilter( ranges=[types.MessageRange( min_id=update.message.id, max_id=update.message.id )] ), pts=pts - pts_count, limit=pts ) ) if not isinstance(diff, types.updates.ChannelDifferenceEmpty): updates.users += diff.users updates.chats += diff.chats if channel_id and pts: if channel_id not in self.channels_pts: self.channels_pts[channel_id] = [] if pts in self.channels_pts[channel_id]: continue self.channels_pts[channel_id].append(pts) if len(self.channels_pts[channel_id]) > 50: self.channels_pts[channel_id] = self.channels_pts[channel_id][25:] self.dispatcher.updates.put((update, updates.users, updates.chats)) elif isinstance(updates, (types.UpdateShortMessage, types.UpdateShortChatMessage)): diff = self.send( functions.updates.GetDifference( pts=updates.pts - updates.pts_count, date=updates.date, qts=-1 ) ) if diff.new_messages: self.dispatcher.updates.put(( types.UpdateNewMessage( message=diff.new_messages[0], pts=updates.pts, pts_count=updates.pts_count ), diff.users, diff.chats )) else: self.dispatcher.updates.put((diff.other_updates[0], [], [])) elif isinstance(updates, types.UpdateShort): self.dispatcher.updates.put((updates.update, [], [])) elif isinstance(updates, types.UpdatesTooLong): log.warning(updates) except Exception as e: log.error(e, exc_info=True) log.debug("{} stopped".format(name)) def send(self, data: Object, retries: int = Session.MAX_RETRIES, timeout: float = Session.WAIT_TIMEOUT): """Use this method to send Raw Function queries. This method makes possible to manually call every single Telegram API method in a low-level manner. Available functions are listed in the :obj:`functions <pyrogram.api.functions>` package and may accept compound data types from :obj:`types <pyrogram.api.types>` as well as bare types such as ``int``, ``str``, etc... Args: data (``Object``): The API Scheme function filled with proper arguments. retries (``int``): Number of retries. timeout (``float``): Timeout in seconds. Raises: :class:`Error <pyrogram.Error>` """ if not self.is_started: raise ConnectionError("Client has not been started") r = self.session.send(data, retries, timeout) self.fetch_peers(getattr(r, "users", [])) self.fetch_peers(getattr(r, "chats", [])) return r def load_config(self): parser = ConfigParser() parser.read(self.config_file) if self.api_id and self.api_hash: pass else: if parser.has_section("pyrogram"): self.api_id = parser.getint("pyrogram", "api_id") self.api_hash = parser.get("pyrogram", "api_hash") else: raise AttributeError( "No API Key found. " "More info: https://docs.pyrogram.ml/start/ProjectSetup#configuration" ) for option in ["app_version", "device_model", "system_version", "lang_code"]: if getattr(self, option): pass else: if parser.has_section("pyrogram"): setattr(self, option, parser.get( "pyrogram", option, fallback=getattr(Client, option.upper()) )) else: setattr(self, option, getattr(Client, option.upper())) if self._proxy: self._proxy["enabled"] = True else: self._proxy = {} if parser.has_section("proxy"): self._proxy["enabled"] = parser.getboolean("proxy", "enabled") self._proxy["hostname"] = parser.get("proxy", "hostname") self._proxy["port"] = parser.getint("proxy", "port") self._proxy["username"] = parser.get("proxy", "username", fallback=None) or None self._proxy["password"] = parser.get("proxy", "password", fallback=None) or None def load_session(self): try: with open(os.path.join(self.workdir, "{}.session".format(self.session_name)), encoding="utf-8") as f: s = json.load(f) except FileNotFoundError: self.dc_id = 1 self.date = 0 self.auth_key = Auth(self.dc_id, self.test_mode, self.ipv6, self._proxy).create() else: self.dc_id = s["dc_id"] self.test_mode = s["test_mode"] self.auth_key = base64.b64decode("".join(s["auth_key"])) self.user_id = s["user_id"] self.date = s.get("date", 0) for k, v in s.get("peers_by_id", {}).items(): self.peers_by_id[int(k)] = utils.get_input_peer(int(k), v) for k, v in s.get("peers_by_username", {}).items(): peer = self.peers_by_id.get(v, None) if peer: self.peers_by_username[k] = peer for k, v in s.get("peers_by_phone", {}).items(): peer = self.peers_by_id.get(v, None) if peer: self.peers_by_phone[k] = peer def save_session(self): auth_key = base64.b64encode(self.auth_key).decode() auth_key = [auth_key[i: i + 43] for i in range(0, len(auth_key), 43)] os.makedirs(self.workdir, exist_ok=True) with open(os.path.join(self.workdir, "{}.session".format(self.session_name)), "w", encoding="utf-8") as f: json.dump( dict( dc_id=self.dc_id, test_mode=self.test_mode, auth_key=auth_key, user_id=self.user_id, date=self.date ), f, indent=4 ) 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(len(self.peers_by_id))) return r def get_initial_dialogs(self): self.send(functions.messages.GetPinnedDialogs()) 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: int or str): """Use this method to get the InputPeer of a known peer_id. This is a utility method intended to be used only when working with Raw Functions (i.e: a Telegram API method you wish to use which is not available yet in the Client class as an easy-to-use method), whenever an InputPeer type is required. Args: peer_id (``int`` | ``str``): The peer id you want to extract the InputPeer from. Can be a direct id (int), a username (str) or a phone number (str). Returns: On success, the resolved peer id is returned in form of an InputPeer object. Raises: :class:`Error <pyrogram.Error>` """ if type(peer_id) is str: if peer_id in ("self", "me"): return types.InputPeerSelf() peer_id = re.sub(r"[@+\s]", "", peer_id.lower()) try: int(peer_id) except ValueError: if peer_id not in self.peers_by_username: self.send(functions.contacts.ResolveUsername(peer_id)) return self.peers_by_username[peer_id] else: try: return self.peers_by_phone[peer_id] except KeyError: raise PeerIdInvalid try: # User return self.peers_by_id[peer_id] except KeyError: try: # Chat return self.peers_by_id[-peer_id] except KeyError: try: # Channel return self.peers_by_id[int("-100" + str(peer_id))] except (KeyError, ValueError): raise PeerIdInvalid def save_file(self, path: str, file_id: int = None, file_part: int = 0, progress: callable = None, progress_args: tuple = ()): part_size = 512 * 1024 file_size = os.path.getsize(path) file_total_parts = int(math.ceil(file_size / part_size)) is_big = True if file_size > 10 * 1024 * 1024 else False is_missing_part = True if file_id is not None else False file_id = file_id or self.rnd_id() md5_sum = md5() if not is_big and not is_missing_part else None session = Session(self, self.dc_id, self.auth_key, is_media=True) session.start() try: with open(path, "rb") as f: f.seek(part_size * file_part) while True: chunk = f.read(part_size) if not chunk: if not is_big: md5_sum = "".join([hex(i)[2:].zfill(2) for i in md5_sum.digest()]) break if is_big: rpc = functions.upload.SaveBigFilePart( file_id=file_id, file_part=file_part, file_total_parts=file_total_parts, bytes=chunk ) else: rpc = functions.upload.SaveFilePart( file_id=file_id, file_part=file_part, bytes=chunk ) assert session.send(rpc), "Couldn't upload file" if is_missing_part: return if not is_big: md5_sum.update(chunk) file_part += 1 if progress: progress(self, min(file_part * part_size, file_size), file_size, *progress_args) except Exception as e: log.error(e, exc_info=True) else: if is_big: return types.InputFileBig( id=file_id, parts=file_total_parts, name=os.path.basename(path), ) else: return types.InputFile( id=file_id, parts=file_total_parts, name=os.path.basename(path), md5_checksum=md5_sum ) finally: session.stop() def get_file(self, dc_id: int, id: int = None, access_hash: int = None, volume_id: int = None, local_id: int = None, secret: int = None, version: int = 0, size: int = None, progress: callable = None, progress_args: tuple = None) -> str: with self.media_sessions_lock: session = self.media_sessions.get(dc_id, None) if session is None: if dc_id != self.dc_id: exported_auth = self.send( functions.auth.ExportAuthorization( dc_id=dc_id ) ) session = Session( self, dc_id, Auth(dc_id, self.test_mode, self.ipv6, self._proxy).create(), is_media=True ) session.start() self.media_sessions[dc_id] = session session.send( functions.auth.ImportAuthorization( id=exported_auth.id, bytes=exported_auth.bytes ) ) else: session = Session( self, dc_id, self.auth_key, is_media=True ) session.start() self.media_sessions[dc_id] = session if volume_id: # Photos are accessed by volume_id, local_id, secret location = types.InputFileLocation( volume_id=volume_id, local_id=local_id, secret=secret ) else: # Any other file can be more easily accessed by id and access_hash location = types.InputDocumentFileLocation( id=id, access_hash=access_hash, version=version ) limit = 1024 * 1024 offset = 0 file_name = "" try: r = session.send( functions.upload.GetFile( location=location, offset=offset, limit=limit ) ) if isinstance(r, types.upload.File): with tempfile.NamedTemporaryFile("wb", delete=False) as f: file_name = f.name while True: chunk = r.bytes if not chunk: break f.write(chunk) offset += limit if progress: progress(self, min(offset, size), size, *progress_args) r = session.send( functions.upload.GetFile( location=location, offset=offset, limit=limit ) ) elif isinstance(r, types.upload.FileCdnRedirect): with self.media_sessions_lock: cdn_session = self.media_sessions.get(r.dc_id, None) if cdn_session is None: cdn_session = Session( self, r.dc_id, Auth(r.dc_id, self.test_mode, self.ipv6, self._proxy).create(), is_media=True, is_cdn=True ) cdn_session.start() self.media_sessions[r.dc_id] = cdn_session try: with tempfile.NamedTemporaryFile("wb", delete=False) as f: file_name = f.name while True: r2 = cdn_session.send( functions.upload.GetCdnFile( file_token=r.file_token, offset=offset, limit=limit ) ) if isinstance(r2, types.upload.CdnFileReuploadNeeded): try: session.send( functions.upload.ReuploadCdnFile( file_token=r.file_token, request_token=r2.request_token ) ) except VolumeLocNotFound: break else: continue chunk = r2.bytes # https://core.telegram.org/cdn#decrypting-files decrypted_chunk = AES.ctr256_decrypt( chunk, r.encryption_key, bytearray( r.encryption_iv[:-4] + (offset // 16).to_bytes(4, "big") ) ) hashes = session.send( functions.upload.GetCdnFileHashes( r.file_token, offset ) ) # https://core.telegram.org/cdn#verifying-files for i, h in enumerate(hashes): cdn_chunk = decrypted_chunk[h.limit * i: h.limit * (i + 1)] assert h.hash == sha256(cdn_chunk).digest(), "Invalid CDN hash part {}".format(i) f.write(decrypted_chunk) offset += limit if progress: progress(self, min(offset, size), size, *progress_args) if len(chunk) < limit: break except Exception as e: raise e except Exception as e: log.error(e, exc_info=True) try: os.remove(file_name) except OSError: pass return "" else: return file_name
def start(self, debug: bool = False): """Use this method to start the Client after creating it. Requires no parameters. Args: debug (``bool``, *optional*): Enable or disable debug mode. When enabled, extra logging lines will be printed out on your console. Raises: :class:`Error <pyrogram.Error>` """ if self.is_started: raise ConnectionError("Client has already been started") if self.BOT_TOKEN_RE.match(self.session_name): self.token = self.session_name self.session_name = self.session_name.split(":")[0] self.load_config() self.load_session() self.session = Session(self.dc_id, self.test_mode, self.proxy, self.auth_key, self.api_id, client=self) self.session.start() self.is_started = True if self.user_id is None: if self.token is None: self.authorize_user() else: self.authorize_bot() self.save_session() if self.token is None: now = time.time() if abs(now - self.date) > Client.OFFLINE_SLEEP: self.peers_by_username = {} self.peers_by_phone = {} self.get_dialogs() self.get_contacts() else: self.send(functions.messages.GetPinnedDialogs()) self.get_dialogs_chunk(0) else: self.send(functions.updates.GetState()) 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() 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() self.dispatcher.start() mimetypes.init() Syncer.add(self)
async def get_file(self, media_type: int, dc_id: int, document_id: int, access_hash: int, thumb_size: str, peer_id: int, peer_type: str, peer_access_hash: int, volume_id: int, local_id: int, file_ref: str, file_size: int, is_big: bool, progress: callable, progress_args: tuple = ()) -> str: async with self.media_sessions_lock: session = self.media_sessions.get(dc_id, None) if session is None: if dc_id != await self.storage.dc_id(): session = Session(self, dc_id, await Auth(self, dc_id, await self.storage.test_mode()).create(), await self.storage.test_mode(), is_media=True) await session.start() for _ in range(3): exported_auth = await self.send( raw.functions.auth.ExportAuthorization(dc_id=dc_id) ) try: await session.send( raw.functions.auth.ImportAuthorization( id=exported_auth.id, bytes=exported_auth.bytes)) except AuthBytesInvalid: continue else: break else: await session.stop() raise AuthBytesInvalid else: session = Session(self, dc_id, await self.storage.auth_key(), await self.storage.test_mode(), is_media=True) await session.start() self.media_sessions[dc_id] = session file_ref = utils.decode_file_ref(file_ref) if media_type == 1: if peer_type == "user": peer = raw.types.InputPeerUser(user_id=peer_id, access_hash=peer_access_hash) elif peer_type == "chat": peer = raw.types.InputPeerChat(chat_id=peer_id) else: peer = raw.types.InputPeerChannel(channel_id=peer_id, access_hash=peer_access_hash) location = raw.types.InputPeerPhotoFileLocation( peer=peer, volume_id=volume_id, local_id=local_id, big=is_big or None) elif media_type in (0, 2): location = raw.types.InputPhotoFileLocation( id=document_id, access_hash=access_hash, file_reference=file_ref, thumb_size=thumb_size) elif media_type == 14: location = raw.types.InputDocumentFileLocation( id=document_id, access_hash=access_hash, file_reference=file_ref, thumb_size=thumb_size) else: location = raw.types.InputDocumentFileLocation( id=document_id, access_hash=access_hash, file_reference=file_ref, thumb_size="") limit = 1024 * 1024 offset = 0 file_name = "" try: r = await session.send(raw.functions.upload.GetFile( location=location, offset=offset, limit=limit), sleep_threshold=30) if isinstance(r, raw.types.upload.File): with tempfile.NamedTemporaryFile("wb", delete=False) as f: file_name = f.name while True: chunk = r.bytes if not chunk: break f.write(chunk) offset += limit if progress: func = functools.partial( progress, min(offset, file_size) if file_size != 0 else offset, file_size, *progress_args) if inspect.iscoroutinefunction(progress): await func() else: await self.loop.run_in_executor( self.executor, func) r = await session.send(raw.functions.upload.GetFile( location=location, offset=offset, limit=limit), sleep_threshold=30) elif isinstance(r, raw.types.upload.FileCdnRedirect): async with self.media_sessions_lock: cdn_session = self.media_sessions.get(r.dc_id, None) if cdn_session is None: cdn_session = Session( self, r.dc_id, await Auth(self, r.dc_id, await self.storage.test_mode()).create(), await self.storage.test_mode(), is_media=True, is_cdn=True) await cdn_session.start() self.media_sessions[r.dc_id] = cdn_session try: with tempfile.NamedTemporaryFile("wb", delete=False) as f: file_name = f.name while True: r2 = await cdn_session.send( raw.functions.upload.GetCdnFile( file_token=r.file_token, offset=offset, limit=limit)) if isinstance( r2, raw.types.upload.CdnFileReuploadNeeded): try: await session.send( raw.functions.upload.ReuploadCdnFile( file_token=r.file_token, request_token=r2.request_token)) except VolumeLocNotFound: break else: continue chunk = r2.bytes # https://core.telegram.org/cdn#decrypting-files decrypted_chunk = aes.ctr256_decrypt( chunk, r.encryption_key, bytearray(r.encryption_iv[:-4] + (offset // 16).to_bytes(4, "big"))) hashes = await session.send( raw.functions.upload.GetCdnFileHashes( file_token=r.file_token, offset=offset)) # https://core.telegram.org/cdn#verifying-files for i, h in enumerate(hashes): cdn_chunk = decrypted_chunk[h.limit * i:h.limit * (i + 1)] assert h.hash == sha256(cdn_chunk).digest( ), f"Invalid CDN hash part {i}" f.write(decrypted_chunk) offset += limit if progress: func = functools.partial( progress, min(offset, file_size) if file_size != 0 else offset, file_size, *progress_args) if inspect.iscoroutinefunction(progress): await func() else: await self.loop.run_in_executor( self.executor, func) if len(chunk) < limit: break except Exception as e: raise e except Exception as e: if not isinstance(e, pyrogram.StopTransmission): log.error(e, exc_info=True) try: os.remove(file_name) except OSError: pass return "" else: return file_name
async def get_file( self, file_id: FileId, file_size: int = 0, limit: int = 0, offset: int = 0, progress: Callable = None, progress_args: tuple = () ) -> Optional[AsyncGenerator[bytes, None]]: dc_id = file_id.dc_id async with self.media_sessions_lock: session = self.media_sessions.get(dc_id, None) if session is None: if dc_id != await self.storage.dc_id(): session = Session(self, dc_id, await Auth(self, dc_id, await self.storage.test_mode()).create(), await self.storage.test_mode(), is_media=True) await session.start() for _ in range(3): exported_auth = await self.invoke( raw.functions.auth.ExportAuthorization(dc_id=dc_id) ) try: await session.invoke( raw.functions.auth.ImportAuthorization( id=exported_auth.id, bytes=exported_auth.bytes)) except AuthBytesInvalid: continue else: break else: await session.stop() raise AuthBytesInvalid else: session = Session(self, dc_id, await self.storage.auth_key(), await self.storage.test_mode(), is_media=True) await session.start() self.media_sessions[dc_id] = session file_type = file_id.file_type if file_type == FileType.CHAT_PHOTO: if file_id.chat_id > 0: peer = raw.types.InputPeerUser( user_id=file_id.chat_id, access_hash=file_id.chat_access_hash) else: if file_id.chat_access_hash == 0: peer = raw.types.InputPeerChat(chat_id=-file_id.chat_id) else: peer = raw.types.InputPeerChannel( channel_id=utils.get_channel_id(file_id.chat_id), access_hash=file_id.chat_access_hash) location = raw.types.InputPeerPhotoFileLocation( peer=peer, photo_id=file_id.media_id, big=file_id.thumbnail_source == ThumbnailSource.CHAT_PHOTO_BIG) elif file_type == FileType.PHOTO: location = raw.types.InputPhotoFileLocation( id=file_id.media_id, access_hash=file_id.access_hash, file_reference=file_id.file_reference, thumb_size=file_id.thumbnail_size) else: location = raw.types.InputDocumentFileLocation( id=file_id.media_id, access_hash=file_id.access_hash, file_reference=file_id.file_reference, thumb_size=file_id.thumbnail_size) current = 0 total = abs(limit) or (1 << 31) - 1 chunk_size = 1024 * 1024 offset_bytes = abs(offset) * chunk_size try: r = await session.invoke(raw.functions.upload.GetFile( location=location, offset=offset_bytes, limit=chunk_size), sleep_threshold=30) if isinstance(r, raw.types.upload.File): while True: chunk = r.bytes yield chunk current += 1 offset_bytes += chunk_size if progress: func = functools.partial( progress, min(offset_bytes, file_size) if file_size != 0 else offset_bytes, file_size, *progress_args) if inspect.iscoroutinefunction(progress): await func() else: await self.loop.run_in_executor( self.executor, func) if len(chunk) < chunk_size or current >= total: break r = await session.invoke(raw.functions.upload.GetFile( location=location, offset=offset_bytes, limit=chunk_size), sleep_threshold=30) elif isinstance(r, raw.types.upload.FileCdnRedirect): async with self.media_sessions_lock: cdn_session = self.media_sessions.get(r.dc_id, None) if cdn_session is None: cdn_session = Session( self, r.dc_id, await Auth(self, r.dc_id, await self.storage.test_mode()).create(), await self.storage.test_mode(), is_media=True, is_cdn=True) await cdn_session.start() self.media_sessions[r.dc_id] = cdn_session try: while True: r2 = await cdn_session.invoke( raw.functions.upload.GetCdnFile( file_token=r.file_token, offset=offset_bytes, limit=chunk_size)) if isinstance(r2, raw.types.upload.CdnFileReuploadNeeded): try: await session.invoke( raw.functions.upload.ReuploadCdnFile( file_token=r.file_token, request_token=r2.request_token)) except VolumeLocNotFound: break else: continue chunk = r2.bytes # https://core.telegram.org/cdn#decrypting-files decrypted_chunk = aes.ctr256_decrypt( chunk, r.encryption_key, bytearray(r.encryption_iv[:-4] + (offset_bytes // 16).to_bytes(4, "big"))) hashes = await session.invoke( raw.functions.upload.GetCdnFileHashes( file_token=r.file_token, offset=offset_bytes)) # https://core.telegram.org/cdn#verifying-files for i, h in enumerate(hashes): cdn_chunk = decrypted_chunk[h.limit * i:h.limit * (i + 1)] CDNFileHashMismatch.check( h.hash == sha256(cdn_chunk).digest()) yield decrypted_chunk current += 1 offset_bytes += chunk_size if progress: func = functools.partial( progress, min(offset_bytes, file_size) if file_size != 0 else offset_bytes, file_size, *progress_args) if inspect.iscoroutinefunction(progress): await func() else: await self.loop.run_in_executor( self.executor, func) if len(chunk) < chunk_size or current >= total: break except Exception as e: raise e except pyrogram.StopTransmission: raise except Exception as e: log.error(e, exc_info=True)