Compare commits

...

11 commits

Author SHA1 Message Date
wulan17
ec209e0b2b
pyrofork: Add missing MessageOriginType enums docs
Some checks failed
Build-docs / build (push) Has been cancelled
Pyrofork / build (macos-latest, 3.10) (push) Has been cancelled
Pyrofork / build (macos-latest, 3.11) (push) Has been cancelled
Pyrofork / build (macos-latest, 3.12) (push) Has been cancelled
Pyrofork / build (macos-latest, 3.13) (push) Has been cancelled
Pyrofork / build (macos-latest, 3.9) (push) Has been cancelled
Pyrofork / build (ubuntu-latest, 3.10) (push) Has been cancelled
Pyrofork / build (ubuntu-latest, 3.11) (push) Has been cancelled
Pyrofork / build (ubuntu-latest, 3.12) (push) Has been cancelled
Pyrofork / build (ubuntu-latest, 3.13) (push) Has been cancelled
Pyrofork / build (ubuntu-latest, 3.9) (push) Has been cancelled
Signed-off-by: wulan17 <wulan17@komodos.id>
2025-05-16 22:42:11 +07:00
Ling-ex
a5cd3b92a6
Feat: Add group Parameter to the Decorator.on_error.
Some checks are pending
Build-docs / build (push) Waiting to run
Pyrofork / build (macos-latest, 3.10) (push) Waiting to run
Pyrofork / build (macos-latest, 3.11) (push) Waiting to run
Pyrofork / build (macos-latest, 3.12) (push) Waiting to run
Pyrofork / build (macos-latest, 3.13) (push) Waiting to run
Pyrofork / build (macos-latest, 3.9) (push) Waiting to run
Pyrofork / build (ubuntu-latest, 3.10) (push) Waiting to run
Pyrofork / build (ubuntu-latest, 3.11) (push) Waiting to run
Pyrofork / build (ubuntu-latest, 3.12) (push) Waiting to run
Pyrofork / build (ubuntu-latest, 3.13) (push) Waiting to run
Pyrofork / build (ubuntu-latest, 3.9) (push) Waiting to run
Signed-off-by: Ling-ex <nekochan@rizkiofficial.com>
2025-05-16 18:55:30 +07:00
wulan17
ce356e02f5
Revert "fix: handle connection closure and retry logic in session management"
This reverts commit 4df4478a80.

Signed-off-by: wulan17 <wulan17@komodos.id>
2025-05-16 18:54:25 +07:00
wulan17
632921b4b2
pyrofork: set Client.hide_password default value to True
Some checks are pending
Build-docs / build (push) Waiting to run
Pyrofork / build (macos-latest, 3.10) (push) Waiting to run
Pyrofork / build (macos-latest, 3.11) (push) Waiting to run
Pyrofork / build (macos-latest, 3.12) (push) Waiting to run
Pyrofork / build (macos-latest, 3.13) (push) Waiting to run
Pyrofork / build (macos-latest, 3.9) (push) Waiting to run
Pyrofork / build (ubuntu-latest, 3.10) (push) Waiting to run
Pyrofork / build (ubuntu-latest, 3.11) (push) Waiting to run
Pyrofork / build (ubuntu-latest, 3.12) (push) Waiting to run
Pyrofork / build (ubuntu-latest, 3.13) (push) Waiting to run
Pyrofork / build (ubuntu-latest, 3.9) (push) Waiting to run
Signed-off-by: wulan17 <wulan17@komodos.id>
2025-05-16 00:49:46 +07:00
wulan17
9401e246b6
pyrofork: Drop accept_terms_of_service and sign_up method
Signed-off-by: wulan17 <wulan17@komodos.id>
2025-05-16 00:49:46 +07:00
Ling-ex
7d85848bef
Add Ping method
Signed-off-by: Ling-ex <nekochan@rizkiofficial.com>
2025-05-16 00:49:46 +07:00
Ling-ex
7c3ed0da75
Add progress and progress_args parameters to Client.send_media_group and Message.reply_media_group
Signed-off-by: Ling-ex <nekochan@rizkiofficial.com>
2025-05-16 00:49:45 +07:00
wulan17
646f53d52f
pyrofork: Add is_frozen and frozen_icon field to User
Signed-off-by: wulan17 <wulan17@komodos.id>
2025-05-16 00:49:45 +07:00
wulan17
e083c5a48f
pyrofork: Refactor Qr Code Signin
Signed-off-by: wulan17 <wulan17@komodos.id>
2025-05-16 00:49:44 +07:00
wulan17
a08e10f5d7
pyrofork: fix MESSAGE_IDS_EMPTY error on get_scheduled_messages method
Signed-off-by: wulan17 <wulan17@komodos.id>
2025-05-15 23:02:25 +07:00
wulan17
0e0a8f7533
pyrofork: disable publish workflows
Signed-off-by: wulan17 <wulan17@nusantararom.org>
2025-05-15 23:02:19 +07:00
19 changed files with 312 additions and 403 deletions

View file

@ -1,40 +0,0 @@
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Upload Python Package
on:
push:
tags:
- '*'
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e '.[dev]'
- name: Build package
run: hatch build
- name: Publish package
uses: pypa/gh-action-pypi-publish@release/v1

