diff --git a/compiler/api/source/main_api.tl b/compiler/api/source/main_api.tl index e3f596a5..f0bc60e6 100644 --- a/compiler/api/source/main_api.tl +++ b/compiler/api/source/main_api.tl @@ -26,6 +26,14 @@ inputPeerChannel#27bcbbfc channel_id:long access_hash:long = InputPeer; inputPeerUserFromMessage#a87b0a1c peer:InputPeer msg_id:int user_id:long = InputPeer; inputPeerChannelFromMessage#bd2a0840 peer:InputPeer msg_id:int channel_id:long = InputPeer; +inputPeerEmpty#7f3b18ea = InputPeer; +inputPeerSelf#7da07ec9 = InputPeer; +inputPeerChat#35a95cb9 chat_id:long = InputPeer; +inputPeerUser#dde8a54c user_id:long access_hash:long = InputPeer; +inputPeerChannel#27bcbbfc channel_id:long access_hash:long = InputPeer; +inputPeerUserFromMessage#a87b0a1c peer:InputPeer msg_id:int user_id:long = InputPeer; +inputPeerChannelFromMessage#bd2a0840 peer:InputPeer msg_id:int channel_id:long = InputPeer; + inputUserEmpty#b98886cf = InputUser; inputUserSelf#f7c1b13f = InputUser; inputUser#f21158c6 user_id:long access_hash:long = InputUser; @@ -244,7 +252,7 @@ inputReportReasonFake#f5ddd6e7 = ReportReason; inputReportReasonIllegalDrugs#a8eb2be = ReportReason; inputReportReasonPersonalDetails#9ec7863d = ReportReason; -userFull#1f58e369 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# sponsored_enabled:flags2.7?true can_view_revenue:flags2.9?true bot_can_manage_emoji_status:flags2.10?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage business_intro:flags2.4?BusinessIntro birthday:flags2.5?Birthday personal_channel_id:flags2.6?long personal_channel_message:flags2.6?int stargifts_count:flags2.8?int = UserFull; +userFull#979d2376 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# sponsored_enabled:flags2.7?true can_view_revenue:flags2.9?true bot_can_manage_emoji_status:flags2.10?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage business_intro:flags2.4?BusinessIntro birthday:flags2.5?Birthday personal_channel_id:flags2.6?long personal_channel_message:flags2.6?int stargifts_count:flags2.8?int starref_program:flags2.11?StarRefProgram = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; @@ -430,7 +438,7 @@ updateBotEditBusinessMessage#7df587c flags:# connection_id:string message:Messag updateBotDeleteBusinessMessage#a02a982e connection_id:string peer:Peer messages:Vector qts:int = Update; updateNewStoryReaction#1824e40b story_id:int peer:Peer reaction:Reaction = Update; updateBroadcastRevenueTransactions#dfd961f5 peer:Peer balances:BroadcastRevenueBalances = Update; -updateStarsBalance#fb85198 balance:long = Update; +updateStarsBalance#4e80a379 balance:StarsAmount = Update; updateBusinessBotCallbackQuery#1ea2fda7 flags:# query_id:long user_id:long connection_id:string message:Message reply_to_message:flags.2?Message chat_instance:long data:flags.0?bytes = Update; updateStarsRevenueStatus#a584b019 peer:Peer status:StarsRevenueStatus = Update; updateBotPurchasedPaidMedia#283bd312 user_id:long payload:string qts:int = Update; @@ -1840,9 +1848,9 @@ starsTransactionPeerAPI#f9677aad = StarsTransactionPeer; starsTopupOption#bd915c0 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsTopupOption; -starsTransaction#35d4f276 flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true reaction:flags.11?true id:string stars:long date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector subscription_period:flags.12?int giveaway_post_id:flags.13?int stargift:flags.14?StarGift floodskip_number:flags.15?int = StarsTransaction; +starsTransaction#64dfc926 flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true reaction:flags.11?true id:string stars:StarsAmount date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector subscription_period:flags.12?int giveaway_post_id:flags.13?int stargift:flags.14?StarGift floodskip_number:flags.15?int starref_commission_permille:flags.16?int starref_peer:flags.17?Peer starref_amount:flags.17?StarsAmount = StarsTransaction; -payments.starsStatus#bbfa316c flags:# balance:long subscriptions:flags.1?Vector subscriptions_next_offset:flags.2?string subscriptions_missing_balance:flags.4?long history:flags.3?Vector next_offset:flags.0?string chats:Vector users:Vector = payments.StarsStatus; +payments.starsStatus#6c9ce8ed flags:# balance:StarsAmount subscriptions:flags.1?Vector subscriptions_next_offset:flags.2?string subscriptions_missing_balance:flags.4?long history:flags.3?Vector next_offset:flags.0?string chats:Vector users:Vector = payments.StarsStatus; foundStory#e87acbc0 peer:Peer story:StoryItem = FoundStory; @@ -1850,7 +1858,7 @@ stories.foundStories#e2de7737 flags:# count:int stories:Vector next_ geoPointAddress#de4c5d93 flags:# country_iso2:string state:flags.0?string city:flags.1?string street:flags.2?string = GeoPointAddress; -starsRevenueStatus#79342946 flags:# withdrawal_enabled:flags.0?true current_balance:long available_balance:long overall_revenue:long next_withdrawal_at:flags.1?int = StarsRevenueStatus; +starsRevenueStatus#febe5491 flags:# withdrawal_enabled:flags.0?true current_balance:StarsAmount available_balance:StarsAmount overall_revenue:StarsAmount next_withdrawal_at:flags.1?int = StarsRevenueStatus; payments.starsRevenueStats#c92bb73b revenue_graph:StatsGraph status:StarsRevenueStatus usd_rate:double = payments.StarsRevenueStats; @@ -1882,18 +1890,36 @@ starGift#49c577cd flags:# limited:flags.0?true sold_out:flags.1?true birthday:fl payments.starGiftsNotModified#a388a368 = payments.StarGifts; payments.starGifts#901689ea hash:int gifts:Vector = payments.StarGifts; + userStarGift#eea49a6e flags:# name_hidden:flags.0?true unsaved:flags.5?true from_id:flags.1?long date:int gift:StarGift message:flags.2?TextWithEntities msg_id:flags.3?int convert_stars:flags.4?long = UserStarGift; + payments.userStarGifts#6b65b517 flags:# count:int gifts:Vector next_offset:flags.0?string users:Vector = payments.UserStarGifts; messageReportOption#7903e3d9 text:string option:bytes = MessageReportOption; + reportResultChooseOption#f0e4e0b6 title:string options:Vector = ReportResult; reportResultAddComment#6f09ac31 flags:# optional:flags.0?true option:bytes = ReportResult; reportResultReported#8db33c4b = ReportResult; messages.botPreparedInlineMessage#8ecf0511 id:string expire_date:int = messages.BotPreparedInlineMessage; + messages.preparedInlineMessage#ff57708d query_id:long result:BotInlineResult peer_types:Vector cache_time:int users:Vector = messages.PreparedInlineMessage; + botAppSettings#c99b1950 flags:# placeholder_path:flags.0?bytes background_color:flags.1?int background_dark_color:flags.2?int header_color:flags.3?int header_dark_color:flags.4?int = BotAppSettings; +starRefProgram#dd0c66f2 flags:# bot_id:long commission_permille:int duration_months:flags.0?int end_date:flags.1?int daily_revenue_per_user:flags.2?StarsAmount = StarRefProgram; + +connectedBotStarRef#19a13f71 flags:# revoked:flags.1?true url:string date:int bot_id:long commission_permille:int duration_months:flags.0?int participants:long revenue:long = ConnectedBotStarRef; + +payments.connectedStarRefBots#98d5ea1d count:int connected_bots:Vector users:Vector = payments.ConnectedStarRefBots; + +payments.suggestedStarRefBots#b4d5d859 flags:# count:int suggested_bots:Vector users:Vector next_offset:flags.0?string = payments.SuggestedStarRefBots; + +starsAmount#bbb6b4a3 amount:long nanos:int = StarsAmount; + +messages.foundStickersNotModified#6010c534 flags:# next_offset:flags.0?int = messages.FoundStickers; +messages.foundStickers#82c9e290 flags:# next_offset:flags.0?int hash:long stickers:Vector = messages.FoundStickers; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -2059,7 +2085,7 @@ contacts.block#2e2e8734 flags:# my_stories_from:flags.0?true id:InputPeer = Bool contacts.unblock#b550d328 flags:# my_stories_from:flags.0?true id:InputPeer = Bool; contacts.getBlocked#9a868f80 flags:# my_stories_from:flags.0?true offset:int limit:int = contacts.Blocked; contacts.search#11f812d8 q:string limit:int = contacts.Found; -contacts.resolveUsername#f93ccba3 username:string = contacts.ResolvedPeer; +contacts.resolveUsername#725afbbc flags:# username:string referer:flags.0?string = contacts.ResolvedPeer; contacts.getTopPeers#973478b6 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true phone_calls:flags.3?true forward_users:flags.4?true forward_chats:flags.5?true groups:flags.10?true channels:flags.15?true bots_app:flags.16?true offset:int limit:int hash:long = contacts.TopPeers; contacts.resetTopPeerRating#1ae373ac category:TopPeerCategory peer:InputPeer = Bool; contacts.resetSaved#879537f1 = Bool; @@ -2299,6 +2325,7 @@ messages.reportSponsoredMessage#1af3dbb8 peer:InputPeer random_id:bytes option:b messages.getSponsoredMessages#9bd2f439 peer:InputPeer = messages.SponsoredMessages; messages.savePreparedInlineMessage#f21f7f2f flags:# result:InputBotInlineResult user_id:InputUser peer_types:flags.0?Vector = messages.BotPreparedInlineMessage; messages.getPreparedInlineMessage#857ebdb8 bot:InputUser id:string = messages.PreparedInlineMessage; +messages.searchStickers#29b1c66a flags:# emojis:flags.0?true q:string emoticon:string lang_code:Vector offset:int limit:int hash:long = messages.FoundStickers; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; @@ -2433,6 +2460,8 @@ bots.getPreviewMedias#a2a5594d bot:InputUser = Vector; bots.updateUserEmojiStatus#ed9f30c5 user_id:InputUser emoji_status:EmojiStatus = Bool; bots.toggleUserEmojiStatusPermission#6de6392 bot:InputUser enabled:Bool = Bool; bots.checkDownloadFileParams#50077589 bot:InputUser file_name:string url:string = Bool; +bots.getAdminedBots#b0711d83 = Vector; +bots.updateStarRefProgram#778b5ab3 flags:# bot:InputUser commission_permille:int duration_months:flags.0?int = StarRefProgram; payments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm; payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt; @@ -2468,7 +2497,12 @@ payments.getStarGifts#c4563590 hash:int = payments.StarGifts; payments.getUserStarGifts#5e72c7e1 user_id:InputUser offset:string limit:int = payments.UserStarGifts; payments.saveStarGift#87acf08e flags:# unsave:flags.0?true user_id:InputUser msg_id:int = Bool; payments.convertStarGift#421e027 user_id:InputUser msg_id:int = Bool; -payments.botCancelStarsSubscription#57f9ece6 flags:# restore:flags.0?true user_id:InputUser invoice_slug:flags.1?string charge_id:flags.2?string = Bool; +payments.botCancelStarsSubscription#6dfa0622 flags:# restore:flags.0?true user_id:InputUser charge_id:string = Bool; +payments.getConnectedStarRefBots#5869a553 flags:# peer:InputPeer offset_date:flags.2?int offset_link:flags.2?string limit:int = payments.ConnectedStarRefBots; +payments.getConnectedStarRefBot#b7d998f0 peer:InputPeer bot:InputUser = payments.ConnectedStarRefBots; +payments.getSuggestedStarRefBots#d6b48f7 flags:# order_by_revenue:flags.0?true order_by_date:flags.1?true peer:InputPeer offset:string limit:int = payments.SuggestedStarRefBots; +payments.connectStarRefBot#7ed5348a peer:InputPeer bot:InputUser = payments.ConnectedStarRefBots; +payments.editConnectedStarRefBot#e4fca4a3 flags:# revoked:flags.0?true peer:InputPeer link:string = payments.ConnectedStarRefBots; stickers.createStickerSet#9021ab67 flags:# masks:flags.0?true emojis:flags.5?true text_color:flags.6?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector software:flags.3?string = messages.StickerSet; stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet; @@ -2588,4 +2622,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; -// LAYER 194 +// LAYER 195 \ No newline at end of file diff --git a/compiler/docs/compiler.py b/compiler/docs/compiler.py index 01d7d13a..6a199443 100644 --- a/compiler/docs/compiler.py +++ b/compiler/docs/compiler.py @@ -165,6 +165,7 @@ def pyrogram_api(): messages=""" Messages send_message + forward_media_group forward_messages copy_message copy_media_group @@ -397,6 +398,7 @@ def pyrogram_api(): sell_gift send_gift toggle_gift_is_saved + get_owned_bots """, business=""" Telegram Business @@ -511,6 +513,7 @@ def pyrogram_api(): Audio AvailableEffect Document + AlternativeVideo Animation Video Voice diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index 8e81f6c6..03bfead4 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -18,7 +18,7 @@ # along with Pyrofork. If not, see . __fork_name__ = "PyroFork" -__version__ = "2.3.52" +__version__ = "2.3.53" __license__ = "GNU Lesser General Public License v3.0 (LGPL-3.0)" __copyright__ = "Copyright (C) 2022-present Mayuri-Chan " diff --git a/pyrogram/dispatcher.py b/pyrogram/dispatcher.py index b6c47658..592f1952 100644 --- a/pyrogram/dispatcher.py +++ b/pyrogram/dispatcher.py @@ -23,9 +23,8 @@ import logging from collections import OrderedDict import pyrogram -from pyrogram import errors -from pyrogram import utils -from pyrogram import raw +from pyrogram import errors, raw, types, utils +from pyrogram.handlers.handler import Handler from pyrogram.handlers import ( BotBusinessConnectHandler, BotBusinessMessageHandler, @@ -33,6 +32,7 @@ from pyrogram.handlers import ( MessageHandler, EditedMessageHandler, EditedBotBusinessMessageHandler, + ErrorHandler, DeletedMessagesHandler, DeletedBotBusinessMessagesHandler, MessageReactionUpdatedHandler, @@ -97,6 +97,7 @@ class Dispatcher: self.handler_worker_tasks = [] self.locks_list = [] + self.error_handlers = [] self.updates_queue = asyncio.Queue() self.groups = OrderedDict() @@ -286,6 +287,7 @@ class Dispatcher: self.handler_worker_tasks.clear() self.groups.clear() + self.error_handlers.clear() log.info("Stopped %s HandlerTasks", self.client.workers) @@ -295,11 +297,14 @@ class Dispatcher: await lock.acquire() try: - if group not in self.groups: - self.groups[group] = [] - self.groups = OrderedDict(sorted(self.groups.items())) - - self.groups[group].append(handler) + if isinstance(handler, ErrorHandler): + if handler not in self.error_handlers: + self.error_handlers.append(handler) + else: + if group not in self.groups: + self.groups[group] = [] + self.groups = OrderedDict(sorted(self.groups.items())) + self.groups[group].append(handler) finally: for lock in self.locks_list: lock.release() @@ -312,71 +317,94 @@ class Dispatcher: await lock.acquire() try: - if group not in self.groups: - raise ValueError(f"Group {group} does not exist. Handler was not removed.") - - self.groups[group].remove(handler) + if isinstance(handler, ErrorHandler): + if handler not in self.error_handlers: + raise ValueError( + f"Error handler {handler} does not exist. Handler was not removed." + ) + self.error_handlers.remove(handler) + else: + if group not in self.groups: + raise ValueError(f"Group {group} does not exist. Handler was not removed.") + self.groups[group].remove(handler) finally: for lock in self.locks_list: lock.release() self.loop.create_task(fn()) - async def handler_worker(self, lock): + async def handler_worker(self, lock: asyncio.Lock): while True: packet = await self.updates_queue.get() - if packet is None: break + await self._process_packet(packet, lock) - try: - update, users, chats = packet - parser = self.update_parsers.get(type(update), None) + async def _process_packet( + self, + packet: tuple[raw.core.TLObject, dict[int, types.Update], dict[int, types.Update]], + lock: asyncio.Lock, + ): + try: + update, users, chats = packet + parser = self.update_parsers.get(type(update)) - parsed_update, handler_type = ( - await parser(update, users, chats) - if parser is not None - else (None, type(None)) - ) - - async with lock: - for group in self.groups.values(): - for handler in group: - args = None - - if isinstance(handler, handler_type): - try: - if await handler.check(self.client, parsed_update): - args = (parsed_update,) - except Exception as e: - log.exception(e) - continue + if parser is not None: + parsed_result = parser(update, users, chats) + if inspect.isawaitable(parsed_result): + parsed_update, handler_type = await parsed_result + else: + parsed_update, handler_type = parsed_result + else: + parsed_update, handler_type = (None, type(None)) + async with lock: + for group in self.groups.values(): + for handler in group: + try: + if parsed_update is not None: + if isinstance(handler, handler_type) and await handler.check( + self.client, parsed_update + ): + await self._execute_callback(handler, parsed_update) + break elif isinstance(handler, RawUpdateHandler): - args = (update, users, chats) - - if args is None: - continue - - try: - if inspect.iscoroutinefunction(handler.callback): - await handler.callback(self.client, *args) - else: - await self.loop.run_in_executor( - self.client.executor, - handler.callback, - self.client, - *args - ) - except pyrogram.StopPropagation: + await self._execute_callback(handler, update, users, chats) + break + except (pyrogram.StopPropagation, pyrogram.ContinuePropagation) as e: + if isinstance(e, pyrogram.StopPropagation): raise - except pyrogram.ContinuePropagation: - continue - except Exception as e: - log.exception(e) + except Exception as exception: + if parsed_update is not None: + await self._handle_exception(parsed_update, exception) + except pyrogram.StopPropagation: + pass + except Exception as e: + log.exception(e) + finally: + self.updates_queue.task_done() - break + async def _handle_exception(self, parsed_update: types.Update, exception: Exception): + handled_error = False + for error_handler in self.error_handlers: + try: + if await error_handler.check(self.client, parsed_update, exception): + handled_error = True + break except pyrogram.StopPropagation: - pass - except Exception as e: - log.exception(e) + raise + except pyrogram.ContinuePropagation: + continue + except Exception as inner_exception: + log.exception("Error in error handler: %s", inner_exception) + + if not handled_error: + log.exception("Unhandled exception: %s", exception) + + async def _execute_callback(self, handler: Handler, *args): + if inspect.iscoroutinefunction(handler.callback): + await handler.callback(self.client, *args) + else: + await self.client.loop.run_in_executor( + self.client.executor, handler.callback, self.client, *args + ) diff --git a/pyrogram/handlers/__init__.py b/pyrogram/handlers/__init__.py index eb879a4d..b4611cc6 100644 --- a/pyrogram/handlers/__init__.py +++ b/pyrogram/handlers/__init__.py @@ -29,6 +29,7 @@ from .deleted_bot_business_messages_handler import DeletedBotBusinessMessagesHan from .disconnect_handler import DisconnectHandler from .edited_message_handler import EditedMessageHandler from .edited_bot_business_message_handler import EditedBotBusinessMessageHandler +from .error_handler import ErrorHandler from .inline_query_handler import InlineQueryHandler from .message_handler import MessageHandler from .poll_handler import PollHandler diff --git a/pyrogram/handlers/error_handler.py b/pyrogram/handlers/error_handler.py new file mode 100644 index 00000000..cf45bc11 --- /dev/null +++ b/pyrogram/handlers/error_handler.py @@ -0,0 +1,79 @@ +# Pyrofork - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-present Dan +# Copyright (C) 2022-present 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 . + +from __future__ import annotations + +from collections.abc import Iterable +from typing import Callable + +from .handler import Handler + +import pyrogram +from pyrogram.types import Update + + +class ErrorHandler(Handler): + """The Error handler class. Used to handle errors. + It is intended to be used with :meth:`~pyrogram.Client.add_handler` + + For a nicer way to register this handler, have a look at the + :meth:`~pyrogram.Client.on_error` decorator. + + Parameters: + callback (``Callable``): + Pass a function that will be called when a new Error arrives. It takes *(client, error)* + as positional arguments (look at the section below for a detailed description). + + exceptions (``Exception`` | Iterable of ``Exception``, *optional*): + Pass one or more exception classes to allow only a subset of errors to be passed + in your callback function. + + Other parameters: + client (:obj:`~pyrogram.Client`): + The Client itself, useful when you want to call other API methods inside the error handler. + + update (:obj:`~pyrogram.Update`): + The update that caused the error. + + error (``Exception``): + The error that was raised. + """ + + def __init__( + self, + callback: Callable, + exceptions: type[Exception] | Iterable[type[Exception]] | None = None, + ): + self.exceptions = ( + tuple(exceptions) + if isinstance(exceptions, Iterable) + else (exceptions,) + if exceptions + else (Exception,) + ) + super().__init__(callback) + + async def check(self, client: pyrogram.Client, update: Update, exception: Exception) -> bool: + if isinstance(exception, self.exceptions): + await self.callback(client, update, exception) + return True + return False + + def check_remove(self, error: type[Exception] | Iterable[type[Exception]]) -> bool: + return isinstance(error, self.exceptions) diff --git a/pyrogram/handlers/raw_update_handler.py b/pyrogram/handlers/raw_update_handler.py index 3ea2bfc3..bb278032 100644 --- a/pyrogram/handlers/raw_update_handler.py +++ b/pyrogram/handlers/raw_update_handler.py @@ -35,6 +35,10 @@ class RawUpdateHandler(Handler): *(client, update, users, chats)* as positional arguments (look at the section below for a detailed description). + filters (:obj:`Filters`): + Pass one or more filters to allow only a subset of callback queries to be passed + in your callback function. + Other Parameters: client (:obj:`~pyrogram.Client`): The Client itself, useful when you want to call other API methods inside the update handler. @@ -64,5 +68,5 @@ class RawUpdateHandler(Handler): - :obj:`~pyrogram.raw.types.ChannelForbidden` """ - def __init__(self, callback: Callable): - super().__init__(callback) + def __init__(self, callback: Callable, filters=None): + super().__init__(callback, filters) diff --git a/pyrogram/methods/bots/__init__.py b/pyrogram/methods/bots/__init__.py index 003bac3e..5d212da1 100644 --- a/pyrogram/methods/bots/__init__.py +++ b/pyrogram/methods/bots/__init__.py @@ -36,6 +36,7 @@ from .set_bot_default_privileges import SetBotDefaultPrivileges from .set_bot_info import SetBotInfo from .set_chat_menu_button import SetChatMenuButton from .set_game_score import SetGameScore +from .get_owned_bots import GetOwnedBots class Bots( @@ -57,6 +58,7 @@ class Bots( SetChatMenuButton, GetChatMenuButton, AnswerWebAppQuery, - GetCollectibleItemInfo + GetCollectibleItemInfo, + GetOwnedBots, ): pass diff --git a/pyrogram/methods/bots/get_owned_bots.py b/pyrogram/methods/bots/get_owned_bots.py new file mode 100644 index 00000000..ca205a09 --- /dev/null +++ b/pyrogram/methods/bots/get_owned_bots.py @@ -0,0 +1,46 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-present Dan +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + + +from typing import List +import pyrogram +from pyrogram import raw, types + + +class GetOwnedBots: + async def get_owned_bots( + self: "pyrogram.Client", + ) -> List["types.User"]: + """Returns the list of bots owned by the current user. + + .. include:: /_includes/usable-by/users.rst + + Returns: + List of :obj:`~pyrogram.types.User`: On success. + + Example: + .. code-block:: python + + bots = await app.get_owned_bots() + """ + + bots = await self.invoke(raw.functions.bots.GetAdminedBots()) + return types.List([ + types.User._parse(self, b) + for b in bots + ]) diff --git a/pyrogram/methods/chats/pin_chat_message.py b/pyrogram/methods/chats/pin_chat_message.py index 1eaf6bcd..db4d1086 100644 --- a/pyrogram/methods/chats/pin_chat_message.py +++ b/pyrogram/methods/chats/pin_chat_message.py @@ -22,14 +22,18 @@ from typing import Union import pyrogram from pyrogram import raw, types +from ..messages.inline_session import get_session + class PinChatMessage: + async def pin_chat_message( self: "pyrogram.Client", chat_id: Union[int, str], message_id: int, disable_notification: bool = False, both_sides: bool = False, + business_connection_id: str = None, ) -> "types.Message": """Pin a message in a group, channel or your own chat. You must be an administrator in the chat for this to work and must have the "can_pin_messages" admin right in @@ -52,6 +56,9 @@ class PinChatMessage: both_sides (``bool``, *optional*): Pass True to pin the message for both sides (you and recipient). Applicable to private chats only. Defaults to False. + + business_connection_id (``str``, *optional*): + Unique identifier of the business connection on behalf of which the message will be pinned. Returns: :obj:`~pyrogram.types.Message`: On success, the service message is returned. @@ -65,19 +72,39 @@ class PinChatMessage: # Pin without notification await app.pin_chat_message(chat_id, message_id, disable_notification=True) """ - r = await self.invoke( - raw.functions.messages.UpdatePinnedMessage( - peer=await self.resolve_peer(chat_id), - id=message_id, - silent=disable_notification or None, - pm_oneside=not both_sides or None - ) + rpc = raw.functions.messages.UpdatePinnedMessage( + peer=await self.resolve_peer(chat_id), + id=message_id, + silent=disable_notification or None, + pm_oneside=not both_sides or None ) + session = None + business_connection = None + if business_connection_id: + business_connection = self.business_user_connection_cache[business_connection_id] + if not business_connection: + business_connection = await self.get_business_connection(business_connection_id) + session = await get_session( + self, + business_connection._raw.connection.dc_id + ) + + if business_connection_id: + r = await session.invoke( + raw.functions.InvokeWithBusinessConnection( + query=rpc, + connection_id=business_connection_id + ) + ) + else: + r = await self.invoke(rpc) + users = {u.id: u for u in r.users} chats = {c.id: c for c in r.chats} for i in r.updates: if isinstance(i, (raw.types.UpdateNewMessage, - raw.types.UpdateNewChannelMessage)): - return await types.Message._parse(self, i.message, users, chats) + raw.types.UpdateNewChannelMessage, + raw.types.UpdateBotNewBusinessMessage)): + return await types.Message._parse(self, i.message, users, chats, business_connection_id=business_connection_id) diff --git a/pyrogram/methods/chats/unpin_all_chat_messages.py b/pyrogram/methods/chats/unpin_all_chat_messages.py index fe34bb14..ad0b0b7d 100644 --- a/pyrogram/methods/chats/unpin_all_chat_messages.py +++ b/pyrogram/methods/chats/unpin_all_chat_messages.py @@ -27,6 +27,7 @@ class UnpinAllChatMessages: async def unpin_all_chat_messages( self: "pyrogram.Client", chat_id: Union[int, str], + message_thread_id: int = None, ) -> bool: """Use this method to clear the list of pinned messages in a chat. If the chat is not a private chat, the bot must be an administrator in the chat for this to work and must have @@ -39,6 +40,9 @@ class UnpinAllChatMessages: Unique identifier (int) or username (str) of the target chat. You can also use chat public link in form of *t.me/* (str). + message_thread_id (``int``, *optional*): + Unique identifier for the target message thread of the forum topic + Returns: ``bool``: True on success. @@ -48,10 +52,10 @@ class UnpinAllChatMessages: # Unpin all chat messages await app.unpin_all_chat_messages(chat_id) """ - await self.invoke( + r = await self.invoke( raw.functions.messages.UnpinAllMessages( - peer=await self.resolve_peer(chat_id) + peer=await self.resolve_peer(chat_id), + top_msg_id=message_thread_id ) ) - - return True + return r.pts_count diff --git a/pyrogram/methods/chats/unpin_chat_message.py b/pyrogram/methods/chats/unpin_chat_message.py index abd01b03..7959962f 100644 --- a/pyrogram/methods/chats/unpin_chat_message.py +++ b/pyrogram/methods/chats/unpin_chat_message.py @@ -22,12 +22,15 @@ from typing import Union import pyrogram from pyrogram import raw +from ..messages.inline_session import get_session + class UnpinChatMessage: async def unpin_chat_message( self: "pyrogram.Client", chat_id: Union[int, str], - message_id: int = 0 + message_id: int = 0, + business_connection_id: str = None, ) -> bool: """Unpin a message in a group, channel or your own chat. You must be an administrator in the chat for this to work and must have the "can_pin_messages" admin @@ -38,12 +41,15 @@ class UnpinChatMessage: Parameters: chat_id (``int`` | ``str``): Unique identifier (int) or username (str) of the target chat. - You can also use chat public link in form of *t.me/* (str). message_id (``int``, *optional*): Identifier of a message to unpin. + Required if ``business_connection_id`` is specified. If not specified, the most recent pinned message (by sending date) will be unpinned. + business_connection_id (``str``, *optional*): + Unique identifier of the business connection on behalf of which the message will be unpinned. + Returns: ``bool``: True on success. @@ -52,12 +58,32 @@ class UnpinChatMessage: await app.unpin_chat_message(chat_id, message_id) """ - await self.invoke( - raw.functions.messages.UpdatePinnedMessage( - peer=await self.resolve_peer(chat_id), - id=message_id, - unpin=True - ) + rpc = raw.functions.messages.UpdatePinnedMessage( + peer=await self.resolve_peer(chat_id), + id=message_id, + unpin=True ) + + session = None + business_connection = None + if business_connection_id: + business_connection = self.business_user_connection_cache[business_connection_id] + if not business_connection: + business_connection = await self.get_business_connection(business_connection_id) + session = await get_session( + self, + business_connection._raw.connection.dc_id + ) + + if business_connection_id: + await session.invoke( + raw.functions.InvokeWithBusinessConnection( + query=rpc, + connection_id=business_connection_id + ) + ) + else: + await self.invoke(rpc) return True + \ No newline at end of file diff --git a/pyrogram/methods/decorators/__init__.py b/pyrogram/methods/decorators/__init__.py index 402b7e26..e7120729 100644 --- a/pyrogram/methods/decorators/__init__.py +++ b/pyrogram/methods/decorators/__init__.py @@ -28,6 +28,7 @@ from .on_deleted_bot_business_messages import OnDeletedBotBusinessMessages from .on_disconnect import OnDisconnect from .on_edited_message import OnEditedMessage from .on_edited_bot_business_message import OnEditedBotBusinessMessage +from .on_error import OnError from .on_inline_query import OnInlineQuery from .on_message import OnMessage from .on_poll import OnPoll @@ -47,6 +48,7 @@ class Decorators( OnBotBusinessMessage, OnEditedMessage, OnEditedBotBusinessMessage, + OnError, OnDeletedMessages, OnDeletedBotBusinessMessages, OnCallbackQuery, diff --git a/pyrogram/methods/decorators/on_error.py b/pyrogram/methods/decorators/on_error.py new file mode 100644 index 00000000..d21d75bb --- /dev/null +++ b/pyrogram/methods/decorators/on_error.py @@ -0,0 +1,50 @@ +# Pyrofork - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-present Dan +# Copyright (C) 2022-present 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 . + +from typing import Callable + +import pyrogram +from pyrogram.filters import Filter + + +class OnError: + def on_error(self=None, errors=None) -> Callable: + """Decorator for handling new errors. + + This does the same thing as :meth:`~pyrogram.Client.add_handler` using the + :obj:`~pyrogram.handlers.MessageHandler`. + + Parameters: + errors (:obj:`~Exception`, *optional*): + Pass one or more errors to allow only a subset of errors to be passed + in your function. + """ + + def decorator(func: Callable) -> Callable: + if isinstance(self, pyrogram.Client): + self.add_handler(pyrogram.handlers.ErrorHandler(func, errors), 0) + elif isinstance(self, Filter) or self is None: + if not hasattr(func, "handlers"): + func.handlers = [] + + func.handlers.append((pyrogram.handlers.ErrorHandler(func, self), 0)) + + return func + + return decorator diff --git a/pyrogram/methods/decorators/on_raw_update.py b/pyrogram/methods/decorators/on_raw_update.py index ff1a8c95..88e842ab 100644 --- a/pyrogram/methods/decorators/on_raw_update.py +++ b/pyrogram/methods/decorators/on_raw_update.py @@ -17,15 +17,17 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrofork. If not, see . -from typing import Callable +from typing import Callable, Optional import pyrogram +from pyrogram.filters import Filter class OnRawUpdate: def on_raw_update( - self=None, - group: int = 0 + self: Optional["OnRawUpdate"] = None, + filters=None, + group: int = 0, ) -> Callable: """Decorator for handling raw updates. @@ -33,24 +35,28 @@ class OnRawUpdate: :obj:`~pyrogram.handlers.RawUpdateHandler`. Parameters: + filters (:obj:`~pyrogram.filters`, *optional*): + Pass one or more filters to allow only a subset of callback queries 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.RawUpdateHandler(func), group) - else: + self.add_handler(pyrogram.handlers.RawUpdateHandler(func, filters), group) + elif isinstance(self, Filter) or self is None: if not hasattr(func, "handlers"): func.handlers = [] func.handlers.append( ( - pyrogram.handlers.RawUpdateHandler(func), - group + pyrogram.handlers.RawUpdateHandler(func, self), + group if filters is None else filters ) ) return func - return decorator + return decorator \ No newline at end of file diff --git a/pyrogram/methods/messages/__init__.py b/pyrogram/methods/messages/__init__.py index dc2ccd98..a29275c4 100644 --- a/pyrogram/methods/messages/__init__.py +++ b/pyrogram/methods/messages/__init__.py @@ -30,6 +30,7 @@ from .edit_message_caption import EditMessageCaption from .edit_message_media import EditMessageMedia from .edit_message_reply_markup import EditMessageReplyMarkup from .edit_message_text import EditMessageText +from .forward_media_group import ForwardMediaGroup from .forward_messages import ForwardMessages from .get_available_effects import GetAvailableEffects from .get_chat_history import GetChatHistory @@ -81,6 +82,7 @@ class Messages( EditMessageReplyMarkup, EditMessageMedia, EditMessageText, + ForwardMediaGroup, ForwardMessages, GetAvailableEffects, GetMediaGroup, diff --git a/pyrogram/methods/messages/forward_media_group.py b/pyrogram/methods/messages/forward_media_group.py new file mode 100644 index 00000000..59e594a3 --- /dev/null +++ b/pyrogram/methods/messages/forward_media_group.py @@ -0,0 +1,120 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-present Dan +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from datetime import datetime +from typing import Union, List + +import pyrogram +from pyrogram import raw, utils +from pyrogram import types + + +class ForwardMediaGroup: + async def forward_media_group( + self: "pyrogram.Client", + chat_id: Union[int, str], + from_chat_id: Union[int, str], + message_id: int, + message_thread_id: int = None, + disable_notification: bool = None, + schedule_date: datetime = None, + hide_sender_name: bool = None, + hide_captions: bool = None, + protect_content: bool = None + ) -> List["types.Message"]: + """Forward a media group by providing one of the message ids. + + .. include:: /_includes/usable-by/users-bots.rst + + Parameters: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + For your personal cloud (Saved Messages) you can simply use "me" or "self". + For a contact that exists in your Telegram address book you can use his phone number (str). + + from_chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the source chat where the original message was sent. + For your personal cloud (Saved Messages) you can simply use "me" or "self". + For a contact that exists in your Telegram address book you can use his phone number (str). + + message_id (``int``): + Message identifier in the chat specified in *from_chat_id*. + + message_thread_id (``int``, *optional*): + Unique identifier of a message thread to which the message belongs. + For supergroups only. + + disable_notification (``bool``, *optional*): + Sends the message silently. + Users will receive a notification with no sound. + + schedule_date (:py:obj:`~datetime.datetime`, *optional*): + Date when the message will be automatically sent. + + hide_sender_name (``bool``, *optional*): + If True, the original author of the message will not be shown. + + hide_captions (``bool``, *optional*): + If True, the original media captions will be removed. + + protect_content (``bool``, *optional*): + Protects the contents of the sent message from forwarding and saving. + + Returns: + List of :obj:`~pyrogram.types.Message`: On success, a list of forwarded messages is returned. + + Example: + .. code-block:: python + + # Forward a media group + await app.forward_media_group(to_chat, from_chat, 123) + """ + message_ids = [i.id for i in await self.get_media_group(from_chat_id, message_id)] + + r = await self.invoke( + raw.functions.messages.ForwardMessages( + to_peer=await self.resolve_peer(chat_id), + from_peer=await self.resolve_peer(from_chat_id), + id=message_ids, + silent=disable_notification or None, + random_id=[self.rnd_id() for _ in message_ids], + schedule_date=utils.datetime_to_timestamp(schedule_date), + drop_author=hide_sender_name, + drop_media_captions=hide_captions, + noforwards=protect_content, + top_msg_id=message_thread_id + ) + ) + + forwarded_messages = [] + + users = {i.id: i for i in r.users} + chats = {i.id: i for i in r.chats} + + for i in r.updates: + if isinstance(i, (raw.types.UpdateNewMessage, + raw.types.UpdateNewChannelMessage, + raw.types.UpdateNewScheduledMessage)): + forwarded_messages.append( + await types.Message._parse( + self, i.message, + users, chats + ) + ) + + return types.List(forwarded_messages) diff --git a/pyrogram/methods/utilities/__init__.py b/pyrogram/methods/utilities/__init__.py index 2184025b..22d97434 100644 --- a/pyrogram/methods/utilities/__init__.py +++ b/pyrogram/methods/utilities/__init__.py @@ -20,6 +20,7 @@ from .add_handler import AddHandler from .export_session_string import ExportSessionString from .remove_handler import RemoveHandler +from .remove_error_handler import RemoveErrorHandler from .restart import Restart from .run import Run from .run_sync import RunSync @@ -32,6 +33,7 @@ class Utilities( AddHandler, ExportSessionString, RemoveHandler, + RemoveErrorHandler, Restart, Run, RunSync, diff --git a/pyrogram/methods/utilities/remove_error_handler.py b/pyrogram/methods/utilities/remove_error_handler.py new file mode 100644 index 00000000..7ad9f7b4 --- /dev/null +++ b/pyrogram/methods/utilities/remove_error_handler.py @@ -0,0 +1,42 @@ +# Pyrofork - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-present Dan +# Copyright (C) 2022-present 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 . + +from __future__ import annotations +from collections.abc import Iterable +import pyrogram + + +class RemoveErrorHandler: + def remove_error_handler( + self: pyrogram.Client, + exception: type[Exception] | Iterable[type[Exception]] = Exception, + ): + """Remove a previously registered error handler using exception classes. + + Parameters: + exception (``Exception`` | Iterable of ``Exception``, *optional*): + The error(s) for handlers to be removed. Defaults to Exception. + """ + to_remove = [ + handler + for handler in self.dispatcher.error_handlers + if handler.check_remove(exception) + ] + for handler in to_remove: + self.dispatcher.error_handlers.remove(handler) diff --git a/pyrogram/parser/markdown.py b/pyrogram/parser/markdown.py index 4daf27ce..4c5a5f2e 100644 --- a/pyrogram/parser/markdown.py +++ b/pyrogram/parser/markdown.py @@ -37,7 +37,7 @@ PRE_DELIM = "```" BLOCKQUOTE_DELIM = ">" BLOCKQUOTE_EXPANDABLE_DELIM = "**>" -MARKDOWN_RE = re.compile(r"({d})|\[(.+?)\]\((.+?)\)".format( +MARKDOWN_RE = re.compile(r"({d})|(!?)\[(.+?)\]\((.+?)\)".format( d="|".join( ["".join(i) for i in [ [rf"\{j}" for j in i] @@ -56,6 +56,7 @@ MARKDOWN_RE = re.compile(r"({d})|\[(.+?)\]\((.+?)\)".format( OPENING_TAG = "<{}>" CLOSING_TAG = "" URL_MARKUP = '{}' +EMOJI_MARKUP = '{}' FIXED_WIDTH_DELIMS = [CODE_DELIM, PRE_DELIM] CODE_TAG_RE = re.compile(r".*?") @@ -117,7 +118,7 @@ class Markdown: for i, match in enumerate(re.finditer(MARKDOWN_RE, text)): start, _ = match.span() - delim, text_url, url = match.groups() + delim, is_emoji, text_url, url = match.groups() full = match.group(0) if delim in FIXED_WIDTH_DELIMS: @@ -126,10 +127,16 @@ class Markdown: if is_fixed_width and delim not in FIXED_WIDTH_DELIMS: continue - if text_url: + if not is_emoji and text_url: text = utils.replace_once(text, full, URL_MARKUP.format(url, text_url), start) continue + if is_emoji: + emoji = text_url + emoji_id = url.lstrip("tg://emoji?id=") + text = utils.replace_once(text, full, EMOJI_MARKUP.format(emoji_id, emoji), start) + continue + if delim == BOLD_DELIM: tag = "b" elif delim == ITALIC_DELIM: @@ -221,6 +228,10 @@ class Markdown: user = entity.user start_tag = "[" end_tag = f"](tg://user?id={user.id})" + elif entity_type == MessageEntityType.CUSTOM_EMOJI: + emoji_id = entity.custom_emoji_id + start_tag = "![" + end_tag = f"](tg://emoji?id={emoji_id})" else: continue diff --git a/pyrogram/types/messages_and_media/__init__.py b/pyrogram/types/messages_and_media/__init__.py index acf61e5e..1acf6d5a 100644 --- a/pyrogram/types/messages_and_media/__init__.py +++ b/pyrogram/types/messages_and_media/__init__.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with PyroFork. If not, see . +from .alternative_video import AlternativeVideo from .animation import Animation from .audio import Audio from .available_effect import AvailableEffect @@ -71,6 +72,7 @@ from .wallpaper_settings import WallpaperSettings from .translated_text import TranslatedText __all__ = [ + "AlternativeVideo", "Animation", "Audio", "AvailableEffect", diff --git a/pyrogram/types/messages_and_media/alternative_video.py b/pyrogram/types/messages_and_media/alternative_video.py new file mode 100644 index 00000000..921830a7 --- /dev/null +++ b/pyrogram/types/messages_and_media/alternative_video.py @@ -0,0 +1,134 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-present Dan +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from datetime import datetime +from typing import List + +import pyrogram +from pyrogram import raw, utils +from pyrogram import types +from pyrogram.file_id import FileId, FileType, FileUniqueId, FileUniqueType, ThumbnailSource +from ..object import Object + + +class AlternativeVideo(Object): + """Describes an alternative reencoded quality of a video file. + + Parameters: + file_id (``str``): + Identifier for this file, which can be used to download or reuse the file. + + file_unique_id (``str``): + Unique identifier for this file, which is supposed to be the same over time and for different accounts. + Can't be used to download or reuse the file. + + width (``int``): + Video width as defined by sender. + + height (``int``): + Video height as defined by sender. + + codec (``str``): + Codec used for video file encoding, for example, "h264", "h265", or "av1". + + duration (``int``): + Duration of the video in seconds as defined by sender. + + file_name (``str``, *optional*): + Video file name. + + mime_type (``str``, *optional*): + Mime type of a file as defined by sender. + + file_size (``int``, *optional*): + File size. + + supports_streaming (``bool``, *optional*): + True, if the video was uploaded with streaming support. + + date (:py:obj:`~datetime.datetime`, *optional*): + Date the video was sent. + + thumbs (List of :obj:`~pyrogram.types.Thumbnail`, *optional*): + Video thumbnails. + + """ + + def __init__( + self, + *, + client: "pyrogram.Client" = None, + file_id: str, + file_unique_id: str, + width: int, + height: int, + codec: str, + duration: int, + file_name: str = None, + mime_type: str = None, + file_size: int = None, + supports_streaming: bool = None, + date: datetime = None, + thumbs: List["types.Thumbnail"] = None + ): + super().__init__(client) + + self.file_id = file_id + self.file_unique_id = file_unique_id + self.width = width + self.height = height + self.codec = codec + self.duration = duration + self.file_name = file_name + self.mime_type = mime_type + self.file_size = file_size + self.supports_streaming = supports_streaming + self.date = date + self.thumbs = thumbs + + @staticmethod + def _parse( + client, + video: "raw.types.Document", + video_attributes: "raw.types.DocumentAttributeVideo", + file_name: str + ) -> "AlternativeVideo": + return AlternativeVideo( + file_id=FileId( + file_type=FileType.VIDEO, + dc_id=video.dc_id, + media_id=video.id, + access_hash=video.access_hash, + file_reference=video.file_reference + ).encode() if video else None, + file_unique_id=FileUniqueId( + file_unique_type=FileUniqueType.DOCUMENT, + media_id=video.id + ).encode() if video else None, + width=video_attributes.w if video_attributes else None, + height=video_attributes.h if video_attributes else None, + codec=video_attributes.video_codec if video_attributes else None, + duration=video_attributes.duration if video_attributes else None, + file_name=file_name, + mime_type=video.mime_type if video else None, + supports_streaming=video_attributes.supports_streaming if video_attributes else None, + file_size=video.size if video else None, + date=utils.timestamp_to_datetime(video.date) if video else None, + thumbs=types.Thumbnail._parse(client, video) if video else None, + client=client + ) diff --git a/pyrogram/types/messages_and_media/message.py b/pyrogram/types/messages_and_media/message.py index 4fbd57ef..d8a352b2 100644 --- a/pyrogram/types/messages_and_media/message.py +++ b/pyrogram/types/messages_and_media/message.py @@ -506,6 +506,7 @@ class Message(Object, Update): screenshot_taken: "types.ScreenshotTaken" = None, invoice: "types.Invoice" = None, story: Union["types.MessageStory", "types.Story"] = None, + alternative_videos: List["types.AlternativeVideo"] = None, video: "types.Video" = None, voice: "types.Voice" = None, video_note: "types.VideoNote" = None, @@ -628,6 +629,7 @@ class Message(Object, Update): self.invoice = invoice self.story = story self.video = video + self.alternative_videos = alternative_videos self.voice = voice self.video_note = video_note self.web_page_preview = web_page_preview @@ -1055,6 +1057,7 @@ class Message(Object, Update): voice = None animation = None video = None + alternative_videos = [] video_note = None web_page_preview = None sticker = None @@ -1122,6 +1125,22 @@ class Message(Object, Update): video = types.Video._parse(client, doc, video_attributes, file_name, media.ttl_seconds) media_type = enums.MessageMediaType.VIDEO has_media_spoiler = media.spoiler + + altdocs = media.alt_documents or [] + for altdoc in altdocs: + if isinstance(altdoc, raw.types.Document): + altdoc_attributes = {type(i): i for i in altdoc.attributes} + + altdoc_file_name = getattr( + altdoc_attributes.get( + raw.types.DocumentAttributeFilename, None + ), "file_name", None + ) + altdoc_video_attribute = altdoc_attributes.get(raw.types.DocumentAttributeVideo, None) + if altdoc_video_attribute: + alternative_videos.append( + types.AlternativeVideo._parse(client, altdoc, altdoc_video_attribute, altdoc_file_name) + ) elif raw.types.DocumentAttributeAudio in attributes: audio_attributes = attributes[raw.types.DocumentAttributeAudio] @@ -1238,6 +1257,7 @@ class Message(Object, Update): invoice=invoice, story=story, video=video, + alternative_videos=types.List(alternative_videos) if alternative_videos else None, video_note=video_note, web_page_preview=web_page_preview, sticker=sticker,