feat: Add advanced message, channel, and thread management

This commit is contained in:
Slipstream 2025-06-11 02:06:16 -06:00
parent 152c0f12be
commit 97505948ee
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD
2 changed files with 274 additions and 4 deletions

View File

@ -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")

View File

@ -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,9 +237,16 @@ 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."""
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:
@ -239,6 +273,125 @@ class Message:
def __repr__(self) -> str:
return f"<Message id='{self.id}' channel_id='{self.channel_id}' author='{self.author!r}'>"
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"<Thread id='{self.id}' name='{self.name}' parent_id='{self.parent_id}'>"
)
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."""