View file

@ -146,6 +146,7 @@ def pyrogram_api():
stop_transmission
export_session_string
set_parse_mode
ping
""",
conversation="""
Conversation
@ -422,12 +423,10 @@ def pyrogram_api():
sign_in
sign_in_bot
sign_in_qrcode
sign_up
get_password_hint
check_password
send_recovery_code
recover_password
accept_terms_of_service
log_out
get_active_sessions
""",

View file

@ -0,0 +1,8 @@
MessageOriginType
=================
.. autoclass:: pyrogram.enums.MessageOriginType()
:members:
.. raw:: html
:file: ./cleanup.html

View file

@ -39,6 +39,7 @@ import pyrogram
from pyrogram import __version__, __license__
from pyrogram import enums
from pyrogram import raw
from pyrogram import types
from pyrogram import utils
from pyrogram.crypto import aes
from pyrogram.errors import CDNFileHashMismatch
@ -51,7 +52,7 @@ from pyrogram.handlers.handler import Handler
from pyrogram.methods import Methods
from pyrogram.session import Auth, Session
from pyrogram.storage import FileStorage, MemoryStorage, Storage
from pyrogram.types import User, TermsOfService
from pyrogram.types import User
from pyrogram.utils import ainput
from .connection import Connection
from .connection.transport import TCPAbridged
@ -272,7 +273,7 @@ class Client(Methods):
skip_updates: bool = True,
takeout: bool = None,
sleep_threshold: int = Session.SLEEP_THRESHOLD,
hide_password: Optional[bool] = False,
hide_password: Optional[bool] = True,
max_concurrent_transmissions: int = MAX_CONCURRENT_TRANSMISSIONS,
client_platform: "enums.ClientPlatform" = enums.ClientPlatform.OTHER,
max_message_cache_size: int = MAX_CACHE_SIZE,
@ -510,41 +511,18 @@ class Client(Methods):
print(e.MESSAGE)
self.password = None
else:
if self.use_qrcode and isinstance(signed_in, raw.types.auth.LoginToken):
if self.use_qrcode and isinstance(signed_in, types.LoginToken):
time_out = signed_in.expires - datetime.timestamp(datetime.now())
try:
await asyncio.wait_for(self._wait_for_update_login_token(), timeout=time_out)
except asyncio.TimeoutError:
print("QR code expired, Requesting new Qr code...")
continue
else:
break
break
if isinstance(signed_in, User):
return signed_in
while True:
first_name = await ainput("Enter first name: ")
last_name = await ainput("Enter last name (empty to skip): ")
try:
signed_up = await self.sign_up(
self.phone_number,
sent_code.phone_code_hash,
first_name,
last_name
)
except BadRequest as e:
print(e.MESSAGE)
else:
break
if isinstance(signed_in, TermsOfService):
print("\n" + signed_in.text + "\n")
await self.accept_terms_of_service(signed_in.id)
return signed_up
def set_parse_mode(self, parse_mode: Optional["enums.ParseMode"]):
"""Set the parse mode to be used globally by the client.

View file

@ -54,14 +54,6 @@ class TCP:
self.lock = asyncio.Lock()
self.loop = asyncio.get_event_loop()
self._closed = True
@property
def closed(self) -> bool:
return (
self._closed or self.writer is None or self.writer.is_closing() or self.reader is None
)
async def _connect_via_proxy(
self,
@ -131,14 +123,11 @@ class TCP:
async def connect(self, address: Tuple[str, int]) -> None:
try:
await asyncio.wait_for(self._connect(address), TCP.TIMEOUT)
self._closed = False
except asyncio.TimeoutError: # Re-raise as TimeoutError. asyncio.TimeoutError is deprecated in 3.11
self._closed = True
raise TimeoutError("Connection timed out")
async def close(self) -> None:
if self.writer is None:
self._closed = True
return None
try:
@ -146,12 +135,10 @@ class TCP:
await asyncio.wait_for(self.writer.wait_closed(), TCP.TIMEOUT)
except Exception as e:
log.info("Close exception: %s %s", type(e).__name__, e)
finally:
self._closed = True
async def send(self, data: bytes) -> None:
if self.writer is None or self._closed:
raise OSError("Connection is closed")
if self.writer is None:
return None
async with self.lock:
try:
@ -159,13 +146,9 @@ class TCP:
await self.writer.drain()
except Exception as e:
log.info("Send exception: %s %s", type(e).__name__, e)
self._closed = True
raise OSError(e)
async def recv(self, length: int = 0) -> Optional[bytes]:
if self._closed or self.reader is None:
return None
data = b""
while len(data) < length:
@ -175,13 +158,11 @@ class TCP:
TCP.TIMEOUT
)
except (OSError, asyncio.TimeoutError):
self._closed = True
return None
else:
if chunk:
data += chunk
else:
self._closed = True
return None
return data

View file

