diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index dc0721577..f183fe33e 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see -__version__ = "2.0.136" +__version__ = "2.0.137" __license__ = "GNU Lesser General Public License v3.0 (LGPL-3.0)" __copyright__ = "Copyright (C) 2017-present Dan " diff --git a/pyrogram/client.py b/pyrogram/client.py index 298bdcbcb..f99808abd 100644 --- a/pyrogram/client.py +++ b/pyrogram/client.py @@ -44,12 +44,12 @@ from pyrogram.errors import ( SessionPasswordNeeded, VolumeLocNotFound, ChannelPrivate, - BadRequest + BadRequest, AuthBytesInvalid ) from pyrogram.handlers.handler import Handler from pyrogram.methods import Methods from pyrogram.session import Auth, Session -from pyrogram.storage import FileStorage, MemoryStorage +from pyrogram.storage import Storage, FileStorage, MemoryStorage from pyrogram.types import User, TermsOfService, ListenerStopped, ListenerTimeout, ListenerTypes from pyrogram.utils import ainput, PyromodConfig from .dispatcher import Dispatcher @@ -177,6 +177,10 @@ class Client(Methods): Set the maximum amount of concurrent transmissions (uploads & downloads). A value that is too high may result in network related issues. Defaults to 1. + + storage_engine (:obj:`~pyrogram.storage.Storage`, *optional*): + Pass an instance of your own implementation of session storage engine. + Useful when you want to store your session in databases like Mongo, Redis, etc. """ APP_VERSION = f"Pyrogram {__version__}" @@ -225,7 +229,8 @@ def __init__( takeout: bool = None, sleep_threshold: int = Session.SLEEP_THRESHOLD, hide_password: bool = False, - max_concurrent_transmissions: int = MAX_CONCURRENT_TRANSMISSIONS + max_concurrent_transmissions: int = MAX_CONCURRENT_TRANSMISSIONS, + storage_engine: Storage = None ): super().__init__() @@ -261,6 +266,8 @@ def __init__( self.storage = MemoryStorage(self.name, self.session_string) elif self.in_memory: self.storage = MemoryStorage(self.name) + elif isinstance(storage_engine, Storage): + self.storage = storage_engine else: self.storage = FileStorage(self.name, self.workdir) @@ -625,15 +632,15 @@ async def fetch_peers(self, peers: List[Union[raw.types.User, raw.types.Chat, ra is_min = True continue - username = None + usernames = None phone_number = None if isinstance(peer, raw.types.User): peer_id = peer.id access_hash = peer.access_hash - username = ( - peer.username.lower() if peer.username - else peer.usernames[0].username.lower() if peer.usernames + usernames = ( + [peer.username.lower()] if peer.username + else [username.username.lower() for username in peer.usernames] if peer.usernames else None ) phone_number = peer.phone @@ -645,9 +652,9 @@ async def fetch_peers(self, peers: List[Union[raw.types.User, raw.types.Chat, ra elif isinstance(peer, raw.types.Channel): peer_id = utils.get_channel_id(peer.id) access_hash = peer.access_hash - username = ( - peer.username.lower() if peer.username - else peer.usernames[0].username.lower() if peer.usernames + usernames = ( + [peer.username.lower()] if peer.username + else [username.username.lower() for username in peer.usernames] if peer.usernames else None ) peer_type = "channel" if peer.broadcast else "supergroup" @@ -658,7 +665,7 @@ async def fetch_peers(self, peers: List[Union[raw.types.User, raw.types.Chat, ra else: continue - parsed_peers.append((peer_id, access_hash, peer_type, username, phone_number)) + parsed_peers.append((peer_id, access_hash, peer_type, usernames, phone_number)) await self.storage.update_peers(parsed_peers) @@ -946,7 +953,7 @@ async def get_file( offset: int = 0, progress: Callable = None, progress_args: tuple = () - ) -> Optional[AsyncGenerator[bytes, None]]: + ) -> AsyncGenerator[bytes, None]: async with self.get_file_semaphore: file_type = file_id.file_type @@ -994,31 +1001,40 @@ async def get_file( dc_id = file_id.dc_id - session = Session( - self, dc_id, - await Auth(self, dc_id, await self.storage.test_mode()).create() - if dc_id != await self.storage.dc_id() - else await self.storage.auth_key(), - await self.storage.test_mode(), - is_media=True - ) - try: - await session.start() - - if dc_id != await self.storage.dc_id(): - exported_auth = await self.invoke( - raw.functions.auth.ExportAuthorization( - dc_id=dc_id - ) + session = self.media_sessions.get(dc_id) + if not session: + session = self.media_sessions[dc_id] = Session( + self, dc_id, + await Auth(self, dc_id, await self.storage.test_mode()).create() + if dc_id != await self.storage.dc_id() + else await self.storage.auth_key(), + await self.storage.test_mode(), + is_media=True ) + await session.start() - await session.invoke( - raw.functions.auth.ImportAuthorization( - id=exported_auth.id, - bytes=exported_auth.bytes - ) - ) + if dc_id != await self.storage.dc_id(): + for _ in range(3): + exported_auth = await self.invoke( + raw.functions.auth.ExportAuthorization( + dc_id=dc_id + ) + ) + + try: + await session.invoke( + raw.functions.auth.ImportAuthorization( + id=exported_auth.id, + bytes=exported_auth.bytes + ) + ) + except AuthBytesInvalid: + continue + else: + break + else: + raise AuthBytesInvalid r = await session.invoke( raw.functions.upload.GetFile( @@ -1152,7 +1168,11 @@ async def get_file( except Exception as e: log.exception(e) finally: - await session.stop() + if session: + try: + await session.stop() + self.media_sessions.pop(session.dc_id) + except: pass def guess_mime_type(self, filename: str) -> Optional[str]: return self.mimetypes.guess_type(filename)[0] diff --git a/pyrogram/dispatcher.py b/pyrogram/dispatcher.py index 096d6989a..a0ded834e 100644 --- a/pyrogram/dispatcher.py +++ b/pyrogram/dispatcher.py @@ -70,8 +70,13 @@ def __init__(self, client: "pyrogram.Client"): async def message_parser(update, users, chats): return ( - await pyrogram.types.Message._parse(self.client, update.message, users, chats, None, - isinstance(update, UpdateNewScheduledMessage)), + await pyrogram.types.Message._parse( + self.client, + update.message, + users, + chats, + is_scheduled=isinstance(update, UpdateNewScheduledMessage) + ), MessageHandler ) diff --git a/pyrogram/enums/__init__.py b/pyrogram/enums/__init__.py index f1f01b567..b0609761e 100644 --- a/pyrogram/enums/__init__.py +++ b/pyrogram/enums/__init__.py @@ -28,6 +28,8 @@ from .next_code_type import NextCodeType from .parse_mode import ParseMode from .poll_type import PollType +from .profile_color import ProfileColor +from .reply_color import ReplyColor from .sent_code_type import SentCodeType from .stories_privacy_rules import StoriesPrivacyRules from .user_status import UserStatus @@ -45,7 +47,9 @@ 'NextCodeType', 'ParseMode', 'PollType', + 'ProfileColor', + 'ReplyColor', 'SentCodeType', 'StoriesPrivacyRules', 'UserStatus' -] \ No newline at end of file +] diff --git a/pyrogram/enums/chat_event_action.py b/pyrogram/enums/chat_event_action.py index b46e01cfd..eff86f378 100644 --- a/pyrogram/enums/chat_event_action.py +++ b/pyrogram/enums/chat_event_action.py @@ -133,4 +133,4 @@ class ChatEventAction(AutoName): "a forum topic has been deleted (see `deleted_forum_topic`)" UNKNOWN = auto() - "Unknown chat event action" \ No newline at end of file + "Unknown chat event action" diff --git a/pyrogram/enums/message_media_type.py b/pyrogram/enums/message_media_type.py index 9da88a655..c68447162 100644 --- a/pyrogram/enums/message_media_type.py +++ b/pyrogram/enums/message_media_type.py @@ -69,5 +69,11 @@ class MessageMediaType(AutoName): GAME = auto() "Game media" + GIVEAWAY = auto() + "Giveaway media" + + GIVEAWAY_RESULT = auto() + "Giveaway result media" + STORY = auto() - "Story media" \ No newline at end of file + "Story media" diff --git a/pyrogram/enums/message_service_type.py b/pyrogram/enums/message_service_type.py index 48e1fd8b0..6914e79a4 100644 --- a/pyrogram/enums/message_service_type.py +++ b/pyrogram/enums/message_service_type.py @@ -24,6 +24,9 @@ class MessageServiceType(AutoName): """Message service type enumeration used in :obj:`~pyrogram.types.Message`.""" + CUSTOM_ACTION = auto() + "Custom action" + NEW_CHAT_MEMBERS = auto() "New members join" @@ -75,6 +78,12 @@ class MessageServiceType(AutoName): GAME_HIGH_SCORE = auto() "Game high score" + GIVEAWAY_LAUNCH = auto() + "Giveaway launch" + + GIFT_CODE = auto() + "Gift code" + VIDEO_CHAT_STARTED = auto() "Video chat started" @@ -89,3 +98,6 @@ class MessageServiceType(AutoName): WEB_APP_DATA = auto() "Web app data" + + REQUESTED_CHAT = auto() + "Requested chat" diff --git a/pyrogram/enums/profile_color.py b/pyrogram/enums/profile_color.py new file mode 100644 index 000000000..8ae81664d --- /dev/null +++ b/pyrogram/enums/profile_color.py @@ -0,0 +1,71 @@ +# 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 .auto_name import AutoName + + +class ProfileColor(AutoName): + """Profile color enumeration used in :meth:`~pyrogram.Client.update_color` and :obj:`~pyrogram.types.ChatColor`.""" + + RED = 0 + "Red color." + + ORANGE = 1 + "Orange color." + + VIOLET = 2 + "Violet color." + + GREEN = 3 + "Green color." + + CYAN = 4 + "Cyan color." + + BLUE = 5 + "Blue color." + + PINK = 6 + "Pink color." + + GRAY = 7 + "Gray color." + + RED_LIGHT_RED = 8 + "Red color with light red gradient." + + ORANGE_LIGHT_ORANGE = 9 + "Orange color with light red gradient." + + VIOLET_LIGHT_VIOLET = 10 + "Violet color with light violet gradient." + + GREEN_LIGHT_GREEN = 11 + "Green color with light green gradient." + + CYAN_LIGHT_CYAN = 12 + "Cyan color with light cyan gradient." + + BLUE_LIGHT_BLUE = 13 + "Blue color with light blue gradient." + + PINK_LIGHT_PINK = 14 + "Pink color with light pink gradient." + + GRAY_LIGHT_GRAY = 15 + "Gray color with light gray gradient." diff --git a/pyrogram/enums/reply_color.py b/pyrogram/enums/reply_color.py new file mode 100644 index 000000000..c5bd25711 --- /dev/null +++ b/pyrogram/enums/reply_color.py @@ -0,0 +1,94 @@ +# 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 .auto_name import AutoName + + +class ReplyColor(AutoName): + """Reply color enumeration used in :meth:`~pyrogram.Client.update_color` and :obj:`~pyrogram.types.ChatColor`.""" + + RED = 0 + "Red color." + + ORANGE = 1 + "Orange color." + + VIOLET = 2 + "Violet color." + + GREEN = 3 + "Green color." + + CYAN = 4 + "Cyan color." + + BLUE = 5 + "Blue color." + + PINK = 6 + "Pink color." + + RED_DARK_RED = 7 + "Red color with dark red stripes." + + ORANGE_DARK_ORANGE = 8 + "Orange color with dark orange stripes." + + VIOLET_DARK_VIOLET = 9 + "Violet color with dark violet stripes." + + GREEN_DARK_GREEN = 10 + "Green color with dark green stripes." + + CYAN_DARK_CYAN = 11 + "Cyan color with dark cyan stripes." + + BLUE_DARK_BLUE = 12 + "Blue color with dark blue stripes." + + PINK_DARK_PINK = 13 + "Pink color with dark pink stripes." + + BLUE_WHITE_RED = 14 + "Blue color with white and red stripes." + + ORANGE_WHITE_GREEN = 15 + "Orange color with white and green stripes." + + GREEN_WHITE_RED = 16 + "Green color with white and red stripes." + + CYAN_WHITE_GREEN = 17 + "Cyan color with white and red green." + + CYAN_YELLOW_PINK = 18 + "Cyan color with yellow and pink stripes." + + VIOLET_YELLOW_ORANGE = 19 + "Violet color with yellow and orange stripes." + + BLUE_WHITE_ORANGE = 20 + "Blue color with white and orange stripes." + + DYNAMIC = 21 + """Secret color that cannot be set. + + For now: + Red - If you use Telegram desktop. + Blue - If you are using Telegram android/ios. + """ diff --git a/pyrogram/enums/stories_privacy_rules.py b/pyrogram/enums/stories_privacy_rules.py index c352f2b14..9696f0968 100644 --- a/pyrogram/enums/stories_privacy_rules.py +++ b/pyrogram/enums/stories_privacy_rules.py @@ -21,7 +21,7 @@ class StoriesPrivacyRules(AutoName): - """Stories privacy rules type enumeration used in :obj:`~pyrogram.method.SendStory`.""" + """Stories privacy rules type enumeration used in :meth:`~pyrogram.Client.send_story`.""" PUBLIC = auto() "Public stories" diff --git a/pyrogram/filters.py b/pyrogram/filters.py index 6b867d4ed..9d566b2c1 100644 --- a/pyrogram/filters.py +++ b/pyrogram/filters.py @@ -182,6 +182,17 @@ async def bot_filter(_, __, m: Message): """Filter messages coming from bots.""" +# endregion + +# region sender_chat_filter +async def sender_chat_filter(_, __, m: Message): + return bool(m.sender_chat) + + +sender_chat = create(sender_chat_filter) +"""Filter messages coming from sender chat.""" + + # endregion # region incoming_filter @@ -315,6 +326,50 @@ async def game_filter(_, __, m: Message): """Filter messages that contain :obj:`~pyrogram.types.Game` objects.""" +# endregion + +# region giveaway_filter +async def giveaway_filter(_, __, m: Message): + return bool(m.giveaway) + + +giveaway = create(giveaway_filter) +"""Filter messages that contain :obj:`~pyrogram.types.Giveaway` objects.""" + + +# endregion + +# region giveaway_result_filter +async def giveaway_result_filter(_, __, m: Message): + return bool(m.giveaway_result) + + +giveaway_result = create(giveaway_result_filter) +"""Filter messages that contain :obj:`~pyrogram.types.GiveawayResult` objects.""" + + +# endregion + +# region gift_code_filter +async def gift_code_filter(_, __, m: Message): + return bool(m.gift_code) + + +gift_code = create(gift_code_filter) +"""Filter messages that contain :obj:`~pyrogram.types.GiftCode` objects.""" + + +# endregion + +# region requested_chats_filter +async def requested_chats_filter(_, __, m: Message): + return bool(m.requested_chats) + + +requested_chats = create(requested_chats_filter) +"""Filter service messages for request chats.""" + + # endregion # region video_filter @@ -424,6 +479,7 @@ async def dice_filter(_, __, m: Message): dice = create(dice_filter) """Filter messages that contain :obj:`~pyrogram.types.Dice` objects.""" + # endregion # region quote_filter @@ -434,6 +490,7 @@ async def quote_filter(_, __, m: Message): quote = create(quote_filter) """Filter quote messages.""" + # endregion # region media_spoiler @@ -481,6 +538,28 @@ async def channel_filter(_, __, u: Update): """Filter messages sent in channels.""" +# endregion + +# region forum_filter +async def forum_filter(_, __, m: Message): + return bool(m.chat and m.chat.is_forum) + + +forum = create(forum_filter) +"""Filter messages sent in forums.""" + + +# endregion + +# region story_filter +async def story_filter(_, __, m: Message): + return bool(m.story) + + +story = create(story_filter) +"""Filter messages that contain :obj:`~pyrogram.types.Story` objects.""" + + # endregion # region new_chat_members_filter @@ -657,6 +736,17 @@ async def via_bot_filter(_, __, m: Message): """Filter messages sent via inline bots""" +# endregion + +# region admin_filter +async def admin_filter(_, __, m: Message): + return bool(m.chat and m.chat.is_admin) + + +admin = create(admin_filter) +"""Filter chats where you have admin rights""" + + # endregion # region video_chat_started_filter diff --git a/pyrogram/methods/__init__.py b/pyrogram/methods/__init__.py index a2cf696f5..f3d164b5e 100644 --- a/pyrogram/methods/__init__.py +++ b/pyrogram/methods/__init__.py @@ -25,8 +25,9 @@ from .invite_links import InviteLinks from .messages import Messages from .password import Password -from .stories import Stories +from .premium import Premium from .users import Users +from .stories import Stories from .utilities import Utilities @@ -36,9 +37,10 @@ class Methods( Bots, Contacts, Password, - Stories, + Premium, Chats, Users, + Stories, Messages, Decorators, Utilities, diff --git a/pyrogram/methods/advanced/save_file.py b/pyrogram/methods/advanced/save_file.py index 453a62af1..54ff537a6 100644 --- a/pyrogram/methods/advanced/save_file.py +++ b/pyrogram/methods/advanced/save_file.py @@ -19,6 +19,7 @@ import asyncio import functools import inspect +import datetime import io import logging import math @@ -139,16 +140,23 @@ async def worker(session): is_missing_part = file_id is not None file_id = file_id or self.rnd_id() md5_sum = md5() if not is_big and not is_missing_part else None - session = Session( - self, await self.storage.dc_id(), await self.storage.auth_key(), - await self.storage.test_mode(), is_media=True - ) + dc_id = await self.storage.dc_id() + + async with self.media_sessions_lock: + session = self.media_sessions.get(dc_id) + if not session: + session = Session( + self, dc_id, await self.storage.auth_key(), + await self.storage.test_mode(), is_media=True + ) + await session.start() + + session.last_used_time = datetime.datetime.now() + workers = [self.loop.create_task(worker(session)) for _ in range(workers_count)] queue = asyncio.Queue(1) try: - await session.start() - fp.seek(part_size * file_part) while True: @@ -220,7 +228,30 @@ async def worker(session): await asyncio.gather(*workers) - await session.stop() - if isinstance(path, (str, PurePath)): fp.close() + + async def close_unused_sessions( + self: "pyrogram.Client", + minutes_passed: int = 20, + ) -> int: + """Closes all the sessions that haven't been used in last X amount of time. + """ + if not minutes_passed: + return None + + now = datetime.datetime.now() + async with self.media_sessions_lock: + all_keys = self.media_sessions.keys() + for current in all_keys: + last_time_used = getattr(self.media_sessions[current], "last_time_used", None) + + if not isinstance(last_time_used, datetime.datetime): + continue + + if (now - last_time_used).seconds > 60*minutes_passed: + current_media = self.media_sessions.pop(current) + if not isinstance(current_media, Session): + continue + + await current_media.stop() diff --git a/pyrogram/methods/bots/send_game.py b/pyrogram/methods/bots/send_game.py index f1aa7e91c..4027e0df5 100644 --- a/pyrogram/methods/bots/send_game.py +++ b/pyrogram/methods/bots/send_game.py @@ -32,7 +32,7 @@ async def send_game( disable_notification: bool = None, message_thread_id: int = None, reply_to_message_id: int = None, - reply_to_chat_id: int = None, + reply_to_chat_id: Union[int, str] = None, protect_content: bool = None, reply_markup: Union[ "types.InlineKeyboardMarkup", @@ -60,11 +60,14 @@ async def send_game( message_thread_id (``int``, *optional*): Unique identifier of a message thread to which the message belongs. - for supergroups only + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. + reply_to_chat_id (``int``, *optional*): + If the message is a reply, ID of the original chat. + protect_content (``bool``, *optional*): Protects the contents of the sent message from forwarding and saving. diff --git a/pyrogram/methods/bots/send_inline_bot_result.py b/pyrogram/methods/bots/send_inline_bot_result.py index 23cca6f2a..4b7e0dbf6 100644 --- a/pyrogram/methods/bots/send_inline_bot_result.py +++ b/pyrogram/methods/bots/send_inline_bot_result.py @@ -32,7 +32,7 @@ async def send_inline_bot_result( disable_notification: bool = None, message_thread_id: int = None, reply_to_message_id: int = None, - reply_to_chat_id: int = None, + reply_to_chat_id: Union[int, str] = None, reply_to_story_id: int = None, quote_text: str = None, quote_entities: List["types.MessageEntity"] = None, diff --git a/pyrogram/methods/chats/__init__.py b/pyrogram/methods/chats/__init__.py index cb287d767..570d8bdf6 100644 --- a/pyrogram/methods/chats/__init__.py +++ b/pyrogram/methods/chats/__init__.py @@ -26,24 +26,30 @@ from .close_forum_topic import CloseForumTopic from .delete_channel import DeleteChannel from .delete_chat_photo import DeleteChatPhoto +from .delete_folder import DeleteFolder from .delete_forum_topic import DeleteForumTopic from .delete_supergroup import DeleteSupergroup from .delete_user_history import DeleteUserHistory from .edit_forum_topic import EditForumTopic +from .export_folder_link import ExportFolderLink from .get_chat import GetChat from .get_chat_event_log import GetChatEventLog from .get_chat_member import GetChatMember from .get_chat_members import GetChatMembers from .get_chat_members_count import GetChatMembersCount from .get_chat_online_count import GetChatOnlineCount +from .get_similar_channels import GetSimilarChannels from .get_dialogs import GetDialogs from .get_dialogs_count import GetDialogsCount +from .get_folders import GetFolders from .get_forum_topics import GetForumTopics from .get_forum_topics_by_id import GetForumTopicsByID from .get_nearby_chats import GetNearbyChats from .get_send_as_chats import GetSendAsChats from .join_chat import JoinChat +from .join_folder import JoinFolder from .leave_chat import LeaveChat +from .leave_folder import LeaveFolder from .mark_chat_unread import MarkChatUnread from .pin_chat_message import PinChatMessage from .promote_chat_member import PromoteChatMember @@ -54,20 +60,26 @@ from .set_chat_photo import SetChatPhoto from .set_chat_protected_content import SetChatProtectedContent from .set_chat_title import SetChatTitle +from .set_chat_ttl import SetChatTTL from .set_chat_username import SetChatUsername from .set_send_as_chat import SetSendAsChat from .set_slow_mode import SetSlowMode +from .toggle_forum_topics import ToggleForumTopics from .unarchive_chats import UnarchiveChats from .unban_chat_member import UnbanChatMember from .unpin_all_chat_messages import UnpinAllChatMessages from .unpin_chat_message import UnpinChatMessage +from .update_chat_notifications import UpdateChatNotifications from .update_color import UpdateColor +from .update_folder import UpdateFolder class Chats( GetChat, LeaveChat, + LeaveFolder, JoinChat, + JoinFolder, BanChatMember, UnbanChatMember, RestrictChatMember, @@ -76,16 +88,21 @@ class Chats( GetChatMember, SetChatPhoto, DeleteChatPhoto, + DeleteFolder, SetChatTitle, + SetChatTTL, SetChatDescription, PinChatMessage, UnpinChatMessage, + UpdateChatNotifications, UpdateColor, + UpdateFolder, GetDialogs, GetChatMembersCount, SetChatUsername, SetChatPermissions, GetDialogsCount, + GetFolders, GetForumTopics, GetForumTopicsByID, ArchiveChats, @@ -100,14 +117,17 @@ class Chats( DeleteForumTopic, DeleteSupergroup, EditForumTopic, + ExportFolderLink, GetNearbyChats, SetAdministratorTitle, SetSlowMode, + ToggleForumTopics, DeleteUserHistory, UnpinAllChatMessages, MarkChatUnread, GetChatEventLog, GetChatOnlineCount, + GetSimilarChannels, GetSendAsChats, SetSendAsChat, SetChatProtectedContent diff --git a/pyrogram/methods/chats/delete_folder.py b/pyrogram/methods/chats/delete_folder.py new file mode 100644 index 000000000..fada0ffb5 --- /dev/null +++ b/pyrogram/methods/chats/delete_folder.py @@ -0,0 +1,51 @@ +# 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 . + +import pyrogram +from pyrogram import raw + + +class DeleteFolder: + async def delete_folder( + self: "pyrogram.Client", + folder_id: int + ) -> bool: + """Delete a user's folder. + + .. include:: /_includes/usable-by/users.rst + + Parameters: + folder_id (``int``): + Unique identifier (int) of the target folder. + + Returns: + ``bool``: True, on success. + + Example: + .. code-block:: python + + # Delete folder + app.delete_folder(folder_id) + """ + r = await self.invoke( + raw.functions.messages.UpdateDialogFilter( + id=folder_id + ) + ) + + return r \ No newline at end of file diff --git a/pyrogram/methods/chats/export_folder_link.py b/pyrogram/methods/chats/export_folder_link.py new file mode 100644 index 000000000..7072b81f3 --- /dev/null +++ b/pyrogram/methods/chats/export_folder_link.py @@ -0,0 +1,69 @@ +# 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 . + +import pyrogram +from pyrogram import raw + + +class ExportFolderLink: + async def export_folder_link( + self: "pyrogram.Client", + folder_id: int + ) -> str: + """Export link to a user's folder. + + .. include:: /_includes/usable-by/users.rst + + Parameters: + folder_id (``int``): + Unique identifier (int) of the target folder. + + Returns: + ``str``: On success, a link to the folder as string is returned. + + Example: + .. code-block:: python + + # Export folder link + app.export_folder_link(folder_id) + """ + folder = await self.get_folders(folder_id) + + if not folder: + return + + peers = [] + + if folder.included_chats: + peers.extend(iter(folder.included_chats)) + + if folder.excluded_chats: + peers.extend(iter(folder.included_chats)) + + if folder.pinned_chats: + peers.extend(iter(folder.included_chats)) + + r = await self.invoke( + raw.functions.chatlists.ExportChatlistInvite( + chatlist=raw.types.InputChatlistDialogFilter(filter_id=folder_id), + title=folder.title, + peers=[await self.resolve_peer(i.id) for i in peers], + ) + ) + + return r.invite.url \ No newline at end of file diff --git a/pyrogram/methods/chats/get_folders.py b/pyrogram/methods/chats/get_folders.py new file mode 100644 index 000000000..66b0ad721 --- /dev/null +++ b/pyrogram/methods/chats/get_folders.py @@ -0,0 +1,90 @@ +# 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 Union, List, Iterable + +import pyrogram +from pyrogram import types, raw + + +class GetFolders: + async def get_folders( + self: "pyrogram.Client", + folder_ids: Union[int, Iterable[int]] = None, + ) -> Union["types.Folder", List["types.Folder"]]: + """Get one or more folders by using folder identifiers. + + .. include:: /_includes/usable-by/users.rst + + Parameters: + folder_ids (``int`` | Iterable of ``int``, *optional*): + Pass a single folder identifier or an iterable of folder ids (as integers) to get the content of the + folders themselves. + By default all folders are returned. + + Returns: + :obj:`~pyrogram.types.Folder` | List of :obj:`~pyrogram.types.Folder`: In case *folder_ids* was not + a list, a single folder is returned, otherwise a list of folders is returned. + + Example: + .. code-block:: python + + # Get one folder + await app.get_folders(12345) + + # Get more than one folders (list of folders) + await app.get_folders([12345, 12346]) + + # Get all folders + await app.get_folders() + """ + is_iterable = hasattr(folder_ids, "__iter__") + ids = list(folder_ids) if is_iterable else [folder_ids] + + raw_folders = await self.invoke(raw.functions.messages.GetDialogFilters()) + dialog_peers = [] + + for folder in raw_folders: + if isinstance(folder, (raw.types.DialogFilter, raw.types.DialogFilterChatlist)): + peers = folder.pinned_peers + folder.include_peers + getattr(folder, "exclude_peers", []) + input_peers = [raw.types.InputDialogPeer(peer=peer) for peer in peers] + [raw.types.InputDialogPeerFolder(folder_id=folder.id)] + + dialog_peers.extend(input_peers) + + r = await self.invoke(raw.functions.messages.GetPeerDialogs(peers=dialog_peers)) + + users = {i.id: i for i in r.users} + chats = {i.id: i for i in r.chats} + + folders = types.List([]) + + for folder in raw_folders: + if isinstance(folder, (raw.types.DialogFilter, raw.types.DialogFilterChatlist)): + folders.append(types.Folder._parse(self, folder, users, chats)) + + if not folders: + return None + + if folder_ids: + folders = types.List([folder for folder in folders if folder.id in ids]) + if is_iterable: + return folders or None + else: + return folders[0] if folders else None + + return folders \ No newline at end of file diff --git a/pyrogram/methods/chats/get_similar_channels.py b/pyrogram/methods/chats/get_similar_channels.py new file mode 100644 index 000000000..356361c77 --- /dev/null +++ b/pyrogram/methods/chats/get_similar_channels.py @@ -0,0 +1,59 @@ +# 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 Union, List, Optional + +import pyrogram +from pyrogram import raw +from pyrogram import types + + +class GetSimilarChannels: + async def get_similar_channels( + self: "pyrogram.Client", + chat_id: Union[int, str] + ) -> Optional[List["types.Chat"]]: + """Get similar channels. + + .. include:: /_includes/usable-by/users.rst + + Parameters: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + + Returns: + List of :obj:`~pyrogram.types.Chat`: On success, the list of channels is returned. + + Example: + .. code-block:: python + + channels = await app.get_similar_channels(chat_id) + print(channels) + """ + chat = await self.resolve_peer(chat_id) + + if isinstance(chat, raw.types.InputPeerChannel): + r = await self.invoke( + raw.functions.channels.GetChannelRecommendations( + channel=chat + ) + ) + + return types.List([types.Chat._parse_channel_chat(self, chat) for chat in r.chats]) or None + else: + raise ValueError(f'The chat_id "{chat_id}" belongs to a user or chat') \ No newline at end of file diff --git a/pyrogram/methods/chats/join_folder.py b/pyrogram/methods/chats/join_folder.py new file mode 100644 index 000000000..e004dcfea --- /dev/null +++ b/pyrogram/methods/chats/join_folder.py @@ -0,0 +1,75 @@ +# 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 . +import re + +import pyrogram +from pyrogram import raw, utils + + +class JoinFolder: + async def join_folder( + self: "pyrogram.Client", + link: str, + ) -> bool: + """Join a folder by its invite link. + + .. include:: /_includes/usable-by/users.rst + + Parameters: + link (``str``): + Invite link of the folder. + + Returns: + ``bool``: True, on success. + + Raises: + BadRequest: In case the folder invite link not exists. + ValueError: In case the folder invite link is invalid. + + Example: + .. code-block:: python + + # join folder + app.join_folder("t.me/addlist/ebXQ0Q0I3RnGQ") + """ + match = re.match(r"^(?:https?://)?(?:www\.)?(?:t(?:elegram)?\.(?:org|me|dog)/(?:addlist/|\+))([\w-]+)$", link) + + if not match: + raise ValueError("Invalid folder invite link") + + r = await self.invoke( + raw.functions.chatlists.CheckChatlistInvite( + slug=match.group(1) + ) + ) + + if isinstance(r, raw.types.chatlists.ChatlistInviteAlready): + peers = r.already_peers + r.missing_peers + else: + peers = r.peers + + await self.invoke( + raw.functions.chatlists.JoinChatlistInvite( + slug=match.group(1), + peers=[ + await self.resolve_peer(utils.get_peer_id(id)) for id in peers + ], + ) + ) + + return True \ No newline at end of file diff --git a/pyrogram/methods/chats/leave_folder.py b/pyrogram/methods/chats/leave_folder.py new file mode 100644 index 000000000..70d93824b --- /dev/null +++ b/pyrogram/methods/chats/leave_folder.py @@ -0,0 +1,79 @@ +# 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 . +import re + +import pyrogram +from pyrogram import raw, utils + + +class LeaveFolder: + async def leave_folder( + self: "pyrogram.Client", + link: str, + keep_chats: bool = True + ) -> bool: + """Leave a folder by its invite link. + + .. include:: /_includes/usable-by/users.rst + + Parameters: + link (``str``): + Invite link of the folder. + + keep_chats (``bool``, *optional*): + If True, the chats from the folder will be kept in the user's account. + Defaults to True. + + Returns: + ``bool``: True, on success. + + Raises: + AttributeError: In case the folder invite link does not exist in the user's account. + BadRequest: In case the folder invite link not exists. + ValueError: In case the folder invite link is invalid. + + Example: + .. code-block:: python + + # leave folder + app.leave_folder("t.me/addlist/ebXQ0Q0I3RnGQ") + """ + match = re.match(r"^(?:https?://)?(?:www\.)?(?:t(?:elegram)?\.(?:org|me|dog)/(?:addlist/|\+))([\w-]+)$", link) + + if not match: + raise ValueError("Invalid folder invite link") + + r = await self.invoke( + raw.functions.chatlists.CheckChatlistInvite( + slug=match.group(1) + ) + ) + + await self.invoke( + raw.functions.chatlists.LeaveChatlist( + chatlist=raw.types.InputChatlistDialogFilter( + filter_id=r.filter_id + ), + peers=[ + await self.resolve_peer(utils.get_peer_id(id)) + for id in r.already_peers + ] if not keep_chats else [], + ) + ) + + return True \ No newline at end of file diff --git a/pyrogram/methods/chats/set_chat_ttl.py b/pyrogram/methods/chats/set_chat_ttl.py new file mode 100644 index 000000000..ef97d3976 --- /dev/null +++ b/pyrogram/methods/chats/set_chat_ttl.py @@ -0,0 +1,64 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2021 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 Union + +import pyrogram +from pyrogram import raw +from pyrogram import types + + +class SetChatTTL: + async def set_chat_ttl( + self: "pyrogram.Client", + chat_id: Union[int, str], + ttl_seconds: int + ) -> "types.Message": + """Set the time-to-live for the chat. + + Parameters: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + + ttl_seconds (``int``): + The time-to-live for the chat. + Either 86000 for 1 day, 604800 for 1 week or 0 (zero) to disable it. + + Returns: + ``bool``: True on success. + + Example: + .. code-block:: python + + # Set TTL for a chat to 1 day + app.set_chat_ttl(chat_id, 86400) + + # Set TTL for a chat to 1 week + app.set_chat_ttl(chat_id, 604800) + + # Disable TTL for this chat + app.set_chat_ttl(chat_id, 0) + """ + await self.invoke( + raw.functions.messages.SetHistoryTTL( + peer=await self.resolve_peer(chat_id), + period=ttl_seconds, + ) + ) + + return True \ No newline at end of file diff --git a/pyrogram/methods/chats/toggle_forum_topics.py b/pyrogram/methods/chats/toggle_forum_topics.py new file mode 100644 index 000000000..2576e0f18 --- /dev/null +++ b/pyrogram/methods/chats/toggle_forum_topics.py @@ -0,0 +1,65 @@ +# 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 Union + +import pyrogram +from pyrogram import raw +from pyrogram import errors + + +class ToggleForumTopics: + async def toggle_forum_topics( + self: "pyrogram.Client", + chat_id: Union[int, str], + enabled: bool = False + ) -> bool: + """Enable or disable forum functionality in a supergroup. + + .. include:: /_includes/usable-by/users.rst + + Parameters: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + + enabled (``bool``): + The new status. Pass True to enable forum topics. + + Returns: + ``bool``: True on success. False otherwise. + + Example: + .. code-block:: python + + # Change status of topics to disabled + await app.toggle_topics() + + # Change status of topics to enabled + await app.toggle_topics(enabled=True) + """ + try: + r = await self.invoke( + raw.functions.channels.ToggleForum( + channel=await self.resolve_peer(chat_id), + enabled=enabled + ) + ) + + return bool(r) + except errors.RPCError: + return False \ No newline at end of file diff --git a/pyrogram/methods/chats/update_chat_notifications.py b/pyrogram/methods/chats/update_chat_notifications.py new file mode 100644 index 000000000..1c3657892 --- /dev/null +++ b/pyrogram/methods/chats/update_chat_notifications.py @@ -0,0 +1,93 @@ +# 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, timedelta +from typing import Union + +import pyrogram +from pyrogram import raw +from pyrogram import types +from pyrogram import utils + +class UpdateChatNotifications: + async def update_chat_notifications( + self: "pyrogram.Client", + chat_id: Union[int, str], + mute: bool = None, + mute_until: datetime = None, + stories_muted: bool = None, + stories_hide_sender: bool = None, + show_previews: bool = None + ) -> "types.Chat": + """Update the notification settings for the selected chat + + Parameters: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + + mute (``bool``, *optional*): + Pass True if you want to mute chat. + + until_date (:py:obj:`~datetime.datetime`, *optional*): + Date when the user will be unmuted. Works only if the mute parameter is set to True. Defaults to forever. + + stories_muted (``bool``, *optional*): + N/A + + stories_hide_sender (``bool``, *optional*): + N/A + + show_previews (``bool``, *optional*): + If the text of the message shall be displayed in notification. + + Returns: + ``bool``: True on success, False otherwise. + + Example: + .. code-block:: python + + # Mute a chat permanently + app.update_chat_notifications(chat_id, mute=True) + + # Mute a chat for 10 minutes + app.update_chat_notifications( + chat_id, + mute=True + mute_until=datetime.timedelta(minutes=10) + ) + + # Unmute a chat + app.update_chat_notifications(chat_id, mute=False) + """ + if not mute_until: + mute_until = utils.max_datetime() if mute else utils.zero_datetime() + + r = await self.invoke( + raw.functions.account.UpdateNotifySettings( + peer=raw.types.InputNotifyPeer(peer=await self.resolve_peer(chat_id)), + settings=raw.types.InputPeerNotifySettings( + show_previews=show_previews, + silent=mute, + mute_until=utils.datetime_to_timestamp(mute_until), + stories_muted=stories_muted, + stories_hide_sender=stories_hide_sender, + ) + ) + ) + + return r \ No newline at end of file diff --git a/pyrogram/methods/chats/update_color.py b/pyrogram/methods/chats/update_color.py index 424e2cb0b..2543c3ff0 100644 --- a/pyrogram/methods/chats/update_color.py +++ b/pyrogram/methods/chats/update_color.py @@ -20,60 +20,57 @@ import pyrogram from pyrogram import raw -from pyrogram import types - -# account.updateColor flags: color:int background_emoji_id:flags.0?long = Bool; -# channels.updateColor flags: channel:InputChannel color:int background_emoji_id:flags.0?long = Updates; +from pyrogram import enums class UpdateColor: async def update_color( self: "pyrogram.Client", chat_id: Union[int, str], - color: int, - background_emoji_id: int = None, - ) -> "types.Chat": + color: Union["enums.ReplyColor", "enums.ProfileColor"], + background_emoji_id: int = None + ) -> bool: """Update color - .. include:: /_includes/usable-by/users-bots.rst + .. include:: /_includes/usable-by/users.rst Parameters: chat_id (``int`` | ``str``): Unique identifier (int) or username (str) of the target chat. - color (``int``): - Color + color (:obj:`~pyrogram.enums.ReplyColor` | :obj:`~pyrogram.enums.ProfileColor`): + Color type. + Pass :obj:`~pyrogram.enums.ReplyColor` to set reply color or + :obj:`~pyrogram.enums.ProfileColor` to set profile color. background_emoji_id (``int``, *optional*): - Background emoji + Unique identifier of the custom emoji. Returns: - ``bool``: True on success. + ``bool``: On success, in case the passed-in session is authorized, True is returned. Example: .. code-block:: python - await app.update_color(chat_id, 1) + await app.update_color(chat_id, enums.ReplyColor.RED) """ - peer = await self.resolve_peer(chat_id) if isinstance(peer, raw.types.InputPeerSelf): - await self.invoke( + r = await self.invoke( raw.functions.account.UpdateColor( - color=color, + for_profile=isinstance(color, enums.ProfileColor), + color=color.value, background_emoji_id=background_emoji_id ) ) - - r = await self.invoke(raw.functions.users.GetUsers(id=[raw.types.InputPeerSelf()])) - return types.Chat._parse_user_chat(self, r[0]) else: r = await self.invoke( raw.functions.channels.UpdateColor( channel=peer, - color=color, + for_profile=isinstance(color, enums.ProfileColor), + color=color.value, background_emoji_id=background_emoji_id ) ) - return types.Chat._parse_channel_chat(self, r.chats[0]) \ No newline at end of file + return bool(r) \ No newline at end of file diff --git a/pyrogram/methods/chats/update_folder.py b/pyrogram/methods/chats/update_folder.py new file mode 100644 index 000000000..b38ace87a --- /dev/null +++ b/pyrogram/methods/chats/update_folder.py @@ -0,0 +1,141 @@ +# 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, Union + +import pyrogram +from pyrogram import raw + + +class UpdateFolder: + async def update_folder( + self: "pyrogram.Client", + folder_id: int, + title: str, + included_chats: Union[Union[int, str], List[Union[int, str]]] = None, + excluded_chats: Union[Union[int, str], List[Union[int, str]]] = None, + pinned_chats: Union[Union[int, str], List[Union[int, str]]] = None, + contacts: bool = None, + non_contacts: bool = None, + groups: bool = None, + channels: bool = None, + bots: bool = None, + exclude_muted: bool = None, + exclude_read: bool = None, + exclude_archived: bool = None, + emoji: str = None + ) -> bool: + """Create or update a user's folder. + + .. include:: /_includes/usable-by/users.rst + + Parameters: + folder_id (``int``): + Unique folder identifier. + + title (``str``): + Folder title. + + included_chats (``int`` | ``str`` | List of ``int`` or ``str``, *optional*): + Users or chats that should added in the folder + You can pass an ID (int), username (str) or phone number (str). + Multiple users can be added by passing a list of IDs, usernames or phone numbers. + + excluded_chats (``int`` | ``str`` | List of ``int`` or ``str``, *optional*): + Users or chats that should excluded from the folder + You can pass an ID (int), username (str) or phone number (str). + Multiple users can be added by passing a list of IDs, usernames or phone numbers. + + pinned_chats (``int`` | ``str`` | List of ``int`` or ``str``, *optional*): + Users or chats that should pinned in the folder + You can pass an ID (int), username (str) or phone number (str). + Multiple users can be added by passing a list of IDs, usernames or phone numbers. + + contacts (``bool``, *optional*): + Pass True if folder should contain contacts. + + non_contacts (``bool``, *optional*): + Pass True if folder should contain non contacts. + + groups (``bool``, *optional*): + Pass True if folder should contain groups. + + channels (``bool``, *optional*): + Pass True if folder should contain channels. + + bots (``bool``, *optional*): + Pass True if folder should contain bots. + + exclude_muted (``bool``, *optional*): + Pass True if folder should exclude muted users. + + exclude_archived (``bool``, *optional*): + Pass True if folder should exclude archived users. + + emoji (``str``, *optional*): + Folder emoji. + Pass None to leave the folder icon as default. + + Returns: + ``bool``: True, on success. + + Example: + .. code-block:: python + + # Create or update folder + app.update_folder(folder_id, title="New folder", included_chats="me") + """ + if not isinstance(included_chats, list): + included_chats = [included_chats] if included_chats else [] + if not isinstance(excluded_chats, list): + excluded_chats = [excluded_chats] if excluded_chats else [] + if not isinstance(pinned_chats, list): + pinned_chats = [pinned_chats] if pinned_chats else [] + + r = await self.invoke( + raw.functions.messages.UpdateDialogFilter( + id=folder_id, + filter=raw.types.DialogFilter( + id=folder_id, + title=title, + pinned_peers=[ + await self.resolve_peer(user_id) + for user_id in pinned_chats + ], + include_peers=[ + await self.resolve_peer(user_id) + for user_id in included_chats + ], + exclude_peers=[ + await self.resolve_peer(user_id) + for user_id in excluded_chats + ], + contacts=contacts, + non_contacts=non_contacts, + groups=groups, + broadcasts=channels, + bots=bots, + exclude_muted=exclude_muted, + exclude_read=exclude_read, + exclude_archived=exclude_archived, + emoticon=emoji + ) + ) + ) + + return r \ No newline at end of file diff --git a/pyrogram/methods/invite_links/get_chat_admin_invite_links.py b/pyrogram/methods/invite_links/get_chat_admin_invite_links.py index 62acca10e..e617098a5 100644 --- a/pyrogram/methods/invite_links/get_chat_admin_invite_links.py +++ b/pyrogram/methods/invite_links/get_chat_admin_invite_links.py @@ -30,13 +30,13 @@ async def get_chat_admin_invite_links( admin_id: Union[int, str], revoked: bool = False, limit: int = 0, - ) -> Optional[AsyncGenerator["types.ChatInviteLink", None]]: + ) -> AsyncGenerator["types.ChatInviteLink", None]: """Get the invite links created by an administrator in a chat. .. note:: As an administrator you can only get your own links you have exported. - As the chat or channel owner you can get everyones links. + As the chat or channel owner you can get everyone's links. .. include:: /_includes/usable-by/users.rst diff --git a/pyrogram/methods/invite_links/get_chat_invite_link_joiners.py b/pyrogram/methods/invite_links/get_chat_invite_link_joiners.py index c1fc43a71..46c105974 100644 --- a/pyrogram/methods/invite_links/get_chat_invite_link_joiners.py +++ b/pyrogram/methods/invite_links/get_chat_invite_link_joiners.py @@ -29,7 +29,7 @@ async def get_chat_invite_link_joiners( chat_id: Union[int, str], invite_link: str, limit: int = 0 - ) -> Optional[AsyncGenerator["types.ChatJoiner", None]]: + ) -> AsyncGenerator["types.ChatJoiner", None]: """Get the members who joined the chat with the invite link. .. include:: /_includes/usable-by/users.rst diff --git a/pyrogram/methods/invite_links/get_chat_join_requests.py b/pyrogram/methods/invite_links/get_chat_join_requests.py index a75498e2f..e0652e379 100644 --- a/pyrogram/methods/invite_links/get_chat_join_requests.py +++ b/pyrogram/methods/invite_links/get_chat_join_requests.py @@ -29,7 +29,7 @@ async def get_chat_join_requests( chat_id: Union[int, str], limit: int = 0, query: str = "" - ) -> Optional[AsyncGenerator["types.ChatJoiner", None]]: + ) -> AsyncGenerator["types.ChatJoiner", None]: """Get the pending join requests of a chat. .. include:: /_includes/usable-by/users.rst diff --git a/pyrogram/methods/messages/__init__.py b/pyrogram/methods/messages/__init__.py index bcd2028b9..ba699c0ed 100644 --- a/pyrogram/methods/messages/__init__.py +++ b/pyrogram/methods/messages/__init__.py @@ -38,6 +38,8 @@ from .get_media_group import GetMediaGroup from .get_messages import GetMessages from .read_chat_history import ReadChatHistory +from .read_mentions import ReadMentions +from .read_reactions import ReadReactions from .retract_vote import RetractVote from .search_global import SearchGlobal from .search_global_count import SearchGlobalCount @@ -61,11 +63,11 @@ from .send_video import SendVideo from .send_video_note import SendVideoNote from .send_voice import SendVoice +from .send_web_page import SendWebPage +from .start_bot import StartBot from .stop_poll import StopPoll from .stream_media import StreamMedia from .vote_poll import VotePoll -from .wait_for_message import WaitForMessage -from .wait_for_callback_query import WaitForCallbackQuery class Messages( @@ -92,7 +94,9 @@ class Messages( SendVideoNote, SendVoice, SendPoll, + SendWebPage, VotePoll, + StartBot, StopPoll, RetractVote, DownloadMedia, @@ -100,6 +104,8 @@ class Messages( SendCachedMedia, GetChatHistoryCount, ReadChatHistory, + ReadMentions, + ReadReactions, EditInlineText, EditInlineCaption, EditInlineMedia, @@ -116,8 +122,6 @@ class Messages( GetDiscussionReplies, GetDiscussionRepliesCount, StreamMedia, - GetCustomEmojiStickers, - WaitForMessage, - WaitForCallbackQuery + GetCustomEmojiStickers ): pass diff --git a/pyrogram/methods/messages/copy_media_group.py b/pyrogram/methods/messages/copy_media_group.py index b2f01516a..c4c3db010 100644 --- a/pyrogram/methods/messages/copy_media_group.py +++ b/pyrogram/methods/messages/copy_media_group.py @@ -17,10 +17,10 @@ # along with Pyrogram. If not, see . from datetime import datetime -from typing import Union, List +from typing import Union, List, Optional import pyrogram -from pyrogram import types, utils, raw +from pyrogram import types, utils, raw, enums class CopyMediaGroup: @@ -33,11 +33,14 @@ async def copy_media_group( disable_notification: bool = None, message_thread_id: int = None, reply_to_message_id: int = None, - reply_to_chat_id: int = None, + reply_to_chat_id: Union[int, str] = None, reply_to_story_id: int = None, quote_text: str = None, + parse_mode: Optional["enums.ParseMode"] = None, quote_entities: List["types.MessageEntity"] = None, + quote_offset: int = None, schedule_date: datetime = None, + invert_media: bool = None, ) -> List["types.Message"]: """Copy a media group by providing one of the message ids. @@ -70,15 +73,39 @@ async def copy_media_group( Sends the message silently. Users will receive a notification with no sound. + message_thread_id (``int``, *optional*): + Unique identifier for the target message thread (topic) of the forum. + For supergroups only. + reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. - message_thread_id (``int``, *optional*): - If the message is in a thread, ID of the original message. + reply_to_chat_id (``int``, *optional*): + If the message is a reply, ID of the original chat. + + reply_to_story_id (``int``, *optional*): + Unique identifier for the target story. + + quote_text (``str``, *optional*): + Text of the quote to be sent. + + parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): + By default, texts are parsed using both Markdown and HTML styles. + You can combine both syntaxes together. + + quote_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): + List of special entities that appear in quote text, which can be specified instead of *parse_mode*. + + quote_offset (``int``, *optional*): + Offset for quote in original message. schedule_date (:py:obj:`~datetime.datetime`, *optional*): Date when the message will be automatically sent. + invert_media (``bool``, *optional*): + If True, link preview will be shown above the message text. + Otherwise, the link preview will be shown below the message text. + Returns: List of :obj:`~pyrogram.types.Message`: On success, a list of copied messages is returned. @@ -93,6 +120,7 @@ async def copy_media_group( await app.copy_media_group(to_chat, from_chat, 123, captions=["caption 1", None, ""]) """ + quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() media_group = await self.get_media_group(from_chat_id, message_id) multi_media = [] @@ -134,8 +162,10 @@ async def copy_media_group( reply_to_story_id=reply_to_story_id, quote_text=quote_text, quote_entities=quote_entities, + quote_offset=quote_offset, ), - schedule_date=utils.datetime_to_timestamp(schedule_date) + schedule_date=utils.datetime_to_timestamp(schedule_date), + invert_media=invert_media ), sleep_threshold=60 ) diff --git a/pyrogram/methods/messages/copy_message.py b/pyrogram/methods/messages/copy_message.py index 0b6624b28..fa45b41f8 100644 --- a/pyrogram/methods/messages/copy_message.py +++ b/pyrogram/methods/messages/copy_message.py @@ -21,7 +21,7 @@ from typing import Union, List, Optional import pyrogram -from pyrogram import types, enums +from pyrogram import types, enums, utils log = logging.getLogger(__name__) @@ -36,9 +36,12 @@ async def copy_message( parse_mode: Optional["enums.ParseMode"] = None, caption_entities: List["types.MessageEntity"] = None, disable_notification: bool = None, + message_thread_id: int = None, reply_to_message_id: int = None, + reply_to_chat_id: Union[int, str] = None, schedule_date: datetime = None, protect_content: bool = None, + has_spoiler: bool = None, reply_markup: Union[ "types.InlineKeyboardMarkup", "types.ReplyKeyboardMarkup", @@ -83,8 +86,15 @@ async def copy_message( Sends the message silently. Users will receive a notification with no sound. + message_thread_id (``int``, *optional*): + Unique identifier for the target message thread (topic) of the forum. + For supergroups only. + reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. + + reply_to_chat_id (``int``, *optional*): + If the message is a reply, ID of the original chat. schedule_date (:py:obj:`~datetime.datetime`, *optional*): Date when the message will be automatically sent. @@ -92,6 +102,9 @@ async def copy_message( protect_content (``bool``, *optional*): Protects the contents of the sent message from forwarding and saving. + has_spoiler (``bool``, *optional*): + True, if the message media is covered by a spoiler animation. + reply_markup (:obj:`~pyrogram.types.InlineKeyboardMarkup` | :obj:`~pyrogram.types.ReplyKeyboardMarkup` | :obj:`~pyrogram.types.ReplyKeyboardRemove` | :obj:`~pyrogram.types.ForceReply`, *optional*): Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. @@ -114,8 +127,11 @@ async def copy_message( parse_mode=parse_mode, caption_entities=caption_entities, disable_notification=disable_notification, + message_thread_id=message_thread_id, reply_to_message_id=reply_to_message_id, + reply_to_chat_id=reply_to_chat_id, schedule_date=schedule_date, protect_content=protect_content, + has_spoiler=has_spoiler, reply_markup=reply_markup ) diff --git a/pyrogram/methods/messages/download_media.py b/pyrogram/methods/messages/download_media.py index 4f44ff254..103810264 100644 --- a/pyrogram/methods/messages/download_media.py +++ b/pyrogram/methods/messages/download_media.py @@ -31,7 +31,7 @@ class DownloadMedia: async def download_media( self: "pyrogram.Client", - message: Union["types.Message", str], + message: Union["types.Message", "types.Story", str], file_name: str = DEFAULT_DOWNLOAD_DIR, in_memory: bool = False, block: bool = True, @@ -43,8 +43,8 @@ async def download_media( .. include:: /_includes/usable-by/users-bots.rst Parameters: - message (:obj:`~pyrogram.types.Message` | ``str``): - Pass a Message containing the media, the media itself (message.audio, message.video, ...) or a file id + message (:obj:`~pyrogram.types.Message` | :obj:`~pyrogram.types.Story` | ``str``): + Pass a Message or Story containing the media, the media itself (message.audio, message.video, ...) or a file id as string. file_name (``str``, *optional*): @@ -122,9 +122,11 @@ async def progress(current, total): available_media = ("audio", "document", "photo", "sticker", "animation", "video", "voice", "video_note", "new_chat_photo") - if isinstance(message, types.Message): + if isinstance(message, (types.Message, types.Story)): + story = getattr(message, "story", None) + for kind in available_media: - media = getattr(message, kind, None) + media = getattr(story or message, kind, None) if media is not None: break diff --git a/pyrogram/methods/messages/edit_inline_media.py b/pyrogram/methods/messages/edit_inline_media.py index 7ab424a4f..e06ded2a3 100644 --- a/pyrogram/methods/messages/edit_inline_media.py +++ b/pyrogram/methods/messages/edit_inline_media.py @@ -108,7 +108,7 @@ async def edit_inline_media( spoiler=media.has_spoiler ) else: - media = utils.get_input_media_from_file_id(media.media, FileType.PHOTO) + media = utils.get_input_media_from_file_id(media.media, FileType.PHOTO, has_spoiler=media.has_spoiler) elif isinstance(media, types.InputMediaVideo): if is_uploaded_file: media = raw.types.InputMediaUploadedDocument( @@ -131,7 +131,7 @@ async def edit_inline_media( spoiler=media.has_spoiler ) else: - media = utils.get_input_media_from_file_id(media.media, FileType.VIDEO) + media = utils.get_input_media_from_file_id(media.media, FileType.VIDEO, has_spoiler=media.has_spoiler) elif isinstance(media, types.InputMediaAudio): if is_uploaded_file: media = raw.types.InputMediaUploadedDocument( @@ -176,7 +176,7 @@ async def edit_inline_media( spoiler=media.has_spoiler ) else: - media = utils.get_input_media_from_file_id(media.media, FileType.ANIMATION) + media = utils.get_input_media_from_file_id(media.media, FileType.ANIMATION, has_spoiler=media.has_spoiler) elif isinstance(media, types.InputMediaDocument): if is_uploaded_file: media = raw.types.InputMediaUploadedDocument( diff --git a/pyrogram/methods/messages/edit_message_caption.py b/pyrogram/methods/messages/edit_message_caption.py index 2e4a45a69..9a8290b79 100644 --- a/pyrogram/methods/messages/edit_message_caption.py +++ b/pyrogram/methods/messages/edit_message_caption.py @@ -16,6 +16,7 @@ # 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, Optional import pyrogram @@ -30,6 +31,7 @@ async def edit_message_caption( caption: str, parse_mode: Optional["enums.ParseMode"] = None, caption_entities: List["types.MessageEntity"] = None, + schedule_date: datetime = None, reply_markup: "types.InlineKeyboardMarkup" = None ) -> "types.Message": """Edit the caption of media messages. @@ -55,6 +57,9 @@ async def edit_message_caption( caption_entities (List of :obj:`~pyrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): + Date when the message will be automatically sent. + reply_markup (:obj:`~pyrogram.types.InlineKeyboardMarkup`, *optional*): An InlineKeyboardMarkup object. @@ -72,5 +77,6 @@ async def edit_message_caption( text=caption, parse_mode=parse_mode, entities=caption_entities, + schedule_date=schedule_date, reply_markup=reply_markup ) diff --git a/pyrogram/methods/messages/edit_message_media.py b/pyrogram/methods/messages/edit_message_media.py index 5a34f1387..ca7552264 100644 --- a/pyrogram/methods/messages/edit_message_media.py +++ b/pyrogram/methods/messages/edit_message_media.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . +from datetime import datetime import io import os import re @@ -34,6 +35,8 @@ async def edit_message_media( chat_id: Union[int, str], message_id: int, media: "types.InputMedia", + invert_media: bool = None, + schedule_date: datetime = None, reply_markup: "types.InlineKeyboardMarkup" = None, file_name: str = None ) -> "types.Message": @@ -56,6 +59,13 @@ async def edit_message_media( media (:obj:`~pyrogram.types.InputMedia`): One of the InputMedia objects describing an animation, audio, document, photo or video. + invert_media (``bool``, *optional*): + If True, link preview will be shown above the message text. + Otherwise, the link preview will be shown below the message text. + + schedule_date (:py:obj:`~datetime.datetime`, *optional*): + Date when the message will be automatically sent. + reply_markup (:obj:`~pyrogram.types.InlineKeyboardMarkup`, *optional*): An InlineKeyboardMarkup object. @@ -117,7 +127,7 @@ async def edit_message_media( spoiler=media.has_spoiler ) else: - media = utils.get_input_media_from_file_id(media.media, FileType.PHOTO) + media = utils.get_input_media_from_file_id(media.media, FileType.PHOTO, has_spoiler=media.has_spoiler) elif isinstance(media, types.InputMediaVideo): if isinstance(media.media, io.BytesIO) or os.path.isfile(media.media): uploaded_media = await self.invoke( @@ -157,7 +167,7 @@ async def edit_message_media( spoiler=media.has_spoiler ) else: - media = utils.get_input_media_from_file_id(media.media, FileType.VIDEO) + media = utils.get_input_media_from_file_id(media.media, FileType.VIDEO, has_spoiler=media.has_spoiler) elif isinstance(media, types.InputMediaAudio): if isinstance(media.media, io.BytesIO) or os.path.isfile(media.media): media = await self.invoke( @@ -234,7 +244,7 @@ async def edit_message_media( spoiler=media.has_spoiler ) else: - media = utils.get_input_media_from_file_id(media.media, FileType.ANIMATION) + media = utils.get_input_media_from_file_id(media.media, FileType.ANIMATION, has_spoiler=media.has_spoiler) elif isinstance(media, types.InputMediaDocument): if isinstance(media.media, io.BytesIO) or os.path.isfile(media.media): media = await self.invoke( @@ -271,7 +281,9 @@ async def edit_message_media( raw.functions.messages.EditMessage( peer=await self.resolve_peer(chat_id), id=message_id, + invert_media=invert_media, media=media, + schedule_date=utils.datetime_to_timestamp(schedule_date), reply_markup=await reply_markup.write(self) if reply_markup else None, message=message, entities=entities diff --git a/pyrogram/methods/messages/edit_message_reply_markup.py b/pyrogram/methods/messages/edit_message_reply_markup.py index 1cd75c1a8..42a631cbe 100644 --- a/pyrogram/methods/messages/edit_message_reply_markup.py +++ b/pyrogram/methods/messages/edit_message_reply_markup.py @@ -16,11 +16,13 @@ # 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 import pyrogram from pyrogram import raw from pyrogram import types +from pyrogram import utils class EditMessageReplyMarkup: @@ -28,6 +30,7 @@ async def edit_message_reply_markup( self: "pyrogram.Client", chat_id: Union[int, str], message_id: int, + schedule_date: datetime = None, reply_markup: "types.InlineKeyboardMarkup" = None, ) -> "types.Message": """Edit only the reply markup of messages sent by the bot. @@ -43,6 +46,9 @@ async def edit_message_reply_markup( message_id (``int``): Message identifier in the chat specified in chat_id. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): + Date when the message will be automatically sent. + reply_markup (:obj:`~pyrogram.types.InlineKeyboardMarkup`, *optional*): An InlineKeyboardMarkup object. @@ -64,6 +70,7 @@ async def edit_message_reply_markup( raw.functions.messages.EditMessage( peer=await self.resolve_peer(chat_id), id=message_id, + schedule_date=utils.datetime_to_timestamp(schedule_date), reply_markup=await reply_markup.write(self) if reply_markup else None, ) ) diff --git a/pyrogram/methods/messages/edit_message_text.py b/pyrogram/methods/messages/edit_message_text.py index 540b6aa63..66868c984 100644 --- a/pyrogram/methods/messages/edit_message_text.py +++ b/pyrogram/methods/messages/edit_message_text.py @@ -16,6 +16,7 @@ # 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, Optional import pyrogram @@ -33,6 +34,7 @@ async def edit_message_text( parse_mode: Optional["enums.ParseMode"] = None, entities: List["types.MessageEntity"] = None, disable_web_page_preview: bool = None, + schedule_date: datetime = None, reply_markup: "types.InlineKeyboardMarkup" = None ) -> "types.Message": """Edit the text of messages. @@ -61,6 +63,9 @@ async def edit_message_text( disable_web_page_preview (``bool``, *optional*): Disables link previews for links in this message. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): + Date when the message will be automatically sent. + reply_markup (:obj:`~pyrogram.types.InlineKeyboardMarkup`, *optional*): An InlineKeyboardMarkup object. @@ -84,6 +89,7 @@ async def edit_message_text( peer=await self.resolve_peer(chat_id), id=message_id, no_webpage=disable_web_page_preview or None, + schedule_date=utils.datetime_to_timestamp(schedule_date), reply_markup=await reply_markup.write(self) if reply_markup else None, **await utils.parse_text_entities(self, text, parse_mode, entities) ) diff --git a/pyrogram/methods/messages/forward_messages.py b/pyrogram/methods/messages/forward_messages.py index 6bc12d46c..aa017ba00 100644 --- a/pyrogram/methods/messages/forward_messages.py +++ b/pyrogram/methods/messages/forward_messages.py @@ -54,7 +54,8 @@ async def forward_messages( An iterable of message identifiers in the chat specified in *from_chat_id* or a single message id. message_thread_id (``int``, *optional*): - Unique identifier of a message thread to which the message belongs; for supergroups only + Unique identifier of a message thread to which the message belongs. + For supergroups only. disable_notification (``bool``, *optional*): Sends the message silently. diff --git a/pyrogram/methods/messages/get_chat_history.py b/pyrogram/methods/messages/get_chat_history.py index b384c6ba8..898b57cd8 100644 --- a/pyrogram/methods/messages/get_chat_history.py +++ b/pyrogram/methods/messages/get_chat_history.py @@ -57,7 +57,7 @@ async def get_chat_history( offset: int = 0, offset_id: int = 0, offset_date: datetime = utils.zero_datetime() - ) -> Optional[AsyncGenerator["types.Message", None]]: + ) -> AsyncGenerator["types.Message", None]: """Get messages from a chat history. The messages are returned in reverse chronological order. diff --git a/pyrogram/methods/messages/get_chat_history_count.py b/pyrogram/methods/messages/get_chat_history_count.py index 1926224b3..78baa1a57 100644 --- a/pyrogram/methods/messages/get_chat_history_count.py +++ b/pyrogram/methods/messages/get_chat_history_count.py @@ -50,7 +50,7 @@ async def get_chat_history_count( Example: .. code-block:: python - await app.get_history_count(chat_id) + await app.get_chat_history_count(chat_id) """ r = await self.invoke( diff --git a/pyrogram/methods/messages/get_discussion_replies.py b/pyrogram/methods/messages/get_discussion_replies.py index dd23751bb..6454e869f 100644 --- a/pyrogram/methods/messages/get_discussion_replies.py +++ b/pyrogram/methods/messages/get_discussion_replies.py @@ -28,7 +28,7 @@ async def get_discussion_replies( chat_id: Union[int, str], message_id: int, limit: int = 0, - ) -> Optional[AsyncGenerator["types.Message", None]]: + ) -> AsyncGenerator["types.Message", None]: """Get the message replies of a discussion thread. .. include:: /_includes/usable-by/users.rst diff --git a/pyrogram/methods/messages/read_mentions.py b/pyrogram/methods/messages/read_mentions.py new file mode 100644 index 000000000..5e29c19f1 --- /dev/null +++ b/pyrogram/methods/messages/read_mentions.py @@ -0,0 +1,64 @@ +# 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 Union + +import pyrogram +from pyrogram import raw + + +class ReadMentions: + async def read_mentions( + self: "pyrogram.Client", + chat_id: Union[int, str], + topic_id: int = None + ) -> bool: + """Mark a mention in the chat as read. + + .. include:: /_includes/usable-by/users.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). + + topic_id (``int``, *optional*): + Mark as read only mentions to messages within the specified forum topic. + By default, no topic is applied and all mentions marked as read. + + Returns: + ``bool`` - On success, True is returned. + + Example: + .. code-block:: python + + # Mark the chat mention as read + await app.read_mentions(chat_id) + + # Mark the chat mention as read in specified topic + await app.read_mentions(chat_id, topic_id) + """ + r = await self.invoke( + raw.functions.messages.ReadMentions( + peer=await self.resolve_peer(chat_id), + top_msg_id=topic_id + ) + ) + + return bool(r) diff --git a/pyrogram/methods/messages/read_reactions.py b/pyrogram/methods/messages/read_reactions.py new file mode 100644 index 000000000..7036cdc6d --- /dev/null +++ b/pyrogram/methods/messages/read_reactions.py @@ -0,0 +1,64 @@ +# 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 Union + +import pyrogram +from pyrogram import raw + + +class ReadReactions: + async def read_reactions( + self: "pyrogram.Client", + chat_id: Union[int, str], + topic_id: bool = None + ) -> bool: + """Mark a reaction in the chat as read. + + .. include:: /_includes/usable-by/users.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). + + topic_id (``int``, *optional*): + Mark as read only reactions to messages within the specified forum topic. + By default, no topic is applied and all reactions marked as read. + + Returns: + ``bool`` - On success, True is returned. + + Example: + .. code-block:: python + + # Mark the chat reaction as read + await app.read_reactions(chat_id) + + # Mark the chat reaction as read in specified topic + await app.read_reactions(chat_id, topic_id) + """ + r = await self.invoke( + raw.functions.messages.ReadReactions( + peer=await self.resolve_peer(chat_id), + top_msg_id=topic_id + ) + ) + + return bool(r) diff --git a/pyrogram/methods/messages/search_global.py b/pyrogram/methods/messages/search_global.py index f566c9815..29738942f 100644 --- a/pyrogram/methods/messages/search_global.py +++ b/pyrogram/methods/messages/search_global.py @@ -30,7 +30,7 @@ async def search_global( query: str = "", filter: "enums.MessagesFilter" = enums.MessagesFilter.EMPTY, limit: int = 0, - ) -> Optional[AsyncGenerator["types.Message", None]]: + ) -> AsyncGenerator["types.Message", None]: """Search messages globally from all of your chats. If you want to get the messages count only, see :meth:`~pyrogram.Client.search_global_count`. @@ -46,7 +46,7 @@ async def search_global( query (``str``, *optional*): Text query string. Use "@" to search for mentions. - + filter (:obj:`~pyrogram.enums.MessagesFilter`, *optional*): Pass a filter in order to search for specific kind of messages only. Defaults to any message (no filter). diff --git a/pyrogram/methods/messages/search_messages.py b/pyrogram/methods/messages/search_messages.py index 4497ff1cd..4bd5f13b8 100644 --- a/pyrogram/methods/messages/search_messages.py +++ b/pyrogram/methods/messages/search_messages.py @@ -67,7 +67,7 @@ async def search_messages( filter: "enums.MessagesFilter" = enums.MessagesFilter.EMPTY, limit: int = 0, from_user: Union[int, str] = None - ) -> Optional[AsyncGenerator["types.Message", None]]: + ) -> AsyncGenerator["types.Message", None]: """Search for text and media messages inside a specific chat. If you want to get the messages count only, see :meth:`~pyrogram.Client.search_messages_count`. diff --git a/pyrogram/methods/messages/send_animation.py b/pyrogram/methods/messages/send_animation.py index 64fe7dae0..b213f719e 100644 --- a/pyrogram/methods/messages/send_animation.py +++ b/pyrogram/methods/messages/send_animation.py @@ -48,10 +48,11 @@ async def send_animation( disable_notification: bool = None, message_thread_id: int = None, reply_to_message_id: int = None, - reply_to_chat_id: int = None, + reply_to_chat_id: Union[int, str] = None, reply_to_story_id: int = None, quote_text: str = None, quote_entities: List["types.MessageEntity"] = None, + quote_offset: int = None, schedule_date: datetime = None, protect_content: bool = None, reply_markup: Union[ @@ -122,20 +123,26 @@ async def send_animation( message_thread_id (``int``, *optional*): Unique identifier for the target message thread (topic) of the forum. - for forum supergroups only. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. + reply_to_chat_id (``int``, *optional*): + If the message is a reply, ID of the original chat. + reply_to_story_id (``int``, *optional*): Unique identifier for the target story. - quote_text (``str``): + quote_text (``str``, *optional*): Text of the quote to be sent. - quote_entities (List of :obj:`~pyrogram.types.MessageEntity`): + quote_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): List of special entities that appear in quote text, which can be specified instead of *parse_mode*. + quote_offset (``int``, *optional*): + Offset for quote in original message. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): Date when the message will be automatically sent. @@ -220,7 +227,7 @@ async def progress(current, total): spoiler=has_spoiler ) else: - media = utils.get_input_media_from_file_id(animation, FileType.ANIMATION) + media = utils.get_input_media_from_file_id(animation, FileType.ANIMATION, has_spoiler=has_spoiler) else: thumb = await self.save_file(thumb) file = await self.save_file(animation, progress=progress, progress_args=progress_args) @@ -240,9 +247,8 @@ async def progress(current, total): raw.types.DocumentAttributeAnimated() ] ) - - if quote_text or quote_entities: - quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() + + quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() while True: try: @@ -259,6 +265,7 @@ async def progress(current, total): reply_to_story_id=reply_to_story_id, quote_text=quote_text, quote_entities=quote_entities, + quote_offset=quote_offset, ), random_id=self.rnd_id(), schedule_date=utils.datetime_to_timestamp(schedule_date), diff --git a/pyrogram/methods/messages/send_audio.py b/pyrogram/methods/messages/send_audio.py index 5844df8db..95a20fc0d 100644 --- a/pyrogram/methods/messages/send_audio.py +++ b/pyrogram/methods/messages/send_audio.py @@ -46,10 +46,11 @@ async def send_audio( disable_notification: bool = None, message_thread_id: int = None, reply_to_message_id: int = None, - reply_to_chat_id: int = None, + reply_to_chat_id: Union[int, str] = None, reply_to_story_id: int = None, quote_text: str = None, quote_entities: List["types.MessageEntity"] = None, + quote_offset: int = None, schedule_date: datetime = None, protect_content: bool = None, reply_markup: Union[ @@ -115,20 +116,26 @@ async def send_audio( message_thread_id (``int``, *optional*): Unique identifier for the target message thread (topic) of the forum. - for forum supergroups only. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. + reply_to_chat_id (``int``, *optional*): + If the message is a reply, ID of the original chat. + reply_to_story_id (``int``, *optional*): Unique identifier for the target story. - quote_text (``str``): + quote_text (``str``, *optional*): Text of the quote to be sent. - quote_entities (List of :obj:`~pyrogram.types.MessageEntity`): + quote_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): List of special entities that appear in quote text, which can be specified instead of *parse_mode*. + quote_offset (``int``, *optional*): + Offset for quote in original message. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): Date when the message will be automatically sent. @@ -228,8 +235,7 @@ async def progress(current, total): ] ) - if quote_text or quote_entities: - quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() + quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() while True: try: @@ -246,6 +252,7 @@ async def progress(current, total): reply_to_story_id=reply_to_story_id, quote_text=quote_text, quote_entities=quote_entities, + quote_offset=quote_offset, ), random_id=self.rnd_id(), schedule_date=utils.datetime_to_timestamp(schedule_date), diff --git a/pyrogram/methods/messages/send_cached_media.py b/pyrogram/methods/messages/send_cached_media.py index 4a217e08d..d1461dc2b 100644 --- a/pyrogram/methods/messages/send_cached_media.py +++ b/pyrogram/methods/messages/send_cached_media.py @@ -35,12 +35,14 @@ async def send_cached_media( disable_notification: bool = None, message_thread_id: int = None, reply_to_message_id: int = None, - reply_to_chat_id: int = None, + reply_to_chat_id: Union[int, str] = None, reply_to_story_id: int = None, quote_text: str = None, + quote_offset: int = None, quote_entities: List["types.MessageEntity"] = None, schedule_date: datetime = None, protect_content: bool = None, + has_spoiler: bool = None, invert_media: bool = None, reply_markup: Union[ "types.InlineKeyboardMarkup", @@ -83,7 +85,7 @@ async def send_cached_media( message_thread_id (``int``, *optional*): Unique identifier for the target message thread (topic) of the forum. - for forum supergroups only. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. @@ -91,20 +93,27 @@ async def send_cached_media( reply_to_story_id (``int``, *optional*): Unique identifier for the target story. - quote_text (``str``): + quote_text (``str``, *optional*): Text of the quote to be sent. - quote_entities (List of :obj:`~pyrogram.types.MessageEntity`): + quote_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): List of special entities that appear in quote text, which can be specified instead of *parse_mode*. + quote_offset (``int``, *optional*): + Offset for quote in original message. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): Date when the message will be automatically sent. protect_content (``bool``, *optional*): Protects the contents of the sent message from forwarding and saving. + has_spoiler (``bool``, *optional*): + True, if the message media is covered by a spoiler animation. + invert_media (``bool``, *optional*): - Invert media. + If True, link preview will be shown above the message text. + Otherwise, the link preview will be shown below the message text. reply_markup (:obj:`~pyrogram.types.InlineKeyboardMarkup` | :obj:`~pyrogram.types.ReplyKeyboardMarkup` | :obj:`~pyrogram.types.ReplyKeyboardRemove` | :obj:`~pyrogram.types.ForceReply`, *optional*): Additional interface options. An object for an inline keyboard, custom reply keyboard, @@ -118,15 +127,13 @@ async def send_cached_media( await app.send_cached_media("me", file_id) """ - - if quote_text or quote_entities: - quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() + quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() peer = await self.resolve_peer(chat_id) r = await self.invoke( raw.functions.messages.SendMedia( peer=peer, - media=utils.get_input_media_from_file_id(file_id), + media=utils.get_input_media_from_file_id(file_id, has_spoiler=has_spoiler), silent=disable_notification or None, invert_media=invert_media, reply_to=utils.get_reply_to( @@ -136,6 +143,7 @@ async def send_cached_media( reply_to_story_id=reply_to_story_id, quote_text=quote_text, quote_entities=quote_entities, + quote_offset=quote_offset, ), random_id=self.rnd_id(), schedule_date=utils.datetime_to_timestamp(schedule_date), diff --git a/pyrogram/methods/messages/send_contact.py b/pyrogram/methods/messages/send_contact.py index fe3e40977..c2a55f8fc 100644 --- a/pyrogram/methods/messages/send_contact.py +++ b/pyrogram/methods/messages/send_contact.py @@ -37,12 +37,13 @@ async def send_contact( disable_notification: bool = None, message_thread_id: int = None, reply_to_message_id: int = None, - reply_to_chat_id: int = None, + reply_to_chat_id: Union[int, str] = None, reply_to_story_id: int = None, quote_text: str = None, + parse_mode: Optional["enums.ParseMode"] = None, quote_entities: List["types.MessageEntity"] = None, + quote_offset: int = None, schedule_date: datetime = None, - parse_mode: Optional["enums.ParseMode"] = None, protect_content: bool = None, reply_markup: Union[ "types.InlineKeyboardMarkup", @@ -79,21 +80,27 @@ async def send_contact( message_thread_id (``int``, *optional*): Unique identifier for the target message thread (topic) of the forum. - for forum supergroups only. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. - quote_text (``str``): + reply_to_chat_id (``int``, *optional*): + If the message is a reply, ID of the original chat. + + quote_text (``str``, *optional*): Text of the quote to be sent. parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): By default, texts are parsed using both Markdown and HTML styles. You can combine both syntaxes together. - quote_entities (List of :obj:`~pyrogram.types.MessageEntity`): + quote_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): List of special entities that appear in quote text, which can be specified instead of *parse_mode*. + quote_offset (``int``, *optional*): + Offset for quote in original message. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): Date when the message will be automatically sent. @@ -112,9 +119,7 @@ async def send_contact( await app.send_contact("me", "+1-123-456-7890", "Name") """ - - if quote_text or quote_entities: - quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() + quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() r = await self.invoke( raw.functions.messages.SendMedia( @@ -134,6 +139,7 @@ async def send_contact( reply_to_story_id=reply_to_story_id, quote_text=quote_text, quote_entities=quote_entities, + quote_offset=quote_offset, ), random_id=self.rnd_id(), schedule_date=utils.datetime_to_timestamp(schedule_date), diff --git a/pyrogram/methods/messages/send_dice.py b/pyrogram/methods/messages/send_dice.py index 6f02a973b..b0d7ebb81 100644 --- a/pyrogram/methods/messages/send_dice.py +++ b/pyrogram/methods/messages/send_dice.py @@ -32,11 +32,12 @@ async def send_dice( disable_notification: bool = None, message_thread_id: int = None, reply_to_message_id: int = None, - reply_to_chat_id: int = None, + reply_to_chat_id: Union[int, str] = None, reply_to_story_id: int = None, quote_text: str = None, - quote_entities: List["types.MessageEntity"] = None, parse_mode: Optional["enums.ParseMode"] = None, + quote_entities: List["types.MessageEntity"] = None, + quote_offset: int = None, schedule_date: datetime = None, protect_content: bool = None, reply_markup: Union[ @@ -69,24 +70,30 @@ async def send_dice( message_thread_id (``int``, *optional*): Unique identifier for the target message thread (topic) of the forum. - for forum supergroups only. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. + reply_to_chat_id (``int``, *optional*): + If the message is a reply, ID of the original chat. + reply_to_story_id (``int``, *optional*): Unique identifier for the target story. - quote_text (``str``): + quote_text (``str``, *optional*): Text of the quote to be sent. parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): By default, texts are parsed using both Markdown and HTML styles. You can combine both syntaxes together. - quote_entities (List of :obj:`~pyrogram.types.MessageEntity`): + quote_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): List of special entities that appear in quote text, which can be specified instead of *parse_mode*. + quote_offset (``int``, *optional*): + Offset for quote in original message. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): Date when the message will be automatically sent. @@ -112,9 +119,7 @@ async def send_dice( # Send a basketball await app.send_dice(chat_id, "🏀") """ - - if quote_text or quote_entities: - quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() + quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() peer = await self.resolve_peer(chat_id) r = await self.invoke( @@ -129,6 +134,7 @@ async def send_dice( reply_to_story_id=reply_to_story_id, quote_text=quote_text, quote_entities=quote_entities, + quote_offset=quote_offset, ), random_id=self.rnd_id(), schedule_date=utils.datetime_to_timestamp(schedule_date), diff --git a/pyrogram/methods/messages/send_document.py b/pyrogram/methods/messages/send_document.py index 952edb96e..887c51e03 100644 --- a/pyrogram/methods/messages/send_document.py +++ b/pyrogram/methods/messages/send_document.py @@ -44,10 +44,11 @@ async def send_document( disable_notification: bool = None, message_thread_id: int = None, reply_to_message_id: int = None, - reply_to_chat_id: int = None, + reply_to_chat_id: Union[int, str] = None, reply_to_story_id: int = None, quote_text: str = None, quote_entities: List["types.MessageEntity"] = None, + quote_offset: int = None, schedule_date: datetime = None, protect_content: bool = None, reply_markup: Union[ @@ -107,20 +108,26 @@ async def send_document( message_thread_id (``int``, *optional*): Unique identifier for the target message thread (topic) of the forum. - for forum supergroups only. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. + reply_to_chat_id (``int``, *optional*): + If the message is a reply, ID of the original chat. + reply_to_story_id (``int``, *optional*): Unique identifier for the target story. - quote_text (``str``): + quote_text (``str``, *optional*): Text of the quote to be sent. - quote_entities (List of :obj:`~pyrogram.types.MessageEntity`): + quote_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): List of special entities that appear in quote text, which can be specified instead of *parse_mode*. + quote_offset (``int``, *optional*): + Offset for quote in original message. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): Date when the message will be automatically sent. @@ -206,8 +213,7 @@ async def progress(current, total): ] ) - if quote_text or quote_entities: - quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() + quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() while True: try: @@ -224,6 +230,7 @@ async def progress(current, total): reply_to_story_id=reply_to_story_id, quote_text=quote_text, quote_entities=quote_entities, + quote_offset=quote_offset, ), random_id=self.rnd_id(), schedule_date=utils.datetime_to_timestamp(schedule_date), diff --git a/pyrogram/methods/messages/send_location.py b/pyrogram/methods/messages/send_location.py index f97474148..ee4390efd 100644 --- a/pyrogram/methods/messages/send_location.py +++ b/pyrogram/methods/messages/send_location.py @@ -33,12 +33,12 @@ async def send_location( disable_notification: bool = None, message_thread_id: int = None, reply_to_message_id: int = None, - reply_to_chat_id: int = None, - reply_to_story_id: int = None, + reply_to_chat_id: Union[int, str] = None, quote_text: str = None, + parse_mode: Optional["enums.ParseMode"] = None, quote_entities: List["types.MessageEntity"] = None, + quote_offset: int = None, schedule_date: datetime = None, - parse_mode: Optional["enums.ParseMode"] = None, protect_content: bool = None, reply_markup: Union[ "types.InlineKeyboardMarkup", @@ -69,21 +69,27 @@ async def send_location( message_thread_id (``int``, *optional*): Unique identifier for the target message thread (topic) of the forum. - for forum supergroups only. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message - quote_text (``str``): + reply_to_chat_id (``int``, *optional*): + If the message is a reply, ID of the original chat. + + quote_text (``str``, *optional*): Text of the quote to be sent. parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): By default, texts are parsed using both Markdown and HTML styles. You can combine both syntaxes together. - quote_entities (List of :obj:`~pyrogram.types.MessageEntity`): + quote_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): List of special entities that appear in quote text, which can be specified instead of *parse_mode*. + quote_offset (``int``, *optional*): + Offset for quote in original message. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): Date when the message will be automatically sent. @@ -102,9 +108,7 @@ async def send_location( app.send_location("me", latitude, longitude) """ - - if quote_text or quote_entities: - quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() + quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() r = await self.invoke( raw.functions.messages.SendMedia( @@ -121,9 +125,9 @@ async def send_location( reply_to_message_id=reply_to_message_id, message_thread_id=message_thread_id, reply_to_peer=await self.resolve_peer(reply_to_chat_id) if reply_to_chat_id else None, - reply_to_story_id=reply_to_story_id, quote_text=quote_text, quote_entities=quote_entities, + quote_offset=quote_offset, ), random_id=self.rnd_id(), schedule_date=utils.datetime_to_timestamp(schedule_date), diff --git a/pyrogram/methods/messages/send_media_group.py b/pyrogram/methods/messages/send_media_group.py index fbf56ec02..f0df2783b 100644 --- a/pyrogram/methods/messages/send_media_group.py +++ b/pyrogram/methods/messages/send_media_group.py @@ -46,11 +46,12 @@ async def send_media_group( disable_notification: bool = None, message_thread_id: int = None, reply_to_message_id: int = None, - reply_to_chat_id: int = None, + reply_to_chat_id: Union[int, str] = None, reply_to_story_id: int = None, quote_text: str = None, - quote_entities: List["types.MessageEntity"] = None, parse_mode: Optional["enums.ParseMode"] = None, + quote_entities: List["types.MessageEntity"] = None, + quote_offset: int = None, schedule_date: datetime = None, protect_content: bool = None, invert_media: bool = None, @@ -74,24 +75,30 @@ async def send_media_group( message_thread_id (``int``, *optional*): Unique identifier for the target message thread (topic) of the forum. - for forum supergroups only. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. + reply_to_chat_id (``int``, *optional*): + If the message is a reply, ID of the original chat. + reply_to_story_id (``int``, *optional*): Unique identifier for the target story. - quote_text (``str``): + quote_text (``str``, *optional*): Text of the quote to be sent. parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): By default, texts are parsed using both Markdown and HTML styles. You can combine both syntaxes together. - quote_entities (List of :obj:`~pyrogram.types.MessageEntity`): + quote_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): List of special entities that appear in quote text, which can be specified instead of *parse_mode*. + quote_offset (``int``, *optional*): + Offset for quote in original message. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): Date when the message will be automatically sent. @@ -99,7 +106,8 @@ async def send_media_group( Protects the contents of the sent message from forwarding and saving. invert_media (``bool``, *optional*): - Invert media. + If True, link preview will be shown above the message text. + Otherwise, the link preview will be shown below the message text. Returns: List of :obj:`~pyrogram.types.Message`: On success, a list of the sent messages is returned. @@ -162,7 +170,7 @@ async def send_media_group( spoiler=i.has_spoiler ) else: - media = utils.get_input_media_from_file_id(i.media, FileType.PHOTO) + media = utils.get_input_media_from_file_id(i.media, FileType.PHOTO, has_spoiler=i.has_spoiler) else: media = await self.invoke( raw.functions.messages.UploadMedia( @@ -234,7 +242,7 @@ async def send_media_group( spoiler=i.has_spoiler ) else: - media = utils.get_input_media_from_file_id(i.media, FileType.VIDEO) + media = utils.get_input_media_from_file_id(i.media, FileType.VIDEO, has_spoiler=i.has_spoiler) else: media = await self.invoke( raw.functions.messages.UploadMedia( @@ -418,8 +426,7 @@ async def send_media_group( ) ) - if quote_text or quote_entities: - quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() + quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() peer = await self.resolve_peer(chat_id) r = await self.invoke( @@ -434,6 +441,7 @@ async def send_media_group( reply_to_story_id=reply_to_story_id, quote_text=quote_text, quote_entities=quote_entities, + quote_offset=quote_offset, ), schedule_date=utils.datetime_to_timestamp(schedule_date), noforwards=protect_content, diff --git a/pyrogram/methods/messages/send_message.py b/pyrogram/methods/messages/send_message.py index 7d8ff39e8..0fa073335 100644 --- a/pyrogram/methods/messages/send_message.py +++ b/pyrogram/methods/messages/send_message.py @@ -34,11 +34,13 @@ async def send_message( disable_web_page_preview: bool = None, disable_notification: bool = None, message_thread_id: int = None, + invert_media: bool = None, reply_to_message_id: int = None, - reply_to_chat_id: int = None, + reply_to_chat_id: Union[int, str] = None, reply_to_story_id: int = None, quote_text: str = None, quote_entities: List["types.MessageEntity"] = None, + quote_offset: int = None, schedule_date: datetime = None, protect_content: bool = None, reply_markup: Union[ @@ -77,20 +79,30 @@ async def send_message( message_thread_id (``int``, *optional*): Unique identifier for the target message thread (topic) of the forum. - for forum supergroups only. + For supergroups only. + + invert_media (``bool``, *optional*): + If True, link preview will be shown above the message text. + Otherwise, the link preview will be shown below the message text. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. + reply_to_chat_id (``int``, *optional*): + If the message is a reply, ID of the original chat. + reply_to_story_id (``int``, *optional*): Unique identifier for the target story. - quote_text (``str``): + quote_text (``str``, *optional*): Text of the quote to be sent. - quote_entities (List of :obj:`~pyrogram.types.MessageEntity`): + quote_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): List of special entities that appear in quote text, which can be specified instead of *parse_mode*. + quote_offset (``int``, *optional*): + Offset for quote in original message. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): Date when the message will be automatically sent. @@ -141,8 +153,7 @@ async def send_message( message, entities = (await utils.parse_text_entities(self, text, parse_mode, entities)).values() - if quote_text or quote_entities: - quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() + quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() peer = await self.resolve_peer(chat_id) r = await self.invoke( @@ -150,6 +161,7 @@ async def send_message( peer=peer, no_webpage=disable_web_page_preview or None, silent=disable_notification or None, + invert_media=invert_media or None, reply_to=utils.get_reply_to( reply_to_message_id=reply_to_message_id, message_thread_id=message_thread_id, @@ -157,6 +169,7 @@ async def send_message( reply_to_story_id=reply_to_story_id, quote_text=quote_text, quote_entities=quote_entities, + quote_offset=quote_offset, ), random_id=self.rnd_id(), schedule_date=utils.datetime_to_timestamp(schedule_date), diff --git a/pyrogram/methods/messages/send_photo.py b/pyrogram/methods/messages/send_photo.py index 895ed6425..e706f45f7 100644 --- a/pyrogram/methods/messages/send_photo.py +++ b/pyrogram/methods/messages/send_photo.py @@ -42,10 +42,11 @@ async def send_photo( disable_notification: bool = None, message_thread_id: int = None, reply_to_message_id: int = None, - reply_to_chat_id: int = None, + reply_to_chat_id: Union[int, str] = None, reply_to_story_id: int = None, quote_text: str = None, quote_entities: List["types.MessageEntity"] = None, + quote_offset: int = None, schedule_date: datetime = None, protect_content: bool = None, reply_markup: Union[ @@ -98,20 +99,26 @@ async def send_photo( message_thread_id (``int``, *optional*): Unique identifier for the target message thread (topic) of the forum. - for forum supergroups only. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. + reply_to_chat_id (``int``, *optional*): + If the message is a reply, ID of the original chat. + reply_to_story_id (``int``, *optional*): Unique identifier for the target story. - quote_text (``str``): + quote_text (``str``, *optional*): Text of the quote to be sent. - quote_entities (List of :obj:`~pyrogram.types.MessageEntity`): + quote_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): List of special entities that appear in quote text, which can be specified instead of *parse_mode*. + quote_offset (``int``, *optional*): + Offset for quote in original message. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): Date when the message will be automatically sent. @@ -181,7 +188,7 @@ async def send_photo( spoiler=has_spoiler ) else: - media = utils.get_input_media_from_file_id(photo, FileType.PHOTO, ttl_seconds=ttl_seconds) + media = utils.get_input_media_from_file_id(photo, FileType.PHOTO, ttl_seconds=ttl_seconds, has_spoiler=has_spoiler) else: file = await self.save_file(photo, progress=progress, progress_args=progress_args) media = raw.types.InputMediaUploadedPhoto( @@ -190,8 +197,7 @@ async def send_photo( spoiler=has_spoiler ) - if quote_text or quote_entities: - quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() + quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() while True: try: @@ -208,6 +214,7 @@ async def send_photo( reply_to_story_id=reply_to_story_id, quote_text=quote_text, quote_entities=quote_entities, + quote_offset=quote_offset, ), random_id=self.rnd_id(), schedule_date=utils.datetime_to_timestamp(schedule_date), diff --git a/pyrogram/methods/messages/send_poll.py b/pyrogram/methods/messages/send_poll.py index 7c0808457..c39f9b57d 100644 --- a/pyrogram/methods/messages/send_poll.py +++ b/pyrogram/methods/messages/send_poll.py @@ -40,15 +40,16 @@ async def send_poll( open_period: int = None, close_date: datetime = None, is_closed: bool = None, - protect_content: bool = None, disable_notification: bool = None, + protect_content: bool = None, message_thread_id: int = None, reply_to_message_id: int = None, - reply_to_chat_id: int = None, + reply_to_chat_id: Union[int, str] = None, reply_to_story_id: int = None, quote_text: str = None, - quote_entities: List["types.MessageEntity"] = None, parse_mode: Optional["enums.ParseMode"] = None, + quote_entities: List["types.MessageEntity"] = None, + quote_offset: int = None, schedule_date: datetime = None, reply_markup: Union[ "types.InlineKeyboardMarkup", @@ -122,21 +123,27 @@ async def send_poll( message_thread_id (``int``, *optional*): Unique identifier for the target message thread (topic) of the forum. - for forum supergroups only. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. - quote_text (``str``): + reply_to_chat_id (``int``, *optional*): + If the message is a reply, ID of the original chat. + + quote_text (``str``, *optional*): Text of the quote to be sent. parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): By default, texts are parsed using both Markdown and HTML styles. You can combine both syntaxes together. - quote_entities (List of :obj:`~pyrogram.types.MessageEntity`): + quote_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): List of special entities that appear in quote text, which can be specified instead of *parse_mode*. + quote_offset (``int``, *optional*): + Offset for quote in original message. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): Date when the message will be automatically sent. @@ -156,8 +163,7 @@ async def send_poll( self, explanation, explanation_parse_mode, explanation_entities )).values() - if quote_text or quote_entities: - quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() + quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() r = await self.invoke( raw.functions.messages.SendMedia( @@ -190,6 +196,7 @@ async def send_poll( reply_to_story_id=reply_to_story_id, quote_text=quote_text, quote_entities=quote_entities, + quote_offset=quote_offset, ), random_id=self.rnd_id(), schedule_date=utils.datetime_to_timestamp(schedule_date), diff --git a/pyrogram/methods/messages/send_reaction.py b/pyrogram/methods/messages/send_reaction.py index f4d1d3434..626878331 100644 --- a/pyrogram/methods/messages/send_reaction.py +++ b/pyrogram/methods/messages/send_reaction.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from typing import Union +from typing import Union, List import pyrogram from pyrogram import raw @@ -26,25 +26,30 @@ class SendReaction: async def send_reaction( self: "pyrogram.Client", chat_id: Union[int, str], - message_id: int, - emoji: str = "", + message_id: int = None, + story_id: int = None, + emoji: Union[int, str, List[Union[int, str]]] = None, big: bool = False ) -> bool: - """Send a reaction to a message. + """Send a reaction to a message or story. - .. include:: /_includes/usable-by/users.rst + .. include:: /_includes/usable-by/users-bots.rst Parameters: chat_id (``int`` | ``str``): Unique identifier (int) or username (str) of the target chat. - message_id (``int``): + message_id (``int``, *optional*): Identifier of the message. - emoji (``str``, *optional*): + story_id (``int``, *optional*): + Identifier of the story. + + emoji (``int`` | ``str`` | List of ``int`` | ``str``, *optional*): Reaction emoji. - Pass "" as emoji (default) to retract the reaction. - + Pass None as emoji (default) to retract the reaction. + Pass list of int or str to react multiple emojis. + big (``bool``, *optional*): Pass True to show a bigger and longer reaction. Defaults to False. @@ -56,18 +61,47 @@ async def send_reaction( .. code-block:: python # Send a reaction - await app.send_reaction(chat_id, message_id, "🔥") + await app.send_reaction(chat_id, message_id=message_id, emoji="🔥") + + # Send a multiple reactions + await app.send_reaction(chat_id, message_id=message_id, emoji=["🔥", "❤️"]) + + # Send a reaction with premium emoji + await app.send_reaction(chat_id, message_id=message_id, emoji=5319161050128459957) + + # Send a reaction to story + await app.send_reaction(chat_id, story_id=story_id, emoji="❤️") # Retract a reaction - await app.send_reaction(chat_id, message_id) + await app.send_reaction(chat_id, message_id=message_id) """ - await self.invoke( - raw.functions.messages.SendReaction( + if isinstance(emoji, list): + emoji = [ + raw.types.ReactionCustomEmoji(document_id=i) + if isinstance(i, int) + else raw.types.ReactionEmoji(emoticon=i) + for i in emoji + ] if emoji else None + else: + if isinstance(emoji, int): + emoji = [raw.types.ReactionCustomEmoji(document_id=emoji)] + else: + emoji = [raw.types.ReactionEmoji(emoticon=emoji)] if emoji else None + + if story_id: + rpc = raw.functions.stories.SendReaction( + peer=await self.resolve_peer(chat_id), + story_id=story_id, + reaction=emoji[0] if emoji else None, + ) + else: + rpc = raw.functions.messages.SendReaction( peer=await self.resolve_peer(chat_id), msg_id=message_id, - reaction=[raw.types.ReactionEmoji(emoticon=emoji)] if emoji else None, + reaction=emoji, big=big ) - ) + + await self.invoke(rpc) return True diff --git a/pyrogram/methods/messages/send_sticker.py b/pyrogram/methods/messages/send_sticker.py index 421dcc064..a9374701a 100644 --- a/pyrogram/methods/messages/send_sticker.py +++ b/pyrogram/methods/messages/send_sticker.py @@ -39,12 +39,13 @@ async def send_sticker( disable_notification: bool = None, message_thread_id: int = None, reply_to_message_id: int = None, - reply_to_chat_id: int = None, + reply_to_chat_id: Union[int, str] = None, reply_to_story_id: int = None, quote_text: str = None, + parse_mode: Optional["enums.ParseMode"] = None, quote_entities: List["types.MessageEntity"] = None, + quote_offset: int = None, schedule_date: datetime = None, - parse_mode: Optional["enums.ParseMode"] = None, protect_content: bool = None, reply_markup: Union[ "types.InlineKeyboardMarkup", @@ -78,24 +79,30 @@ async def send_sticker( message_thread_id (``int``, *optional*): Unique identifier for the target message thread (topic) of the forum. - for forum supergroups only. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. + reply_to_chat_id (``int``, *optional*): + If the message is a reply, ID of the original chat. + reply_to_story_id (``int``, *optional*): Unique identifier for the target story. - quote_text (``str``): + quote_text (``str``, *optional*): Text of the quote to be sent. parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): By default, texts are parsed using both Markdown and HTML styles. You can combine both syntaxes together. - quote_entities (List of :obj:`~pyrogram.types.MessageEntity`): + quote_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): List of special entities that appear in quote text, which can be specified instead of *parse_mode*. + quote_offset (``int``, *optional*): + Offset for quote in original message. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): Date when the message will be automatically sent. @@ -171,8 +178,7 @@ async def send_sticker( ] ) - if quote_text or quote_entities: - quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() + quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() while True: try: @@ -189,6 +195,7 @@ async def send_sticker( reply_to_story_id=reply_to_story_id, quote_text=quote_text, quote_entities=quote_entities, + quote_offset=quote_offset, ), random_id=self.rnd_id(), schedule_date=utils.datetime_to_timestamp(schedule_date), diff --git a/pyrogram/methods/messages/send_venue.py b/pyrogram/methods/messages/send_venue.py index b6ac6cb91..f54a11f51 100644 --- a/pyrogram/methods/messages/send_venue.py +++ b/pyrogram/methods/messages/send_venue.py @@ -37,11 +37,12 @@ async def send_venue( disable_notification: bool = None, message_thread_id: int = None, reply_to_message_id: int = None, - reply_to_chat_id: int = None, + reply_to_chat_id: Union[int, str] = None, reply_to_story_id: int = None, quote_text: str = None, - quote_entities: List["types.MessageEntity"] = None, parse_mode: Optional["enums.ParseMode"] = None, + quote_entities: List["types.MessageEntity"] = None, + quote_offset: int = None, schedule_date: datetime = None, protect_content: bool = None, reply_markup: Union[ @@ -86,21 +87,27 @@ async def send_venue( message_thread_id (``int``, *optional*): Unique identifier for the target message thread (topic) of the forum. - for forum supergroups only. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message - quote_text (``str``): + reply_to_chat_id (``int``, *optional*): + If the message is a reply, ID of the original chat. + + quote_text (``str``, *optional*): Text of the quote to be sent. parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): By default, texts are parsed using both Markdown and HTML styles. You can combine both syntaxes together. - quote_entities (List of :obj:`~pyrogram.types.MessageEntity`): + quote_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): List of special entities that appear in quote text, which can be specified instead of *parse_mode*. + quote_offset (``int``, *optional*): + Offset for quote in original message. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): Date when the message will be automatically sent. @@ -121,9 +128,7 @@ async def send_venue( "me", latitude, longitude, "Venue title", "Venue address") """ - - if quote_text or quote_entities: - quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() + quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() r = await self.invoke( raw.functions.messages.SendMedia( @@ -148,6 +153,7 @@ async def send_venue( reply_to_story_id=reply_to_story_id, quote_text=quote_text, quote_entities=quote_entities, + quote_offset=quote_offset, ), random_id=self.rnd_id(), schedule_date=utils.datetime_to_timestamp(schedule_date), diff --git a/pyrogram/methods/messages/send_video.py b/pyrogram/methods/messages/send_video.py index 76dcbac1d..51809c4dc 100644 --- a/pyrogram/methods/messages/send_video.py +++ b/pyrogram/methods/messages/send_video.py @@ -49,10 +49,11 @@ async def send_video( disable_notification: bool = None, message_thread_id: int = None, reply_to_message_id: int = None, - reply_to_chat_id: int = None, + reply_to_chat_id: Union[int, str] = None, reply_to_story_id: int = None, quote_text: str = None, quote_entities: List["types.MessageEntity"] = None, + quote_offset: int = None, schedule_date: datetime = None, protect_content: bool = None, reply_markup: Union[ @@ -128,20 +129,26 @@ async def send_video( message_thread_id (``int``, *optional*): Unique identifier for the target message thread (topic) of the forum. - for forum supergroups only. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. + reply_to_chat_id (``int``, *optional*): + If the message is a reply, ID of the original chat. + reply_to_story_id (``int``, *optional*): Unique identifier for the target story. - quote_text (``str``): + quote_text (``str``, *optional*): Text of the quote to be sent. - quote_entities (List of :obj:`~pyrogram.types.MessageEntity`): + quote_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): List of special entities that appear in quote text, which can be specified instead of *parse_mode*. + quote_offset (``int``, *optional*): + Offset for quote in original message. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): Date when the message will be automatically sent. @@ -226,7 +233,7 @@ async def progress(current, total): spoiler=has_spoiler ) else: - media = utils.get_input_media_from_file_id(video, FileType.VIDEO, ttl_seconds=ttl_seconds) + media = utils.get_input_media_from_file_id(video, FileType.VIDEO, ttl_seconds=ttl_seconds, has_spoiler=has_spoiler) else: thumb = await self.save_file(thumb) file = await self.save_file(video, progress=progress, progress_args=progress_args) @@ -247,8 +254,7 @@ async def progress(current, total): ] ) - if quote_text or quote_entities: - quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() + quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() while True: try: @@ -265,6 +271,7 @@ async def progress(current, total): reply_to_story_id=reply_to_story_id, quote_text=quote_text, quote_entities=quote_entities, + quote_offset=quote_offset, ), random_id=self.rnd_id(), schedule_date=utils.datetime_to_timestamp(schedule_date), diff --git a/pyrogram/methods/messages/send_video_note.py b/pyrogram/methods/messages/send_video_note.py index 9acf1982d..a9b9aa6fd 100644 --- a/pyrogram/methods/messages/send_video_note.py +++ b/pyrogram/methods/messages/send_video_note.py @@ -41,12 +41,13 @@ async def send_video_note( disable_notification: bool = None, message_thread_id: int = None, reply_to_message_id: int = None, - reply_to_chat_id: int = None, + reply_to_chat_id: Union[int, str] = None, reply_to_story_id: int = None, quote_text: str = None, + parse_mode: Optional["enums.ParseMode"] = None, quote_entities: List["types.MessageEntity"] = None, + quote_offset: int = None, schedule_date: datetime = None, - parse_mode: Optional["enums.ParseMode"] = None, protect_content: bool = None, reply_markup: Union[ "types.InlineKeyboardMarkup", @@ -92,24 +93,30 @@ async def send_video_note( message_thread_id (``int``, *optional*): Unique identifier for the target message thread (topic) of the forum. - for forum supergroups only. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message + reply_to_chat_id (``int``, *optional*): + If the message is a reply, ID of the original chat. + reply_to_story_id (``int``, *optional*): Unique identifier for the target story. - quote_text (``str``): + quote_text (``str``, *optional*): Text of the quote to be sent. parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): By default, texts are parsed using both Markdown and HTML styles. You can combine both syntaxes together. - quote_entities (List of :obj:`~pyrogram.types.MessageEntity`): + quote_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): List of special entities that appear in quote text, which can be specified instead of *parse_mode*. + quote_offset (``int``, *optional*): + Offset for quote in original message. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): Date when the message will be automatically sent. @@ -195,8 +202,7 @@ async def send_video_note( ] ) - if quote_text or quote_entities: - quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() + quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() while True: try: @@ -213,6 +219,7 @@ async def send_video_note( reply_to_story_id=reply_to_story_id, quote_text=quote_text, quote_entities=quote_entities, + quote_offset=quote_offset, ), random_id=self.rnd_id(), schedule_date=utils.datetime_to_timestamp(schedule_date), diff --git a/pyrogram/methods/messages/send_voice.py b/pyrogram/methods/messages/send_voice.py index 12d0d35d5..b0c249b46 100644 --- a/pyrogram/methods/messages/send_voice.py +++ b/pyrogram/methods/messages/send_voice.py @@ -42,12 +42,14 @@ async def send_voice( disable_notification: bool = None, message_thread_id: int = None, reply_to_message_id: int = None, - reply_to_chat_id: int = None, + reply_to_chat_id: Union[int, str] = None, reply_to_story_id: int = None, quote_text: str = None, quote_entities: List["types.MessageEntity"] = None, + quote_offset: int = None, schedule_date: datetime = None, protect_content: bool = None, + ttl_seconds: int = None, reply_markup: Union[ "types.InlineKeyboardMarkup", "types.ReplyKeyboardMarkup", @@ -93,25 +95,37 @@ async def send_voice( message_thread_id (``int``, *optional*): Unique identifier for the target message thread (topic) of the forum. - for forum supergroups only. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message + reply_to_chat_id (``int``, *optional*): + If the message is a reply, ID of the original chat. + reply_to_story_id (``int``, *optional*): Unique identifier for the target story. - quote_text (``str``): + quote_text (``str``, *optional*): Text of the quote to be sent. - quote_entities (List of :obj:`~pyrogram.types.MessageEntity`): + quote_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): List of special entities that appear in quote text, which can be specified instead of *parse_mode*. + + quote_offset (``int``, *optional*): + Offset for quote in original message. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): Date when the message will be automatically sent. protect_content (``bool``, *optional*): Protects the contents of the sent message from forwarding and saving. + ttl_seconds (``int``, *optional*): + Self-Destruct Timer. + If you set a timer, the voice note will self-destruct in *ttl_seconds* + seconds after it was listened. + reply_markup (:obj:`~pyrogram.types.InlineKeyboardMarkup` | :obj:`~pyrogram.types.ReplyKeyboardMarkup` | :obj:`~pyrogram.types.ReplyKeyboardRemove` | :obj:`~pyrogram.types.ForceReply`, *optional*): Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. @@ -153,6 +167,9 @@ async def send_voice( # Set voice note duration await app.send_voice("me", "voice.ogg", duration=20) + + # Send self-destructing voice note + await app.send_voice("me", "voice.ogg", ttl_seconds=(1 << 31) - 1) """ file = None @@ -168,7 +185,8 @@ async def send_voice( voice=True, duration=duration ) - ] + ], + ttl_seconds=ttl_seconds ) elif re.match("^https?://", voice): media = raw.types.InputMediaDocumentExternal( @@ -186,11 +204,11 @@ async def send_voice( voice=True, duration=duration ) - ] + ], + ttl_seconds=ttl_seconds ) - if quote_text or quote_entities: - quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() + quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() while True: try: @@ -207,6 +225,7 @@ async def send_voice( reply_to_story_id=reply_to_story_id, quote_text=quote_text, quote_entities=quote_entities, + quote_offset=quote_offset, ), random_id=self.rnd_id(), schedule_date=utils.datetime_to_timestamp(schedule_date), diff --git a/pyrogram/methods/messages/send_web_page.py b/pyrogram/methods/messages/send_web_page.py new file mode 100644 index 000000000..7727f6c2b --- /dev/null +++ b/pyrogram/methods/messages/send_web_page.py @@ -0,0 +1,207 @@ +# 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, Optional + +import pyrogram +from pyrogram import raw, utils, enums +from pyrogram import types + +class SendWebPage: + async def send_web_page( + self: "pyrogram.Client", + chat_id: Union[int, str], + url: str, + text: str = None, + force_large_media: bool = None, + force_small_media: bool = None, + parse_mode: Optional["enums.ParseMode"] = None, + entities: List["types.MessageEntity"] = None, + disable_notification: bool = None, + message_thread_id: int = None, + invert_media: bool = None, + reply_to_message_id: int = None, + reply_to_chat_id: Union[int, str] = None, + reply_to_story_id: int = None, + quote_text: str = None, + quote_entities: List["types.MessageEntity"] = None, + quote_offset: int = None, + schedule_date: datetime = None, + protect_content: bool = None, + reply_markup: Union[ + "types.InlineKeyboardMarkup", + "types.ReplyKeyboardMarkup", + "types.ReplyKeyboardRemove", + "types.ForceReply" + ] = None + ) -> "types.Message": + """Send text Web Page Preview. + + .. 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). + + url (``str``): + Link that will be previewed. + + text (``str``, *optional*): + Text of the message to be sent. + + parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): + By default, texts are parsed using both Markdown and HTML styles. + You can combine both syntaxes together. + + entities (List of :obj:`~pyrogram.types.MessageEntity`): + List of special entities that appear in message text, which can be specified instead of *parse_mode*. + + force_large_media (``bool``, *optional*): + If True, media in the link preview will be larger. + Ignored if the URL isn't explicitly specified or media size change isn't supported for the preview. + + force_small_media (``bool``, *optional*): + If True, media in the link preview will be smaller. + Ignored if the URL isn't explicitly specified or media size change isn't supported for the preview. + + invert_media (``bool``, *optional*): + If True, link preview will be shown above the message text. + Otherwise, the link preview will be shown below the message text. + + disable_notification (``bool``, *optional*): + Sends the message silently. + Users will receive a notification with no sound. + + message_thread_id (``int``, *optional*): + Unique identifier for the target message thread (topic) of the forum. + for forum supergroups only. + + reply_to_message_id (``int``, *optional*): + If the message is a reply, ID of the original message. + + reply_to_chat_id (``int`` | ``str``, *optional*): + If the message is a reply, ID of the original chat. + + reply_to_story_id (``int``, *optional*): + Unique identifier for the target story. + + quote_text (``str``, *optional*): + Text of the quote to be sent. + + quote_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): + List of special entities that appear in quote text, which can be specified instead of *parse_mode*. + + quote_offset (``int``, *optional*): + Offset for quote in original message. + + schedule_date (:py:obj:`~datetime.datetime`, *optional*): + Date when the message will be automatically sent. + + protect_content (``bool``, *optional*): + Protects the contents of the sent message from forwarding and saving. + + reply_markup (:obj:`~pyrogram.types.InlineKeyboardMarkup` | :obj:`~pyrogram.types.ReplyKeyboardMarkup` | :obj:`~pyrogram.types.ReplyKeyboardRemove` | :obj:`~pyrogram.types.ForceReply`, *optional*): + Additional interface options. An object for an inline keyboard, custom reply keyboard, + instructions to remove reply keyboard or to force a reply from the user. + + Returns: + :obj:`~pyrogram.types.Message`: On success, the sent text message is returned. + + Example: + .. code-block:: python + + # Simple example + await app.send_web_page("me", "https://docs.pyrogram.org") + + # Make web preview image larger + await app.send_web_page("me", "https://docs.pyrogram.org", force_large_media=True) + + """ + + message, entities = (await utils.parse_text_entities(self, text, parse_mode, entities)).values() + + quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() + + r = await self.invoke( + raw.functions.messages.SendMedia( + peer=await self.resolve_peer(chat_id), + silent=disable_notification or None, + reply_to=utils.get_reply_to( + reply_to_message_id=reply_to_message_id, + message_thread_id=message_thread_id, + reply_to_peer=await self.resolve_peer(reply_to_chat_id) if reply_to_chat_id else None, + reply_to_story_id=reply_to_story_id, + quote_text=quote_text, + quote_entities=quote_entities, + quote_offset=quote_offset, + ), + random_id=self.rnd_id(), + schedule_date=utils.datetime_to_timestamp(schedule_date), + reply_markup=await reply_markup.write(self) if reply_markup else None, + message=message, + media=raw.types.InputMediaWebPage( + url=url, + force_large_media=force_large_media, + force_small_media=force_small_media + ), + invert_media=invert_media, + entities=entities, + noforwards=protect_content + ) + ) + + if isinstance(r, raw.types.UpdateShortSentMessage): + peer = await self.resolve_peer(chat_id) + + peer_id = ( + peer.user_id + if isinstance(peer, raw.types.InputPeerUser) + else -peer.chat_id + ) + + return types.Message( + id=r.id, + chat=types.Chat( + id=peer_id, + type=enums.ChatType.PRIVATE, + client=self + ), + text=message, + date=utils.timestamp_to_datetime(r.date), + outgoing=r.out, + reply_markup=reply_markup, + entities=[ + types.MessageEntity._parse(None, entity, {}) + for entity in entities + ] if entities else None, + client=self + ) + + for i in r.updates: + if isinstance(i, (raw.types.UpdateNewMessage, + raw.types.UpdateNewChannelMessage, + raw.types.UpdateNewScheduledMessage)): + return await types.Message._parse( + self, i.message, + {i.id: i for i in r.users}, + {i.id: i for i in r.chats}, + is_scheduled=isinstance(i, raw.types.UpdateNewScheduledMessage) + ) diff --git a/pyrogram/methods/messages/start_bot.py b/pyrogram/methods/messages/start_bot.py new file mode 100644 index 000000000..66a0fa535 --- /dev/null +++ b/pyrogram/methods/messages/start_bot.py @@ -0,0 +1,77 @@ +# 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 Union + +import pyrogram +from pyrogram import raw +from pyrogram import types + + +class StartBot: + async def start_bot( + self: "pyrogram.Client", + chat_id: Union[int, str], + param: str = "" + ) -> bool: + """Start bot + + .. include:: /_includes/usable-by/users.rst + + Parameters: + chat_id (``int`` | ``str``): + Unique identifier of the bot you want to be started. You can specify + a @username (str) or a bot ID (int). + + param (``str``): + Text of the deep linking parameter (up to 64 characters). + Defaults to "" (empty string). + + Returns: + :obj:`~pyrogram.types.Message`: On success, the sent message is returned. + + Example: + .. code-block:: python + + # Start bot + await app.start_bot("pyrogrambot") + + # Start bot with param + await app.start_bot("pyrogrambot", "ref123456") + """ + if not param: + return await self.send_message(chat_id, "/start") + + peer = await self.resolve_peer(chat_id) + + r = await self.invoke( + raw.functions.messages.StartBot( + bot=peer, + peer=peer, + random_id=self.rnd_id(), + start_param=param + ) + ) + + for i in r.updates: + if isinstance(i, raw.types.UpdateNewMessage): + return await types.Message._parse( + self, i.message, + {i.id: i for i in r.users}, + {i.id: i for i in r.chats} + ) diff --git a/pyrogram/types/user_and_chats/peer_channel.py b/pyrogram/methods/premium/__init__.py similarity index 61% rename from pyrogram/types/user_and_chats/peer_channel.py rename to pyrogram/methods/premium/__init__.py index c8c8f6d77..29699583b 100644 --- a/pyrogram/types/user_and_chats/peer_channel.py +++ b/pyrogram/methods/premium/__init__.py @@ -16,29 +16,13 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from pyrogram import raw -from ..object import Object - - -class PeerChannel(Object): - """A PeerChannel. - - - Parameters: - channel_id (``int``): - Id of the channel. - """ - - def __init__( - self, *, - channel_id: int - ): - super().__init__() - - self.channel_id = channel_id - - @staticmethod - def _parse(action: "raw.types.PeerChannel") -> "PeerChannel": - return PeerChannel( - channel_id=getattr(action, "channel_id", None) - ) +from .apply_boost import ApplyBoost +from .get_boosts_status import GetBoostsStatus +from .get_boosts import GetBoosts + +class Premium( + ApplyBoost, + GetBoostsStatus, + GetBoosts +): + pass diff --git a/pyrogram/methods/stories/apply_boost.py b/pyrogram/methods/premium/apply_boost.py similarity index 79% rename from pyrogram/methods/stories/apply_boost.py rename to pyrogram/methods/premium/apply_boost.py index c7d00052b..20774ea6e 100644 --- a/pyrogram/methods/stories/apply_boost.py +++ b/pyrogram/methods/premium/apply_boost.py @@ -20,6 +20,7 @@ import pyrogram from pyrogram import raw +from pyrogram import types class ApplyBoost: @@ -29,14 +30,14 @@ async def apply_boost( ) -> bool: """Apply boost - .. include:: /_includes/usable-by/users-bots.rst + .. include:: /_includes/usable-by/users.rst Parameters: chat_id (``int`` | ``str``): Unique identifier (int) or username (str) of the target chat. Returns: - ``str``: On success, a bool is returned. + :obj:`~pyrogram.types.MyBoost`: On success, a boost object is returned. Example: .. code-block:: python @@ -45,9 +46,14 @@ async def apply_boost( app.apply_boost(chat_id) """ r = await self.invoke( - raw.functions.stories.ApplyBoost( + raw.functions.premium.ApplyBoost( peer=await self.resolve_peer(chat_id), ) ) - return r \ No newline at end of file + return types.MyBoost._parse( + self, + r.my_boosts[0], + {i.id: i for i in r.users}, + {i.id: i for i in r.chats} + ) diff --git a/pyrogram/methods/premium/get_boosts.py b/pyrogram/methods/premium/get_boosts.py new file mode 100644 index 000000000..95776889b --- /dev/null +++ b/pyrogram/methods/premium/get_boosts.py @@ -0,0 +1,57 @@ +# 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 Union + +import pyrogram +from pyrogram import raw +from pyrogram import types + + +class GetBoosts: + async def get_boosts( + self: "pyrogram.Client", + ) -> bool: + """Get your boosts list + + .. include:: /_includes/usable-by/users.rst + + Returns: + List of :obj:`~pyrogram.types.MyBoost`: On success. + + Example: + .. code-block:: python + + # get boosts list + app.get_boosts() + """ + r = await self.invoke( + raw.functions.premium.GetMyBoosts() + ) + + users = {i.id: i for i in r.users} + chats = {i.id: i for i in r.chats} + + return types.List( + types.MyBoost._parse( + self, + boost, + users, + chats, + ) for boost in r.my_boosts + ) diff --git a/pyrogram/methods/premium/get_boosts_status.py b/pyrogram/methods/premium/get_boosts_status.py new file mode 100644 index 000000000..bff45b7b3 --- /dev/null +++ b/pyrogram/methods/premium/get_boosts_status.py @@ -0,0 +1,52 @@ +# 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 Union + +import pyrogram +from pyrogram import raw +from pyrogram import types + + +class GetBoostsStatus: + async def get_boosts_status( + self: "pyrogram.Client", + chat_id: Union[int, str] + ) -> bool: + """Get boosts status of channel + + .. include:: /_includes/usable-by/users.rst + + Parameters: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + + Returns: + :obj:`~pyrogram.types.BoostsStatus`: On success. + + Example: + .. code-block:: python + + # get boosts list + app.get_boosts() + """ + r = await self.invoke( + raw.functions.premium.GetBoostsStatus(peer=await self.resolve_peer(chat_id)) + ) + + return types.BoostsStatus._parse(r) diff --git a/pyrogram/methods/stories/__init__.py b/pyrogram/methods/stories/__init__.py index 2e24dab38..870889c59 100644 --- a/pyrogram/methods/stories/__init__.py +++ b/pyrogram/methods/stories/__init__.py @@ -1,6 +1,58 @@ +# 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 .can_send_story import CanSendStory +from .copy_story import CopyStory +from .delete_stories import DeleteStories +from .edit_story_caption import EditStoryCaption +from .edit_story_media import EditStoryMedia +from .edit_story_privacy import EditStoryPrivacy +from .export_story_link import ExportStoryLink +from .forward_story import ForwardStory +from .get_all_stories import GetAllStories +from .get_chat_stories import GetChatStories +from .get_pinned_stories import GetPinnedStories +from .get_stories import GetStories +from .get_stories_archive import GetStoriesArchive +from .hide_stories import HideStories +from .increment_story_views import IncrementStoryViews +from .pin_stories import PinStories +from .read_stories import ReadStories from .send_story import SendStory class Stories( - SendStory + CanSendStory, + CopyStory, + DeleteStories, + EditStoryCaption, + EditStoryMedia, + EditStoryPrivacy, + ExportStoryLink, + ForwardStory, + GetAllStories, + GetChatStories, + GetPinnedStories, + GetStories, + GetStoriesArchive, + HideStories, + IncrementStoryViews, + PinStories, + ReadStories, + SendStory, ): pass diff --git a/pyrogram/methods/stories/can_send_story.py b/pyrogram/methods/stories/can_send_story.py index 67c5fe584..5a7c3399c 100644 --- a/pyrogram/methods/stories/can_send_story.py +++ b/pyrogram/methods/stories/can_send_story.py @@ -16,11 +16,10 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from typing import Union, Iterable +from typing import Union import pyrogram from pyrogram import raw -from pyrogram import types class CanSendStory: @@ -30,7 +29,7 @@ async def can_send_story( ) -> bool: """Can send story - .. include:: /_includes/usable-by/users-bots.rst + .. include:: /_includes/usable-by/users.rst Parameters: chat_id (``int`` | ``str``): @@ -51,4 +50,4 @@ async def can_send_story( ) ) - return r \ No newline at end of file + return r diff --git a/pyrogram/methods/stories/copy_story.py b/pyrogram/methods/stories/copy_story.py index 744a564a0..28eb60577 100644 --- a/pyrogram/methods/stories/copy_story.py +++ b/pyrogram/methods/stories/copy_story.py @@ -17,11 +17,10 @@ # along with Pyrogram. If not, see . import logging -from datetime import datetime from typing import Union, List, Optional import pyrogram -from pyrogram import types, enums, utils +from pyrogram import types, enums log = logging.getLogger(__name__) @@ -43,7 +42,7 @@ async def copy_story( ) -> "types.Story": """Copy story. - .. include:: /_includes/usable-by/users-bots.rst + .. include:: /_includes/usable-by/users.rst Parameters: chat_id (``int`` | ``str``): @@ -71,13 +70,13 @@ async def copy_story( Story privacy. Defaults to :obj:`~pyrogram.enums.StoriesPrivacyRules.PUBLIC` - allowed_users (List of ``int``, *optional*): + allowed_users (List of ``int`` | ``str``, *optional*): List of user_id or chat_id of chat users who are allowed to view stories. Note: chat_id available only with :obj:`~pyrogram.enums.StoriesPrivacyRules.SELECTED_USERS`. Works with :obj:`~pyrogram.enums.StoriesPrivacyRules.CLOSE_FRIENDS` and :obj:`~pyrogram.enums.StoriesPrivacyRules.SELECTED_USERS` only - disallowed_users (List of ``int``, *optional*): + disallowed_users (List of ``int`` | ``str``, *optional*): List of user_id whos disallow to view the stories. Note: Works with :obj:`~pyrogram.enums.StoriesPrivacyRules.PUBLIC` and :obj:`~pyrogram.enums.StoriesPrivacyRules.CONTACTS` only @@ -114,4 +113,4 @@ async def copy_story( privacy=privacy, allowed_users=allowed_users, disallowed_users=disallowed_users - ) \ No newline at end of file + ) diff --git a/pyrogram/methods/stories/delete_stories.py b/pyrogram/methods/stories/delete_stories.py index f02696c21..87c29c155 100644 --- a/pyrogram/methods/stories/delete_stories.py +++ b/pyrogram/methods/stories/delete_stories.py @@ -31,7 +31,7 @@ async def delete_stories( ) -> List[int]: """Delete stories. - .. include:: /_includes/usable-by/users-bots.rst + .. include:: /_includes/usable-by/users.rst Parameters: chat_id (``int`` | ``str``): @@ -39,7 +39,7 @@ async def delete_stories( 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). - story_ids (``int`` | ``list``): + story_ids (``int`` | Iterable of ``int``, *optional*): Unique identifier (int) or list of unique identifiers (list of int) for the target stories. Returns: @@ -64,4 +64,4 @@ async def delete_stories( ) ) - return types.List(r) \ No newline at end of file + return types.List(r) diff --git a/pyrogram/methods/stories/edit_story_caption.py b/pyrogram/methods/stories/edit_story_caption.py new file mode 100644 index 000000000..211ca0d3c --- /dev/null +++ b/pyrogram/methods/stories/edit_story_caption.py @@ -0,0 +1,83 @@ +# 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, Union + +import pyrogram +from pyrogram import enums, raw, types, utils + +class EditStoryCaption: + async def edit_story_caption( + self: "pyrogram.Client", + chat_id: Union[int, str], + story_id: int, + caption: str, + parse_mode: "enums.ParseMode" = None, + caption_entities: List["types.MessageEntity"] = None, + ) -> "types.Story": + """Edit the caption of story. + + .. include:: /_includes/usable-by/users.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". + + story_id (``int``): + Story identifier in the chat specified in chat_id. + + caption (``str``): + New caption of the story, 0-1024 characters. + + parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): + By default, texts are parsed using both Markdown and HTML styles. + You can combine both syntaxes together. + + caption_entities (List of :obj:`~pyrogram.types.MessageEntity`): + List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + + Returns: + :obj:`~pyrogram.types.Story`: On success, the edited story is returned. + + Example: + .. code-block:: python + + await app.edit_story(chat_id, story_id, "new media caption") + """ + + message, entities = (await utils.parse_text_entities(self, caption, parse_mode, caption_entities)).values() + + r = await self.invoke( + raw.functions.stories.EditStory( + peer=await self.resolve_peer(chat_id), + id=story_id, + caption=message, + entities=entities, + ) + ) + + for i in r.updates: + if isinstance(i, raw.types.UpdateStory): + return await types.Story._parse( + self, + i.story, + {i.id: i for i in r.users}, + {i.id: i for i in r.chats}, + i.peer + ) diff --git a/pyrogram/methods/stories/edit_story.py b/pyrogram/methods/stories/edit_story_media.py similarity index 55% rename from pyrogram/methods/stories/edit_story.py rename to pyrogram/methods/stories/edit_story_media.py index df10e6613..70a0f301a 100644 --- a/pyrogram/methods/stories/edit_story.py +++ b/pyrogram/methods/stories/edit_story_media.py @@ -17,56 +17,45 @@ # along with Pyrogram. If not, see . import os -import re -from typing import List, Union, BinaryIO, Callable +from typing import Union, BinaryIO, Callable import pyrogram -from pyrogram import enums, raw, types, utils, StopTransmission +from pyrogram import raw, types, utils, StopTransmission from pyrogram.errors import FilePartMissing -class EditStory: - async def edit_story( +class EditStoryMedia: + async def edit_story_media( self: "pyrogram.Client", chat_id: Union[int, str], story_id: int, media: Union[str, BinaryIO] = None, - caption: str = None, duration: int = 0, width: int = 0, height: int = 0, thumb: Union[str, BinaryIO] = None, supports_streaming: bool = True, file_name: str = None, - privacy: "enums.StoriesPrivacyRules" = None, - allowed_users: List[Union[int, str]] = None, - disallowed_users: List[Union[int, str]] = None, - parse_mode: "enums.ParseMode" = None, - caption_entities: List["types.MessageEntity"] = None, progress: Callable = None, progress_args: tuple = () ) -> "types.Story": - """Edit story. + """Edit story media. .. include:: /_includes/usable-by/users.rst - Note: You must pass one of following paramater *animation*, *photo*, *video* - 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). + + story_id (``int``): + Story identifier in the chat specified in chat_id. media (``str`` | ``BinaryIO``, *optional*): Video or photo to send. Pass a file_id as string to send a animation that exists on the Telegram servers, - pass an HTTP URL as a string for Telegram to get a animation from the Internet, pass a file path as string to upload a new animation that exists on your local machine, or pass a binary file-like object with its attribute ".name" set for in-memory uploads. - caption (``str``, *optional*): - Story caption, 0-1024 characters. - duration (``int``, *optional*): Duration of sent video in seconds. @@ -82,28 +71,6 @@ async def edit_story( A thumbnail's width and height should not exceed 320 pixels. Thumbnails can't be reused and can be only uploaded as a new file. - privacy (:obj:`~pyrogram.enums.StoriesPrivacyRules`, *optional*): - Story privacy. - Defaults to :obj:`~pyrogram.enums.StoriesPrivacyRules.PUBLIC` - - allowed_users (List of ``int``, *optional*): - List of user_id or chat_id of chat users who are allowed to view stories. - Note: chat_id available only with :obj:`~pyrogram.enums.StoriesPrivacyRules.SELECTED_USERS`. - Works with :obj:`~pyrogram.enums.StoriesPrivacyRules.CLOSE_FRIENDS` - and :obj:`~pyrogram.enums.StoriesPrivacyRules.SELECTED_USERS` only - - disallowed_users (List of ``int``, *optional*): - List of user_id whos disallow to view the stories. - Note: Works with :obj:`~pyrogram.enums.StoriesPrivacyRules.PUBLIC` - and :obj:`~pyrogram.enums.StoriesPrivacyRules.CONTACTS` only - - parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): - By default, texts are parsed using both Markdown and HTML styles. - You can combine both syntaxes together. - - caption_entities (List of :obj:`~pyrogram.types.MessageEntity`): - List of special entities that appear in the caption, which can be specified instead of *parse_mode*. - 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 @@ -116,25 +83,19 @@ async def edit_story( object or a Client instance in order to edit the message with the updated progress status. Returns: - :obj:`~pyrogram.types.Story` a single story is returned. + :obj:`~pyrogram.types.Story`: On success, the edited story is returned. Example: .. code-block:: python - # Send new photo story - photo_id = "abcd12345" - await app.edit_story(meida=photo_id, caption='Hello guys.') + # Replace the current media with a local photo + await app.edit_story_media(chat_id, story_id, "new_photo.jpg") - Raises: - ValueError: In case of invalid arguments. + # Replace the current media with a local video + await app.edit_story_media(chat_id, story_id, "new_video.mp4") """ # TODO: media_areas - if privacy: - privacy_rules = [types.StoriesPrivacyRules(type=privacy)] - - message, entities = (await utils.parse_text_entities(self, caption, parse_mode, caption_entities)).values() - try: if isinstance(media, str): if os.path.isfile(media): @@ -159,16 +120,6 @@ async def edit_story( media = raw.types.InputMediaUploadedPhoto( file=file, ) - elif re.match("^https?://", media): - mime_type = self.guess_mime_type(media) - if mime_type == "video/mp4": - media = raw.types.InputMediaDocumentExternal( - url=media, - ) - else: - media = raw.types.InputMediaPhotoExternal( - url=media, - ) else: media = utils.get_input_media_from_file_id(media) else: @@ -195,42 +146,6 @@ async def edit_story( file=file, ) - privacy_rules = [] - - if privacy: - if privacy == enums.StoriesPrivacyRules.PUBLIC: - privacy_rules.append(raw.types.InputPrivacyValueAllowAll()) - if disallowed_users: - users = [await self.resolve_peer(user_id) for user_id in disallowed_users] - privacy_rules.append(raw.types.InputPrivacyValueDisallowUsers(users=users)) - elif privacy == enums.StoriesPrivacyRules.CONTACTS: - privacy_rules = [raw.types.InputPrivacyValueAllowContacts()] - if disallowed_users: - users = [await self.resolve_peer(user_id) for user_id in disallowed_users] - privacy_rules.append(raw.types.InputPrivacyValueDisallowUsers(users=users)) - elif privacy == enums.StoriesPrivacyRules.CLOSE_FRIENDS: - privacy_rules = [raw.types.InputPrivacyValueAllowCloseFriends()] - if allowed_users: - users = [await self.resolve_peer(user_id) for user_id in allowed_users] - privacy_rules.append(raw.types.InputPrivacyValueAllowUsers(users=users)) - elif privacy == enums.StoriesPrivacyRules.SELECTED_USERS: - _allowed_users = [] - _allowed_chats = [] - - for user in allowed_users: - peer = await self.resolve_peer(user) - if isinstance(peer, raw.types.InputPeerUser): - _allowed_users.append(peer) - elif isinstance(peer, raw.types.InputPeerChat): - _allowed_chats.append(peer) - - if _allowed_users: - privacy_rules.append(raw.types.InputPrivacyValueAllowUsers(users=_allowed_users)) - if _allowed_chats: - privacy_rules.append(raw.types.InputPrivacyValueAllowChatParticipants(chats=_allowed_chats)) - else: - privacy_rules.append(raw.types.InputPrivacyValueAllowAll()) - while True: try: r = await self.invoke( @@ -238,9 +153,6 @@ async def edit_story( peer=await self.resolve_peer(chat_id), id=story_id, media=media, - caption=message, - entities=entities, - privacy_rules=privacy_rules, ) ) except FilePartMissing as e: diff --git a/pyrogram/methods/stories/edit_story_privacy.py b/pyrogram/methods/stories/edit_story_privacy.py new file mode 100644 index 000000000..a57adc50a --- /dev/null +++ b/pyrogram/methods/stories/edit_story_privacy.py @@ -0,0 +1,130 @@ +# 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, Union + +import pyrogram +from pyrogram import enums, raw, types + +class EditStoryPrivacy: + async def edit_story_privacy( + self: "pyrogram.Client", + chat_id: Union[int, str], + story_id: int, + privacy: "enums.StoriesPrivacyRules" = None, + allowed_users: List[Union[int, str]] = None, + disallowed_users: List[Union[int, str]] = None, + ) -> "types.Story": + """Edit the privacy of story. + + .. include:: /_includes/usable-by/users.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). + + story_id (``int``): + Story identifier in the chat specified in chat_id. + + privacy (:obj:`~pyrogram.enums.StoriesPrivacyRules`, *optional*): + Story privacy. + + allowed_users (List of ``int`` | ``str``, *optional*): + List of user_id or chat_id of chat users who are allowed to view stories. + Note: chat_id available only with :obj:`~pyrogram.enums.StoriesPrivacyRules.SELECTED_USERS`. + Works with :obj:`~pyrogram.enums.StoriesPrivacyRules.CLOSE_FRIENDS` + and :obj:`~pyrogram.enums.StoriesPrivacyRules.SELECTED_USERS` only + + disallowed_users (List of ``int`` | ``str``, *optional*): + List of user_id whos disallow to view the stories. + Note: Works with :obj:`~pyrogram.enums.StoriesPrivacyRules.PUBLIC` + and :obj:`~pyrogram.enums.StoriesPrivacyRules.CONTACTS` only + + Returns: + :obj:`~pyrogram.types.Story`: On success, the edited story is returned. + + Example: + .. code-block:: python + + # Edit story privacy to public + await app.edit_story_privacy(chat_id, story_id, enums.StoriesPrivacyRules.PUBLIC) + + # Edit the privacy of the story to allow selected users to view the story + await app.edit_story_privacy( + chat_id, + story_id, + enums.StoriesPrivacyRules.SELECTED_USERS, + allowed_users=[123, 456] + ) + """ + privacy_rules = [] + + if privacy: + if privacy == enums.StoriesPrivacyRules.PUBLIC: + privacy_rules.append(raw.types.InputPrivacyValueAllowAll()) + if disallowed_users: + users = [await self.resolve_peer(user_id) for user_id in disallowed_users] + privacy_rules.append(raw.types.InputPrivacyValueDisallowUsers(users=users)) + elif privacy == enums.StoriesPrivacyRules.CONTACTS: + privacy_rules = [raw.types.InputPrivacyValueAllowContacts()] + if disallowed_users: + users = [await self.resolve_peer(user_id) for user_id in disallowed_users] + privacy_rules.append(raw.types.InputPrivacyValueDisallowUsers(users=users)) + elif privacy == enums.StoriesPrivacyRules.CLOSE_FRIENDS: + privacy_rules = [raw.types.InputPrivacyValueAllowCloseFriends()] + if allowed_users: + users = [await self.resolve_peer(user_id) for user_id in allowed_users] + privacy_rules.append(raw.types.InputPrivacyValueAllowUsers(users=users)) + elif privacy == enums.StoriesPrivacyRules.SELECTED_USERS: + _allowed_users = [] + _allowed_chats = [] + + for user in allowed_users: + peer = await self.resolve_peer(user) + if isinstance(peer, raw.types.InputPeerUser): + _allowed_users.append(peer) + elif isinstance(peer, (raw.types.InputPeerChat, raw.types.InputPeerChannel)): + _allowed_chats.append(peer) + + if _allowed_users: + privacy_rules.append(raw.types.InputPrivacyValueAllowUsers(users=_allowed_users)) + if _allowed_chats: + privacy_rules.append(raw.types.InputPrivacyValueAllowChatParticipants(chats=_allowed_chats)) + else: + privacy_rules.append(raw.types.InputPrivacyValueAllowAll()) + + + r = await self.invoke( + raw.functions.stories.EditStory( + peer=await self.resolve_peer(chat_id), + id=story_id, + privacy_rules=privacy_rules, + ) + ) + + for i in r.updates: + if isinstance(i, raw.types.UpdateStory): + return await types.Story._parse( + self, + i.story, + {i.id: i for i in r.users}, + {i.id: i for i in r.chats}, + i.peer + ) diff --git a/pyrogram/methods/stories/export_story_link.py b/pyrogram/methods/stories/export_story_link.py index ee4f4d3c5..c7e48e81c 100644 --- a/pyrogram/methods/stories/export_story_link.py +++ b/pyrogram/methods/stories/export_story_link.py @@ -20,7 +20,6 @@ import pyrogram from pyrogram import raw -from pyrogram import types class ExportStoryLink: @@ -28,10 +27,10 @@ async def export_story_link( self: "pyrogram.Client", chat_id: Union[int, str], story_id: int, - ) -> "types.ExportedStoryLink": + ) -> str: """Export a story link. - .. include:: /_includes/usable-by/users-bots.rst + .. include:: /_includes/usable-by/users.rst Parameters: chat_id (``int`` | ``str``): @@ -58,4 +57,4 @@ async def export_story_link( ) ) - return r.link \ No newline at end of file + return r.link diff --git a/pyrogram/methods/stories/forward_story.py b/pyrogram/methods/stories/forward_story.py index e2c3bd844..9bd6358c5 100644 --- a/pyrogram/methods/stories/forward_story.py +++ b/pyrogram/methods/stories/forward_story.py @@ -34,5 +34,68 @@ async def forward_story( message_thread_id: int = None, schedule_date: datetime = None, ) -> Optional["types.Message"]: - #TODO - pass \ No newline at end of file + """Send story. + + .. include:: /_includes/usable-by/users.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 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). + + story_id (``int``): + Unique identifier of story. + + disable_notification (``bool``, *optional*): + Sends the message with story silently. + Users will receive a notification with no sound. + + message_thread_id (``int``, *optional*): + Unique identifier for the target message thread (topic) of the forum. + For supergroups only. + + schedule_date (:py:obj:`~datetime.datetime`, *optional*): + Date when the message will be automatically sent. + + Returns: + :obj:`~pyrogram.types.Message`: On success, the sent story message is returned. + + Example: + .. code-block:: python + + # Send your story to chat_id + await app.forward_story(to_chat, from_chat, 123) + """ + r = await self.invoke( + raw.functions.messages.SendMedia( + peer=await self.resolve_peer(chat_id), + media=raw.types.InputMediaStory( + peer=await self.resolve_peer(from_chat_id), + id=story_id + ), + silent=disable_notification or None, + random_id=self.rnd_id(), + schedule_date=utils.datetime_to_timestamp(schedule_date), + message="", + reply_to=utils.get_reply_to( + message_thread_id=message_thread_id + ), + ) + ) + + for i in r.updates: + if isinstance(i, (raw.types.UpdateNewMessage, + raw.types.UpdateNewChannelMessage, + raw.types.UpdateNewScheduledMessage)): + return await types.Message._parse( + self, i.message, + {i.id: i for i in r.users}, + {i.id: i for i in r.chats}, + is_scheduled=isinstance(i, raw.types.UpdateNewScheduledMessage) + ) diff --git a/pyrogram/methods/stories/get_all_stories.py b/pyrogram/methods/stories/get_all_stories.py index ef0456502..20c1b00e1 100644 --- a/pyrogram/methods/stories/get_all_stories.py +++ b/pyrogram/methods/stories/get_all_stories.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from typing import AsyncGenerator, Union, Optional +from typing import AsyncGenerator, Optional import pyrogram from pyrogram import raw @@ -29,7 +29,7 @@ async def get_all_stories( next: Optional[bool] = None, hidden: Optional[bool] = None, state: Optional[str] = None, - ) -> Optional[AsyncGenerator["types.Story", None]]: + ) -> AsyncGenerator["types.Story", None]: """Get all active stories. .. include:: /_includes/usable-by/users.rst @@ -56,12 +56,15 @@ async def get_all_stories( ) ) + users = {i.id: i for i in r.users} + chats = {i.id: i for i in r.chats} + for peer_story in r.peer_stories: for story in peer_story.stories: yield await types.Story._parse( self, story, - {i.id: i for i in r.users}, - {i.id: i for i in r.chats}, + users, + chats, peer_story.peer - ) \ No newline at end of file + ) diff --git a/pyrogram/methods/stories/get_peer_stories.py b/pyrogram/methods/stories/get_chat_stories.py similarity index 79% rename from pyrogram/methods/stories/get_peer_stories.py rename to pyrogram/methods/stories/get_chat_stories.py index 3640eda78..1ea5d89c3 100644 --- a/pyrogram/methods/stories/get_peer_stories.py +++ b/pyrogram/methods/stories/get_chat_stories.py @@ -16,19 +16,19 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from typing import AsyncGenerator, Union, Optional +from typing import AsyncGenerator, Union import pyrogram from pyrogram import raw from pyrogram import types -class GetPeerStories: - async def get_peer_stories( +class GetChatStories: + async def get_chat_stories( self: "pyrogram.Client", chat_id: Union[int, str] - ) -> Optional[AsyncGenerator["types.Story", None]]: - """Get all active stories from an user by using user identifiers. + ) -> AsyncGenerator["types.Story", None]: + """Get all non expired stories from a chat by using chat identifier. .. include:: /_includes/usable-by/users.rst @@ -44,8 +44,8 @@ async def get_peer_stories( Example: .. code-block:: python - # Get all active story from spesific user - async for story in app.get_peer_stories(chat_id): + # Get all non expired stories from spesific chat + async for story in app.get_chat_stories(chat_id): print(story) Raises: @@ -59,11 +59,14 @@ async def get_peer_stories( ) ) + users = {i.id: i for i in r.users} + chats = {i.id: i for i in r.chats} + for story in r.stories.stories: yield await types.Story._parse( self, story, - {i.id: i for i in r.users}, - {i.id: i for i in r.chats}, + users, + chats, r.stories.peer - ) \ No newline at end of file + ) diff --git a/pyrogram/methods/stories/get_pinned_stories.py b/pyrogram/methods/stories/get_pinned_stories.py index d62f31407..347bf98dd 100644 --- a/pyrogram/methods/stories/get_pinned_stories.py +++ b/pyrogram/methods/stories/get_pinned_stories.py @@ -16,11 +16,12 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from typing import AsyncGenerator, Union, Optional +from typing import AsyncGenerator, Union import pyrogram from pyrogram import raw from pyrogram import types +from pyrogram import utils class GetPinnedStories: @@ -29,8 +30,8 @@ async def get_pinned_stories( chat_id: Union[int, str], offset_id: int = 0, limit: int = 0, - ) -> Optional[AsyncGenerator["types.Story", None]]: - """Get pinned stories stories. + ) -> AsyncGenerator["types.Story", None]: + """Get all pinned stories from a chat by using chat identifier. .. include:: /_includes/usable-by/users.rst @@ -53,15 +54,16 @@ async def get_pinned_stories( .. code-block:: python # Get all pinned story - async for story in app.get_pinned_stories(): + async for story in app.get_pinned_stories(chat_id): print(story) """ current = 0 total = abs(limit) or (1 << 31) limit = min(100, total) + peer = await self.resolve_peer(chat_id) + while True: - peer = await self.resolve_peer(chat_id) r = await self.invoke( raw.functions.stories.GetPinnedStories( peer=peer, @@ -73,6 +75,15 @@ async def get_pinned_stories( if not r.stories: return + users = {i.id: i for i in r.users} + chats = {i.id: i for i in r.chats} + + if isinstance(peer, raw.types.InputPeerChannel): + peer_id = utils.get_raw_peer_id(peer) + if peer_id not in r.chats: + channel = await self.invoke(raw.functions.channels.GetChannels(id=[peer])) + chats.update({peer_id: channel.chats[0]}) + last = r.stories[-1] offset_id = last.id @@ -80,12 +91,12 @@ async def get_pinned_stories( yield await types.Story._parse( self, story, - {i.id: i for i in r.users}, - {i.id: i for i in r.chats}, + users, + chats, peer ) current += 1 if current >= total: - return \ No newline at end of file + return diff --git a/pyrogram/methods/stories/get_stories.py b/pyrogram/methods/stories/get_stories.py index 997eec044..40ae2922b 100644 --- a/pyrogram/methods/stories/get_stories.py +++ b/pyrogram/methods/stories/get_stories.py @@ -29,9 +29,9 @@ async def get_stories( chat_id: Union[int, str], story_ids: Union[int, Iterable[int]], ) -> "types.Stories": - """Get stories by id. + """Get one or more stories from a chat by using stories identifiers. - .. include:: /_includes/usable-by/users-bots.rst + .. include:: /_includes/usable-by/users.rst Parameters: chat_id (``int`` | ``str``): @@ -39,7 +39,7 @@ async def get_stories( For your personal story you can simply use "me" or "self". For a contact that exists in your Telegram address book you can use his phone number (str). - story_ids (List of ``int`` ``32-bit``): + story_ids (``int`` | Iterable of ``int``, *optional*): Pass a single story identifier or an iterable of story ids (as integers) to get the content of the story themselves. @@ -51,7 +51,7 @@ async def get_stories( .. code-block:: python # Get stories by id - stories = await app.get_stories_by_id(chat_id, [1, 2, 3]) + stories = await app.get_stories(chat_id, [1, 2, 3]) for story in stories: print(story) @@ -69,15 +69,18 @@ async def get_stories( stories = [] + users = {i.id: i for i in r.users} + chats = {i.id: i for i in r.chats} + for story in r.stories: stories.append( await types.Story._parse( self, story, - {i.id: i for i in r.users}, - {i.id: i for i in r.chats}, + users, + chats, peer ) ) - return types.List(stories) if is_iterable else stories[0] \ No newline at end of file + return types.List(stories) if is_iterable else stories[0] if stories else None diff --git a/pyrogram/methods/stories/get_stories_archive.py b/pyrogram/methods/stories/get_stories_archive.py index df0bedf8c..49c70ca31 100644 --- a/pyrogram/methods/stories/get_stories_archive.py +++ b/pyrogram/methods/stories/get_stories_archive.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from typing import AsyncGenerator, Union, Optional +from typing import AsyncGenerator, Union import pyrogram from pyrogram import raw @@ -29,8 +29,8 @@ async def get_stories_archive( chat_id: Union[int, str], limit: int = 0, offset_id: int = 0 - ) -> Optional[AsyncGenerator["types.Story", None]]: - """Get stories archive. + ) -> AsyncGenerator["types.Story", None]: + """Get all archived stories from a chat by using chat identifier. .. include:: /_includes/usable-by/users.rst @@ -77,16 +77,19 @@ async def get_stories_archive( last = r.stories[-1] offset_id = last.id + users = {i.id: i for i in r.users} + chats = {i.id: i for i in r.chats} + for story in r.stories: yield await types.Story._parse( self, story, - {i.id: i for i in r.users}, - {i.id: i for i in r.chats}, + users, + chats, peer ) current += 1 if current >= total: - return \ No newline at end of file + return diff --git a/pyrogram/methods/stories/hide_stories.py b/pyrogram/methods/stories/hide_stories.py index 1664b9525..e8ec242b4 100644 --- a/pyrogram/methods/stories/hide_stories.py +++ b/pyrogram/methods/stories/hide_stories.py @@ -30,7 +30,7 @@ async def hide_stories( ) -> bool: """Toggle peer stories hidden - .. include:: /_includes/usable-by/users-bots.rst + .. include:: /_includes/usable-by/users.rst Parameters: chat_id (``int`` | ``str``): @@ -54,4 +54,4 @@ async def hide_stories( ) ) - return r \ No newline at end of file + return r diff --git a/pyrogram/methods/stories/increment_story_views.py b/pyrogram/methods/stories/increment_story_views.py index 345e0f136..c0fa937c7 100644 --- a/pyrogram/methods/stories/increment_story_views.py +++ b/pyrogram/methods/stories/increment_story_views.py @@ -30,12 +30,11 @@ async def increment_story_views( ) -> bool: """Increment story views. - .. include:: /_includes/usable-by/users-bots.rst + .. include:: /_includes/usable-by/users.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). story_id (``int``): @@ -57,4 +56,4 @@ async def increment_story_views( ) ) - return r \ No newline at end of file + return r diff --git a/pyrogram/methods/stories/pin_stories.py b/pyrogram/methods/stories/pin_stories.py index 3b1dbf6d5..530352c82 100644 --- a/pyrogram/methods/stories/pin_stories.py +++ b/pyrogram/methods/stories/pin_stories.py @@ -30,16 +30,16 @@ async def pin_stories( stories_ids: Union[int, Iterable[int]], pinned: bool = False, ) -> List[int]: - """Toggle stories pinned. + """Pin one or more stories in a chat by using stories identifiers. - .. include:: /_includes/usable-by/users-bots.rst + .. include:: /_includes/usable-by/users.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". - stories_ids (List of ``int`` ``32-bit``): + stories_ids (``int`` | Iterable of ``int``, *optional*): List of unique identifiers of the target stories. pinned (``bool``): @@ -66,4 +66,4 @@ async def pin_stories( ) ) - return types.List(r) \ No newline at end of file + return types.List(r) diff --git a/pyrogram/methods/stories/read_stories.py b/pyrogram/methods/stories/read_stories.py index c7686d30f..2ee52b581 100644 --- a/pyrogram/methods/stories/read_stories.py +++ b/pyrogram/methods/stories/read_stories.py @@ -30,16 +30,16 @@ async def read_stories( ) -> List[int]: """Read stories. - .. include:: /_includes/usable-by/users-bots.rst + .. include:: /_includes/usable-by/users.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). max_id (``int``, *optional*): - Maximum identifier of the target story to read. + The id of the last story you want to mark as read; all the stories before this one will be marked as + read as well. Defaults to 0 (mark every unread message as read). Returns: List of ``int``: On success, a list of read stories is returned. @@ -47,14 +47,17 @@ async def read_stories( Example: .. code-block:: python - # Read stories + # Read all stories await app.read_stories(chat_id) + + # Mark stories as read only up to the given story id + await app.read_stories(chat_id, 123) """ r = await self.invoke( raw.functions.stories.ReadStories( peer=await self.resolve_peer(chat_id), - max_id=max_id + max_id=max_id or (1 << 31) - 1 ) ) - return types.List(r) \ No newline at end of file + return types.List(r) diff --git a/pyrogram/methods/stories/send_story.py b/pyrogram/methods/stories/send_story.py index 28df3728b..f82440428 100644 --- a/pyrogram/methods/stories/send_story.py +++ b/pyrogram/methods/stories/send_story.py @@ -27,8 +27,8 @@ class SendStory: async def send_story( self: "pyrogram.Client", + chat_id: Union[int, str], media: Union[str, BinaryIO], - chat_id: Union[int, str]="me", caption: str = None, period: int = None, duration: int = 0, @@ -52,7 +52,7 @@ async def send_story( .. include:: /_includes/usable-by/users.rst - Note: You must pass one of following paramater *animation*, *photo*, *video* + Note: You must pass one of following parameters *animation*, *photo*, *video* Parameters: chat_id (``int`` | ``str``): @@ -99,7 +99,7 @@ async def send_story( and :obj:`~pyrogram.enums.StoriesPrivacyRules.SELECTED_USERS` only disallowed_users (List of ``int``, *optional*): - List of user_id whos disallow to view the stories. + List of user_id who are disallowed to view the stories. Note: Works with :obj:`~pyrogram.enums.StoriesPrivacyRules.PUBLIC` and :obj:`~pyrogram.enums.StoriesPrivacyRules.CONTACTS` only @@ -135,8 +135,11 @@ async def send_story( Example: .. code-block:: python - # Send new story - await app.send_story(media=file_id, caption='Hello guys.') + # Post story to your profile + await app.send_story("me", "story.png", caption='My new story!') + + # Post story to channel + await app.send_story(123456, "story.png", caption='My new story!') Raises: ValueError: In case of invalid arguments. @@ -240,7 +243,7 @@ async def send_story( peer = await self.resolve_peer(user) if isinstance(peer, raw.types.InputPeerUser): _allowed_users.append(peer) - elif isinstance(peer, raw.types.InputPeerChat): + elif isinstance(peer, (raw.types.InputPeerChat, raw.types.InputPeerChannel)): _allowed_chats.append(peer) if _allowed_users: @@ -279,4 +282,4 @@ async def send_story( i.peer ) except StopTransmission: - return None \ No newline at end of file + return None diff --git a/pyrogram/methods/users/__init__.py b/pyrogram/methods/users/__init__.py index 31fda1dc3..b1b8bc71a 100644 --- a/pyrogram/methods/users/__init__.py +++ b/pyrogram/methods/users/__init__.py @@ -29,6 +29,7 @@ from .set_username import SetUsername from .unblock_user import UnblockUser from .update_profile import UpdateProfile +from .update_status import UpdateStatus class Users( @@ -43,6 +44,7 @@ class Users( GetChatPhotosCount, UnblockUser, UpdateProfile, + UpdateStatus, GetDefaultEmojiStatuses, SetEmojiStatus ): diff --git a/pyrogram/methods/users/get_chat_photos.py b/pyrogram/methods/users/get_chat_photos.py index d22c68dcd..05dfd92d9 100644 --- a/pyrogram/methods/users/get_chat_photos.py +++ b/pyrogram/methods/users/get_chat_photos.py @@ -27,7 +27,7 @@ async def get_chat_photos( self: "pyrogram.Client", chat_id: Union[int, str], limit: int = 0, - ) -> Optional[AsyncGenerator["types.Photo", None]]: + ) -> AsyncGenerator["types.Photo", None]: """Get a chat or a user profile photos sequentially. .. include:: /_includes/usable-by/users-bots.rst diff --git a/pyrogram/methods/users/update_status.py b/pyrogram/methods/users/update_status.py new file mode 100644 index 000000000..2119f3f54 --- /dev/null +++ b/pyrogram/methods/users/update_status.py @@ -0,0 +1,54 @@ +# 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 . + +import pyrogram +from pyrogram import raw + + +class UpdateStatus: + async def update_status( + self: "pyrogram.Client", + offline: bool = False + ) -> bool: + """Update your profile status. + + .. include:: /_includes/usable-by/users.rst + + Parameters: + offline (``bool``): + The new status. Pass True to appear offline. + + Returns: + ``bool``: True on success. + + Example: + .. code-block:: python + + # Change status to online + await app.update_status() + + # Change status to offline + await app.update_status(offline=True) + """ + r = await self.invoke( + raw.functions.account.UpdateStatus( + offline=offline + ) + ) + + return bool(r) diff --git a/pyrogram/session/session.py b/pyrogram/session/session.py index 0ed967a1a..c140fa8b3 100644 --- a/pyrogram/session/session.py +++ b/pyrogram/session/session.py @@ -19,6 +19,7 @@ import asyncio import bisect import logging +import datetime import os from hashlib import sha1 from io import BytesIO @@ -59,6 +60,9 @@ class Session: 444: "invalid DC" } + # last time used date of this session. + last_used_time: datetime.datetime = None + def __init__( self, client: "pyrogram.Client", diff --git a/pyrogram/storage/file_storage.py b/pyrogram/storage/file_storage.py index aebe91767..1c77688c5 100644 --- a/pyrogram/storage/file_storage.py +++ b/pyrogram/storage/file_storage.py @@ -25,6 +25,17 @@ log = logging.getLogger(__name__) +SCHEMA = """ +CREATE TABLE usernames +( + id INTEGER, + username TEXT, + FOREIGN KEY (id) REFERENCES peers(id) +); + +CREATE INDEX idx_usernames_username ON usernames (username); +""" + class FileStorage(SQLiteStorage): FILE_EXTENSION = ".session" @@ -49,6 +60,12 @@ def update(self): version += 1 + if version == 3: + with self.conn: + self.conn.executescript(SCHEMA) + + version += 1 + self.version(version) async def open(self): diff --git a/pyrogram/storage/sqlite_storage.py b/pyrogram/storage/sqlite_storage.py index e28b9b746..d249d765a 100644 --- a/pyrogram/storage/sqlite_storage.py +++ b/pyrogram/storage/sqlite_storage.py @@ -43,19 +43,25 @@ id INTEGER PRIMARY KEY, access_hash INTEGER, type INTEGER NOT NULL, - username TEXT, phone_number TEXT, last_update_on INTEGER NOT NULL DEFAULT (CAST(STRFTIME('%s', 'now') AS INTEGER)) ); +CREATE TABLE usernames +( + id INTEGER, + username TEXT, + FOREIGN KEY (id) REFERENCES peers(id) +); + CREATE TABLE version ( number INTEGER PRIMARY KEY ); CREATE INDEX idx_peers_id ON peers (id); -CREATE INDEX idx_peers_username ON peers (username); CREATE INDEX idx_peers_phone_number ON peers (phone_number); +CREATE INDEX idx_usernames_username ON usernames (username); CREATE TRIGGER trg_peers_last_update_on AFTER UPDATE @@ -90,7 +96,7 @@ def get_input_peer(peer_id: int, access_hash: int, peer_type: str): class SQLiteStorage(Storage): - VERSION = 3 + VERSION = 4 USERNAME_TTL = 8 * 60 * 60 def __init__(self, name: str): @@ -125,12 +131,25 @@ async def close(self): async def delete(self): raise NotImplementedError - async def update_peers(self, peers: List[Tuple[int, int, str, str, str]]): - self.conn.executemany( - "REPLACE INTO peers (id, access_hash, type, username, phone_number)" - "VALUES (?, ?, ?, ?, ?)", - peers - ) + async def update_peers(self, peers: List[Tuple[int, int, str, List[str], str]]): + for peer_data in peers: + id, access_hash, type, usernames, phone_number = peer_data + + self.conn.execute( + "REPLACE INTO peers (id, access_hash, type, phone_number)" + "VALUES (?, ?, ?, ?)", + (id, access_hash, type, phone_number) + ) + + self.conn.execute( + "DELETE FROM usernames WHERE id = ?", + (id,) + ) + + self.conn.executemany( + "REPLACE INTO usernames (id, username) VALUES (?, ?)", + [(id, username) for username in usernames] if usernames else [(id, None)] + ) async def get_peer_by_id(self, peer_id: int): r = self.conn.execute( @@ -145,8 +164,10 @@ async def get_peer_by_id(self, peer_id: int): async def get_peer_by_username(self, username: str): r = self.conn.execute( - "SELECT id, access_hash, type, last_update_on FROM peers WHERE username = ?" - "ORDER BY last_update_on DESC", + "SELECT p.id, p.access_hash, p.type, p.last_update_on FROM peers p " + "JOIN usernames u ON p.id = u.id " + "WHERE u.username = ? " + "ORDER BY p.last_update_on DESC", (username,) ).fetchone() diff --git a/pyrogram/storage/storage.py b/pyrogram/storage/storage.py index 0689b6826..6af890d02 100644 --- a/pyrogram/storage/storage.py +++ b/pyrogram/storage/storage.py @@ -33,51 +33,133 @@ def __init__(self, name: str): self.name = name async def open(self): + """Opens the storage engine.""" raise NotImplementedError async def save(self): + """Saves the current state of the storage engine.""" raise NotImplementedError async def close(self): + """Closes the storage engine.""" raise NotImplementedError async def delete(self): + """Deletes the storage.""" raise NotImplementedError - async def update_peers(self, peers: List[Tuple[int, int, str, str, str]]): + async def update_peers(self, peers: List[Tuple[int, int, str, List[str], str]]): + """ + Update the peers table with the provided information. + + Parameters: + peers (``List[Tuple[int, int, str, List[str], str]]``): A list of tuples containing the + information of the peers to be updated. Each tuple must contain the following + information: + - ``int``: The peer id. + - ``int``: The peer access hash. + - ``str``: The peer type (user, chat or channel). + - List of ``str``: The peer username (if any). + - ``str``: The peer phone number (if any). + """ raise NotImplementedError async def get_peer_by_id(self, peer_id: int): + """Retrieve a peer by its ID. + + Parameters: + peer_id (``int``): + The ID of the peer to retrieve. + """ raise NotImplementedError async def get_peer_by_username(self, username: str): + """Retrieve a peer by its username. + + Parameters: + username (``str``): + The username of the peer to retrieve. + """ raise NotImplementedError async def get_peer_by_phone_number(self, phone_number: str): + """Retrieve a peer by its phone number. + + Parameters: + phone_number (``str``): + The phone number of the peer to retrieve. + """ raise NotImplementedError async def dc_id(self, value: int = object): + """Get or set the DC ID of the current session. + + Parameters: + value (``int``, *optional*): + The DC ID to set. + """ raise NotImplementedError async def api_id(self, value: int = object): + """Get or set the API ID of the current session. + + Parameters: + value (``int``, *optional*): + The API ID to set. + """ raise NotImplementedError async def test_mode(self, value: bool = object): + """Get or set the test mode of the current session. + + Parameters: + value (``bool``, *optional*): + The test mode to set. + """ raise NotImplementedError async def auth_key(self, value: bytes = object): + """Get or set the authorization key of the current session. + + Parameters: + value (``bytes``, *optional*): + The authorization key to set. + """ raise NotImplementedError async def date(self, value: int = object): + """Get or set the date of the current session. + + Parameters: + value (``int``, *optional*): + The date to set. + """ raise NotImplementedError async def user_id(self, value: int = object): + """Get or set the user ID of the current session. + + Parameters: + value (``int``, *optional*): + The user ID to set. + """ raise NotImplementedError async def is_bot(self, value: bool = object): + """Get or set the bot flag of the current session. + + Parameters: + value (``bool``, *optional*): + The bot flag to set. + """ raise NotImplementedError async def export_session_string(self): + """Exports the session string for the current session. + + Returns: + ``str``: The session string for the current session. + """ packed = struct.pack( self.SESSION_STRING_FORMAT, await self.dc_id(), diff --git a/pyrogram/types/bots_and_keyboards/__init__.py b/pyrogram/types/bots_and_keyboards/__init__.py index 6f05a3b48..a9dc8ea6b 100644 --- a/pyrogram/types/bots_and_keyboards/__init__.py +++ b/pyrogram/types/bots_and_keyboards/__init__.py @@ -39,6 +39,10 @@ from .menu_button_web_app import MenuButtonWebApp from .reply_keyboard_markup import ReplyKeyboardMarkup from .reply_keyboard_remove import ReplyKeyboardRemove +from .request_channel_info import RequestChannelInfo +from .request_chat_info import RequestChatInfo +from .request_user_info import RequestUserInfo +from .request_poll_info import RequestPollInfo from .sent_web_app_message import SentWebAppMessage from .web_app_info import WebAppInfo @@ -52,6 +56,10 @@ "KeyboardButton", "ReplyKeyboardMarkup", "ReplyKeyboardRemove", + "RequestChannelInfo", + "RequestChatInfo", + "RequestUserInfo", + "RequestPollInfo", "LoginUrl", "BotCommand", "BotCommandScope", diff --git a/pyrogram/types/bots_and_keyboards/request_channel_info.py b/pyrogram/types/bots_and_keyboards/request_channel_info.py new file mode 100644 index 000000000..e639c4a26 --- /dev/null +++ b/pyrogram/types/bots_and_keyboards/request_channel_info.py @@ -0,0 +1,61 @@ +# 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 pyrogram import types + +from ..object import Object + + +class RequestChannelInfo(Object): + """Contains information about a channel peer type. + + Parameters: + button_id (``int``): + Identifier of button. + + is_creator (``bool``, *optional*): + If True, returns the list of chats owned by the user. + + has_username (``bool``, *optional*): + If True, returns the list of chats with a username. + If False, returns the list of chats without a username. + If not specified, no additional restrictions are applied. + Defaults to None. + + user_privileges (:obj:`~pyrogram.types.ChatPrivileges`, *optional*): + Privileged actions that an user administrator is able to take. + + bot_privileges (:obj:`~pyrogram.types.ChatPrivileges`, *optional*): + Privileged actions that an bot administrator is able to take. + """ + + def __init__( + self, *, + button_id: int, + is_creator: bool = None, + has_username: bool = None, + user_privileges: "types.ChatPrivileges" = None, + bot_privileges: "types.ChatPrivileges" = None + ): + super().__init__() + + self.button_id = button_id + self.is_creator = is_creator + self.has_username = has_username + self.user_privileges = user_privileges + self.bot_privileges = bot_privileges \ No newline at end of file diff --git a/pyrogram/types/bots_and_keyboards/request_chat_info.py b/pyrogram/types/bots_and_keyboards/request_chat_info.py new file mode 100644 index 000000000..62f3412f6 --- /dev/null +++ b/pyrogram/types/bots_and_keyboards/request_chat_info.py @@ -0,0 +1,74 @@ +# 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 pyrogram import types + +from ..object import Object + + +class RequestChatInfo(Object): + """Contains information about a chat peer type. + + Parameters: + button_id (``int``): + Identifier of button. + + is_creator (``bool``, *optional*): + If True, returns the list of chats owned by the user. + + is_bot_participant (``bool``, *optional*): + If True, returns the list of chats with the bot as a member. + + has_username (``bool``, *optional*): + If True, returns the list of chats with a username. + If False, returns the list of chats without a username. + If not specified, no additional restrictions are applied. + Defaults to None. + + has_forum (``bool``, *optional*): + If True, returns the list of chats with a forum topics enabled. + If False, returns the list of chats without a forum topics. + If not specified, no additional restrictions are applied. + Defaults to None. + + user_privileges (:obj:`~pyrogram.types.ChatPrivileges`, *optional*): + Privileged actions that an user administrator is able to take. + + bot_privileges (:obj:`~pyrogram.types.ChatPrivileges`, *optional*): + Privileged actions that a bot administrator is able to take. + """ + + def __init__( + self, *, + button_id: int, + is_creator: bool = None, + is_bot_participant: bool = None, + has_username: bool = None, + has_forum: bool = None, + user_privileges: "types.ChatPrivileges" = None, + bot_privileges: "types.ChatPrivileges" = None + ): + super().__init__() + + self.button_id = button_id + self.is_creator = is_creator + self.is_bot_participant = is_bot_participant + self.has_username = has_username + self.has_forum = has_forum + self.user_privileges = user_privileges + self.bot_privileges = bot_privileges \ No newline at end of file diff --git a/pyrogram/types/user_and_chats/peer_user.py b/pyrogram/types/bots_and_keyboards/request_poll_info.py similarity index 73% rename from pyrogram/types/user_and_chats/peer_user.py rename to pyrogram/types/bots_and_keyboards/request_poll_info.py index 2728e7336..e800ac2dd 100644 --- a/pyrogram/types/user_and_chats/peer_user.py +++ b/pyrogram/types/bots_and_keyboards/request_poll_info.py @@ -16,29 +16,21 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from pyrogram import raw from ..object import Object -class PeerUser(Object): - """A PeerUser. - +class RequestPollInfo(Object): + """Contains information about a poll type. Parameters: - user_id (``int``): - Id of the user. + is_quiz (``bool``): + If True, the requested poll will be sent as quiz. """ def __init__( self, *, - user_id: int + is_quiz: bool = None ): super().__init__() - self.user_id = user_id - - @staticmethod - def _parse(action: "raw.types.PeerUser") -> "PeerUser": - return PeerUser( - user_id=getattr(action, "user_id", None) - ) + self.is_quiz = is_quiz \ No newline at end of file diff --git a/pyrogram/types/bots_and_keyboards/request_user_info.py b/pyrogram/types/bots_and_keyboards/request_user_info.py new file mode 100644 index 000000000..99a46f5a2 --- /dev/null +++ b/pyrogram/types/bots_and_keyboards/request_user_info.py @@ -0,0 +1,58 @@ +# 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 ..object import Object + + +class RequestUserInfo(Object): + """Contains information about a user peer type. + + Parameters: + button_id (``int``): + Identifier of button. + + is_bot (``bool``, *optional*): + If True, returns bots. + If False to returns regular users. + If not specified, no additional restrictions are applied. + Defaults to None. + + is_premium (``bool``, *optional*): + If True, returns premium users. + If False to request non-premium users. + If not specified, no additional restrictions are applied. + Defaults to None. + + max_quantity(``int``, *optional*): + The maximum number of users to be selected; 1-10. Defaults to 1. + Defaults to None (One peer only). + """ + + def __init__( + self, *, + button_id: int, + is_bot: bool = None, + is_premium: bool = None, + max_quantity: int = None, + ): + super().__init__() + + self.button_id = button_id + self.is_bot = is_bot + self.is_premium = is_premium + self.max_quantity = max_quantity or 1 \ No newline at end of file diff --git a/pyrogram/types/messages_and_media/__init__.py b/pyrogram/types/messages_and_media/__init__.py index ebba631e8..0b908cc40 100644 --- a/pyrogram/types/messages_and_media/__init__.py +++ b/pyrogram/types/messages_and_media/__init__.py @@ -18,6 +18,7 @@ from .animation import Animation from .audio import Audio +from .boosts_status import BoostsStatus from .contact import Contact from .dice import Dice from .document import Document @@ -29,6 +30,9 @@ from .general_forum_topic_hidden import GeneralTopicHidden from .general_forum_topic_unhidden import GeneralTopicUnhidden from .game import Game +from .gift_code import GiftCode +from .giveaway import Giveaway +from .giveaway_result import GiveawayResult from .location import Location from .message import Message from .message_entity import MessageEntity @@ -39,9 +43,6 @@ from .sticker import Sticker from .stripped_thumbnail import StrippedThumbnail from .story import Story -from .story_deleted import StoryDeleted -from .story_skipped import StorySkipped -from .story_views import StoryViews from .thumbnail import Thumbnail from .venue import Venue from .video import Video @@ -50,14 +51,13 @@ from .web_app_data import WebAppData from .web_page import WebPage from .message_reactions import MessageReactions -from .message_story import MessageStory from .my_boost import MyBoost __all__ = [ - "Animation", "Audio", "Contact", "Document", "ForumTopic", "ForumTopicCreated", + "Animation", "Audio", "BoostsStatus", "Contact", "Document", "ForumTopic", "ForumTopicCreated", "ForumTopicClosed", "ForumTopicReopened", "ForumTopicEdited", "GeneralTopicHidden", - "GeneralTopicUnhidden", "Game", "Location", "Message", "MessageEntity", "Photo", "Thumbnail", - "StrippedThumbnail", "Story", "StoryDeleted", "StorySkipped", "StoryViews", "Poll", "PollOption", "Sticker", - "Venue", "Video", "VideoNote", "Voice", "WebPage", "Dice", "Reaction", "WebAppData", - "MessageReactions", "MessageStory", "MyBoost" -] + "GeneralTopicUnhidden", "Game", "GiftCode", "Giveaway", "GiveawayResult", "Location", + "Message", "MessageEntity", "Photo", "Thumbnail", "StrippedThumbnail", "Story", "Poll", + "PollOption", "Sticker", "Venue", "Video", "VideoNote", "Voice", "WebPage", "Dice", "Reaction", + "WebAppData", "MessageReactions", "MyBoost" +] \ No newline at end of file diff --git a/pyrogram/types/messages_and_media/boosts_status.py b/pyrogram/types/messages_and_media/boosts_status.py new file mode 100644 index 000000000..f2e6ac43c --- /dev/null +++ b/pyrogram/types/messages_and_media/boosts_status.py @@ -0,0 +1,88 @@ +# 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 + +from pyrogram import raw, types +from ..object import Object + + +class BoostsStatus(Object): + """Contains information about boost. + + Parameters: + level (``int``): + Level of channel. + + current_level_boosts (``int``): + Number of boosts required for the current level. + + boosts (``int``): + Total number of boosts. + + boost_url (``str``): + Link that can be used to give a boost to a channel. + + my_boost (``bool``, *optional*): + True, if you boost this channel. + + gift_boosts (``int``, *optional*): + N/A + + next_level_boosts (``int``, *optional*): + Number of boosts at which the next level will be reached. + + my_boost_slots (List of ``int``, *optional*): + Boost slots that are given to the channel. + """ + + def __init__( + self, + *, + level: int, + current_level_boosts: int, + boosts: int, + boost_url: str, + my_boost: bool = None, + gift_boosts: int = None, + next_level_boosts: int = None, + my_boost_slots: List[int] = None + ): + super().__init__() + + self.level = level + self.current_level_boosts = current_level_boosts + self.boosts = boosts + self.boost_url = boost_url + self.my_boost = my_boost + self.gift_boosts = gift_boosts + self.next_level_boosts = next_level_boosts + self.my_boost_slots = my_boost_slots + + @staticmethod + def _parse(boosts_status: "raw.types.premium.BoostsStatus") -> "BoostsStatus": + return BoostsStatus( + level=boosts_status.level, + current_level_boosts=boosts_status.current_level_boosts, + boosts=boosts_status.boosts, + boost_url=boosts_status.boost_url, + my_boost=getattr(boosts_status, "my_boost", None), + gift_boosts=getattr(boosts_status, "gift_boosts", None), + next_level_boosts=getattr(boosts_status, "next_level_boosts", None), + my_boost_slots=types.List(boosts_status.my_boost_slots) or None, + ) diff --git a/pyrogram/types/messages_and_media/forum_topic.py b/pyrogram/types/messages_and_media/forum_topic.py index 9d5a80ee6..a2525c697 100644 --- a/pyrogram/types/messages_and_media/forum_topic.py +++ b/pyrogram/types/messages_and_media/forum_topic.py @@ -16,135 +16,126 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from pyrogram import raw, types -from typing import Union +import pyrogram +from pyrogram import types, raw, utils from ..object import Object class ForumTopic(Object): - # todo - # notify_settings: `~pyrogram.types.PeerNotifySettings` - # draft: `~pyrogram.types.DraftMessage` """A forum topic. Parameters: - id (``Integer``): - Id of the topic + id (``int``): + Unique topic identifier inside this chat. - date (``Integer``): - Date topic created + title (``str``): + The topic title. - title (``String``): - Name of the topic + date (``int``, *optional*): + Date when the topic was created. - icon_color (``Integer``): - Color of the topic icon in RGB format + icon_color (``str``, *optional*): + Color of the topic icon in HEX format - top_message (``Integer``): - N/A - - read_inbox_max_id (``Integer``): - N/A + icon_emoji_id (``int``, *optional*): + Unique identifier of the custom emoji shown as the topic icon - read_outbox_max_id (``Integer``): - N/A + creator (:obj:`~pyrogram.types.Chat`, *optional*): + Topic creator. - unread_count (``Integer``): - N/A + top_message (:obj:`~pyrogram.types.Message`, *optional*): + The last message sent in the topic at this time. - unread_mentions_count (``Integer``): - N/A + unread_count (``int``, *optional*): + Amount of unread messages in this topic. - unread_reactions_count (``Integer``): - N/A + unread_mentions_count (``int``, *optional*): + Amount of unread messages containing a mention in this topic. - from_id (:obj:`~pyrogram.types.PeerChannel` | :obj:`~pyrogram.types.PeerUser`): - Topic creator. + unread_reactions_count (``int``, *optional*): + Amount of unread messages containing a reaction in this topic. - my (``Boolean``, *optional*): - N/A + is_my (``bool``, *optional*): + True, if you are creator of topic. - closed (``Boolean``, *optional*): - N/A + is_closed (``bool``, *optional*): + True, if the topic is closed. - pinned (``Boolean``, *optional*): - N/A + is_pinned (``bool``, *optional*): + True, if the topic is pinned. - short (``Boolean``, *optional*): - N/A + is_short (``bool``, *optional*): + True, if the topic is short. - icon_emoji_id (``Integer``, *optional*): - Unique identifier of the custom emoji shown as the topic icon + is_hidden (``bool``, *optional*): + True, if the topic is hidden. """ def __init__( self, *, id: int, - date: int, - title: str, - icon_color: int, - top_message: int, - read_inbox_max_id: int, - read_outbox_max_id: int, - unread_count: int, - unread_mentions_count: int, - unread_reactions_count: int, - from_id: Union["types.PeerChannel", "types.PeerUser"], - # notify_settings: "types.PeerNotifySettings", //todo - my: bool = None, - closed: bool = None, - pinned: bool = None, - short: bool = None, + title: str = None, + date: int = None, + icon_color: str = None, icon_emoji_id: int = None, - # draft: "types.DraftMessage" = None //todo + creator: "types.Chat" = None, + top_message: "types.Message" = None, + unread_count: int = None, + unread_mentions_count: int = None, + unread_reactions_count: int = None, + is_my: bool = None, + is_closed: bool = None, + is_pinned: bool = None, + is_short: bool = None, + is_hidden: bool = None ): super().__init__() self.id = id - self.date = date self.title = title + self.date = date self.icon_color = icon_color + self.icon_emoji_id = icon_emoji_id + self.creator = creator self.top_message = top_message - self.read_inbox_max_id = read_inbox_max_id - self.read_outbox_max_id = read_outbox_max_id self.unread_count = unread_count self.unread_mentions_count = unread_mentions_count self.unread_reactions_count = unread_reactions_count - self.from_id = from_id - # self.notify_settings = notify_settings //todo - self.my = my - self.closed = closed - self.pinned = pinned - self.short = short - self.icon_emoji_id = icon_emoji_id - # self.draft = draft //todo + self.is_my = is_my + self.is_closed = is_closed + self.is_pinned = is_pinned + self.is_short = is_short + self.is_hidden = is_hidden @staticmethod - def _parse(forum_topic: "raw.types.forum_topic") -> "ForumTopic": - from_id = forum_topic.from_id - if isinstance(from_id, raw.types.PeerChannel): - peer = types.PeerChannel._parse(from_id) - if isinstance(from_id, raw.types.PeerUser): - peer = types.PeerUser._parse(from_id) + def _parse(client: "pyrogram.Client", forum_topic: "raw.types.ForumTopic", messages: dict = {}, users: dict = {}, chats: dict = {}) -> "ForumTopic": + creator = None + + peer = getattr(forum_topic, "from_id", None) + + if peer: + peer_id = utils.get_raw_peer_id(peer) + + if isinstance(peer, raw.types.PeerUser): + creator = types.Chat._parse_user_chat(client, users[peer_id]) + else: + creator = types.Chat._parse_channel_chat(client, chats[peer_id]) return ForumTopic( - id=getattr(forum_topic, "id", None), - date=getattr(forum_topic, "date", None), - title=getattr(forum_topic, "title", None), - icon_color=getattr(forum_topic, "icon_color", None), - top_message=getattr(forum_topic, "top_message", None), - read_inbox_max_id=getattr(forum_topic, "read_inbox_max_id", None), - read_outbox_max_id=getattr(forum_topic, "read_outbox_max_id", None), + id=forum_topic.id, + title=forum_topic.title, + date=utils.timestamp_to_datetime(forum_topic.date), + icon_color=format(forum_topic.icon_color, "x") if getattr(forum_topic, "icon_color", None) else None, + icon_emoji_id=getattr(forum_topic, "icon_emoji_id", None), + creator=creator, + top_message=messages.get(getattr(forum_topic, "top_message", None)), unread_count=getattr(forum_topic, "unread_count", None), unread_mentions_count=getattr(forum_topic, "unread_mentions_count", None), unread_reactions_count=getattr(forum_topic, "unread_reactions_count", None), - from_id=peer, - # notify_settings=None, //todo - my=getattr(forum_topic, "my", None), - closed=getattr(forum_topic, "closed", None), - pinned=getattr(forum_topic, "pinned", None), - short=getattr(forum_topic, "short", None), - icon_emoji_id=getattr(forum_topic, "icon_emoji_id", None), - # draft=None //todo + is_my=getattr(forum_topic, "my", None), + is_closed=getattr(forum_topic, "closed", None), + is_pinned=getattr(forum_topic, "pinned", None), + is_short=getattr(forum_topic, "short", None), + is_hidden=getattr(forum_topic, "hidden", None), ) diff --git a/pyrogram/types/messages_and_media/gift_code.py b/pyrogram/types/messages_and_media/gift_code.py new file mode 100644 index 000000000..70b5571dc --- /dev/null +++ b/pyrogram/types/messages_and_media/gift_code.py @@ -0,0 +1,79 @@ +# 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 pyrogram import raw, types, utils +from ..object import Object + + +class GiftCode(Object): + """Contains gift code data. + + Parameters: + via_giveaway (``bool``): + True if the gift code is received via giveaway. + + unclaimed (``bool``): + True if the gift code is unclaimed. + + boost_peer (:obj:`~pyrogram.types.Chat`): + The channel where the gift code was won. + + months (``int``): + Number of months of subscription. + + slug (``str``): + Identifier of gift code. + You can combine it with `t.me/giftcode/{slug}` + to get link for this gift. + + link (``str``, *property*): + Generate a link to this gift code. + """ + + def __init__( + self, + *, + via_giveaway: bool, + unclaimed: bool, + boost_peer, + months: int, + slug: str + ): + super().__init__() + + self.via_giveaway = via_giveaway + self.unclaimed = unclaimed + self.boost_peer = boost_peer + self.months = months + self.slug = slug + + @staticmethod + def _parse(client, giftcode: "raw.types.MessageActionGiftCode", chats): + peer = chats.get(utils.get_raw_peer_id(giftcode.boost_peer)) + + return GiftCode( + via_giveaway=giftcode.via_giveaway, + unclaimed=giftcode.unclaimed, + boost_peer=types.Chat._parse_chat(client, peer) if peer else None, + months=giftcode.months, + slug=giftcode.slug + ) + + @property + def link(self) -> str: + return f"https://t.me/giftcode/{self.slug}" \ No newline at end of file diff --git a/pyrogram/types/messages_and_media/giveaway.py b/pyrogram/types/messages_and_media/giveaway.py new file mode 100644 index 000000000..04c18c00a --- /dev/null +++ b/pyrogram/types/messages_and_media/giveaway.py @@ -0,0 +1,100 @@ +# 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 ..object import Object + + +class Giveaway(Object): + """An giveaway. + + Parameters: + chats (List of :obj:`~pyrogram.types.Chat`): + Get the list of channels you need to subscribe to. + + quantity (``int``): + Number of subscriptions. + + months (``int``): + Number of months for which a subscription is given. + + until_date (:py:obj:`~datetime.datetime`): + Date when the giveaway will end. + + description (``str``, *optional*): + Prize description. + + only_new_subscribers (``bool``, *optional*): + True, if this giveaway is for new subscribers only. + + only_for_countries (List of ``str`` , *optional*): + A list of two-letter ISO 3166-1 alpha-2 country codes indicating the countries + from which eligible users for the giveaway must come. + If None, then all users can participate in the giveaway. + Users with a phone number that was bought on Fragment can always participate in giveaways. + + winners_are_visible (``bool``, *optional*): + True, if this giveaway winners is visible. + """ + + def __init__( + self, + *, + client: "pyrogram.Client" = None, + chats: List["types.Chat"] = None, + quantity: int = None, + months: int = None, + until_date: datetime = None, + description: str = None, + only_new_subscribers: bool = None, + only_for_countries: List[str] = None, + winners_are_visible: bool = None + ): + super().__init__(client) + + self.chats = chats + self.quantity = quantity + self.months = months + self.until_date = until_date + self.description = description + self.only_new_subscribers = only_new_subscribers + self.only_for_countries = only_for_countries + self.winners_are_visible = winners_are_visible + + @staticmethod + def _parse( + client, + giveaway: "raw.types.MessageMediaGiveaway", + chats: dict + ) -> "Giveaway": + return Giveaway( + chats=types.List(types.Chat._parse_channel_chat(client, chats.get(i)) for i in giveaway.channels), + quantity=giveaway.quantity, + months=giveaway.months, + until_date=utils.timestamp_to_datetime(giveaway.until_date), + description=getattr(giveaway, "prize_description", None) or None, + only_new_subscribers=getattr(giveaway, "only_new_subscribers", None), + only_for_countries=types.List(getattr(giveaway, "countries_iso2", [])) or None, + winners_are_visible=getattr(giveaway, "winners_are_visible", None), + client=client + ) \ No newline at end of file diff --git a/pyrogram/types/messages_and_media/giveaway_result.py b/pyrogram/types/messages_and_media/giveaway_result.py new file mode 100644 index 000000000..842ad1470 --- /dev/null +++ b/pyrogram/types/messages_and_media/giveaway_result.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 errors, raw, utils +from pyrogram import types +from ..object import Object + + +class GiveawayResult(Object): + """An giveaway result. + + Parameters: + chat (List of :obj:`~pyrogram.types.Chat`): + Channel which host the giveaway. + + quantity (``int``): + Total number of subscriptions in this giveaway. + + winners_count (``int``): + Number of winners who claimed their gift. + + unclaimed_count (``int``): + Unclaimed giveaway subscriptions count. + + winners (List of :obj:`~pyrogram.types.User`): + A list of giveaway winners. + + months (``int``): + Number of months for which a subscription is given. + + until_date (:py:obj:`~datetime.datetime`): + Date when the giveaway will end. + + launch_message_id (``int``): + Identifier of the original message with the giveaway. + + launch_message (:obj:`~pyrogram.types.Message`, *optional*): + Returns the original giveaway start message. + If the channel is private, returns None + + description (``str``, *optional*): + Prize description. + + only_new_subscribers (``bool``, *optional*): + True, if this giveaway is for new subscribers only. + + is_refunded (``bool``, *optional*): + True, if this giveaway was refunded. + """ + + def __init__( + self, + *, + client: "pyrogram.Client" = None, + chat: "types.Chat", + quantity: int, + winners_count: int, + unclaimed_count: int, + winners: List["types.User"], + months: int, + until_date: datetime, + launch_message_id: int, + launch_message: "types.Message" = None, + description: str = None, + only_new_subscribers: bool = None, + is_refunded: bool = None + ): + super().__init__(client) + + self.chat = chat + self.quantity = quantity + self.winners_count = winners_count + self.unclaimed_count = unclaimed_count + self.winners = winners + self.months = months + self.until_date = until_date + self.launch_message_id = launch_message_id + self.launch_message = launch_message + self.description = description + self.only_new_subscribers = only_new_subscribers + self.is_refunded = is_refunded + + @staticmethod + async def _parse( + client, + giveaway_result: "raw.types.MessageMediaGiveawayResults", + users: dict, + chats: dict + ) -> "GiveawayResult": + launch_message = None + + try: + launch_message = await client.get_messages( + utils.get_channel_id(giveaway_result.channel_id), + giveaway_result.launch_msg_id, + replies=0 + ) + except (errors.ChannelPrivate, errors.ChannelInvalid): + pass + + return GiveawayResult( + chat=types.Chat._parse_channel_chat(client, chats[giveaway_result.channel_id]), + quantity=giveaway_result.winners_count + giveaway_result.unclaimed_count, + winners_count=giveaway_result.winners_count, + unclaimed_count=giveaway_result.unclaimed_count, + winners=types.List(types.User._parse(client, users.get(i)) for i in giveaway_result.winners) or None, + months=giveaway_result.months, + until_date=utils.timestamp_to_datetime(giveaway_result.until_date), + launch_message_id=giveaway_result.launch_msg_id, + only_new_subscribers=getattr(giveaway_result, "only_new_subscribers", None), + is_refunded=getattr(giveaway_result, "refunded", None), + launch_message=launch_message, + description=getattr(giveaway_result, "prize_description", None) or None, + client=client + ) \ No newline at end of file diff --git a/pyrogram/types/messages_and_media/message.py b/pyrogram/types/messages_and_media/message.py index 8392585e8..0591f5ba0 100644 --- a/pyrogram/types/messages_and_media/message.py +++ b/pyrogram/types/messages_and_media/message.py @@ -25,7 +25,7 @@ from pyrogram import raw, enums from pyrogram import types from pyrogram import utils -from pyrogram.errors import MessageIdsEmpty, PeerIdInvalid +from pyrogram.errors import MessageIdsEmpty, PeerIdInvalid, ChannelPrivate, BotMethodInvalid from pyrogram.parser import utils as parser_utils, Parser from ..object import Object from ..update import Update @@ -78,7 +78,7 @@ class Message(Object, Update): chat (:obj:`~pyrogram.types.Chat`, *optional*): Conversation the message belongs to. - topics (:obj:`~pyrogram.types.ForumTopic`, *optional*): + topic (:obj:`~pyrogram.types.ForumTopic`, *optional*): Topic the message belongs to. forward_from (:obj:`~pyrogram.types.User`, *optional*): @@ -99,12 +99,9 @@ class Message(Object, Update): forward_date (:py:obj:`~datetime.datetime`, *optional*): For forwarded messages, date the original message was sent. - is_topic_message (``bool``, *optional*): - True, if the message is sent to a forum topic - message_thread_id (``int``, *optional*): Unique identifier of a message thread to which the message belongs. - for supergroups only + For supergroups only. reply_to_message_id (``int``, *optional*): The id of the message which this message directly replied to. @@ -143,12 +140,17 @@ class Message(Object, Update): You can use ``media = getattr(message, message.media.value)`` to access the media message. invert_media (``bool``, *optional*): - Invert media. + If True, link preview will be shown above the message text. + Otherwise, the link preview will be shown below the message text. edit_date (:py:obj:`~datetime.datetime`, *optional*): Date the message was last edited. - media_group_id (``str``, *optional*): + edit_hidden (``bool``, *optional*): + The message shown as not modified. + A message can be not modified in case it has received a reaction. + + media_group_id (``int``, *optional*): The unique identifier of a media message group this message belongs to. author_signature (``str``, *optional*): @@ -201,7 +203,10 @@ class Message(Object, Update): game (:obj:`~pyrogram.types.Game`, *optional*): Message is a game, information about the game. - story (:obj:`~pyrogram.types.MessageStory`, *optional*): + giveaway (:obj:`~pyrogram.types.Giveaway`, *optional*): + Message is a giveaway, information about the giveaway. + + story (:obj:`~pyrogram.types.Story`, *optional*): Message is a story, information about the story. video (:obj:`~pyrogram.types.Video`, *optional*): @@ -349,6 +354,12 @@ class Message(Object, Update): web_app_data (:obj:`~pyrogram.types.WebAppData`, *optional*): Service message: web app data sent to the bot. + gift_code (:obj:`~pyrogram.types.GiftCode`, *optional*): + Service message: gift code information. + + requested_chats (List of :obj:`~pyrogram.types.Chat`, *optional*): + Service message: requested chats information. + reply_markup (:obj:`~pyrogram.types.InlineKeyboardMarkup` | :obj:`~pyrogram.types.ReplyKeyboardMarkup` | :obj:`~pyrogram.types.ReplyKeyboardRemove` | :obj:`~pyrogram.types.ForceReply`, *optional*): Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. @@ -371,14 +382,13 @@ def __init__( sender_chat: "types.Chat" = None, date: datetime = None, chat: "types.Chat" = None, - topics: "types.ForumTopic" = None, + topic: "types.ForumTopic" = None, forward_from: "types.User" = None, forward_sender_name: str = None, forward_from_chat: "types.Chat" = None, forward_from_message_id: int = None, forward_signature: str = None, forward_date: datetime = None, - is_topic_message: bool = None, message_thread_id: int = None, reply_to_message_id: int = None, reply_to_story_id: int = None, @@ -394,7 +404,8 @@ def __init__( media: "enums.MessageMediaType" = None, invert_media: bool = None, edit_date: datetime = None, - media_group_id: str = None, + edit_hidden: bool = None, + media_group_id: int = None, author_signature: str = None, has_protected_content: bool = None, has_media_spoiler: bool = None, @@ -409,7 +420,9 @@ def __init__( sticker: "types.Sticker" = None, animation: "types.Animation" = None, game: "types.Game" = None, - story: "types.MessageStory" = None, + giveaway: "types.Giveaway" = None, + giveaway_result: "types.GiveawayResult" = None, + story: "types.Story" = None, video: "types.Video" = None, voice: "types.Voice" = None, video_note: "types.VideoNote" = None, @@ -450,6 +463,9 @@ def __init__( video_chat_ended: "types.VideoChatEnded" = None, video_chat_members_invited: "types.VideoChatMembersInvited" = None, web_app_data: "types.WebAppData" = None, + gift_code: "types.GiftCode" = None, + requested_chats: List["types.Chat"] = None, + giveaway_launched: bool = None, reply_markup: Union[ "types.InlineKeyboardMarkup", "types.ReplyKeyboardMarkup", @@ -465,14 +481,13 @@ def __init__( self.sender_chat = sender_chat self.date = date self.chat = chat - self.topics = topics + self.topic = topic self.forward_from = forward_from self.forward_sender_name = forward_sender_name self.forward_from_chat = forward_from_chat self.forward_from_message_id = forward_from_message_id self.forward_signature = forward_signature self.forward_date = forward_date - self.is_topic_message = is_topic_message self.message_thread_id = message_thread_id self.reply_to_message_id = reply_to_message_id self.reply_to_story_id = reply_to_story_id @@ -488,6 +503,7 @@ def __init__( self.media = media self.invert_media = invert_media self.edit_date = edit_date + self.edit_hidden = edit_hidden self.media_group_id = media_group_id self.author_signature = author_signature self.has_protected_content = has_protected_content @@ -503,6 +519,8 @@ def __init__( self.sticker = sticker self.animation = animation self.game = game + self.giveaway = giveaway + self.giveaway_result = giveaway_result self.story = story self.video = video self.voice = voice @@ -545,6 +563,9 @@ def __init__( self.video_chat_ended = video_chat_ended self.video_chat_members_invited = video_chat_members_invited self.web_app_data = web_app_data + self.gift_code = gift_code + self.requested_chats = requested_chats + self.giveaway_launched = giveaway_launched self.reactions = reactions @staticmethod @@ -584,6 +605,7 @@ async def _parse( message_thread_id = None action = message.action + text = None new_chat_members = None left_chat_member = None new_chat_title = None @@ -593,7 +615,6 @@ async def _parse( group_chat_created = None channel_chat_created = None new_chat_photo = None - is_topic_message = None forum_topic_created = None forum_topic_closed = None forum_topic_reopened = None @@ -605,6 +626,9 @@ async def _parse( video_chat_ended = None video_chat_members_invited = None web_app_data = None + gift_code = None + giveaway_launched = None + requested_chats = None service_type = None @@ -638,6 +662,9 @@ async def _parse( elif isinstance(action, raw.types.MessageActionChatEditPhoto): new_chat_photo = types.Photo._parse(client, action.photo) service_type = enums.MessageServiceType.NEW_CHAT_PHOTO + elif isinstance(action, raw.types.MessageActionCustomAction): + text = message.action.message + service_type = enums.MessageServiceType.CUSTOM_ACTION elif isinstance(action, raw.types.MessageActionTopicCreate): forum_topic_created = types.ForumTopicCreated._parse(message) service_type = enums.MessageServiceType.FORUM_TOPIC_CREATED @@ -674,6 +701,37 @@ async def _parse( elif isinstance(action, raw.types.MessageActionWebViewDataSentMe): web_app_data = types.WebAppData._parse(action) service_type = enums.MessageServiceType.WEB_APP_DATA + elif isinstance(action, raw.types.MessageActionGiveawayLaunch): + giveaway_launched = True + service_type = enums.MessageServiceType.GIVEAWAY_LAUNCH + elif isinstance(action, raw.types.MessageActionGiftCode): + gift_code = types.GiftCode._parse(client, action, chats) + service_type = enums.MessageServiceType.GIFT_CODE + elif isinstance(action, raw.types.MessageActionRequestedPeer): + _requested_chats = [] + + for requested_peer in action.peers: + chat_id = utils.get_peer_id(requested_peer) + peer_type = utils.get_peer_type(chat_id) + + if peer_type == "user": + chat_type = enums.ChatType.PRIVATE + elif peer_type == "chat": + chat_type = enums.ChatType.GROUP + else: + chat_type = enums.ChatType.CHANNEL + + _requested_chats.append( + types.Chat( + id=chat_id, + type=chat_type, + client=client + ) + ) + + requested_chats = types.List(_requested_chats) or None + + service_type = enums.MessageServiceType.REQUESTED_CHAT from_user = types.User._parse(client, users.get(user_id, None)) sender_chat = types.Chat._parse(client, message, users, chats, is_chat=False) if not from_user else None @@ -683,10 +741,10 @@ async def _parse( message_thread_id=message_thread_id, date=utils.timestamp_to_datetime(message.date), chat=types.Chat._parse(client, message, users, chats, is_chat=True), - topics=None, from_user=from_user, sender_chat=sender_chat, service=service_type, + text=text, new_chat_members=new_chat_members, left_chat_member=left_chat_member, new_chat_title=new_chat_title, @@ -696,7 +754,6 @@ async def _parse( migrate_from_chat_id=-migrate_from_chat_id if migrate_from_chat_id else None, group_chat_created=group_chat_created, channel_chat_created=channel_chat_created, - is_topic_message=is_topic_message, forum_topic_created=forum_topic_created, forum_topic_closed=forum_topic_closed, forum_topic_reopened=forum_topic_reopened, @@ -708,6 +765,9 @@ async def _parse( video_chat_ended=video_chat_ended, video_chat_members_invited=video_chat_members_invited, web_app_data=web_app_data, + giveaway_launched=giveaway_launched, + gift_code=gift_code, + requested_chats=requested_chats, client=client # TODO: supergroup_chat_created ) @@ -741,13 +801,11 @@ async def _parse( client.message_cache[(parsed_message.chat.id, parsed_message.id)] = parsed_message - if message.reply_to: - if message.reply_to.forum_topic: - if message.reply_to.reply_to_top_id: - parsed_message.message_thread_id = message.reply_to.reply_to_top_id - else: - parsed_message.message_thread_id = message.reply_to.reply_to_msg_id - parsed_message.is_topic_message = True + if message.reply_to and message.reply_to.forum_topic: + if message.reply_to.reply_to_top_id: + parsed_message.message_thread_id = message.reply_to.reply_to_top_id + else: + parsed_message.message_thread_id = message.reply_to.reply_to_msg_id return parsed_message @@ -762,7 +820,6 @@ async def _parse( forward_from_message_id = None forward_signature = None forward_date = None - is_topic_message = None forward_header = message.fwd_from # type: raw.types.MessageFwdHeader @@ -787,6 +844,8 @@ async def _parse( contact = None venue = None game = None + giveaway = None + giveaway_result = None story = None audio = None voice = None @@ -820,8 +879,21 @@ async def _parse( elif isinstance(media, raw.types.MessageMediaGame): game = types.Game._parse(client, message) media_type = enums.MessageMediaType.GAME + elif isinstance(media, raw.types.MessageMediaGiveaway): + giveaway = types.Giveaway._parse(client, media, chats) + media_type = enums.MessageMediaType.GIVEAWAY + elif isinstance(media, raw.types.MessageMediaGiveawayResults): + giveaway_result = await types.GiveawayResult._parse(client, media, users, chats) + media_type = enums.MessageMediaType.GIVEAWAY_RESULT elif isinstance(media, raw.types.MessageMediaStory): - story = types.MessageStory._parse(client, media, users, chats) + if media.story: + story = await types.Story._parse(client, media.story, users, chats, media.peer) + else: + try: + story = await client.get_stories(utils.get_peer_id(media.peer), media.id) + except BotMethodInvalid: + story = await types.Story._parse(client, media, users, chats, media.peer) + media_type = enums.MessageMediaType.STORY elif isinstance(media, raw.types.MessageMediaDocument): doc = media.document @@ -867,7 +939,14 @@ async def _parse( media_type = enums.MessageMediaType.DOCUMENT elif isinstance(media, raw.types.MessageMediaWebPage): if isinstance(media.webpage, raw.types.WebPage): - web_page = types.WebPage._parse(client, media.webpage) + web_page = types.WebPage._parse( + client, + media.webpage, + getattr(media, "force_large_media", None), + getattr(media, "force_small_media", None), + getattr(media, "manual", None), + getattr(media, "safe", None) + ) media_type = enums.MessageMediaType.WEB_PAGE else: media = None @@ -904,7 +983,6 @@ async def _parse( message_thread_id=message_thread_id, date=utils.timestamp_to_datetime(message.date), chat=types.Chat._parse(client, message, users, chats, is_chat=True), - topics=None, from_user=from_user, sender_chat=sender_chat, text=( @@ -936,13 +1014,13 @@ async def _parse( forward_from_message_id=forward_from_message_id, forward_signature=forward_signature, forward_date=forward_date, - is_topic_message=is_topic_message, mentioned=message.mentioned, scheduled=is_scheduled, from_scheduled=message.from_scheduled, media=media_type, invert_media=getattr(message, "invert_media", None), edit_date=utils.timestamp_to_datetime(message.edit_date), + edit_hidden=message.edit_hide, media_group_id=message.grouped_id, photo=photo, location=location, @@ -952,6 +1030,8 @@ async def _parse( voice=voice, animation=animation, game=game, + giveaway=giveaway, + giveaway_result=giveaway_result, story=story, video=video, video_note=video_note, @@ -980,17 +1060,11 @@ async def _parse( parsed_message.reply_to_message_id = message.reply_to.reply_to_msg_id else: thread_id = message.reply_to.reply_to_msg_id + parsed_message.message_thread_id = thread_id - parsed_message.is_topic_message = True + if topics: - parsed_message.topics = types.ForumTopic._parse(topics[thread_id]) - else: - try: - msg = await client.get_messages(parsed_message.chat.id,message.id) - if getattr(msg, "topics"): - parsed_message.topics = msg.topics - except Exception: - pass + parsed_message.topic = types.ForumTopic._parse(client, topics[thread_id], users=users, chats=chats) else: if message.reply_to.quote: quote_entities = [types.MessageEntity._parse(client, entity, users) for entity in message.reply_to.quote_entities] @@ -1007,42 +1081,35 @@ async def _parse( if media is None or web_page is not None else None ) + parsed_message.reply_to_message_id = message.reply_to.reply_to_msg_id parsed_message.reply_to_top_message_id = message.reply_to.reply_to_top_id else: parsed_message.reply_to_story_id = message.reply_to.story_id parsed_message.reply_to_story_user_id = message.reply_to.user_id - setattr(parsed_message, "is_cross_chat", False) if replies: if parsed_message.reply_to_message_id: + is_cross_chat = getattr(message.reply_to, "reply_to_peer_id", None) and getattr(message.reply_to.reply_to_peer_id, "channel_id", None) + + if is_cross_chat: + key = (utils.get_channel_id(message.reply_to.reply_to_peer_id.channel_id), message.reply_to.reply_to_msg_id) + reply_to_params = {"chat_id": key[0], 'message_ids': key[1]} + else: + key = (parsed_message.chat.id, parsed_message.reply_to_message_id) + reply_to_params = {'chat_id': key[0], 'reply_to_message_ids': message.id} + try: - is_cross_chat = \ - message.reply_to and \ - message.reply_to.reply_to_peer_id and \ - hasattr(message.reply_to.reply_to_peer_id, "channel_id") and \ - message.reply_to.reply_to_peer_id.channel_id - if is_cross_chat: - key = (utils.get_channel_id(message.reply_to.reply_to_peer_id.channel_id), - message.reply_to.reply_to_msg_id) - setattr(parsed_message, "is_cross_chat", True) - else: - key = (parsed_message.chat.id, parsed_message.reply_to_message_id) reply_to_message = client.message_cache[key] if not reply_to_message: - if is_cross_chat: + try: reply_to_message = await client.get_messages( - chat_id=key[0], - message_ids=key[1], - replies=replies - 1 - ) - else: - reply_to_message = await client.get_messages( - chat_id=key[0], - reply_to_message_ids=message.id, - replies=replies - 1 + replies=replies - 1, + **reply_to_params ) + except ChannelPrivate: + pass if reply_to_message and not reply_to_message.forum_topic_created: parsed_message.reply_to_message = reply_to_message except MessageIdsEmpty: @@ -1053,11 +1120,20 @@ async def _parse( parsed_message.reply_to_story_user_id, parsed_message.reply_to_story_id ) - except Exception: + except BotMethodInvalid: pass else: parsed_message.reply_to_story = reply_to_story + if parsed_message.topic is None and parsed_message.chat.is_forum: + try: + parsed_message.topic = await client.get_forum_topics_by_id( + chat_id=parsed_message.chat.id, + topic_ids=parsed_message.message_thread_id or 1 + ) + except BotMethodInvalid: + pass + if not parsed_message.poll: # Do not cache poll messages client.message_cache[(parsed_message.chat.id, parsed_message.id)] = parsed_message @@ -1119,6 +1195,7 @@ async def reply_text( disable_web_page_preview: bool = None, disable_notification: bool = None, message_thread_id: int = None, + invert_media: bool = None, reply_to_message_id: int = None, quote_text: str = None, quote_entities: List["types.MessageEntity"] = None, @@ -1169,7 +1246,12 @@ async def reply_text( Users will receive a notification with no sound. message_thread_id (``int``, *optional*): - Unique identifier of a message thread to which the message belongs; for supergroups only + Unique identifier of a message thread to which the message belongs. + For supergroups only. + + invert_media (``bool``, *optional*): + If True, link preview will be shown above the message text. + Otherwise, the link preview will be shown below the message text. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. @@ -1202,6 +1284,9 @@ async def reply_text( if reply_to_message_id is None and quote: reply_to_message_id = self.id + if message_thread_id is None: + message_thread_id = self.message_thread_id + return await self._client.send_message( chat_id=self.chat.id, text=text, @@ -1210,6 +1295,7 @@ async def reply_text( disable_web_page_preview=disable_web_page_preview, disable_notification=disable_notification, message_thread_id=message_thread_id, + invert_media=invert_media, reply_to_message_id=reply_to_message_id, quote_text=quote_text, quote_entities=quote_entities, @@ -1307,7 +1393,8 @@ async def reply_animation( Users will receive a notification with no sound. message_thread_id (``int``, *optional*): - Unique identifier of a message thread to which the message belongs; for supergroups only + Unique identifier of a message thread to which the message belongs. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. @@ -1358,6 +1445,9 @@ async def reply_animation( if reply_to_message_id is None and quote: reply_to_message_id = self.id + if message_thread_id is None: + message_thread_id = self.message_thread_id + return await self._client.send_animation( chat_id=self.chat.id, animation=animation, @@ -1462,7 +1552,8 @@ async def reply_audio( Users will receive a notification with no sound. message_thread_id (``int``, *optional*): - Unique identifier of a message thread to which the message belongs; for supergroups only + Unique identifier of a message thread to which the message belongs. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. @@ -1513,6 +1604,9 @@ async def reply_audio( if reply_to_message_id is None and quote: reply_to_message_id = self.id + if message_thread_id is None: + message_thread_id = self.message_thread_id + return await self._client.send_audio( chat_id=self.chat.id, audio=audio, @@ -1593,7 +1687,8 @@ async def reply_cached_media( Users will receive a notification with no sound. message_thread_id (``int``, *optional*): - Unique identifier of a message thread to which the message belongs; for supergroups only + Unique identifier of a message thread to which the message belongs. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. @@ -1620,6 +1715,9 @@ async def reply_cached_media( if reply_to_message_id is None and quote: reply_to_message_id = self.id + if message_thread_id is None: + message_thread_id = self.message_thread_id + return await self._client.send_cached_media( chat_id=self.chat.id, file_id=file_id, @@ -1731,7 +1829,8 @@ async def reply_contact( Users will receive a notification with no sound. message_thread_id (``int``, *optional*): - Unique identifier of a message thread to which the message belongs; for supergroups only + Unique identifier of a message thread to which the message belongs. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. @@ -1762,6 +1861,9 @@ async def reply_contact( if reply_to_message_id is None and quote: reply_to_message_id = self.id + if message_thread_id is None: + message_thread_id = self.message_thread_id + return await self._client.send_contact( chat_id=self.chat.id, phone_number=phone_number, @@ -1860,7 +1962,8 @@ async def reply_document( Users will receive a notification with no sound. message_thread_id (``int``, *optional*): - Unique identifier of a message thread to which the message belongs; for supergroups only + Unique identifier of a message thread to which the message belongs. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. @@ -1918,6 +2021,9 @@ async def reply_document( if reply_to_message_id is None and quote: reply_to_message_id = self.id + if message_thread_id is None: + message_thread_id = self.message_thread_id + return await self._client.send_document( chat_id=self.chat.id, document=document, @@ -1982,7 +2088,8 @@ async def reply_game( Users will receive a notification with no sound. message_thread_id (``int``, *optional*): - Unique identifier of a message thread to which the message belongs; for supergroups only + Unique identifier of a message thread to which the message belongs. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. @@ -2003,6 +2110,9 @@ async def reply_game( if reply_to_message_id is None and quote: reply_to_message_id = self.id + if message_thread_id is None: + message_thread_id = self.message_thread_id + return await self._client.send_game( chat_id=self.chat.id, game_short_name=game_short_name, @@ -2058,7 +2168,8 @@ async def reply_inline_bot_result( Users will receive a notification with no sound. message_thread_id (``int``, *optional*): - Unique identifier of a message thread to which the message belongs; for supergroups only + Unique identifier of a message thread to which the message belongs. + For supergroups only. reply_to_message_id (``bool``, *optional*): If the message is a reply, ID of the original message. @@ -2085,6 +2196,9 @@ async def reply_inline_bot_result( if reply_to_message_id is None and quote: reply_to_message_id = self.id + if message_thread_id is None: + message_thread_id = self.message_thread_id + return await self._client.send_inline_bot_result( chat_id=self.chat.id, query_id=query_id, @@ -2148,7 +2262,8 @@ async def reply_location( Users will receive a notification with no sound. message_thread_id (``int``, *optional*): - Unique identifier of a message thread to which the message belongs; for supergroups only + Unique identifier of a message thread to which the message belongs. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message @@ -2175,6 +2290,9 @@ async def reply_location( if reply_to_message_id is None and quote: reply_to_message_id = self.id + if message_thread_id is None: + message_thread_id = self.message_thread_id + return await self._client.send_location( chat_id=self.chat.id, latitude=latitude, @@ -2230,7 +2348,8 @@ async def reply_media_group( Users will receive a notification with no sound. message_thread_id (``int``, *optional*): - Unique identifier of a message thread to which the message belongs; for supergroups only + Unique identifier of a message thread to which the message belongs. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. @@ -2258,6 +2377,9 @@ async def reply_media_group( if reply_to_message_id is None and quote: reply_to_message_id = self.id + if message_thread_id is None: + message_thread_id = self.message_thread_id + return await self._client.send_media_group( chat_id=self.chat.id, media=media, @@ -2343,7 +2465,8 @@ async def reply_photo( Users will receive a notification with no sound. message_thread_id (``int``, *optional*): - Unique identifier of a message thread to which the message belongs; for supergroups only + Unique identifier of a message thread to which the message belongs. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. @@ -2394,6 +2517,9 @@ async def reply_photo( if reply_to_message_id is None and quote: reply_to_message_id = self.id + if message_thread_id is None: + message_thread_id = self.message_thread_id + return await self._client.send_photo( chat_id=self.chat.id, photo=photo, @@ -2519,7 +2645,8 @@ async def reply_poll( Protects the contents of the sent message from forwarding and saving. message_thread_id (``int``, *optional*): - Unique identifier of a message thread to which the message belongs; for supergroups only + Unique identifier of a message thread to which the message belongs. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. @@ -2553,6 +2680,9 @@ async def reply_poll( if reply_to_message_id is None and quote: reply_to_message_id = self.id + if message_thread_id is None: + message_thread_id = self.message_thread_id + return await self._client.send_poll( chat_id=self.chat.id, question=question, @@ -2630,7 +2760,8 @@ async def reply_sticker( Users will receive a notification with no sound. message_thread_id (``int``, *optional*): - Unique identifier of a message thread to which the message belongs; for supergroups only + Unique identifier of a message thread to which the message belongs. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. @@ -2685,6 +2816,9 @@ async def reply_sticker( if reply_to_message_id is None and quote: reply_to_message_id = self.id + if message_thread_id is None: + message_thread_id = self.message_thread_id + return await self._client.send_sticker( chat_id=self.chat.id, sticker=sticker, @@ -2770,7 +2904,8 @@ async def reply_venue( Users will receive a notification with no sound. message_thread_id (``int``, *optional*): - Unique identifier of a message thread to which the message belongs; for supergroups only + Unique identifier of a message thread to which the message belongs. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message @@ -2801,6 +2936,9 @@ async def reply_venue( if reply_to_message_id is None and quote: reply_to_message_id = self.id + if message_thread_id is None: + message_thread_id = self.message_thread_id + return await self._client.send_venue( chat_id=self.chat.id, latitude=latitude, @@ -2915,7 +3053,8 @@ async def reply_video( Users will receive a notification with no sound. message_thread_id (``int``, *optional*): - Unique identifier of a message thread to which the message belongs; for supergroups only + Unique identifier of a message thread to which the message belongs. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. @@ -2966,6 +3105,9 @@ async def reply_video( if reply_to_message_id is None and quote: reply_to_message_id = self.id + if message_thread_id is None: + message_thread_id = self.message_thread_id + return await self._client.send_video( chat_id=self.chat.id, video=video, @@ -3056,7 +3198,8 @@ async def reply_video_note( Users will receive a notification with no sound. message_thread_id (``int``, *optional*): - Unique identifier of a message thread to which the message belongs; for supergroups only + Unique identifier of a message thread to which the message belongs. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message @@ -3111,6 +3254,9 @@ async def reply_video_note( if reply_to_message_id is None and quote: reply_to_message_id = self.id + if message_thread_id is None: + message_thread_id = self.message_thread_id + return await self._client.send_video_note( chat_id=self.chat.id, video_note=video_note, @@ -3141,6 +3287,7 @@ async def reply_voice( reply_to_message_id: int = None, quote_text: str = None, quote_entities: List["types.MessageEntity"] = None, + ttl_seconds: int = None, reply_markup: Union[ "types.InlineKeyboardMarkup", "types.ReplyKeyboardMarkup", @@ -3196,7 +3343,8 @@ async def reply_voice( Users will receive a notification with no sound. message_thread_id (``int``, *optional*): - Unique identifier of a message thread to which the message belongs; for supergroups only + Unique identifier of a message thread to which the message belongs. + For supergroups only. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message @@ -3207,6 +3355,11 @@ async def reply_voice( quote_entities (List of :obj:`~pyrogram.types.MessageEntity`): List of special entities that appear in quote text, which can be specified instead of *parse_mode*. + ttl_seconds (``int``, *optional*): + Self-Destruct Timer. + If you set a timer, the voice note will self-destruct in *ttl_seconds* + seconds after it was listened. + reply_markup (:obj:`~pyrogram.types.InlineKeyboardMarkup` | :obj:`~pyrogram.types.ReplyKeyboardMarkup` | :obj:`~pyrogram.types.ReplyKeyboardRemove` | :obj:`~pyrogram.types.ForceReply`, *optional*): Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. @@ -3247,6 +3400,9 @@ async def reply_voice( if reply_to_message_id is None and quote: reply_to_message_id = self.id + if message_thread_id is None: + message_thread_id = self.message_thread_id + return await self._client.send_voice( chat_id=self.chat.id, voice=voice, @@ -3259,6 +3415,7 @@ async def reply_voice( reply_to_message_id=reply_to_message_id, quote_text=quote_text, quote_entities=quote_entities, + ttl_seconds=ttl_seconds, reply_markup=reply_markup, progress=progress, progress_args=progress_args @@ -3487,7 +3644,8 @@ async def forward( For a contact that exists in your Telegram address book you can use his phone number (str). message_thread_id (``int``, *optional*): - Unique identifier of a message thread to which the message belongs; for supergroups only + Unique identifier of a message thread to which the message belongs. + For supergroups only. disable_notification (``bool``, *optional*): Sends the message silently. @@ -3519,9 +3677,13 @@ async def copy( caption_entities: List["types.MessageEntity"] = None, disable_notification: bool = None, message_thread_id: int = None, + reply_to_chat_id: Union[int, str] = None, reply_to_message_id: int = None, + quote_text: str = None, + quote_entities: List["types.MessageEntity"] = None, schedule_date: datetime = None, protect_content: bool = None, + has_spoiler: bool = None, reply_markup: Union[ "types.InlineKeyboardMarkup", "types.ReplyKeyboardMarkup", @@ -3570,17 +3732,29 @@ async def copy( message_thread_id (``int``, *optional*): Unique identifier for the target message thread (topic) of the forum. - for forum supergroups only. + For supergroups only. + + reply_to_chat_id (``int``, *optional*): + If the message is a reply, ID of the original chat. reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. + quote_text (``str``): + Text of the quote to be sent. + + quote_entities (List of :obj:`~pyrogram.types.MessageEntity`): + List of special entities that appear in quote text, which can be specified instead of *parse_mode*. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): Date when the message will be automatically sent. protect_content (``bool``, *optional*): Protects the contents of the sent message from forwarding and saving. + has_spoiler (``bool``, *optional*): + True, if the message media is covered by a spoiler animation. + reply_markup (:obj:`~pyrogram.types.InlineKeyboardMarkup` | :obj:`~pyrogram.types.ReplyKeyboardMarkup` | :obj:`~pyrogram.types.ReplyKeyboardRemove` | :obj:`~pyrogram.types.ForceReply`, *optional*): Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. @@ -3610,7 +3784,10 @@ async def copy( disable_web_page_preview=not self.web_page, disable_notification=disable_notification, message_thread_id=message_thread_id, + reply_to_chat_id=reply_to_chat_id, reply_to_message_id=reply_to_message_id, + quote_text=quote_text, + quote_entities=quote_entities, schedule_date=schedule_date, protect_content=protect_content, reply_markup=self.reply_markup if reply_markup is object else reply_markup @@ -3622,8 +3799,12 @@ async def copy( disable_notification=disable_notification, message_thread_id=message_thread_id, reply_to_message_id=reply_to_message_id, + reply_to_chat_id=reply_to_chat_id, + quote_text=quote_text, + quote_entities=quote_entities, schedule_date=schedule_date, protect_content=protect_content, + has_spoiler=has_spoiler, reply_markup=self.reply_markup if reply_markup is object else reply_markup ) @@ -3872,7 +4053,7 @@ async def click(self, x: Union[int, str] = 0, y: int = None, quote: bool = None, else: await self.reply(button, quote=quote) - async def react(self, emoji: Union[int, str] = None, big: bool = False) -> bool: + async def react(self, emoji: Union[int, str, List[Union[int, str]]] = None, big: bool = False) -> bool: """Bound method *react* of :obj:`~pyrogram.types.Message`. Use as a shortcut for: @@ -3891,9 +4072,10 @@ async def react(self, emoji: Union[int, str] = None, big: bool = False) -> bool: await message.react(emoji="🔥") Parameters: - emoji (``str``, *optional*): + emoji (``int`` | ``str`` | List of ``int`` | ``str``, *optional*): Reaction emoji. - Pass "" as emoji (default) to retract the reaction. + Pass None as emoji (default) to retract the reaction. + Pass list of int or str to react multiple emojis. big (``bool``, *optional*): Pass True to show a bigger and longer reaction. diff --git a/pyrogram/types/messages_and_media/sticker.py b/pyrogram/types/messages_and_media/sticker.py index de266b2fd..8b37df2db 100644 --- a/pyrogram/types/messages_and_media/sticker.py +++ b/pyrogram/types/messages_and_media/sticker.py @@ -45,10 +45,13 @@ class Sticker(Object): Sticker height. is_animated (``bool``): - True, if the sticker is animated + True, if the sticker is animated. is_video (``bool``): - True, if the sticker is a video sticker + True, if the sticker is a video sticker. + + is_premium (``bool``): + True, if the sticker is a premium only. file_name (``str``, *optional*): Sticker file name. @@ -84,6 +87,7 @@ def __init__( height: int, is_animated: bool, is_video: bool, + is_premium: bool, file_name: str = None, mime_type: str = None, file_size: int = None, @@ -104,6 +108,7 @@ def __init__( self.height = height self.is_animated = is_animated self.is_video = is_video + self.is_premium = is_premium self.emoji = emoji self.set_name = set_name self.thumbs = thumbs @@ -194,6 +199,7 @@ async def _parse( ), is_animated=sticker.mime_type == "application/x-tgsticker", is_video=sticker.mime_type == "video/webm", + is_premium=bool(sticker.video_thumbs), # TODO: mask_position set_name=set_name, emoji=sticker_attributes.alt or None, diff --git a/pyrogram/types/messages_and_media/story.py b/pyrogram/types/messages_and_media/story.py index 20fc006a6..0260cd399 100644 --- a/pyrogram/types/messages_and_media/story.py +++ b/pyrogram/types/messages_and_media/story.py @@ -39,11 +39,23 @@ class Story(Object, Update): sender_chat (:obj:`~pyrogram.types.Chat`, *optional*): Sender of the story, sent on behalf of a chat. + date (:py:obj:`~datetime.datetime`, *optional*): + Date the story was sent. + chat (:obj:`~pyrogram.types.Chat`, *optional*): Conversation the story belongs to. - date (:py:obj:`~datetime.datetime`, *optional*): - Date the story was sent. + forward_from (:obj:`~pyrogram.types.User`, *optional*): + For forwarded stories, sender of the original story. + + forward_sender_name (``str``, *optional*): + For stories forwarded from users who have hidden their accounts, name of the user. + + forward_from_chat (:obj:`~pyrogram.types.Chat`, *optional*): + For stories forwarded from channels, information about the original channel. + + forward_from_story_id (``int``, *optional*): + For stories forwarded from channels, identifier of the original story in the channel. expire_date (:py:obj:`~datetime.datetime`, *optional*): Date the story will be expired. @@ -51,7 +63,7 @@ class Story(Object, Update): media (:obj:`~pyrogram.enums.MessageMediaType`, *optional*): The media type of the Story. This field will contain the enumeration type of the media message. - You can use ``media = getattr(message, message.media.value)`` to access the media message. + You can use ``media = getattr(story, story.media.value)`` to access the media message. has_protected_content (``bool``, *optional*): True, if the story can't be forwarded. @@ -86,9 +98,12 @@ class Story(Object, Update): caption_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): For text messages, special entities like usernames, URLs, bot commands, etc. that appear in the caption. - views (:obj:`~pyrogram.types.StoryViews`, *optional*): + views (``int``, *optional*): Stories views. + forwards (``int``, *optional*): + Stories forwards. + privacy (:obj:`~pyrogram.enums.StoryPrivacyRules`, *optional*): Story privacy. @@ -97,6 +112,17 @@ class Story(Object, Update): disallowed_users (List of ``int`` | ``str``, *optional*): List of user_ids whos denied to view the story. + + reactions (List of :obj:`~pyrogram.types.Reaction`): + List of the reactions to this story. + + skipped (``bool``, *optional*): + The story is skipped. + A story can be skipped in case it was skipped. + + deleted (``bool``, *optional*): + The story is deleted. + A story can be deleted in case it was deleted or you tried to retrieve a story that doesn't exist yet. """ # TODO: Add Media Areas @@ -108,10 +134,14 @@ def __init__( id: int, from_user: "types.User" = None, sender_chat: "types.Chat" = None, - chat: "types.Chat" = None, date: datetime = None, + chat: "types.Chat" = None, + forward_from: "types.User" = None, + forward_sender_name: str = None, + forward_from_chat: "types.Chat" = None, + forward_from_story_id: int = None, expire_date: datetime = None, - media: "enums.MessageMediaType", + media: "enums.MessageMediaType" = None, has_protected_content: bool = None, photo: "types.Photo" = None, video: "types.Video" = None, @@ -123,18 +153,26 @@ def __init__( selected_contacts: bool = None, caption: str = None, caption_entities: List["types.MessageEntity"] = None, - views: "types.StoryViews" = None, - privacy: "enums.StoryPrivacy" = None, + views: int = None, + forwards: int = None, + privacy: "enums.StoryPrivacyRules" = None, allowed_users: List[Union[int, str]] = None, disallowed_users: List[Union[int, str]] = None, + reactions: List["types.Reaction"] = None, + skipped: bool = None, + deleted: bool = None ): super().__init__(client) self.id = id self.from_user = from_user self.sender_chat = sender_chat - self.chat = chat self.date = date + self.chat = chat + self.forward_from = forward_from + self.forward_sender_name = forward_sender_name + self.forward_from_chat = forward_from_chat + self.forward_from_story_id = forward_from_story_id self.expire_date = expire_date self.media = media self.has_protected_content = has_protected_content @@ -149,51 +187,33 @@ def __init__( self.caption = caption self.caption_entities = caption_entities self.views = views + self.forwards = forwards self.privacy = privacy self.allowed_users = allowed_users self.disallowed_users = disallowed_users + self.reactions = reactions + self.skipped = skipped + self.deleted = deleted @staticmethod async def _parse( client: "pyrogram.Client", - stories: raw.base.StoryItem, + story: raw.types.StoryItem, users: dict, chats: dict, peer: Union["raw.types.PeerChannel", "raw.types.PeerUser"] ) -> "Story": - if isinstance(stories, raw.types.StoryItemSkipped): - return await types.StorySkipped._parse(client, stories, users, chats, peer) - if isinstance(stories, raw.types.StoryItemDeleted): - return await types.StoryDeleted._parse(client, stories, users, chats, peer) - - entities = [e for e in (types.MessageEntity._parse(client, entity, {}) for entity in stories.entities) if e] - - photo = None - video = None - from_user = None - sender_chat = None - chat = None - privacy = None - allowed_users = None - disallowed_users = None - media_type = None - - if isinstance(stories.media, raw.types.MessageMediaPhoto): - photo = types.Photo._parse(client, stories.media.photo, stories.media.ttl_seconds) - media_type = enums.MessageMediaType.PHOTO - else: - doc = stories.media.document - attributes = {type(i): i for i in doc.attributes} - video_attributes = attributes.get(raw.types.DocumentAttributeVideo, None) - video = types.Video._parse(client, doc, video_attributes, None) - media_type = enums.MessageMediaType.VIDEO - if isinstance(peer, raw.types.InputPeerSelf): r = await client.invoke(raw.functions.users.GetUsers(id=[raw.types.InputPeerSelf()])) peer_id = r[0].id users.update({i.id: i for i in r}) - elif isinstance(peer, (raw.types.InputPeerUser, raw.types.InputPeerChannel)): - peer_id = utils.get_input_peer_id(peer) + elif isinstance(peer, raw.types.InputPeerUser): + peer_id = utils.get_raw_peer_id(peer) + elif isinstance(peer, raw.types.InputPeerChannel): + peer_id = utils.get_raw_peer_id(peer) + if peer_id not in chats: + r = await client.invoke(raw.functions.channels.GetChannels(id=[peer])) + chats.update({peer_id: r.chats[0]}) else: peer_id = utils.get_raw_peer_id(peer) @@ -211,10 +231,65 @@ async def _parse( else: users.update({i.id: i for i in r}) + photo = None + video = None + from_user = None + sender_chat = None + chat = None + privacy = None + allowed_users = None + disallowed_users = None + media_type = None + views = None + forwards = None + reactions = None + from_user = types.User._parse(client, users.get(peer_id, None)) sender_chat = types.Chat._parse_channel_chat(client, chats[peer_id]) if not from_user else None chat = sender_chat if not from_user else types.Chat._parse_user_chat(client, users.get(peer_id, None)) + if isinstance(story, raw.types.StoryItemDeleted): + return Story(client=client, id=story.id, deleted=True, from_user=from_user, sender_chat=sender_chat, chat=chat) + if isinstance(story, raw.types.StoryItemSkipped): + return Story(client=client, id=story.id, skipped=True, from_user=from_user, sender_chat=sender_chat, chat=chat) + if isinstance(story, raw.types.MessageMediaStory): + return Story(client=client, id=story.id, from_user=from_user, sender_chat=sender_chat, chat=chat) + + forward_from = None + forward_sender_name = None + forward_from_chat = None + forward_from_story_id = None + + forward_header = story.fwd_from # type: raw.types.StoryFwdHeader + + if forward_header: + fwd_raw_peer_id = utils.get_raw_peer_id(forward_header.from_peer) + fwd_peer_id = utils.get_peer_id(forward_header.from_peer) + + if fwd_peer_id > 0: + forward_from = types.User._parse(client, users[fwd_raw_peer_id]) + else: + forward_from_chat = types.Chat._parse_channel_chat(client, chats[fwd_raw_peer_id]) + forward_from_story_id = forward_header.story_id + + if story.views: + views=getattr(story.views, "views_count", None) + forwards=getattr(story.views, "forwards_count", None) + reactions=[ + types.Reaction._parse_count(client, reaction) + for reaction in getattr(story.views, "reactions", []) + ] or None + + if isinstance(story.media, raw.types.MessageMediaPhoto): + photo = types.Photo._parse(client, story.media.photo, story.media.ttl_seconds) + media_type = enums.MessageMediaType.PHOTO + else: + doc = story.media.document + attributes = {type(i): i for i in doc.attributes} + video_attributes = attributes.get(raw.types.DocumentAttributeVideo, None) + video = types.Video._parse(client, doc, video_attributes, None) + media_type = enums.MessageMediaType.VIDEO + privacy_map = { raw.types.PrivacyValueAllowAll: enums.StoriesPrivacyRules.PUBLIC, raw.types.PrivacyValueAllowContacts: enums.StoriesPrivacyRules.CONTACTS, @@ -222,7 +297,7 @@ async def _parse( raw.types.PrivacyValueDisallowAll: enums.StoriesPrivacyRules.SELECTED_USERS, } - for priv in stories.privacy: + for priv in story.privacy: privacy = privacy_map.get(type(priv), None) if isinstance(priv, raw.types.PrivacyValueAllowUsers): @@ -234,29 +309,37 @@ async def _parse( elif isinstance(priv, raw.types.PrivacyValueDisallowChatParticipants): disallowed_users = types.List(types.Chat._parse_chat_chat(client, chats.get(chat_id, None)) for chat_id in priv.chats) + entities = [e for e in (types.MessageEntity._parse(client, entity, {}) for entity in story.entities) if e] + return Story( - id=stories.id, + id=story.id, from_user=from_user, sender_chat=sender_chat, + date=utils.timestamp_to_datetime(story.date), chat=chat, - date=utils.timestamp_to_datetime(stories.date), - expire_date=utils.timestamp_to_datetime(stories.expire_date), + forward_from=forward_from, + forward_sender_name=forward_sender_name, + forward_from_chat=forward_from_chat, + forward_from_story_id=forward_from_story_id, + expire_date=utils.timestamp_to_datetime(story.expire_date), media=media_type, - has_protected_content=stories.noforwards, + has_protected_content=story.noforwards, photo=photo, video=video, - edited=stories.edited, - pinned=stories.pinned, - public=stories.public, - close_friends=stories.close_friends, - contacts=stories.contacts, - selected_contacts=stories.selected_contacts, - caption=stories.caption, + edited=story.edited, + pinned=story.pinned, + public=story.public, + close_friends=story.close_friends, + contacts=story.contacts, + selected_contacts=story.selected_contacts, + caption=story.caption, caption_entities=entities or None, - views=types.StoryViews._parse(client, stories.views) if stories.views else None, + views=views, + forwards=forwards, privacy=privacy, allowed_users=allowed_users, disallowed_users=disallowed_users, + reactions=reactions, client=client ) @@ -1355,22 +1438,13 @@ async def copy( Raises: RPCError: In case of a Telegram RPC error. """ - file_id = None - - if self.photo: - file_id = self.photo.file_id - elif self.video: - file_id = self.video.file_id - else: - raise ValueError("Unknown media type") - if caption is None: caption = self.caption or "" caption_entities = self.caption_entities return await self._client.send_story( chat_id=chat_id, - media=file_id, + media=await self.download(in_memory=True), caption=caption, period=period, protect_content=protect_content, @@ -1405,15 +1479,9 @@ async def delete(self): """ return await self._client.delete_stories(chat_id=self.chat.id, story_ids=self.id) - async def edit( + async def edit_media( self, media: Union[str, BinaryIO] = None, - privacy: "enums.StoriesPrivacyRules" = None, - allowed_users: List[Union[int, str]] = None, - disallowed_users: List[Union[int, str]] = None, - caption: str = None, - parse_mode: "enums.ParseMode" = None, - caption_entities: List["types.MessageEntity"] = None ) -> "types.Story": """Bound method *edit* of :obj:`~pyrogram.types.Story`. @@ -1429,12 +1497,9 @@ async def edit( Example: .. code-block:: python - await story.edit_caption("hello") + await story.edit_media("new_video.mp4") Parameters: - story_id (``int``): - Unique identifier (int) of the target story. - media (``str`` | ``BinaryIO``, *optional*): New story media. Pass a file_id as string to send a photo that exists on the Telegram servers, @@ -1442,46 +1507,16 @@ async def edit( pass a file path as string to upload a new photo that exists on your local machine, or pass a binary file-like object with its attribute ".name" set for in-memory uploads. - privacy (:obj:`~pyrogram.enums.StoriesPrivacyRules`, *optional*): - Story privacy. - - allowed_users (List of ``int``, *optional*): - List of user_id or chat_id of chat users who are allowed to view stories. - Note: chat_id available only with :obj:`~pyrogram.enums.StoriesPrivacyRules.SELECTED_USERS`. - Works with :obj:`~pyrogram.enums.StoriesPrivacyRules.CLOSE_FRIENDS` - and :obj:`~pyrogram.enums.StoriesPrivacyRules.SELECTED_USERS` only - - disallowed_users (List of ``int``, *optional*): - List of user_id whos disallow to view the stories. - Note: Works with :obj:`~pyrogram.enums.StoriesPrivacyRules.PUBLIC` - and :obj:`~pyrogram.enums.StoriesPrivacyRules.CONTACTS` only - - caption (``str``, *optional*): - Story caption, 0-1024 characters. - - parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): - By default, texts are parsed using both Markdown and HTML styles. - You can combine both syntaxes together. - - caption_entities (List of :obj:`~pyrogram.types.MessageEntity`): - List of special entities that appear in the caption, which can be specified instead of *parse_mode*. - Returns: On success, the edited :obj:`~pyrogram.types.Story` is returned. Raises: RPCError: In case of a Telegram RPC error. """ - return await self._client.edit_story( + return await self._client.edit_story_media( chat_id=self.chat.id, story_id=self.id, - media=media, - privacy=privacy, - allowed_users=allowed_users, - disallowed_users=disallowed_users, - caption=caption, - parse_mode=parse_mode, - caption_entities=caption_entities + media=media ) async def edit_caption( @@ -1496,7 +1531,7 @@ async def edit_caption( .. code-block:: python - await client.edit_story( + await client.edit_story_caption( story_id=story.id, caption="hello" ) @@ -1523,7 +1558,7 @@ async def edit_caption( Raises: RPCError: In case of a Telegram RPC error. """ - return await self._client.edit_story( + return await self._client.edit_story_caption( chat_id=self.chat.id, story_id=self.id, caption=caption, @@ -1543,7 +1578,7 @@ async def edit_privacy( .. code-block:: python - await client.edit_story( + await client.edit_story_privacy( story_id=story.id, privacy=enums.StoriesPrivacyRules.PUBLIC ) @@ -1557,13 +1592,13 @@ async def edit_privacy( privacy (:obj:`~pyrogram.enums.StoriesPrivacyRules`, *optional*): Story privacy. - allowed_users (List of ``int``, *optional*): + allowed_users (List of ``int`` | ``str``, *optional*): List of user_id or chat_id of chat users who are allowed to view stories. Note: chat_id available only with :obj:`~pyrogram.enums.StoriesPrivacyRules.SELECTED_USERS`. Works with :obj:`~pyrogram.enums.StoriesPrivacyRules.CLOSE_FRIENDS` and :obj:`~pyrogram.enums.StoriesPrivacyRules.SELECTED_USERS` only - disallowed_users (List of ``int``, *optional*): + disallowed_users (List of ``int`` | ``str``, *optional*): List of user_id whos disallow to view the stories. Note: Works with :obj:`~pyrogram.enums.StoriesPrivacyRules.PUBLIC` and :obj:`~pyrogram.enums.StoriesPrivacyRules.CONTACTS` only @@ -1574,7 +1609,7 @@ async def edit_privacy( Raises: RPCError: In case of a Telegram RPC error. """ - return await self._client.edit_story( + return await self._client.edit_story_privacy( chat_id=self.chat.id, story_id=self.id, privacy=privacy, @@ -1673,7 +1708,8 @@ async def forward( For a contact that exists in your Telegram address book you can use his phone number (str). message_thread_id (``int``, *optional*): - Unique identifier of a message thread to which the message belongs; for supergroups only + Unique identifier of a message thread to which the message belongs. + For supergroups only. disable_notification (``bool``, *optional*): Sends the message silently. @@ -1771,3 +1807,25 @@ async def download( progress=progress, progress_args=progress_args, ) + + async def read(self) -> List[int]: + """Bound method *read* of :obj:`~pyrogram.types.Story`. + + Example: + .. code-block:: python + + await story.read() + + Returns: + List of ``int``: On success, a list of read stories is returned. + + Example: + .. code-block:: python + + # Read stories + await app.read_stories(chat_id) + """ + return await self._client.read_stories( + chat_id=self.chat.id, + max_id=self.id + ) \ No newline at end of file diff --git a/pyrogram/types/messages_and_media/story_deleted.py b/pyrogram/types/messages_and_media/story_deleted.py deleted file mode 100644 index c6c707c52..000000000 --- a/pyrogram/types/messages_and_media/story_deleted.py +++ /dev/null @@ -1,97 +0,0 @@ -# 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 . - -import pyrogram - -from pyrogram import raw, types, utils -from pyrogram.errors import PeerIdInvalid -from typing import Union -from ..object import Object -from ..update import Update - -class StoryDeleted(Object, Update): - """A deleted story. - - Parameters: - id (``int``): - Unique story identifier. - - from_user (:obj:`~pyrogram.types.User`, *optional*): - Sender of the story. - - sender_chat (:obj:`~pyrogram.types.Chat`, *optional*): - Sender of the story. If the story is from channel. - """ - - def __init__( - self, - *, - client: "pyrogram.Client" = None, - id: int, - from_user: "types.User" = None, - sender_chat: "types.Chat" = None - ): - super().__init__(client) - - self.id = id - self.from_user = from_user - self.sender_chat = sender_chat - - @staticmethod - async def _parse( - client: "pyrogram.Client", - stories: raw.base.StoryItem, - users: dict, - chats: dict, - peer: Union["raw.types.PeerChannel", "raw.types.PeerUser"] - ) -> "StoryDeleted": - from_user = None - sender_chat = None - - if isinstance(peer, raw.types.InputPeerSelf): - r = await client.invoke(raw.functions.users.GetUsers(id=[raw.types.InputPeerSelf()])) - peer_id = r[0].id - users.update({i.id: i for i in r}) - elif isinstance(peer, (raw.types.InputPeerUser, raw.types.InputPeerChannel)): - peer_id = utils.get_input_peer_id(peer) - else: - peer_id = utils.get_raw_peer_id(peer) - - if isinstance(peer, (raw.types.PeerUser, raw.types.InputPeerUser)) and peer_id not in users: - try: - r = await client.invoke( - raw.functions.users.GetUsers( - id=[ - await client.resolve_peer(peer_id) - ] - ) - ) - except PeerIdInvalid: - pass - else: - users.update({i.id: i for i in r}) - - from_user = types.User._parse(client, users.get(peer_id, None)) - sender_chat = types.Chat._parse_channel_chat(client, chats[peer_id]) if not from_user else None - - return StoryDeleted( - id=stories.id, - from_user=from_user, - sender_chat=sender_chat, - client=client - ) \ No newline at end of file diff --git a/pyrogram/types/messages_and_media/story_skipped.py b/pyrogram/types/messages_and_media/story_skipped.py deleted file mode 100644 index 1fb9a9f42..000000000 --- a/pyrogram/types/messages_and_media/story_skipped.py +++ /dev/null @@ -1,116 +0,0 @@ -# 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 . - -import pyrogram - -from datetime import datetime -from pyrogram import raw, types, utils -from pyrogram.errors import PeerIdInvalid -from typing import Union -from ..object import Object -from ..update import Update - -class StorySkipped(Object, Update): - """A skipped story. - - Parameters: - id (``int``): - Unique story identifier. - - from_user (:obj:`~pyrogram.types.User`, *optional*): - Sender of the story. - - sender_chat (:obj:`~pyrogram.types.Chat`, *optional*): - Sender of the story. If the story is from channel. - - date (:py:obj:`~datetime.datetime`, *optional*): - Date the story was sent. - - expire_date (:py:obj:`~datetime.datetime`, *optional*): - Date the story will be expired. - - close_friends (``bool``, *optional*): - True, if the Story is shared with close_friends only. - """ - - def __init__( - self, - *, - client: "pyrogram.Client" = None, - id: int, - from_user: "types.User" = None, - sender_chat: "types.Chat" = None, - date: datetime, - expire_date: datetime, - close_friends: bool = None - ): - super().__init__(client) - - self.id = id - self.from_user = from_user - self.sender_chat = sender_chat - self.date = date - self.expire_date = expire_date - self.close_friends = close_friends - - @staticmethod - async def _parse( - client: "pyrogram.Client", - stories: raw.base.StoryItem, - users: dict, - chats: dict, - peer: Union["raw.types.PeerChannel", "raw.types.PeerUser"] - ) -> "StorySkipped": - from_user = None - sender_chat = None - - if isinstance(peer, raw.types.InputPeerSelf): - r = await client.invoke(raw.functions.users.GetUsers(id=[raw.types.InputPeerSelf()])) - peer_id = r[0].id - users.update({i.id: i for i in r}) - elif isinstance(peer, (raw.types.InputPeerUser, raw.types.InputPeerChannel)): - peer_id = utils.get_input_peer_id(peer) - else: - peer_id = utils.get_raw_peer_id(peer) - - if isinstance(peer, (raw.types.PeerUser, raw.types.InputPeerUser)) and peer_id not in users: - try: - r = await client.invoke( - raw.functions.users.GetUsers( - id=[ - await client.resolve_peer(peer_id) - ] - ) - ) - except PeerIdInvalid: - pass - else: - users.update({i.id: i for i in r}) - - from_user = types.User._parse(client, users.get(peer_id, None)) - sender_chat = types.Chat._parse_channel_chat(client, chats[peer_id]) if not from_user else None - - return StorySkipped( - id=stories.id, - from_user=from_user, - sender_chat=sender_chat, - date=utils.timestamp_to_datetime(stories.date), - expire_date=utils.timestamp_to_datetime(stories.expire_date), - close_friends=stories.close_friends, - client=client - ) \ No newline at end of file diff --git a/pyrogram/types/messages_and_media/story_views.py b/pyrogram/types/messages_and_media/story_views.py deleted file mode 100644 index 6e2af2d4f..000000000 --- a/pyrogram/types/messages_and_media/story_views.py +++ /dev/null @@ -1,75 +0,0 @@ -# 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 pyrogram import raw, types -from typing import List -from ..object import Object - -class StoryViews(Object): - """Contains information about a story viewers. - - Parameters: - views_count (``int`` ``32-bit``): - Views count. - - has_viewers (``bool``, *optional*): - Has viewers. - - forwards_count (``int`` ``32-bit``, *optional*): - Forwards count. - - reactions (List of :obj:`~pyrogram.types.Reaction`, *optional*): - Reactions list. - - reactions_count (``int`` ``32-bit``, *optional*): - Reactions count. - - recent_viewers (List of ``int`` ``64-bit``, *optional*): - Viewers list. - """ - - def __init__( - self, *, - views_count: int, - has_viewers: bool = None, - forwards_count: int = None, - reactions: List["types.Reaction"] = None, - reactions_count: int = None, - recent_viewers: List[int] = None - ): - super().__init__() - - self.views_count = views_count - self.has_viewers = has_viewers - self.forwards_count = forwards_count - self.reactions = reactions - self.reactions_count = reactions_count - self.recent_viewers = recent_viewers - - @staticmethod - def _parse(client, story_views: "raw.types.StoryViews") -> "StoryViews": - return StoryViews( - views_count=getattr(story_views, "views_count", None), - has_viewers=getattr(story_views, "has_viewers", None), - forwards_count=getattr(story_views, "forwards_count", None), - reactions=[ - types.Reaction._parse_count(client, reaction) - for reaction in getattr(story_views, "reactions", []) - ] or None, - recent_viewers=getattr(story_views, "recent_viewers", None) or None, - ) diff --git a/pyrogram/types/messages_and_media/web_page.py b/pyrogram/types/messages_and_media/web_page.py index 34e51d88c..9cf55478d 100644 --- a/pyrogram/types/messages_and_media/web_page.py +++ b/pyrogram/types/messages_and_media/web_page.py @@ -77,6 +77,21 @@ class WebPage(Object): embed_height (``int``, *optional*): Embedded content height. + has_large_media (``bool``, *optional*): + Whether the webpage preview is large. + + force_large_media (``bool``, *optional*): + Whether the webpage preview is forced large. + + force_small_media (``bool``, *optional*): + Whether the webpage preview is forced small. + + manual (``bool``, *optional*): + Whether the webpage preview was changed by the user. + + safe (``bool``, *optional*): + Whether the webpage preview is safe. + duration (``int``, *optional*): Unknown at the time of writing. @@ -104,6 +119,11 @@ def __init__( embed_type: str = None, embed_width: int = None, embed_height: int = None, + has_large_media: bool = None, + force_large_media: bool = None, + force_small_media: bool = None, + manual: bool = None, + safe: bool = None, duration: int = None, author: str = None ): @@ -125,11 +145,23 @@ def __init__( self.embed_type = embed_type self.embed_width = embed_width self.embed_height = embed_height + self.has_large_media = has_large_media + self.force_large_media = force_large_media + self.force_small_media = force_small_media + self.manual = manual + self.safe = safe self.duration = duration self.author = author @staticmethod - def _parse(client, webpage: "raw.types.WebPage") -> "WebPage": + def _parse( + client, + webpage: "raw.types.WebPage", + force_large_media: bool = None, + force_small_media: bool = None, + manual: bool = None, + safe: bool = None + ) -> "WebPage": audio = None document = None photo = None @@ -182,6 +214,11 @@ def _parse(client, webpage: "raw.types.WebPage") -> "WebPage": embed_type=webpage.embed_type, embed_width=webpage.embed_width, embed_height=webpage.embed_height, + has_large_media=webpage.has_large_media, + force_large_media=force_large_media, + force_small_media=force_small_media, + manual=manual, + safe=safe, duration=webpage.duration, author=webpage.author ) diff --git a/pyrogram/types/user_and_chats/__init__.py b/pyrogram/types/user_and_chats/__init__.py index d7b1a382d..cfc2f5eab 100644 --- a/pyrogram/types/user_and_chats/__init__.py +++ b/pyrogram/types/user_and_chats/__init__.py @@ -18,6 +18,7 @@ from .chat import Chat from .chat_admin_with_invite_links import ChatAdminWithInviteLinks +from .chat_color import ChatColor from .chat_event import ChatEvent from .chat_event_filter import ChatEventFilter from .chat_invite_link import ChatInviteLink @@ -32,10 +33,9 @@ from .chat_reactions import ChatReactions from .dialog import Dialog from .emoji_status import EmojiStatus +from .folder import Folder from .invite_link_importer import InviteLinkImporter from .restriction import Restriction -from .peer_channel import PeerChannel -from .peer_user import PeerUser from .user import User from .username import Username from .video_chat_ended import VideoChatEnded @@ -50,8 +50,6 @@ "ChatPhoto", "ChatPreview", "Dialog", - "PeerChannel", - "PeerUser", "User", "Username", "Restriction", @@ -60,6 +58,7 @@ "ChatInviteLink", "InviteLinkImporter", "ChatAdminWithInviteLinks", + "ChatColor", "VideoChatStarted", "VideoChatEnded", "VideoChatMembersInvited", @@ -69,5 +68,6 @@ "ChatPrivileges", "ChatJoiner", "EmojiStatus", + "Folder", "ChatReactions" ] \ No newline at end of file diff --git a/pyrogram/types/user_and_chats/chat.py b/pyrogram/types/user_and_chats/chat.py index ce1540c3c..001f570e0 100644 --- a/pyrogram/types/user_and_chats/chat.py +++ b/pyrogram/types/user_and_chats/chat.py @@ -37,12 +37,12 @@ class Chat(Object): Type of chat. is_forum (``bool``, *optional*): - True, if the supergroup chat is a forum + True, if the supergroup chat is a forum. is_verified (``bool``, *optional*): True, if this chat has been verified by Telegram. Supergroups, channels and bots only. - is_participants_hidden (``bool``, *optional*): + is_members_hidden (``bool``, *optional*): True, if the chat members are hidden. is_restricted (``bool``, *optional*): @@ -52,19 +52,25 @@ class Chat(Object): is_creator (``bool``, *optional*): True, if this chat owner is the current user. Supergroups, channels and groups only. + is_admin (``bool``, *optional*): + True, if the current user is admin. Supergroups, channels and groups only. + is_scam (``bool``, *optional*): True, if this chat has been flagged for scam. is_fake (``bool``, *optional*): True, if this chat has been flagged for impersonation. - is_support (``bool``): + is_deactivated (``bool``, *optional*): + True, if this chat has been flagged for deactivated. + + is_support (``bool``, *optional*): True, if this chat is part of the Telegram support team. Users and bots only. - is_stories_hidden (``bool``): + is_stories_hidden (``bool``, *optional*): True, if this chat has hidden stories. - is_stories_unavailable (``bool``): + is_stories_unavailable (``bool``, *optional*): True, if this chat stories is unavailable. title (``str``, *optional*): @@ -88,6 +94,12 @@ class Chat(Object): photo (:obj:`~pyrogram.types.ChatPhoto`, *optional*): Chat photo. Suitable for downloads only. + stories (List of :obj:`~pyrogram.types.Story`, *optional*): + The list of chat's stories if available. + + wallpaper (:obj:`~pyrogram.types.Document`, *optional*): + Chat wallpaper. + bio (``str``, *optional*): Bio of the other party in a private chat. Returned only in :meth:`~pyrogram.Client.get_chat`. @@ -102,6 +114,9 @@ class Chat(Object): It is accurate only in case the owner has set the chat photo, otherwise the dc_id will be the one assigned to the administrator who set the current chat photo. + folder_id (``int``, *optional*): + The folder identifier where the chat is located. + has_protected_content (``bool``, *optional*): True, if messages from the chat can't be forwarded to other chats. @@ -148,11 +163,14 @@ class Chat(Object): Available reactions in the chat. Returned only in :meth:`~pyrogram.Client.get_chat`. - color (``int``, *optional*) - Chat color. + level (``int``, *optional*): + Channel boosts level. + + reply_color (:obj:`~pyrogram.types.ChatColor`, *optional*): + Chat reply color. - background_emoji_id (``int``, *optional*) - Chat background emoji id. + profile_color (:obj:`~pyrogram.types.ChatColor`, *optional*): + Chat profile color. """ def __init__( @@ -163,11 +181,13 @@ def __init__( type: "enums.ChatType", is_forum: bool = None, is_verified: bool = None, - is_participants_hidden: bool = None, + is_members_hidden: bool = None, is_restricted: bool = None, is_creator: bool = None, + is_admin: bool = None, is_scam: bool = None, is_fake: bool = None, + is_deactivated: bool = None, is_support: bool = None, is_stories_hidden: bool = None, is_stories_unavailable: bool = None, @@ -177,9 +197,12 @@ def __init__( first_name: str = None, last_name: str = None, photo: "types.ChatPhoto" = None, + stories: List["types.Story"] = None, + wallpaper: "types.Document" = None, bio: str = None, description: str = None, dc_id: int = None, + folder_id: int = None, has_protected_content: bool = None, invite_link: str = None, pinned_message=None, @@ -192,8 +215,9 @@ def __init__( linked_chat: "types.Chat" = None, send_as_chat: "types.Chat" = None, available_reactions: Optional["types.ChatReactions"] = None, - color: int = None, - background_emoji_id: int = None + level: int = None, + reply_color: "types.ChatColor" = None, + profile_color: "types.ChatColor" = None ): super().__init__(client) @@ -201,11 +225,13 @@ def __init__( self.type = type self.is_forum = is_forum self.is_verified = is_verified - self.is_participants_hidden = is_participants_hidden + self.is_members_hidden = is_members_hidden self.is_restricted = is_restricted self.is_creator = is_creator + self.is_admin = is_admin self.is_scam = is_scam self.is_fake = is_fake + self.is_deactivated = is_deactivated self.is_support = is_support self.is_stories_hidden = is_stories_hidden self.is_stories_unavailable = is_stories_unavailable @@ -215,9 +241,12 @@ def __init__( self.first_name = first_name self.last_name = last_name self.photo = photo + self.stories = stories + self.wallpaper = wallpaper self.bio = bio self.description = description self.dc_id = dc_id + self.folder_id = folder_id self.has_protected_content = has_protected_content self.invite_link = invite_link self.pinned_message = pinned_message @@ -230,8 +259,9 @@ def __init__( self.linked_chat = linked_chat self.send_as_chat = send_as_chat self.available_reactions = available_reactions - self.color = color - self.background_emoji_id = background_emoji_id + self.level = level + self.reply_color = reply_color + self.profile_color = profile_color @staticmethod def _parse_user_chat(client, user: raw.types.User) -> "Chat": @@ -245,15 +275,15 @@ def _parse_user_chat(client, user: raw.types.User) -> "Chat": is_scam=getattr(user, "scam", None), is_fake=getattr(user, "fake", None), is_support=getattr(user, "support", None), - username=user.username, + username=user.username or (user.usernames[0].username if user.usernames else None), usernames=types.List([types.Username._parse(r) for r in user.usernames]) or None, first_name=user.first_name, last_name=user.last_name, photo=types.ChatPhoto._parse(client, user.photo, peer_id, user.access_hash), restrictions=types.List([types.Restriction._parse(r) for r in user.restriction_reason]) or None, dc_id=getattr(getattr(user, "photo", None), "dc_id", None), - color=getattr(user, "color", None), - background_emoji_id=getattr(user, "background_emoji_id", None), + reply_color=types.ChatColor._parse(getattr(user, "color", None)), + profile_color=types.ChatColor._parse_profile_color(getattr(user, "profile_color", None)), client=client ) @@ -261,12 +291,15 @@ def _parse_user_chat(client, user: raw.types.User) -> "Chat": def _parse_chat_chat(client, chat: raw.types.Chat) -> "Chat": peer_id = -chat.id usernames = getattr(chat, "usernames", []) + admin_rights = getattr(chat, "admin_rights", None) return Chat( id=peer_id, type=enums.ChatType.GROUP, title=chat.title, is_creator=getattr(chat, "creator", None), + is_admin=True if admin_rights else None, + is_deactivated=getattr(chat, "deactivated", None), usernames=types.List([types.Username._parse(r) for r in usernames]) or None, photo=types.ChatPhoto._parse(client, getattr(chat, "photo", None), peer_id, 0), permissions=types.ChatPermissions._parse(getattr(chat, "default_banned_rights", None)), @@ -281,14 +314,16 @@ def _parse_channel_chat(client, channel: raw.types.Channel) -> "Chat": peer_id = utils.get_channel_id(channel.id) restriction_reason = getattr(channel, "restriction_reason", []) usernames = getattr(channel, "usernames", []) + admin_rights = getattr(channel, "admin_rights", None) return Chat( id=peer_id, type=enums.ChatType.SUPERGROUP if getattr(channel, "megagroup", None) else enums.ChatType.CHANNEL, - is_forum=getattr(channel, "forum", None), + is_forum=getattr(channel, "forum", None) if getattr(channel, "megagroup", None) else None, is_verified=getattr(channel, "verified", None), is_restricted=getattr(channel, "restricted", None), is_creator=getattr(channel, "creator", None), + is_admin=True if admin_rights else None, is_scam=getattr(channel, "scam", None), is_fake=getattr(channel, "fake", None), is_stories_hidden=getattr(channel, "stories_hidden", None), @@ -303,8 +338,9 @@ def _parse_channel_chat(client, channel: raw.types.Channel) -> "Chat": members_count=getattr(channel, "participants_count", None), dc_id=getattr(getattr(channel, "photo", None), "dc_id", None), has_protected_content=getattr(channel, "noforwards", None), - color=getattr(channel, "color", None), - background_emoji_id=getattr(channel, "background_emoji_id", None), + level=getattr(channel, "level", None), + reply_color=types.ChatColor._parse(getattr(channel, "color", None)), + profile_color=types.ChatColor._parse(getattr(channel, "profile_color", None)), client=client ) @@ -330,9 +366,9 @@ def _parse( @staticmethod def _parse_dialog(client, peer, users: dict, chats: dict): - if isinstance(peer, raw.types.PeerUser): + if isinstance(peer, (raw.types.PeerUser, raw.types.InputPeerUser)): return Chat._parse_user_chat(client, users[peer.user_id]) - elif isinstance(peer, raw.types.PeerChat): + elif isinstance(peer, (raw.types.PeerChat, raw.types.InputPeerChat)): return Chat._parse_chat_chat(client, chats[peer.chat_id]) else: return Chat._parse_channel_chat(client, chats[peer.channel_id]) @@ -347,12 +383,27 @@ async def _parse_full(client, chat_full: Union[raw.types.messages.ChatFull, raw. parsed_chat = Chat._parse_user_chat(client, users[full_user.id]) parsed_chat.bio = full_user.about + parsed_chat.folder_id = getattr(full_user, "folder_id", None) if full_user.pinned_msg_id: parsed_chat.pinned_message = await client.get_messages( parsed_chat.id, message_ids=full_user.pinned_msg_id ) + + if getattr(full_user, "stories"): + peer_stories: raw.types.PeerStories = full_user.stories + parsed_chat.stories = types.List( + [ + await types.Story._parse( + client, story, users, chats, peer_stories.peer + ) + for story in peer_stories.stories + ] + ) or None + + if getattr(full_user, "wallpaper") and isinstance(full_user.wallpaper, raw.types.WallPaper): + parsed_chat.wallpaper = types.Document._parse(client, full_user.wallpaper.document, "wallpaper.jpg") else: full_chat = chat_full.full_chat chat_raw = chats[full_chat.id] @@ -370,7 +421,8 @@ async def _parse_full(client, chat_full: Union[raw.types.messages.ChatFull, raw. # TODO: Add StickerSet type parsed_chat.can_set_sticker_set = full_chat.can_set_stickers parsed_chat.sticker_set_name = getattr(full_chat.stickerset, "short_name", None) - parsed_chat.is_participants_hidden = full_chat.participants_hidden + parsed_chat.is_members_hidden = full_chat.participants_hidden + parsed_chat.folder_id = getattr(full_chat, "folder_id", None) linked_chat_raw = chats.get(full_chat.linked_chat_id, None) @@ -387,6 +439,20 @@ async def _parse_full(client, chat_full: Union[raw.types.messages.ChatFull, raw. parsed_chat.send_as_chat = Chat._parse_chat(client, send_as_raw) + if getattr(full_chat, "stories"): + peer_stories: raw.types.PeerStories = full_chat.stories + parsed_chat.stories = types.List( + [ + await types.Story._parse( + client, story, users, chats, peer_stories.peer + ) + for story in peer_stories.stories + ] + ) or None + + if getattr(full_chat, "wallpaper") and isinstance(full_chat.wallpaper, raw.types.WallPaper): + parsed_chat.wallpaper = types.Document._parse(client, full_chat.wallpaper.document, "wallpaper.jpg") + if full_chat.pinned_msg_id: parsed_chat.pinned_message = await client.get_messages( parsed_chat.id, @@ -411,7 +477,7 @@ def _parse_chat(client, chat: Union[raw.types.Chat, raw.types.User, raw.types.Ch @property def full_name(self) -> str: - return " ".join(filter(None, [self.first_name, self.last_name])) or None + return " ".join(filter(None, [self.first_name, self.last_name])) or self.title or None async def archive(self): """Bound method *archive* of :obj:`~pyrogram.types.Chat`. @@ -595,6 +661,31 @@ async def set_photo( video_start_ts=video_start_ts ) + async def set_ttl(self, ttl_seconds: int) -> bool: + """Bound method *set_ttl* of :obj:`~pyrogram.types.Chat`. + + Use as a shortcut for: + + .. code-block:: python + + await client.set_chat_ttl( + chat_id=chat_id, + ttl_seconds=ttl_seconds + ) + + Example: + .. code-block:: python + + await chat.set_ttl(86400) + + Returns: + ``bool``: True on success. + """ + return await self._client.set_chat_ttl( + chat_id=self.id, + ttl_seconds=ttl_seconds + ) + async def ban_member( self, user_id: Union[int, str], @@ -880,7 +971,7 @@ def get_members( query: str = "", limit: int = 0, filter: "enums.ChatMembersFilter" = enums.ChatMembersFilter.SEARCH - ) -> Optional[AsyncGenerator["types.ChatMember", None]]: + ) -> AsyncGenerator["types.ChatMember", None]: """Bound method *get_members* of :obj:`~pyrogram.types.Chat`. Use as a shortcut for: @@ -1016,3 +1107,50 @@ async def unpin_all_messages(self) -> bool: """ return await self._client.unpin_all_chat_messages(self.id) + + async def mute(self, mute_until: datetime = None) -> bool: + """Bound method *mute* of :obj:`~pyrogram.types.Chat`. + + Use as a shortcut for: + + .. code-block:: python + + client.update_chat_notifications(chat_id, mute=True, mute_until=mute_until) + + Parameters: + mute (``bool``, *optional*): + Pass True if you want to mute chat. + + until_date (:py:obj:`~datetime.datetime`, *optional*): + Date when the user will be unmuted. Defaults to forever. + + Example: + .. code-block:: python + + chat.mute() + + Returns: + ``bool``: On success, True is returned. + """ + + return await self._client.update_chat_notifications(self.id, mute=True, mute_until=mute_until) + + async def unmute(self) -> bool: + """Bound method *unmute* of :obj:`~pyrogram.types.Chat`. + + Use as a shortcut for: + + .. code-block:: python + + client.update_chat_notifications(chat_id, mute=False) + + Example: + .. code-block:: python + + chat.unmute() + + Returns: + ``bool``: On success, True is returned. + """ + + return await self._client.update_chat_notifications(self.id, mute=False) diff --git a/pyrogram/types/user_and_chats/chat_color.py b/pyrogram/types/user_and_chats/chat_color.py new file mode 100644 index 000000000..49dee0ad6 --- /dev/null +++ b/pyrogram/types/user_and_chats/chat_color.py @@ -0,0 +1,64 @@ +# 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 Optional, Union + +from pyrogram import raw +from pyrogram import enums +from ..object import Object + + +class ChatColor(Object): + """Reply or profile color status. + + Parameters: + color (:obj:`~pyrogram.enums.ReplyColor` | :obj:`~pyrogram.enums.ProfileColor`, *optional*): + Color type. + + background_emoji_id (``int``, *optional*): + Unique identifier of the custom emoji. + """ + + def __init__( + self, + *, + color: Union["enums.ReplyColor", "enums.ProfileColor"] = None, + background_emoji_id: int = None + ): + self.color = color + self.background_emoji_id = background_emoji_id + + @staticmethod + def _parse(color: "raw.types.PeerColor" = None) -> Optional["ChatColor"]: + if not color: + return None + + return ChatColor( + color=enums.ReplyColor(color.color) if getattr(color, "color", None) else None, + background_emoji_id=getattr(color, "background_emoji_id", None) + ) + + @staticmethod + def _parse_profile_color(color: "raw.types.PeerColor" = None) -> Optional["ChatColor"]: + if not color: + return None + + return ChatColor( + color=enums.ProfileColor(color.color) if getattr(color, "color", None) else None, + background_emoji_id=getattr(color, "background_emoji_id", None) + ) diff --git a/pyrogram/types/user_and_chats/chat_event.py b/pyrogram/types/user_and_chats/chat_event.py index 2c4ddb411..4f9da9cb7 100644 --- a/pyrogram/types/user_and_chats/chat_event.py +++ b/pyrogram/types/user_and_chats/chat_event.py @@ -454,16 +454,16 @@ async def _parse( action = enums.ChatEventAction.INVITE_LINK_DELETED elif isinstance(action, raw.types.ChannelAdminLogEventActionCreateTopic): - created_forum_topic = types.ForumTopic._parse(action.topic) + created_forum_topic = types.ForumTopic._parse(client, action.topic, users=users, chats=chats) action = enums.ChatEventAction.CREATED_FORUM_TOPIC elif isinstance(action, raw.types.ChannelAdminLogEventActionEditTopic): - old_forum_topic = types.ForumTopic._parse(action.prev_topic) - new_forum_topic = types.ForumTopic._parse(action.new_topic) + old_forum_topic = types.ForumTopic._parse(client, action.prev_topic, users=users, chats=chats) + new_forum_topic = types.ForumTopic._parse(client, action.new_topic, users=users, chats=chats) action = enums.ChatEventAction.EDITED_FORUM_TOPIC elif isinstance(action, raw.types.ChannelAdminLogEventActionDeleteTopic): - created_forum_topic = types.ForumTopic._parse(action.topic) + created_forum_topic = types.ForumTopic._parse(client, action.topic, users=users, chats=chats) action = enums.ChatEventAction.DELETED_FORUM_TOPIC else: diff --git a/pyrogram/types/user_and_chats/chat_permissions.py b/pyrogram/types/user_and_chats/chat_permissions.py index d1c99c70d..a96f59dac 100644 --- a/pyrogram/types/user_and_chats/chat_permissions.py +++ b/pyrogram/types/user_and_chats/chat_permissions.py @@ -56,7 +56,7 @@ class ChatPermissions(Object): can_manage_topics (``bool``, *optional*): True, if the user is allowed to create, rename, close, and reopen forum topics. - supergroups only. + Supergroups only. """ def __init__( diff --git a/pyrogram/types/user_and_chats/chat_privileges.py b/pyrogram/types/user_and_chats/chat_privileges.py index 3507614c2..ab53e7640 100644 --- a/pyrogram/types/user_and_chats/chat_privileges.py +++ b/pyrogram/types/user_and_chats/chat_privileges.py @@ -32,6 +32,10 @@ class ChatPrivileges(Object): can_delete_messages (``bool``, *optional*): True, if the administrator can delete messages of other users. + can_delete_stories (``bool``, *optional*): + Channels only. + True, if the administrator can delete stories of other users. + can_manage_video_chats (``bool``, *optional*): Groups and supergroups only. True, if the administrator can manage video chats (also called group calls). @@ -51,10 +55,18 @@ class ChatPrivileges(Object): Channels only. True, if the administrator can post messages in the channel. + can_post_stories (``bool``, *optional*): + Channels only. + True, if the administrator can post stories in the channel. + can_edit_messages (``bool``, *optional*): Channels only. True, if the administrator can edit messages of other users and can pin messages. + can_edit_stories (``bool``, *optional*): + Channels only. + True, if the administrator can edit stories of other users. + can_invite_users (``bool``, *optional*): True, if the user is allowed to invite new users to the chat. @@ -63,7 +75,7 @@ class ChatPrivileges(Object): True, if the user is allowed to pin messages. can_manage_topics (``bool``, *optional*): - supergroups only. + Supergroups only. True, if the user is allowed to create, rename, close, and reopen forum topics. is_anonymous (``bool``, *optional*): @@ -75,12 +87,15 @@ def __init__( *, can_manage_chat: bool = True, can_delete_messages: bool = False, + can_delete_stories: bool = False, # Channels only can_manage_video_chats: bool = False, # Groups and supergroups only can_restrict_members: bool = False, can_promote_members: bool = False, can_change_info: bool = False, can_post_messages: bool = False, # Channels only + can_post_stories: bool = False, # Channels only can_edit_messages: bool = False, # Channels only + can_edit_stories: bool = False, # Channels only can_invite_users: bool = False, can_pin_messages: bool = False, # Groups and supergroups only can_manage_topics: bool = False, # Supergroups only @@ -90,12 +105,15 @@ def __init__( self.can_manage_chat: bool = can_manage_chat self.can_delete_messages: bool = can_delete_messages + self.can_delete_stories: bool = can_delete_stories self.can_manage_video_chats: bool = can_manage_video_chats self.can_restrict_members: bool = can_restrict_members self.can_promote_members: bool = can_promote_members self.can_change_info: bool = can_change_info self.can_post_messages: bool = can_post_messages + self.can_post_stories: bool = can_post_stories self.can_edit_messages: bool = can_edit_messages + self.can_edit_stories: bool = can_edit_stories self.can_invite_users: bool = can_invite_users self.can_pin_messages: bool = can_pin_messages self.can_manage_topics: bool = can_manage_topics @@ -106,12 +124,15 @@ def _parse(admin_rights: "raw.base.ChatAdminRights") -> "ChatPrivileges": return ChatPrivileges( can_manage_chat=admin_rights.other, can_delete_messages=admin_rights.delete_messages, + can_delete_stories=admin_rights.delete_stories, can_manage_video_chats=admin_rights.manage_call, can_restrict_members=admin_rights.ban_users, can_promote_members=admin_rights.add_admins, can_change_info=admin_rights.change_info, can_post_messages=admin_rights.post_messages, + can_post_stories=admin_rights.post_stories, can_edit_messages=admin_rights.edit_messages, + can_edit_stories=admin_rights.edit_stories, can_invite_users=admin_rights.invite_users, can_pin_messages=admin_rights.pin_messages, can_manage_topics=admin_rights.manage_topics, diff --git a/pyrogram/types/user_and_chats/folder.py b/pyrogram/types/user_and_chats/folder.py new file mode 100644 index 000000000..60f2b0e2c --- /dev/null +++ b/pyrogram/types/user_and_chats/folder.py @@ -0,0 +1,443 @@ +# 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, Union + +import pyrogram +from pyrogram import raw +from pyrogram import types +from pyrogram import utils +from ..object import Object + + +class Folder(Object): + """A user's folder. + + Parameters: + id (``int``): + The folder id. + + title (``str``): + The folder title. + + included_chats (List of :obj:`~pyrogram.types.Chat`, *optional*): + A list of included chats in folder. + + excluded_chats (List of :obj:`~pyrogram.types.Chat`, *optional*): + A list of excluded chats in folder. + + pinned_chats (List of :obj:`~pyrogram.types.Chat`, *optional*): + A list of pinned chats in folder. + + contacts (``bool``, *optional*): + True, if the folder includes contacts. + + non_contacts (``bool``, *optional*): + True, if the folder includes non contacts. + + groups (``bool``, *optional*): + True, if the folder includes groups. + + channels (``bool``, *optional*): + True, if the folder includes channels. + + bots (``bool``, *optional*): + True, if the folder includes bots. + + exclude_muted (``bool``, *optional*): + True, if the folder exclude muted. + + exclude_read (``bool``, *optional*): + True, if the folder exclude read. + + exclude_archived (``bool``, *optional*): + True, if the folder exclude archived. + + emoji (``str``, *optional*): + Folder emoji. + """ + + def __init__( + self, + *, + client: "pyrogram.Client" = None, + id: int, + title: str, + included_chats: List["types.Chat"] = None, + excluded_chats: List["types.Chat"] = None, + pinned_chats: List["types.Chat"] = None, + contacts: bool = None, + non_contacts: bool = None, + groups: bool = None, + channels: bool = None, + bots: bool = None, + exclude_muted: bool = None, + exclude_read: bool = None, + exclude_archived: bool = None, + emoji: str = None, + has_my_invites: bool = None + ): + super().__init__(client) + + self.id = id + self.title = title + self.included_chats = included_chats + self.excluded_chats = excluded_chats + self.pinned_chats = pinned_chats + self.contacts = contacts + self.non_contacts = non_contacts + self.groups = groups + self.channels = channels + self.bots = bots + self.exclude_muted = exclude_muted + self.exclude_read = exclude_read + self.exclude_archived = exclude_archived + self.emoji = emoji + self.has_my_invites = has_my_invites + + @staticmethod + def _parse(client, folder: "raw.types.DialogFilter", users, chats) -> "Folder": + included_chats = [] + excluded_chats = [] + pinned_chats = [] + + for peer in folder.include_peers: + try: + included_chats.append(types.Chat._parse_dialog(client, peer, users, chats)) + except KeyError: + pass + + if getattr(folder, "exclude_peers", None): + for peer in folder.exclude_peers: + try: + excluded_chats.append(types.Chat._parse_dialog(client, peer, users, chats)) + except KeyError: + pass + + for peer in folder.pinned_peers: + try: + pinned_chats.append(types.Chat._parse_dialog(client, peer, users, chats)) + except KeyError: + pass + + return Folder( + id=folder.id, + title=folder.title, + included_chats=types.List(included_chats) or None, + excluded_chats=types.List(excluded_chats) or None, + pinned_chats=types.List(pinned_chats) or None, + contacts=getattr(folder, "contacts", None), + non_contacts=getattr(folder, "non_contacts", None), + groups=getattr(folder, "groups", None), + channels=getattr(folder, "broadcasts", None), + bots=getattr(folder, "bots", None), + exclude_muted=getattr(folder, "exclude_muted", None), + exclude_read=getattr(folder, "exclude_read", None), + exclude_archived=getattr(folder, "exclude_archived", None), + emoji=folder.emoticon or None, + has_my_invites=getattr(folder, "has_my_invites", None), + client=client + ) + + async def delete(self): + """Bound method *delete* of :obj:`~pyrogram.types.Folder`. + + Use as a shortcut for: + + .. code-block:: python + + await client.delete_folder(123456789) + + Example: + .. code-block:: python + + await folder.delete() + + Returns: + True on success. + """ + + return await self._client.delete_folder(self.id) + + async def update( + self, + included_chats: List[Union[int, str]] = None, + excluded_chats: List[Union[int, str]] = None, + pinned_chats: List[Union[int, str]] = None, + title: str = None, + contacts: bool = None, + non_contacts: bool = None, + groups: bool = None, + channels: bool = None, + bots: bool = None, + exclude_muted: bool = None, + exclude_read: bool = None, + exclude_archived: bool = None, + emoji: str = None + ): + """Bound method *update_peers* of :obj:`~pyrogram.types.Folder`. + + Use as a shortcut for: + + .. code-block:: python + + await client.update_folder( + folder_id, + title="New folder", + included_chats=["me"] + ) + + Example: + .. code-block:: python + + await folder.update(included_chats=["me"]) + + Parameters: + included_chats (``int`` | ``str`` | List of ``int`` or ``str``, *optional*): + Users or chats that should added in the folder + You can pass an ID (int), username (str) or phone number (str). + Multiple users can be added by passing a list of IDs, usernames or phone numbers. + + excluded_chats (``int`` | ``str`` | List of ``int`` or ``str``, *optional*): + Users or chats that should excluded from the folder + You can pass an ID (int), username (str) or phone number (str). + Multiple users can be added by passing a list of IDs, usernames or phone numbers. + + pinned_chats (``int`` | ``str`` | List of ``int`` or ``str``, *optional*): + Users or chats that should pinned in the folder + You can pass an ID (int), username (str) or phone number (str). + Multiple users can be added by passing a list of IDs, usernames or phone numbers. + + title (``str``, *optional*): + A folder title was changed to this value. + + contacts (``bool``, *optional*): + Pass True if folder should contain contacts. + + non_contacts (``bool``, *optional*): + Pass True if folder should contain non contacts. + + groups (``bool``, *optional*): + Pass True if folder should contain groups. + + channels (``bool``, *optional*): + Pass True if folder should contain channels. + + bots (``bool``, *optional*): + Pass True if folder should contain bots. + + exclude_muted (``bool``, *optional*): + Pass True if folder should exclude muted users. + + exclude_archived (``bool``, *optional*): + Pass True if folder should exclude archived users. + + emoji (``str``, *optional*): + Folder emoji. + Pass None to leave the folder icon as default. + + Returns: + True on success. + """ + if not included_chats: + included_chats = [i.id for i in self.included_chats or []] + + if not included_chats: + excluded_chats = [i.id for i in self.excluded_chats or []] + + if not included_chats: + pinned_chats = [i.id for i in self.pinned_chats or []] + + return await self._client.update_folder( + folder_id=self.id, + title=title or self.title, + included_chats=included_chats, + excluded_chats=excluded_chats, + pinned_chats=pinned_chats, + contacts=contacts or self.contacts, + non_contacts=non_contacts or self.non_contacts, + groups=groups or self.groups, + channels=channels or self.channels, + bots=bots or self.bots, + exclude_muted=exclude_muted or self.exclude_muted, + exclude_read=exclude_read or self.exclude_read, + exclude_archived=exclude_archived or self.exclude_archived, + emoji=emoji or self.emoji + ) + + async def include_chat(self, chat_id: Union[int, str]): + """Bound method *include_chat* of :obj:`~pyrogram.types.Folder`. + + Use as a shortcut for: + + .. code-block:: python + + await client.update_folder( + folder_id=123456789, + included_chats=[chat_id], + excluded_chats=[...], + pinned_chats=[...] + ) + + Example: + .. code-block:: python + + await folder.include_chat(chat_id) + + Parameters: + chat_id (``int`` | ``str``): + Unique identifier for the target chat or username of the target user/channel/supergroup + (in the format @username). + + Returns: + True on success. + """ + + return await self.update( + included_chats=[i.id for i in self.included_chats or []] + [chat_id], + excluded_chats=[i.id for i in self.excluded_chats or []], + pinned_chats=[i.id for i in self.pinned_chats or []] + ) + + async def exclude_chat(self, chat_id: Union[int, str]): + """Bound method *exclude_chat* of :obj:`~pyrogram.types.Folder`. + + Use as a shortcut for: + + .. code-block:: python + + await client.update_folder( + folder_id=123456789, + included_chats=[...], + excluded_chats=[chat_id], + pinned_chats=[...] + ) + + Example: + .. code-block:: python + + await folder.exclude_chat(chat_id) + + Parameters: + chat_id (``int`` | ``str``): + Unique identifier for the target chat or username of the target user/channel/supergroup + (in the format @username). + + Returns: + True on success. + """ + + return await self.update( + included_chats=[i.id for i in self.included_chats or []], + excluded_chats=[i.id for i in self.excluded_chats or []] + [chat_id], + pinned_chats=[i.id for i in self.pinned_chats or []], + ) + + async def pin_chat(self, chat_id: Union[int, str]): + """Bound method *pin_chat* of :obj:`~pyrogram.types.Folder`. + + Use as a shortcut for: + + .. code-block:: python + + await client.update_folder( + folder_id=123456789, + included_chats=[chat_id], + excluded_chats=[chat_id], + pinned_chats=[...] + ) + + Example: + .. code-block:: python + + await folder.pin_chat(chat_id) + + Parameters: + chat_id (``int`` | ``str``): + Unique identifier for the target chat or username of the target user/channel/supergroup + (in the format @username). + + Returns: + True on success. + """ + + return await self.update( + included_chats=[i.id for i in self.included_chats or []] + [chat_id], + excluded_chats=[i.id for i in self.excluded_chats or []], + pinned_chats=[i.id for i in self.pinned_chats or []] + [chat_id] + ) + + async def remove_chat(self, chat_id: Union[int, str]): + """Bound method *remove_chat* of :obj:`~pyrogram.types.Folder`. + + Remove chat from included, excluded and pinned chats. + + Use as a shortcut for: + + .. code-block:: python + + await client.update_folder( + folder_id=123456789, + included_chats=[...], + excluded_chats=[...], + pinned_chats=[...] + ) + + Example: + .. code-block:: python + + await folder.remove_chat(chat_id) + + Parameters: + chat_id (``int`` | ``str``): + Unique identifier for the target chat or username of the target user/channel/supergroup + (in the format @username). + + Returns: + True on success. + """ + peer = await self._client.resolve_peer(chat_id) + peer_id = utils.get_peer_id(peer) + + return await self.update( + included_chats=[i.id for i in self.included_chats or [] if peer_id != i.id], + excluded_chats=[i.id for i in self.excluded_chats or [] if peer_id != i.id], + pinned_chats=[i.id for i in self.pinned_chats or [] if peer_id != i.id] + ) + + async def export_link(self): + """Bound method *export_link* of :obj:`~pyrogram.types.Folder`. + + Use as a shortcut for: + + .. code-block:: python + + await client.export_folder_link(123456789) + + Example: + .. code-block:: python + + await folder.export_link() + + Returns: + ``str``: On success, a link to the folder as string is returned. + """ + + return await self._client.export_folder_link( + folder_id=self.id + ) diff --git a/pyrogram/types/user_and_chats/user.py b/pyrogram/types/user_and_chats/user.py index 70745b983..a705fd849 100644 --- a/pyrogram/types/user_and_chats/user.py +++ b/pyrogram/types/user_and_chats/user.py @@ -100,13 +100,13 @@ class User(Object, Update): is_premium (``bool``, *optional*): True, if this user is a premium user. - is_close_friend (``bool``): + is_close_friend (``bool``, *optional*): True, if this user is a close friend. - is_stories_hidden (``bool``): + is_stories_hidden (``bool``, *optional*): True, if this user has hidden stories. - is_stories_unavailable (``bool``): + is_stories_unavailable (``bool``, *optional*): True, if this chat stories is unavailable. first_name (``str``, *optional*): @@ -164,11 +164,11 @@ class User(Object, Update): ``user.mention("another name")`` for a custom name. To choose a different style ("html" or "md"/"markdown") use ``user.mention(style="md")``. - color (``int``, *optional*) - Chat color. + reply_color (:obj:`~pyrogram.types.ChatColor`, *optional*) + Chat reply color. - background_emoji_id (``int``, *optional*) - Chat background emoji id. + profile_color (:obj:`~pyrogram.types.ChatColor`, *optional*) + Chat profile color. """ def __init__( @@ -203,8 +203,8 @@ def __init__( phone_number: str = None, photo: "types.ChatPhoto" = None, restrictions: List["types.Restriction"] = None, - color: int = None, - background_emoji_id: int = None + reply_color: "types.ChatColor" = None, + profile_color: "types.ChatColor" = None ): super().__init__(client) @@ -236,8 +236,8 @@ def __init__( self.phone_number = phone_number self.photo = photo self.restrictions = restrictions - self.color = color - self.background_emoji_id = background_emoji_id + self.reply_color = reply_color + self.profile_color = profile_color @property def full_name(self) -> str: @@ -275,7 +275,7 @@ def _parse(client, user: "raw.base.User") -> Optional["User"]: first_name=user.first_name, last_name=user.last_name, **User._parse_status(user.status, user.bot), - username=user.username, + username=user.username or (user.usernames[0].username if user.usernames else None), usernames=types.List([types.Username._parse(r) for r in user.usernames]) or None, language_code=user.lang_code, emoji_status=types.EmojiStatus._parse(client, user.emoji_status), @@ -283,8 +283,8 @@ def _parse(client, user: "raw.base.User") -> Optional["User"]: phone_number=user.phone, photo=types.ChatPhoto._parse(client, user.photo, user.id, user.access_hash), restrictions=types.List([types.Restriction._parse(r) for r in user.restriction_reason]) or None, - color=getattr(user, "color", None), - background_emoji_id=getattr(user, "background_emoji_id", None), + reply_color=types.ChatColor._parse(getattr(user, "color", None)), + profile_color=types.ChatColor._parse_profile_color(getattr(user, "profile_color", None)), client=client ) diff --git a/pyrogram/utils.py b/pyrogram/utils.py index 7c492facc..0bc9474b4 100644 --- a/pyrogram/utils.py +++ b/pyrogram/utils.py @@ -53,7 +53,8 @@ async def ainput(prompt: str = "", *, hide: bool = False): def get_input_media_from_file_id( file_id: str, expected_file_type: FileType = None, - ttl_seconds: int = None + ttl_seconds: int = None, + has_spoiler: bool = None ) -> Union["raw.types.InputMediaPhoto", "raw.types.InputMediaDocument"]: try: decoded = FileId.decode(file_id) @@ -78,6 +79,7 @@ def get_input_media_from_file_id( access_hash=decoded.access_hash, file_reference=decoded.file_reference ), + spoiler=has_spoiler, ttl_seconds=ttl_seconds ) @@ -88,6 +90,7 @@ def get_input_media_from_file_id( access_hash=decoded.access_hash, file_reference=decoded.file_reference ), + spoiler=has_spoiler, ttl_seconds=ttl_seconds ) @@ -129,7 +132,7 @@ async def parse_messages( # 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 not isinstance(m, pyrogram.types.Message): + if not isinstance(m, types.Message): continue if m.chat: @@ -138,13 +141,10 @@ async def parse_messages( else: chat_id = 0 - # whether all messages are inside of our current chat - is_all_within_chat = True - for current_message_id in messages_with_replies: - if messages_with_replies[current_message_id].reply_to_peer_id: - is_all_within_chat = False - break - + is_all_within_chat = not any( + value.reply_to_peer_id + for value in messages_with_replies.values() + ) reply_messages: List[pyrogram.types.Message] = [] if is_all_within_chat: # fast path: fetch all messages within the same chat @@ -155,12 +155,11 @@ async def parse_messages( ) else: # slow path: fetch all messages individually - for current_message_id in messages_with_replies: - target_reply_to = messages_with_replies[current_message_id] + for target_reply_to in messages_with_replies.values(): to_be_added_msg = None the_chat_id = chat_id if target_reply_to.reply_to_peer_id: - the_chat_id = f"-100{target_reply_to.reply_to_peer_id.channel_id}" + the_chat_id = get_channel_id(target_reply_to.reply_to_peer_id.channel_id) to_be_added_msg = await client.get_messages( chat_id=the_chat_id, message_ids=target_reply_to.reply_to_msg_id, @@ -180,13 +179,12 @@ async def parse_messages( reply_id = reply_to.reply_to_msg_id for reply in reply_messages: - if reply.id == reply_id: - if not reply.forum_topic_created: - message.reply_to_message = reply + if reply.id == reply_id and not reply.forum_topic_created: + message.reply_to_message = reply if message_reply_to_story: for m in parsed_messages: - if not isinstance(m, pyrogram.types.Message): + if not isinstance(m, types.Message): continue if m.chat: @@ -196,7 +194,7 @@ async def parse_messages( chat_id = 0 reply_messages = {} - for msg_id in message_reply_to_story.keys(): + for msg_id in message_reply_to_story: reply_messages[msg_id] = await client.get_stories( message_reply_to_story[msg_id]['user_id'], message_reply_to_story[msg_id]['story_id'] @@ -276,47 +274,34 @@ def unpack_inline_message_id(inline_message_id: str) -> "raw.base.InputBotInline MIN_CHANNEL_ID = -1002147483647 MAX_CHANNEL_ID = -1000000000000 -MIN_CHAT_ID = -2147483647 +MIN_CHAT_ID = -999999999999 MAX_USER_ID_OLD = 2147483647 MAX_USER_ID = 999999999999 -def get_raw_peer_id(peer: raw.base.Peer) -> Optional[int]: +def get_raw_peer_id(peer: Union[raw.base.Peer, raw.base.InputPeer]) -> Optional[int]: """Get the raw peer id from a Peer object""" - if isinstance(peer, raw.types.PeerUser): - return peer.user_id - - if isinstance(peer, raw.types.PeerChat): - return peer.chat_id - - if isinstance(peer, raw.types.PeerChannel): - return peer.channel_id - - return None - -def get_input_peer_id(peer: raw.base.InputPeer) -> Optional[int]: - """Get the raw peer id from a InputPeer object""" - if isinstance(peer, raw.types.InputPeerUser): + if isinstance(peer, (raw.types.PeerUser, raw.types.InputPeerUser)): return peer.user_id - if isinstance(peer, raw.types.InputPeerChat): + if isinstance(peer, (raw.types.PeerChat, raw.types.InputPeerChat)): return peer.chat_id - if isinstance(peer, raw.types.InputPeerChannel): + if isinstance(peer, (raw.types.PeerChannel, raw.types.InputPeerChannel)): return peer.channel_id return None -def get_peer_id(peer: raw.base.Peer) -> int: +def get_peer_id(peer: Union[raw.base.Peer, raw.base.InputPeer]) -> int: """Get the non-raw peer id from a Peer object""" - if isinstance(peer, raw.types.PeerUser): + if isinstance(peer, (raw.types.PeerUser, raw.types.InputPeerUser)): return peer.user_id - if isinstance(peer, raw.types.PeerChat): + if isinstance(peer, (raw.types.PeerChat, raw.types.InputPeerChat)): return -peer.chat_id - if isinstance(peer, raw.types.PeerChannel): + if isinstance(peer, (raw.types.PeerChannel, raw.types.InputPeerChannel)): return MAX_CHANNEL_ID - peer.channel_id raise ValueError(f"Peer type invalid: {peer}") @@ -334,12 +319,14 @@ def get_peer_type(peer_id: int) -> str: raise ValueError(f"Peer id invalid: {peer_id}") + def get_reply_to( reply_to_message_id: Optional[int] = None, message_thread_id: Optional[int] = None, reply_to_peer: Optional[raw.base.InputPeer] = None, quote_text: Optional[str] = None, quote_entities: Optional[List[raw.base.MessageEntity]] = None, + quote_offset: Optional[int] = None, reply_to_story_id: Optional[int] = None ) -> Optional[Union[raw.types.InputReplyToMessage, raw.types.InputReplyToStory]]: """Get InputReply for reply_to argument""" @@ -353,10 +340,12 @@ def get_reply_to( reply_to_peer_id=reply_to_peer, quote_text=quote_text, quote_entities=quote_entities, + quote_offset=quote_offset, ) return None + def get_channel_id(peer_id: int) -> int: return MAX_CHANNEL_ID - peer_id @@ -469,73 +458,17 @@ async def parse_text_entities( "entities": entities } -async def parse_caption_entities( - client: "pyrogram.Client", - text: str, - parse_mode: enums.ParseMode, - entities: List["types.MessageEntity"] -) -> Dict[str, Union[str, List[raw.base.MessageEntity]]]: - if entities: - # Inject the client instance because parsing user mentions requires it - for entity in entities: - entity._client = client - - text, entities = text, [await entity.write() for entity in entities] or None - else: - text, entities = (await client.parser.parse(text, parse_mode)).values() - - return { - "caption": text, - "entities": entities - } - -async def parse_caption_entities( - client: "pyrogram.Client", - text: str, - parse_mode: enums.ParseMode, - entities: List["types.MessageEntity"] -) -> Dict[str, Union[str, List[raw.base.MessageEntity]]]: - if entities: - # Inject the client instance because parsing user mentions requires it - for entity in entities: - entity._client = client - - text, entities = text, [await entity.write() for entity in entities] or None - else: - text, entities = (await client.parser.parse(text, parse_mode)).values() - - return { - "caption": text, - "entities": entities - } - def zero_datetime() -> datetime: return datetime.fromtimestamp(0, timezone.utc) +def max_datetime() -> datetime: + return datetime.fromtimestamp((1 << 31) - 1, timezone.utc) + def timestamp_to_datetime(ts: Optional[int]) -> Optional[datetime]: return datetime.fromtimestamp(ts) if ts else None def datetime_to_timestamp(dt: Optional[datetime]) -> Optional[int]: return int(dt.timestamp()) if dt else None - - -def get_reply_head_fm(message_thread_id: int, reply_to_message_id: int) -> raw.types.InputReplyToMessage: - reply_to = None - if ( - reply_to_message_id or - message_thread_id - ): - if not reply_to_message_id: - reply_to = raw.types.InputReplyToMessage( - reply_to_msg_id=message_thread_id, - top_msg_id=message_thread_id - ) - else: - reply_to = raw.types.InputReplyToMessage( - reply_to_msg_id=reply_to_message_id, - top_msg_id=message_thread_id - ) - return reply_to diff --git a/tests/parser/test_html.py b/tests/parser/test_html.py index b9138f3cf..5f1c4fd89 100644 --- a/tests/parser/test_html.py +++ b/tests/parser/test_html.py @@ -100,6 +100,19 @@ def test_html_unparse_pre(): assert HTML.unparse(text=text, entities=entities) == expected +def test_html_unparse_blockquote(): + expected = """
Quote text
+ from pyrogram""" + + text = """Quote text + from pyrogram""" + + entities = pyrogram.types.List([pyrogram.types.MessageEntity(type=pyrogram.enums.MessageEntityType.BLOCKQUOTE, offset=0, + length=10)]) + + assert HTML.unparse(text=text, entities=entities) == expected + + def test_html_unparse_mixed(): expected = "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd" \ "eeeeeeeeeeffffffffffgggggggggghhhhhhhhhh" diff --git a/tests/parser/test_markdown.py b/tests/parser/test_markdown.py new file mode 100644 index 000000000..81aaacb33 --- /dev/null +++ b/tests/parser/test_markdown.py @@ -0,0 +1,115 @@ +# 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 . + +import pyrogram +from pyrogram.parser.markdown import Markdown + + +# expected: the expected unparsed Markdown +# text: original text without entities +# entities: message entities coming from the server + +def test_markdown_unparse_bold(): + expected = "**bold**" + text = "bold" + entities = pyrogram.types.List( + [pyrogram.types.MessageEntity(type=pyrogram.enums.MessageEntityType.BOLD, offset=0, length=4)]) + + assert Markdown.unparse(text=text, entities=entities) == expected + + +def test_markdown_unparse_italic(): + expected = "__italic__" + text = "italic" + entities = pyrogram.types.List( + [pyrogram.types.MessageEntity(type=pyrogram.enums.MessageEntityType.ITALIC, offset=0, length=6)]) + + assert Markdown.unparse(text=text, entities=entities) == expected + + +def test_markdown_unparse_strike(): + expected = "~~strike~~" + text = "strike" + entities = pyrogram.types.List( + [pyrogram.types.MessageEntity(type=pyrogram.enums.MessageEntityType.STRIKETHROUGH, offset=0, length=6)]) + + assert Markdown.unparse(text=text, entities=entities) == expected + + +def test_markdown_unparse_spoiler(): + expected = "||spoiler||" + text = "spoiler" + entities = pyrogram.types.List( + [pyrogram.types.MessageEntity(type=pyrogram.enums.MessageEntityType.SPOILER, offset=0, length=7)]) + + assert Markdown.unparse(text=text, entities=entities) == expected + + +def test_markdown_unparse_url(): + expected = '[URL](https://pyrogram.org/)' + text = "URL" + entities = pyrogram.types.List([pyrogram.types.MessageEntity(type=pyrogram.enums.MessageEntityType.TEXT_LINK, + offset=0, length=3, url='https://pyrogram.org/')]) + + assert Markdown.unparse(text=text, entities=entities) == expected + + +def test_markdown_unparse_code(): + expected = '`code`' + text = "code" + entities = pyrogram.types.List( + [pyrogram.types.MessageEntity(type=pyrogram.enums.MessageEntityType.CODE, offset=0, length=4)]) + + assert Markdown.unparse(text=text, entities=entities) == expected + + +def test_markdown_unparse_pre(): + expected = """```python +for i in range(10): + print(i) +```""" + + text = """for i in range(10): + print(i)""" + + entities = pyrogram.types.List([pyrogram.types.MessageEntity(type=pyrogram.enums.MessageEntityType.PRE, offset=0, + length=32, language='python')]) + + assert Markdown.unparse(text=text, entities=entities) == expected + + +def test_markdown_unparse_mixed(): + expected = "**aaaaaaa__aaabbb**__~~dddddddd||ddeee~~||||eeeeeeefff||ffff`fffggggggg`ggghhhhhhhhhh" + text = "aaaaaaaaaabbbddddddddddeeeeeeeeeeffffffffffgggggggggghhhhhhhhhh" + entities = pyrogram.types.List( + [pyrogram.types.MessageEntity(type=pyrogram.enums.MessageEntityType.BOLD, offset=0, length=13), + pyrogram.types.MessageEntity(type=pyrogram.enums.MessageEntityType.ITALIC, offset=7, length=6), + pyrogram.types.MessageEntity(type=pyrogram.enums.MessageEntityType.STRIKETHROUGH, offset=13, length=13), + pyrogram.types.MessageEntity(type=pyrogram.enums.MessageEntityType.SPOILER, offset=21, length=5), + pyrogram.types.MessageEntity(type=pyrogram.enums.MessageEntityType.SPOILER, offset=26, length=10), + pyrogram.types.MessageEntity(type=pyrogram.enums.MessageEntityType.CODE, offset=40, length=10)]) + + assert Markdown.unparse(text=text, entities=entities) == expected + + +def test_markdown_unparse_no_entities(): + expected = "text" + text = "text" + entities = [] + + assert Markdown.unparse(text=text, entities=entities) == expected