diff --git a/disagreement/http.py b/disagreement/http.py index c094f69..354bff3 100644 --- a/disagreement/http.py +++ b/disagreement/http.py @@ -368,6 +368,20 @@ class HTTPClient: f"/channels/{channel_id}/messages/{message_id}/reactions/{encoded}/@me", ) + async def delete_user_reaction( + self, + channel_id: "Snowflake", + message_id: "Snowflake", + emoji: str, + user_id: "Snowflake", + ) -> None: + """Removes another user's reaction from a message.""" + encoded = quote(emoji) + await self.request( + "DELETE", + f"/channels/{channel_id}/messages/{message_id}/reactions/{encoded}/{user_id}", + ) + async def get_reactions( self, channel_id: "Snowflake", message_id: "Snowflake", emoji: str ) -> List[Dict[str, Any]]: @@ -400,6 +414,27 @@ class HTTPClient: ) return messages + async def get_pinned_messages( + self, channel_id: "Snowflake" + ) -> List[Dict[str, Any]]: + """Fetches all pinned messages in a channel.""" + + return await self.request("GET", f"/channels/{channel_id}/pins") + + async def pin_message( + self, channel_id: "Snowflake", message_id: "Snowflake" + ) -> None: + """Pins a message in a channel.""" + + await self.request("PUT", f"/channels/{channel_id}/pins/{message_id}") + + async def unpin_message( + self, channel_id: "Snowflake", message_id: "Snowflake" + ) -> None: + """Unpins a message from a channel.""" + + await self.request("DELETE", f"/channels/{channel_id}/pins/{message_id}") + async def delete_channel( self, channel_id: str, reason: Optional[str] = None ) -> None: @@ -420,6 +455,21 @@ class HTTPClient: custom_headers=custom_headers if custom_headers else None, ) + async def edit_channel( + self, + channel_id: "Snowflake", + payload: Dict[str, Any], + reason: Optional[str] = None, + ) -> Dict[str, Any]: + """Edits a channel.""" + headers = {"X-Audit-Log-Reason": reason} if reason else None + return await self.request( + "PATCH", + f"/channels/{channel_id}", + payload=payload, + custom_headers=headers, + ) + async def get_channel(self, channel_id: str) -> Dict[str, Any]: """Fetches a channel by ID.""" return await self.request("GET", f"/channels/{channel_id}") @@ -1039,3 +1089,32 @@ class HTTPClient: async def get_voice_regions(self) -> List[Dict[str, Any]]: """Returns available voice regions.""" return await self.request("GET", "/voice/regions") + + async def start_thread_from_message( + self, + channel_id: "Snowflake", + message_id: "Snowflake", + payload: Dict[str, Any], + ) -> Dict[str, Any]: + """Starts a new thread from an existing message.""" + return await self.request( + "POST", + f"/channels/{channel_id}/messages/{message_id}/threads", + payload=payload, + ) + + async def start_thread_without_message( + self, channel_id: "Snowflake", payload: Dict[str, Any] + ) -> Dict[str, Any]: + """Starts a new thread that is not attached to a message.""" + return await self.request( + "POST", f"/channels/{channel_id}/threads", payload=payload + ) + + async def join_thread(self, channel_id: "Snowflake") -> None: + """Joins the current user to a thread.""" + await self.request("PUT", f"/channels/{channel_id}/thread-members/@me") + + async def leave_thread(self, channel_id: "Snowflake") -> None: + """Removes the current user from a thread.""" + await self.request("DELETE", f"/channels/{channel_id}/thread-members/@me") diff --git a/disagreement/models.py b/disagreement/models.py index c10c5a4..0666926 100644 --- a/disagreement/models.py +++ b/disagreement/models.py @@ -105,11 +105,38 @@ class Message: self.attachments: List[Attachment] = [ Attachment(a) for a in data.get("attachments", []) ] + self.pinned: bool = data.get("pinned", False) # Add other fields as needed, e.g., attachments, embeds, reactions, etc. # self.mentions: List[User] = [User(u) for u in data.get("mentions", [])] # self.mention_roles: List[str] = data.get("mention_roles", []) # self.mention_everyone: bool = data.get("mention_everyone", False) + async def pin(self) -> None: + """|coro| + + Pins this message to its channel. + + Raises + ------ + HTTPException + Pinning the message failed. + """ + await self._client._http.pin_message(self.channel_id, self.id) + self.pinned = True + + async def unpin(self) -> None: + """|coro| + + Unpins this message from its channel. + + Raises + ------ + HTTPException + Unpinning the message failed. + """ + await self._client._http.unpin_message(self.channel_id, self.id) + self.pinned = False + async def reply( self, content: Optional[str] = None, @@ -210,10 +237,17 @@ class Message: await self._client.add_reaction(self.channel_id, self.id, emoji) - async def remove_reaction(self, emoji: str) -> None: - """|coro| Remove the bot's reaction from this message.""" - - await self._client.remove_reaction(self.channel_id, self.id, emoji) + async def remove_reaction(self, emoji: str, member: Optional[User] = None) -> None: + """|coro| + Removes a reaction from this message. + If no ``member`` is provided, removes the bot's own reaction. + """ + if member: + await self._client._http.delete_user_reaction( + self.channel_id, self.id, emoji, member.id + ) + else: + await self._client.remove_reaction(self.channel_id, self.id, emoji) async def clear_reactions(self) -> None: """|coro| Remove all reactions from this message.""" @@ -239,6 +273,125 @@ class Message: def __repr__(self) -> str: return f"" + async def create_thread( + self, + name: str, + *, + auto_archive_duration: Optional[int] = None, + rate_limit_per_user: Optional[int] = None, + reason: Optional[str] = None, + ) -> "Thread": + """|coro| + + Creates a new thread from this message. + + Parameters + ---------- + name: str + The name of the thread. + auto_archive_duration: Optional[int] + The duration in minutes to automatically archive the thread after recent activity. + Can be one of 60, 1440, 4320, 10080. + rate_limit_per_user: Optional[int] + The number of seconds a user has to wait before sending another message. + reason: Optional[str] + The reason for creating the thread. + + Returns + ------- + Thread + The created thread. + """ + payload: Dict[str, Any] = {"name": name} + if auto_archive_duration is not None: + payload["auto_archive_duration"] = auto_archive_duration + if rate_limit_per_user is not None: + payload["rate_limit_per_user"] = rate_limit_per_user + + data = await self._client._http.start_thread_from_message( + self.channel_id, self.id, payload + ) + return cast("Thread", self._client.parse_channel(data)) + + +class PartialMessage: + """Represents a partial message, identified by its ID and channel. + + This model is used to perform actions on a message without having the + full message object in the cache. + + Attributes: + id (str): The message's unique ID. + channel (TextChannel): The text channel this message belongs to. + """ + + def __init__(self, *, id: str, channel: "TextChannel"): + self.id = id + self.channel = channel + self._client = channel._client + + async def fetch(self) -> "Message": + """|coro| + + Fetches the full message data from Discord. + + Returns + ------- + Message + The complete message object. + """ + data = await self._client._http.get_message(self.channel.id, self.id) + return self._client.parse_message(data) + + async def delete(self, *, delay: Optional[float] = None) -> None: + """|coro| + + Deletes this message. + + Parameters + ---------- + delay: Optional[float] + If provided, wait this many seconds before deleting. + """ + if delay is not None: + await asyncio.sleep(delay) + await self._client._http.delete_message(self.channel.id, self.id) + + async def pin(self) -> None: + """|coro| + + Pins this message to its channel. + """ + await self._client._http.pin_message(self.channel.id, self.id) + + async def unpin(self) -> None: + """|coro| + + Unpins this message from its channel. + """ + await self._client._http.unpin_message(self.channel.id, self.id) + + async def add_reaction(self, emoji: str) -> None: + """|coro| + + Adds a reaction to this message. + """ + await self._client._http.create_reaction(self.channel.id, self.id, emoji) + + async def remove_reaction(self, emoji: str, member: Optional[User] = None) -> None: + """|coro| + + Removes a reaction from this message. + + If no ``member`` is provided, removes the bot's own reaction. + """ + if member: + await self._client._http.delete_user_reaction( + self.channel.id, self.id, emoji, member.id + ) + else: + await self._client._http.delete_reaction(self.channel.id, self.id, emoji) + class EmbedFooter: """Represents an embed footer.""" @@ -1305,6 +1458,44 @@ class Thread(TextChannel): # Threads are a specialized TextChannel f"" ) + async def join(self) -> None: + """|coro| + + Joins this thread. + """ + await self._client._http.join_thread(self.id) + + async def leave(self) -> None: + """|coro| + + Leaves this thread. + """ + await self._client._http.leave_thread(self.id) + + async def archive(self, locked: bool = False, *, reason: Optional[str] = None) -> "Thread": + """|coro| + + Archives this thread. + + Parameters + ---------- + locked: bool + Whether to lock the thread. + reason: Optional[str] + The reason for archiving the thread. + + Returns + ------- + Thread + The updated thread. + """ + payload = { + "archived": True, + "locked": locked, + } + data = await self._client._http.edit_channel(self.id, payload, reason=reason) + return cast("Thread", self._client.parse_channel(data)) + class DMChannel(Channel): """Represents a Direct Message channel."""