@ -17,7 +17,6 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrofork. If not, see <http://www.gnu.org/licenses/>.
from .accept_terms_of_service import AcceptTermsOfService
from .check_password import CheckPassword
from .connect import Connect
from .disconnect import Disconnect
@ -32,12 +31,10 @@ from .send_recovery_code import SendRecoveryCode
from .sign_in import SignIn
from .sign_in_bot import SignInBot
from .sign_in_qrcode import SignInQrcode
from .sign_up import SignUp
from .terminate import Terminate
class Auth(
AcceptTermsOfService,
CheckPassword,
Connect,
Disconnect,
@ -52,7 +49,6 @@ class Auth(
SignIn,
SignInBot,
SignInQrcode,
SignUp,
Terminate
):
pass

View file

@ -23,6 +23,7 @@ from typing import Union
import pyrogram
from pyrogram import raw
from pyrogram import types
from pyrogram.errors import PhoneNumberUnoccupied
log = logging.getLogger(__name__)
@ -49,15 +50,13 @@ class SignIn:
The valid confirmation code you received (either as Telegram message or as SMS in your phone number).
Returns:
:obj:`~pyrogram.types.User` | :obj:`~pyrogram.types.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:`~pyrogram.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.
:obj:`~pyrogram.types.User` | bool: On success, in case the
authorization completed, the user is returned.
Raises:
BadRequest: In case the arguments are invalid.
SessionPasswordNeeded: In case a password is needed to sign in.
PhoneNumberUnoccupied: In case the phone number is not registered on Telegram.
"""
phone_number = phone_number.strip(" +")
@ -70,10 +69,7 @@ class SignIn:
)
if isinstance(r, raw.types.auth.AuthorizationSignUpRequired):
if r.terms_of_service:
return types.TermsOfService._parse(terms_of_service=r.terms_of_service)
return False
raise PhoneNumberUnoccupied("The phone number is not registered on Telegram. Please use official Telegram app to register it.")
else:
await self.storage.user_id(r.user.id)
await self.storage.is_bot(False)

View file

@ -28,14 +28,6 @@ from pyrogram.session import Session, Auth
log = logging.getLogger(__name__)
QRCODE_AVAIL = False
try:
import qrcode
QRCODE_AVAIL = True
except ImportError:
QRCODE_AVAIL = False
class SignInQrcode:
async def sign_in_qrcode(
self: "pyrogram.Client"
@ -54,7 +46,9 @@ class SignInQrcode:
SessionPasswordNeeded: In case a password is needed to sign in.
"""
if not QRCODE_AVAIL:
try:
import qrcode
except ImportError:
raise ImportError("qrcode is missing! "
"Please install it with `pip install qrcode`")
r = await self.session.invoke(
@ -79,13 +73,13 @@ class SignInQrcode:
print("Scan the QR code with your Telegram app.")
qr.print_ascii()
return r
elif isinstance(r, raw.types.auth.LoginTokenSuccess):
return types.LoginToken._parse(r)
if isinstance(r, raw.types.auth.LoginTokenSuccess):
await self.storage.user_id(r.authorization.user.id)
await self.storage.is_bot(False)
return types.User._parse(self, r.authorization.user)
elif isinstance(r, raw.types.auth.LoginTokenMigrateTo):
if isinstance(r, raw.types.auth.LoginTokenMigrateTo):
# pylint: disable=access-member-before-definition
await self.session.stop()
@ -107,33 +101,11 @@ class SignInQrcode:
token=r.token
)
)
if isinstance(r, raw.types.auth.LoginToken):
base64_token = b64encode(r.token).decode("utf-8")
login_url = f"tg://login?token={base64_token}"
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(login_url)
qr.make(fit=True)
print("Scan the QR code below with your Telegram app.")
qr.print_ascii()
return types.LoginToken(
self,
r.token,
r.expires
)
elif isinstance(r, raw.types.auth.LoginTokenSuccess):
if isinstance(r, raw.types.auth.LoginTokenSuccess):
await self.storage.user_id(r.authorization.user.id)
await self.storage.is_bot(False)
return types.User._parse(self, r.authorization.user)
else:
raise pyrogram.exceptions.RPCError(
"Unknown response type from Telegram API"
)
return r
raise pyrogram.exceptions.RPCError(
"Unknown response type from Telegram API"
)

View file

@ -1,74 +0,0 @@
# Pyrofork - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-present Dan <https://github.com/delivrance>
# Copyright (C) 2022-present Mayuri-Chan <https://github.com/Mayuri-Chan>
#
# This file is part of Pyrofork.
#
# Pyrofork 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.
#
# Pyrofork 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 Pyrofork. If not, see <http://www.gnu.org/licenses/>.
import logging
import pyrogram
from pyrogram import raw
from pyrogram import types
log = logging.getLogger(__name__)
class SignUp:
async def sign_up(
self: "pyrogram.Client",
phone_number: str,
phone_code_hash: str,
first_name: str,
last_name: str = ""
) -> "types.User":
"""Register a new user in Telegram.
.. include:: /_includes/usable-by/users.rst
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:`~pyrogram.Client.send_code`.
first_name (``str``):
New user first name.
last_name (``str``, *optional*):
New user last name. Defaults to "" (empty string, no last name).
Returns:
:obj:`~pyrogram.types.User`: On success, the new registered user is returned.
Raises:
BadRequest: In case the arguments are invalid.
"""
phone_number = phone_number.strip(" +")
r = await self.invoke(
raw.functions.auth.SignUp(
phone_number=phone_number,
first_name=first_name,
last_name=last_name,
phone_code_hash=phone_code_hash
)
)
await self.storage.user_id(r.user.id)
await self.storage.is_bot(False)
return types.User._parse(self, r.user)

