mirror of
https://github.com/stoatchat/python-client-sdk.git
synced 2026-07-01 20:44:04 -04:00
270 lines
9.7 KiB
Python
Executable File
270 lines
9.7 KiB
Python
Executable File
from __future__ import annotations
|
|
|
|
import datetime
|
|
from typing import TYPE_CHECKING, Any, Coroutine, Optional, Union
|
|
|
|
from revolt.types.message import SystemMessageContent
|
|
|
|
from .asset import Asset, PartialAsset
|
|
from .channel import DMChannel, GroupDMChannel, TextChannel
|
|
from .embed import Embed, SendableEmbed, to_embed
|
|
from .utils import Ulid
|
|
|
|
if TYPE_CHECKING:
|
|
from .server import Server
|
|
from .state import State
|
|
from .types import Embed as EmbedPayload
|
|
from .types import Interactions as InteractionsPayload
|
|
from .types import Masquerade as MasqueradePayload
|
|
from .types import Message as MessagePayload
|
|
from .types import MessageReplyPayload
|
|
from .user import User
|
|
from .member import Member
|
|
|
|
__all__ = (
|
|
"Message",
|
|
"MessageReply",
|
|
"Masquerade",
|
|
"MessageInteractions"
|
|
)
|
|
|
|
class Message(Ulid):
|
|
"""Represents a message
|
|
|
|
Attributes
|
|
-----------
|
|
id: :class:`str`
|
|
The id of the message
|
|
content: :class:`str`
|
|
The content of the message, this will not include system message's content
|
|
attachments: list[:class:`Asset`]
|
|
The attachments of the message
|
|
embeds: list[Union[:class:`WebsiteEmbed`, :class:`ImageEmbed`, :class:`TextEmbed`, :class:`NoneEmbed`]]
|
|
The embeds of the message
|
|
channel: :class:`Messageable`
|
|
The channel the message was sent in
|
|
author: Union[:class:`Member`, :class:`User`]
|
|
The author of the message, will be :class:`User` in DMs
|
|
edited_at: Optional[:class:`datetime.datetime`]
|
|
The time at which the message was edited, will be None if the message has not been edited
|
|
mentions: list[Union[:class:`Member`, :class:`User`]]
|
|
The users or members that where mentioned in the message
|
|
replies: list[:class:`Message`]
|
|
The message's this message has replied to, this may not contain all the messages if they are outside the cache
|
|
reply_ids: list[:class:`str`]
|
|
The message's ids this message has replies to
|
|
reactions: dict[str, list[:class:`User`]]
|
|
The reactions on the message
|
|
interactions: Optional[:class:`MessageInteractions`]
|
|
The interactions on the message, if any
|
|
"""
|
|
__slots__ = ("state", "id", "content", "attachments", "embeds", "channel", "author", "edited_at", "mentions", "replies", "reply_ids", "reactions", "interactions")
|
|
|
|
def __init__(self, data: MessagePayload, state: State):
|
|
self.state: State = state
|
|
|
|
self.id: str = data["_id"]
|
|
|
|
content = data.get("content", "")
|
|
|
|
if not isinstance(content, str):
|
|
self.system_content: SystemMessageContent = content
|
|
self.content: str = ""
|
|
else:
|
|
self.content = content
|
|
|
|
self.attachments: list[Asset] = [Asset(attachment, state) for attachment in data.get("attachments", [])]
|
|
self.embeds: list[Embed] = [to_embed(embed, state) for embed in data.get("embeds", [])]
|
|
|
|
channel = state.get_channel(data["channel"])
|
|
assert isinstance(channel, Union[TextChannel, GroupDMChannel, DMChannel])
|
|
self.channel: TextChannel | GroupDMChannel | DMChannel = channel
|
|
|
|
self.server_id: str | None = self.channel.server_id
|
|
|
|
self.mentions: list[Member | User]
|
|
|
|
if self.server_id:
|
|
author = state.get_member(self.server_id, data["author"])
|
|
self.mentions = [self.server.get_member(member_id) for member_id in data.get("mentions", [])]
|
|
else:
|
|
author = state.get_user(data["author"])
|
|
self.mentions = [state.get_user(member_id) for member_id in data.get("mentions", [])]
|
|
|
|
self.author: Member | User = author
|
|
|
|
if masquerade := data.get("masquerade"):
|
|
if name := masquerade.get("name"):
|
|
self.author.masquerade_name = name
|
|
|
|
if avatar := masquerade.get("avatar"):
|
|
self.author.masquerade_avatar = PartialAsset(avatar, state)
|
|
|
|
if edited_at := data.get("edited"):
|
|
self.edited_at: Optional[datetime.datetime] = datetime.datetime.strptime(edited_at, "%Y-%m-%dT%H:%M:%S.%f%z")
|
|
|
|
self.replies: list[Message] = []
|
|
self.reply_ids: list[str] = []
|
|
|
|
for reply in data.get("replies", []):
|
|
try:
|
|
message = state.get_message(reply)
|
|
self.replies.append(message)
|
|
except LookupError:
|
|
pass
|
|
|
|
self.reply_ids.append(reply)
|
|
|
|
reactions = data.get("reactions", {})
|
|
|
|
self.reactions: dict[str, list[User]] = {}
|
|
|
|
for emoji, users in reactions.items():
|
|
self.reactions[emoji] = [self.state.get_user(user_id) for user_id in users]
|
|
|
|
self.interactions: MessageInteractions | None
|
|
|
|
if interactions := data.get("interactions"):
|
|
self.interactions = MessageInteractions(reactions=interactions.get("reactions"), restrict_reactions=interactions.get("restrict_reactions", False))
|
|
else:
|
|
self.interactions = None
|
|
|
|
def _update(self, *, content: Optional[str] = None, embeds: Optional[list[EmbedPayload]] = None, edited: Optional[Union[str, int]] = None):
|
|
if content is not None:
|
|
self.content = content
|
|
|
|
if embeds is not None:
|
|
self.embeds = [to_embed(embed, self.state) for embed in embeds]
|
|
|
|
if edited is not None:
|
|
if isinstance(edited, int):
|
|
self.edited_at = datetime.datetime.fromtimestamp(edited / 1000, tz=datetime.timezone.utc)
|
|
else:
|
|
self.edited_at = datetime.datetime.strptime(edited, "%Y-%m-%dT%H:%M:%S.%f%z")
|
|
|
|
async def edit(self, *, content: Optional[str] = None, embeds: Optional[list[SendableEmbed]] = None) -> None:
|
|
"""Edits the message. The bot can only edit its own message
|
|
Parameters
|
|
-----------
|
|
content: :class:`str`
|
|
The new content of the message
|
|
"""
|
|
|
|
new_embeds = [embed.to_dict() for embed in embeds] if embeds else None
|
|
|
|
await self.state.http.edit_message(self.channel.id, self.id, content, new_embeds)
|
|
|
|
async def delete(self) -> None:
|
|
"""Deletes the message. The bot can only delete its own messages and messages it has permission to delete """
|
|
await self.state.http.delete_message(self.channel.id, self.id)
|
|
|
|
def reply(self, *args: Any, mention: bool = False, **kwargs: Any) -> Coroutine[Any, Any, Message]:
|
|
"""Replies to this message, equivilant to:
|
|
|
|
.. code-block:: python
|
|
|
|
await channel.send(..., replies=[MessageReply(message, mention)])
|
|
|
|
"""
|
|
return self.channel.send(*args, **kwargs, replies=[MessageReply(self, mention)])
|
|
|
|
async def add_reaction(self, emoji: str) -> None:
|
|
await self.state.http.add_reaction(self.channel.id, self.id, emoji)
|
|
|
|
async def remove_reaction(self, emoji: str, user: Optional[User] = None, remove_all: bool = False) -> None:
|
|
await self.state.http.remove_reaction(self.channel.id, self.id, emoji, user.id if user else None, remove_all)
|
|
|
|
async def remove_all_reactions(self) -> None:
|
|
await self.state.http.remove_all_reactions(self.channel.id, self.id)
|
|
|
|
|
|
@property
|
|
def server(self) -> Server:
|
|
""":class:`Server` The server this voice channel belongs too
|
|
|
|
Raises
|
|
-------
|
|
:class:`LookupError`
|
|
Raises if the channel is not part of a server
|
|
"""
|
|
return self.channel.server
|
|
|
|
class MessageReply:
|
|
"""represents a reply to a message.
|
|
|
|
Parameters
|
|
-----------
|
|
message: :class:`Message`
|
|
The message being replied to.
|
|
mention: :class:`bool`
|
|
Whether the reply should mention the author of the message. Defaults to false.
|
|
"""
|
|
__slots__ = ("message", "mention")
|
|
|
|
def __init__(self, message: Message, mention: bool = False):
|
|
self.message: Message = message
|
|
self.mention: bool = mention
|
|
|
|
def to_dict(self) -> MessageReplyPayload:
|
|
return { "id": self.message.id, "mention": self.mention }
|
|
|
|
class Masquerade:
|
|
"""represents a message's masquerade.
|
|
|
|
Parameters
|
|
-----------
|
|
name: Optional[:class:`str`]
|
|
The name to display for the message
|
|
avatar: Optional[:class:`str`]
|
|
The avatar's url to display for the message
|
|
colour: Optional[:class:`str`]
|
|
The colour of the name, similar to role colours
|
|
"""
|
|
__slots__ = ("name", "avatar", "colour")
|
|
|
|
def __init__(self, name: Optional[str] = None, avatar: Optional[str] = None, colour: Optional[str] = None):
|
|
self.name: str | None = name
|
|
self.avatar: str | None = avatar
|
|
self.colour: str | None = colour
|
|
|
|
def to_dict(self) -> MasqueradePayload:
|
|
output: MasqueradePayload = {}
|
|
|
|
if name := self.name:
|
|
output["name"] = name
|
|
|
|
if avatar := self.avatar:
|
|
output["avatar"] = avatar
|
|
|
|
if colour := self.colour:
|
|
output["colour"] = colour
|
|
|
|
return output
|
|
|
|
class MessageInteractions:
|
|
"""Represents a message's interactions, this is for allowing preset reactions and restricting adding reactions to only those.
|
|
|
|
Parameters
|
|
-----------
|
|
reactions: Optional[list[:class:`str`]]
|
|
The preset reactions on the message
|
|
restrict_reactions: bool
|
|
Whether or not users can only react to the interaction's reactions
|
|
"""
|
|
__slots__ = ("reactions", "restrict_reactions")
|
|
|
|
def __init__(self, *, reactions: Optional[list[str]] = None, restrict_reactions: bool = False):
|
|
self.reactions: list[str] | None = reactions
|
|
self.restrict_reactions: bool = restrict_reactions
|
|
|
|
def to_dict(self) -> InteractionsPayload:
|
|
output: InteractionsPayload = {}
|
|
|
|
if reactions := self.reactions:
|
|
output["reactions"] = reactions
|
|
|
|
if restrict_reactions := self.restrict_reactions:
|
|
output["restrict_reactions"] = restrict_reactions
|
|
|
|
return output
|