View file

@ -24,7 +24,11 @@ from pyrogram.filters import Filter
class OnError:
def on_error(self=None, errors=None) -> Callable:
def on_error(
self=None,
errors=None,
group: int = 0,
) -> Callable:
"""Decorator for handling new errors.
This does the same thing as :meth:`~pyrogram.Client.add_handler` using the
@ -34,16 +38,19 @@ class OnError:
errors (:obj:`~Exception`, *optional*):
Pass one or more errors to allow only a subset of errors to be passed
in your function.
group (``int``, *optional*):
The group identifier, defaults to 0.
"""
def decorator(func: Callable) -> Callable:
if isinstance(self, pyrogram.Client):
self.add_handler(pyrogram.handlers.ErrorHandler(func, errors), 0)
self.add_handler(pyrogram.handlers.ErrorHandler(func, errors), group)
elif isinstance(self, Filter) or self is None:
if not hasattr(func, "handlers"):
func.handlers = []
func.handlers.append((pyrogram.handlers.ErrorHandler(func, self), 0))
func.handlers.append((pyrogram.handlers.ErrorHandler(func, self), group))
return func

View file

@ -78,6 +78,6 @@ class GetScheduledMessages:
r = await self.invoke(rpc, sleep_threshold=-1)
messages = await utils.parse_messages(self, r)
messages = await utils.parse_messages(self, r, is_scheduled=True)
return messages if is_iterable else messages[0] if messages else None

View file

@ -22,7 +22,12 @@ import os
import re
from datetime import datetime
from pymediainfo import MediaInfo
from typing import Union, List, Optional
from typing import (
Union,
List,
Optional,
Callable,
)
import pyrogram
from pyrogram import enums
@ -35,7 +40,6 @@ log = logging.getLogger(__name__)
class SendMediaGroup:
# TODO: Add progress parameter
async def send_media_group(
self: "pyrogram.Client",
chat_id: Union[int, str],
@ -59,7 +63,9 @@ class SendMediaGroup:
protect_content: bool = None,
allow_paid_broadcast: bool = None,
message_effect_id: int = None,
invert_media: bool = None
invert_media: bool = None,
progress: Callable = None,
progress_args: tuple = (),
) -> List["types.Message"]:
"""Send a group of photos or videos as an album.
@ -89,7 +95,7 @@ class SendMediaGroup:
reply_to_message_id (``int``, *optional*):
If the message is a reply, ID of the original message.
reply_to_story_id (``int``, *optional*):
Unique identifier for the target story.
@ -126,6 +132,28 @@ class SendMediaGroup:
invert_media (``bool``, *optional*):
Inverts the position of the media and caption.
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:
List of :obj:`~pyrogram.types.Message`: On success, a list of the sent messages is returned.
@ -154,21 +182,22 @@ class SendMediaGroup:
reply_to_chat_id=reply_to_chat_id,
quote_text=quote_text,
quote_entities=quote_entities,
parse_mode=parse_mode
parse_mode=parse_mode,
)
for i in media:
if isinstance(i, types.InputMediaPhoto):
if isinstance(i.media, str):
if os.path.isfile(i.media):
file = await self.save_file(i.media, progress=progress, progress_args=progress_args)
media = await self.invoke(
raw.functions.messages.UploadMedia(
peer=await self.resolve_peer(chat_id),
media=raw.types.InputMediaUploadedPhoto(
file=await self.save_file(i.media),
spoiler=i.has_spoiler
)
)
file=file,
spoiler=i.has_spoiler,
),
),
)
media = raw.types.InputMediaPhoto(
@ -177,7 +206,7 @@ class SendMediaGroup:
access_hash=media.photo.access_hash,
file_reference=media.photo.file_reference
),
spoiler=i.has_spoiler
spoiler=i.has_spoiler,
)
elif re.match("^https?://", i.media):
media = await self.invoke(
@ -185,9 +214,9 @@ class SendMediaGroup:
peer=await self.resolve_peer(chat_id),
media=raw.types.InputMediaPhotoExternal(
url=i.media,
spoiler=i.has_spoiler
)
)
spoiler=i.has_spoiler,
),
),
)
media = raw.types.InputMediaPhoto(
@ -196,19 +225,20 @@ class SendMediaGroup:
access_hash=media.photo.access_hash,
file_reference=media.photo.file_reference
),
spoiler=i.has_spoiler
spoiler=i.has_spoiler,
)
else:
media = utils.get_input_media_from_file_id(i.media, FileType.PHOTO)
else:
file = await self.save_file(i.media, progress=progress, progress_args=progress_args)
media = await self.invoke(
raw.functions.messages.UploadMedia(
peer=await self.resolve_peer(chat_id),
media=raw.types.InputMediaUploadedPhoto(
file=await self.save_file(i.media),
spoiler=i.has_spoiler
)
)
file=file,
spoiler=i.has_spoiler,
),
),
)
media = raw.types.InputMediaPhoto(
@ -217,7 +247,7 @@ class SendMediaGroup:
access_hash=media.photo.access_hash,
file_reference=media.photo.file_reference
),
spoiler=i.has_spoiler
spoiler=i.has_spoiler,
)
elif (
isinstance(i, types.InputMediaVideo)
@ -241,22 +271,25 @@ class SendMediaGroup:
w=i.width,
h=i.height
),
raw.types.DocumentAttributeFilename(file_name=os.path.basename(i.media))
raw.types.DocumentAttributeFilename(file_name=os.path.basename(i.media)),
]
if is_animation:
attributes.append(raw.types.DocumentAttributeAnimated())
thumb = await self.save_file(i.thumb)
file = await self.save_file(i.media, progress=progress, progress_args=progress_args)
media = await self.invoke(
raw.functions.messages.UploadMedia(
peer=await self.resolve_peer(chat_id),
media=raw.types.InputMediaUploadedDocument(
file=await self.save_file(i.media),
thumb=await self.save_file(i.thumb),
file=file,
thumb=thumb,
spoiler=i.has_spoiler,
mime_type=self.guess_mime_type(i.media) or "video/mp4",
nosound_video=is_animation,
attributes=attributes
)
)
attributes=attributes,
),
),
)
media = raw.types.InputMediaDocument(
@ -265,7 +298,7 @@ class SendMediaGroup:
access_hash=media.document.access_hash,
file_reference=media.document.file_reference
),
spoiler=i.has_spoiler
spoiler=i.has_spoiler,
)
elif re.match("^https?://", i.media):
media = await self.invoke(
@ -273,9 +306,9 @@ class SendMediaGroup:
peer=await self.resolve_peer(chat_id),
media=raw.types.InputMediaDocumentExternal(
url=i.media,
spoiler=i.has_spoiler
)
)
spoiler=i.has_spoiler,
),
),
)
media = raw.types.InputMediaDocument(
@ -284,17 +317,19 @@ class SendMediaGroup:
access_hash=media.document.access_hash,
file_reference=media.document.file_reference
),
spoiler=i.has_spoiler
spoiler=i.has_spoiler,
)
else:
media = utils.get_input_media_from_file_id(i.media, FileType.VIDEO)
else:
thumb = await self.save_file(i.thumb)
file = await self.save_file(i.media, progress=progress, progress_args=progress_args)
media = await self.invoke(
raw.functions.messages.UploadMedia(
peer=await self.resolve_peer(chat_id),
media=raw.types.InputMediaUploadedDocument(
file=await self.save_file(i.media),
thumb=await self.save_file(i.thumb),
file=file,
thumb=thumb,
spoiler=i.has_spoiler,
mime_type=self.guess_mime_type(getattr(i.media, "name", "video.mp4")) or "video/mp4",
attributes=[
@ -304,10 +339,10 @@ class SendMediaGroup:
w=i.width,
h=i.height
),
raw.types.DocumentAttributeFilename(file_name=getattr(i.media, "name", "video.mp4"))
]
)
)
raw.types.DocumentAttributeFilename(file_name=getattr(i.media, "name", "video.mp4")),
],
),
),
)
media = raw.types.InputMediaDocument(
@ -316,127 +351,135 @@ class SendMediaGroup:
access_hash=media.document.access_hash,
file_reference=media.document.file_reference
),
spoiler=i.has_spoiler
spoiler=i.has_spoiler,
)
elif isinstance(i, types.InputMediaAudio):
if isinstance(i.media, str):
if os.path.isfile(i.media):
thumb = await self.save_file(i.thumb)
file = await self.save_file(i.media, progress=progress, progress_args=progress_args)
media = await self.invoke(
raw.functions.messages.UploadMedia(
peer=await self.resolve_peer(chat_id),
media=raw.types.InputMediaUploadedDocument(
mime_type=self.guess_mime_type(i.media) or "audio/mpeg",
file=await self.save_file(i.media),
thumb=await self.save_file(i.thumb),
file=file,
thumb=thumb,
attributes=[
raw.types.DocumentAttributeAudio(
duration=i.duration,
performer=i.performer,
title=i.title
),
raw.types.DocumentAttributeFilename(file_name=os.path.basename(i.media))
]
)
)
raw.types.DocumentAttributeFilename(file_name=os.path.basename(i.media)),
],
),
),
)
media = raw.types.InputMediaDocument(
id=raw.types.InputDocument(
id=media.document.id,
access_hash=media.document.access_hash,
file_reference=media.document.file_reference
)
file_reference=media.document.file_reference,
),
)
elif re.match("^https?://", i.media):
media = await self.invoke(
raw.functions.messages.UploadMedia(
peer=await self.resolve_peer(chat_id),
media=raw.types.InputMediaDocumentExternal(
url=i.media
)
)
url=i.media,
),
),
)
media = raw.types.InputMediaDocument(
id=raw.types.InputDocument(
id=media.document.id,
access_hash=media.document.access_hash,
file_reference=media.document.file_reference
)
file_reference=media.document.file_reference,
),
)
else:
media = utils.get_input_media_from_file_id(i.media, FileType.AUDIO)
else:
thumb = await self.save_file(i.thumb)
file = await self.save_file(i.media, progress=progress, progress_args=progress_args)
media = await self.invoke(
raw.functions.messages.UploadMedia(
peer=await self.resolve_peer(chat_id),
media=raw.types.InputMediaUploadedDocument(
mime_type=self.guess_mime_type(getattr(i.media, "name", "audio.mp3")) or "audio/mpeg",
file=await self.save_file(i.media),
thumb=await self.save_file(i.thumb),
file=file,
thumb=thumb,
attributes=[
raw.types.DocumentAttributeAudio(
duration=i.duration,
performer=i.performer,
title=i.title
),
raw.types.DocumentAttributeFilename(file_name=getattr(i.media, "name", "audio.mp3"))
]
)
)
raw.types.DocumentAttributeFilename(file_name=getattr(i.media, "name", "audio.mp3")),
],
),
),
)
media = raw.types.InputMediaDocument(
id=raw.types.InputDocument(
id=media.document.id,
access_hash=media.document.access_hash,
file_reference=media.document.file_reference
)
file_reference=media.document.file_reference,
),
)
elif isinstance(i, types.InputMediaDocument):
if isinstance(i.media, str):
if os.path.isfile(i.media):
thumb = await self.save_file(i.thumb)
file = await self.save_file(i.media, progress=progress, progress_args=progress_args)
media = await self.invoke(
raw.functions.messages.UploadMedia(
peer=await self.resolve_peer(chat_id),
media=raw.types.InputMediaUploadedDocument(
mime_type=self.guess_mime_type(i.media) or "application/zip",
file=await self.save_file(i.media),
thumb=await self.save_file(i.thumb),
file=file,
thumb=thumb,
attributes=[
raw.types.DocumentAttributeFilename(file_name=os.path.basename(i.media))
]
)
)
raw.types.DocumentAttributeFilename(file_name=os.path.basename(i.media)),
],
),
),
)
media = raw.types.InputMediaDocument(
id=raw.types.InputDocument(
id=media.document.id,
access_hash=media.document.access_hash,
file_reference=media.document.file_reference
)
file_reference=media.document.file_reference,
),
)
elif re.match("^https?://", i.media):
media = await self.invoke(
raw.functions.messages.UploadMedia(
peer=await self.resolve_peer(chat_id),
media=raw.types.InputMediaDocumentExternal(
url=i.media
)
)
url=i.media,
),
),
)
media = raw.types.InputMediaDocument(
id=raw.types.InputDocument(
id=media.document.id,
access_hash=media.document.access_hash,
file_reference=media.document.file_reference
)
file_reference=media.document.file_reference,
),
)
else:
media = utils.get_input_media_from_file_id(i.media, FileType.DOCUMENT)
else:
thumb = await self.save_file(i.thumb)
file = await self.save_file(i.media, progress=progress, progress_args=progress_args)
media = await self.invoke(
raw.functions.messages.UploadMedia(
peer=await self.resolve_peer(chat_id),
@ -444,21 +487,21 @@ class SendMediaGroup:
mime_type=self.guess_mime_type(
getattr(i.media, "name", "file.zip")
) or "application/zip",
file=await self.save_file(i.media),
thumb=await self.save_file(i.thumb),
file=file,
thumb=thumb,
attributes=[
raw.types.DocumentAttributeFilename(file_name=getattr(i.media, "name", "file.zip"))
]
)
)
raw.types.DocumentAttributeFilename(file_name=getattr(i.media, "name", "file.zip")),
],
),
),
)
media = raw.types.InputMediaDocument(
id=raw.types.InputDocument(
id=media.document.id,
access_hash=media.document.access_hash,
file_reference=media.document.file_reference
)
file_reference=media.document.file_reference,
),
)
else:
raise ValueError(f"{i.__class__.__name__} is not a supported type for send_media_group")
@ -467,8 +510,8 @@ class SendMediaGroup:
raw.types.InputSingleMedia(
media=media,
random_id=self.rnd_id(),
**(await utils.parse_text_entities(self, i.caption, i.parse_mode, i.caption_entities))
)
**(await utils.parse_text_entities(self, i.caption, i.parse_mode, i.caption_entities)),
),
)
rpc = raw.functions.messages.SendMultiMedia(
@ -480,20 +523,20 @@ class SendMediaGroup:
noforwards=protect_content,
allow_paid_floodskip=allow_paid_broadcast,
effect=message_effect_id,
invert_media=invert_media
invert_media=invert_media,
)
if business_connection_id is not None:
r = await self.invoke(
raw.functions.InvokeWithBusinessConnection(
connection_id=business_connection_id,
query=rpc
),
sleep_threshold=60
sleep_threshold=60,
)
else:
r = await self.invoke(rpc, sleep_threshold=60)
return await utils.parse_messages(
self,
raw.types.messages.Messages(
@ -505,7 +548,7 @@ class SendMediaGroup:
r.updates
)],
users=r.users,
chats=r.chats
chats=r.chats,
),
business_connection_id=business_connection_id
business_connection_id=business_connection_id,
)

View file

@ -19,6 +19,7 @@
from .add_handler import AddHandler
from .export_session_string import ExportSessionString
from .ping import Ping
from .remove_handler import RemoveHandler
from .remove_error_handler import RemoveErrorHandler
from .restart import Restart
@ -32,6 +33,7 @@ from .stop_transmission import StopTransmission
class Utilities(
AddHandler,
ExportSessionString,
Ping,
RemoveHandler,
RemoveErrorHandler,
Restart,

View file

@ -17,29 +17,30 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrofork. If not, see <http://www.gnu.org/licenses/>.
from time import time
import pyrogram
from pyrogram import raw
class AcceptTermsOfService:
async def accept_terms_of_service(
self: "pyrogram.Client",
terms_of_service_id: str
) -> bool:
"""Accept the given terms of service.
class Ping:
async def ping(self: "pyrogram.Client"):
"""Measure the round-trip time (RTT) to the Telegram server.
.. include:: /_includes/usable-by/users.rst
The ping method sends a request to the Telegram server and measures the time it takes to receive a response.
This can be useful for monitoring network latency and ensuring a stable connection to the server.
Parameters:
terms_of_service_id (``str``):
The terms of service identifier.
Returns:
float: The round-trip time in milliseconds (ms).
Example:
.. code-block:: python
latency = await app.ping()
print(f"Ping: {latency} ms")
"""
r = await self.invoke(
raw.functions.help.AcceptTermsOfService(
id=raw.types.DataJSON(
data=terms_of_service_id
)
)
start_time = time()
await self.invoke(
raw.functions.ping.Ping(ping_id=self.rnd_id()),
)
return bool(r)
return round((time() - start_time) * 1000.0, 3)

View file

@ -417,19 +417,6 @@ class Session:
while True:
try:
if (
self.connection is None
or self.connection.protocol is None
or getattr(self.connection.protocol, "closed", True)
):
log.warning(
"[%s] Connection is closed or not established. Attempting to reconnect...",
self.client.name,
)
await self.restart()
await asyncio.sleep(1)
continue
return await self.send(query, timeout=timeout)
except (FloodWait, FloodPremiumWait) as e:
amount = e.value
@ -451,16 +438,6 @@ class Session:
query_name, str(e) or repr(e)
)
if isinstance(e, OSError) and retries > 1:
try:
await self.restart()
except Exception as restart_error:
log.warning(
"[%s] Failed to restart session: %s",
self.client.name,
str(restart_error) or repr(restart_error),
)
await asyncio.sleep(0.5)
return await self.invoke(query, retries - 1, timeout)

View file

@ -18,6 +18,8 @@
from ..object import Object
from pyrogram import raw
class LoginToken(Object):
"""Contains info on a login token.
@ -35,3 +37,10 @@ class LoginToken(Object):
self.token = token
self.expires = expires
@staticmethod
def _parse(login_token: "raw.base.LoginToken") -> "LoginToken":
return LoginToken(
token=login_token.token,
expires=login_token.expires,
)

View file

@ -2779,7 +2779,9 @@ class Message(Object, Update):
allow_paid_broadcast: bool = None,
message_effect_id: int = None,
parse_mode: Optional["enums.ParseMode"] = None,
invert_media: bool = None
invert_media: bool = None,
progress: Callable = None,
progress_args: tuple = (),
) -> List["types.Message"]:
"""Bound method *reply_media_group* of :obj:`~pyrogram.types.Message`.
@ -2840,6 +2842,28 @@ class Message(Object, Update):
invert_media (``bool``, *optional*):
Inverts the position of the media and caption.
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:
On success, a :obj:`~pyrogram.types.Messages` object is returned containing all the
single messages sent.
@ -2878,7 +2902,9 @@ class Message(Object, Update):
quote_entities=quote_entities,
allow_paid_broadcast=allow_paid_broadcast,
message_effect_id=message_effect_id,
invert_media=invert_media
invert_media=invert_media,
progress=progress,
progress_args=progress_args,
)
async def reply_photo(

View file

@ -79,6 +79,9 @@ class User(Object, Update):
is_deleted(``bool``, *optional*):
True, if this user is deleted.
is_frozen(``bool``, *optional*):
True, if this user is frozen.
is_bot (``bool``, *optional*):
True, if this user is a bot.
@ -168,6 +171,10 @@ class User(Object, Update):
active_users (``int``, *optional*):
Bot's active users count.
frozen_icon (``int``, *optional*):
Frozen account icon.
This field is available only in case *is_frozen* is True.
"""
def __init__(
@ -179,6 +186,7 @@ class User(Object, Update):
is_contact: bool = None,
is_mutual_contact: bool = None,
is_deleted: bool = None,
is_frozen: bool = None,
is_bot: bool = None,
is_verified: bool = None,
is_restricted: bool = None,
@ -203,7 +211,8 @@ class User(Object, Update):
restrictions: List["types.Restriction"] = None,
reply_color: "types.ChatColor" = None,
profile_color: "types.ChatColor" = None,
active_users: int = None
active_users: int = None,
frozen_icon: int = None
):
super().__init__(client)
@ -212,6 +221,7 @@ class User(Object, Update):
self.is_contact = is_contact
self.is_mutual_contact = is_mutual_contact
self.is_deleted = is_deleted
self.is_frozen = is_frozen
self.is_bot = is_bot
self.is_verified = is_verified
self.is_restricted = is_restricted
@ -237,6 +247,7 @@ class User(Object, Update):
self.reply_color = reply_color
self.profile_color = profile_color
self.active_users = active_users
self.frozen_icon = frozen_icon
@property
def full_name(self) -> str:
@ -268,6 +279,8 @@ class User(Object, Update):
if user_name is None and usernames is not None and len(usernames) > 0:
user_name = usernames[0].username
usernames.pop(0)
frozen_icon = getattr(user, "bot_verification_icon", None)
return User(
id=user.id,
@ -275,6 +288,7 @@ class User(Object, Update):
is_contact=user.contact,
is_mutual_contact=user.mutual_contact,
is_deleted=user.deleted,
is_frozen=True if frozen_icon else False,
is_bot=user.bot,
is_verified=user.verified,
is_restricted=user.restricted,
@ -298,6 +312,7 @@ class User(Object, Update):
reply_color=types.ChatColor._parse(getattr(user, "color", None)),
profile_color=types.ChatColor._parse_profile_color(getattr(user, "profile_color", None)),
active_users=active_users,
frozen_icon=frozen_icon,
client=client
)

View file

@ -103,7 +103,8 @@ async def parse_messages(
client,
messages: "raw.types.messages.Messages",
replies: int = 1,
business_connection_id: str = None
business_connection_id: str = None,
is_scheduled: bool = False
) -> List["types.Message"]:
users = {i.id: i for i in messages.users}
chats = {i.id: i for i in messages.chats}
@ -120,64 +121,76 @@ async def parse_messages(
parsed_messages.append(await types.Message._parse(client, message, users, chats, topics, replies=0, business_connection_id=business_connection_id))
if replies:
messages_with_replies = {
i.id: i.reply_to.reply_to_msg_id
for i in messages.messages
if (
not isinstance(i, raw.types.MessageEmpty)
and i.reply_to
and isinstance(i.reply_to, raw.types.MessageReplyHeader)
and i.reply_to.reply_to_msg_id is not None
)
}
if not is_scheduled:
messages_with_replies = {
i.id: i.reply_to.reply_to_msg_id
for i in messages.messages
if (
not isinstance(i, raw.types.MessageEmpty)
and i.reply_to
and isinstance(i.reply_to, raw.types.MessageReplyHeader)
and i.reply_to.reply_to_msg_id is not None
)
}
message_reply_to_story = {
i.id: {'user_id': i.reply_to.user_id, 'story_id': i.reply_to.story_id}
for i in messages.messages
if not isinstance(i, raw.types.MessageEmpty) and i.reply_to and isinstance(i.reply_to, raw.types.MessageReplyStoryHeader)
}
message_reply_to_story = {
i.id: {'user_id': i.reply_to.user_id, 'story_id': i.reply_to.story_id}
for i in messages.messages
if not isinstance(i, raw.types.MessageEmpty) and i.reply_to and isinstance(i.reply_to, raw.types.MessageReplyStoryHeader)
}
if messages_with_replies:
# We need a chat id, but some messages might be empty (no chat attribute available)
# Scan until we find a message with a chat available (there must be one, because we are fetching replies)
for m in parsed_messages:
if m.chat:
chat_id = m.chat.id
break
else:
chat_id = 0
if messages_with_replies:
# We need a chat id, but some messages might be empty (no chat attribute available)
# Scan until we find a message with a chat available (there must be one, because we are fetching replies)
for m in parsed_messages:
if m.chat:
chat_id = m.chat.id
break
else:
chat_id = 0
reply_messages = await client.get_messages(
chat_id,
reply_to_message_ids=messages_with_replies.keys(),
replies=replies - 1
)
for message in parsed_messages:
reply_id = messages_with_replies.get(message.id, None)
for reply in reply_messages:
if reply.id == reply_id:
if not reply.forum_topic_created:
message.reply_to_message = reply
if message_reply_to_story:
for m in parsed_messages:
if m.chat:
chat_id = m.chat.id
break
else:
chat_id = 0
reply_messages = {}
for msg_id in message_reply_to_story.keys():
reply_messages[msg_id] = await client.get_stories(
message_reply_to_story[msg_id]['user_id'],
message_reply_to_story[msg_id]['story_id']
reply_messages = await client.get_messages(
chat_id,
reply_to_message_ids=messages_with_replies.keys(),
replies=replies - 1
)
for message in parsed_messages:
reply_id = messages_with_replies.get(message.id, None)
for reply in reply_messages:
if reply.id == reply_id:
if not reply.forum_topic_created:
message.reply_to_message = reply
if message_reply_to_story:
for m in parsed_messages:
if m.chat:
chat_id = m.chat.id
break
else:
chat_id = 0
reply_messages = {}
for msg_id in message_reply_to_story.keys():
reply_messages[msg_id] = await client.get_stories(
message_reply_to_story[msg_id]['user_id'],
message_reply_to_story[msg_id]['story_id']
)
for message in parsed_messages:
if message.id in reply_messages:
message.reply_to_story = reply_messages[message.id]
else:
for message in parsed_messages:
if message.id in reply_messages:
message.reply_to_story = reply_messages[message.id]
if (
message.reply_to_message_id
and not message.external_reply
):
message.reply_to_message = await client.get_messages(
message.chat.id,
message_ids=message.reply_to_message_id,
replies=replies - 1
)
return types.List(parsed_messages)