mirror of
https://github.com/stoatchat/python-client-sdk.git
synced 2026-07-25 16:35:33 -04:00
Compare commits
16 Commits
v0.1.4
...
list_remove
| Author | SHA1 | Date | |
|---|---|---|---|
| ef6508dc69 | |||
| a4438e73f8 | |||
| be2af590af | |||
| 16177e6698 | |||
| 40e9e3a26e | |||
| c29a763576 | |||
| 5e757d129f | |||
| b17b7fc171 | |||
| 9dfcf69e52 | |||
| b1a91cecb1 | |||
| 7cf3fa3e01 | |||
| bff242c3f6 | |||
| 0fbb90f65e | |||
| c7963f02e9 | |||
| 5641390d5b | |||
| 7a37da571f |
@@ -7,3 +7,4 @@ docs/_build
|
||||
.vscode
|
||||
.env
|
||||
.mypy_cache
|
||||
build
|
||||
|
||||
@@ -9,13 +9,13 @@ venv:
|
||||
python -m pip install -r requirements.txt
|
||||
python -m pip install -r docs_requirements.txt
|
||||
|
||||
test: venv
|
||||
test:
|
||||
python test.py
|
||||
|
||||
build: venv
|
||||
build:
|
||||
python -m build
|
||||
|
||||
upload: venv
|
||||
upload:
|
||||
python -m twine upload dist/* -u $PYPI_USERNAME -p $PYPI_PASSWORD
|
||||
|
||||
lint:
|
||||
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
include requirements.txt
|
||||
include LICENSE
|
||||
include README.md
|
||||
@@ -2,7 +2,21 @@
|
||||
|
||||
An async library to interact with the https://revolt.chat API. This library will **only support bots** and I will **not** implement anything exclusively for user accounts.
|
||||
|
||||
You can join the support server [here](https://rvlt.gg/FDXER6hr) and find the library's documentation [here](https://revoltpy.readthedocs.io/en/latest/).
|
||||
You can join the support server [here](https://rvlt.gg/FDXER6hr) and find the library's documentation [here](https://revoltpy.readthedocs.io/en/latest/).
|
||||
|
||||
## Installing
|
||||
|
||||
You can use `pip` to install revolt.py. It differs slightly depending on what OS/Distro you use.
|
||||
|
||||
On Windows
|
||||
```
|
||||
py -m pip install -U revolt.py # -U to update
|
||||
```
|
||||
|
||||
On macOS and Linux
|
||||
```
|
||||
python3 -m pip install -U revolt.py
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
|
||||
@@ -90,21 +90,16 @@ BadBoolArgument
|
||||
:members:
|
||||
|
||||
CategoryConverterError
|
||||
~~~~~~~~~~~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. autoexception:: revolt.ext.commands.CategoryConverterError
|
||||
:members:
|
||||
|
||||
ChannelConverterError
|
||||
~~~~~~~~~~~~~~~~
|
||||
.. autoexception:: revolt.ext.commands.ChannelConverterError
|
||||
:members:
|
||||
|
||||
UserConverterError
|
||||
~~~~~~~~~~~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
.. autoexception:: revolt.ext.commands.UserConverterError
|
||||
:members:
|
||||
|
||||
MemberConverterError
|
||||
~~~~~~~~~~~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
.. autoexception:: revolt.ext.commands.MemberConverterError
|
||||
:members:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
aiohttp==3.7.4.post0
|
||||
ulid-py==1.1.0
|
||||
aenum==3.1.0
|
||||
typing_extensions==4.1.1
|
||||
|
||||
+17
-19
@@ -1,21 +1,19 @@
|
||||
from .asset import Asset
|
||||
from .category import Category
|
||||
from .channel import (Channel, DMChannel, GroupDMChannel, SavedMessageChannel,
|
||||
TextChannel, VoiceChannel)
|
||||
from .client import Client
|
||||
from .embed import Embed
|
||||
from .enums import (AssetType, ChannelType, PresenceType, RelationshipType,
|
||||
SortType)
|
||||
from .errors import HTTPError, RevoltError, ServerError
|
||||
from .file import File
|
||||
from .flags import UserBadges
|
||||
from .member import Member
|
||||
from .message import Masquerade, Message, MessageReply
|
||||
from .messageable import Messageable
|
||||
from .permissions import ChannelPermissions, ServerPermissions
|
||||
from .role import Role
|
||||
from .server import Server, SystemMessages
|
||||
from .user import Relation, Status, User
|
||||
from . import utils
|
||||
from .asset import *
|
||||
from .category import *
|
||||
from .channel import *
|
||||
from .client import *
|
||||
from .embed import *
|
||||
from .enums import *
|
||||
from .errors import *
|
||||
from .file import *
|
||||
from .flags import *
|
||||
from .member import *
|
||||
from .message import *
|
||||
from .messageable import *
|
||||
from .permissions import *
|
||||
from .role import *
|
||||
from .server import *
|
||||
from .user import *
|
||||
|
||||
__version__ = (0, 1, 3)
|
||||
__version__ = (0, 1, 6)
|
||||
|
||||
+133
-23
@@ -1,10 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, cast
|
||||
from typing import TYPE_CHECKING, Literal, Optional, Union
|
||||
|
||||
from revolt.utils import Missing
|
||||
|
||||
from .asset import Asset
|
||||
from .enums import ChannelType
|
||||
from .messageable import Messageable
|
||||
from .permissions import ChannelPermissions
|
||||
from .utils import Missing
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .message import Message
|
||||
@@ -13,13 +17,28 @@ if TYPE_CHECKING:
|
||||
from .state import State
|
||||
from .types import Channel as ChannelPayload
|
||||
from .types import DMChannel as DMChannelPayload
|
||||
from .types import Group as GroupDMChannelPayload
|
||||
from .types import GroupDMChannel as GroupDMChannelPayload
|
||||
from .types import SavedMessages as SavedMessagesPayload
|
||||
from .types import TextChannel as TextChannelPayload
|
||||
from .types import VoiceChannel as VoiceChannelPayload
|
||||
from .user import User
|
||||
|
||||
__all__ = ("Channel",)
|
||||
__all__ = ("DMChannel", "GroupDMChannel", "SavedMessageChannel", "TextChannel", "VoiceChannel", "Channel")
|
||||
|
||||
class EditableChannel:
|
||||
__slots__ = ()
|
||||
|
||||
state: State
|
||||
id: str
|
||||
|
||||
async def edit(self, **kwargs):
|
||||
remove = []
|
||||
if kwargs.get("icon", Missing) == None:
|
||||
remove.append("Icon")
|
||||
elif kwargs.get("description", Missing) == None:
|
||||
remove.append("Description")
|
||||
|
||||
await self.state.http.edit_channel(self.id, remove, kwargs)
|
||||
|
||||
class Channel:
|
||||
"""Base class for all channels
|
||||
@@ -30,8 +49,6 @@ class Channel:
|
||||
The id of the channel
|
||||
channel_type: ChannelType
|
||||
The type of the channel
|
||||
server: Optional[:class:`Server`]
|
||||
The server the channel is part of
|
||||
"""
|
||||
__slots__ = ("state", "id", "channel_type", "server_id")
|
||||
|
||||
@@ -41,13 +58,21 @@ class Channel:
|
||||
self.channel_type = ChannelType(data["channel_type"])
|
||||
self.server_id = ""
|
||||
|
||||
@property
|
||||
def server(self) -> Optional[Server]:
|
||||
return self.state.get_server(self.server_id) if self.server_id else None
|
||||
async def _get_channel_id(self) -> str:
|
||||
return self.id
|
||||
|
||||
def _update(self):
|
||||
pass
|
||||
|
||||
async def delete(self):
|
||||
"""Deletes or closes the channel"""
|
||||
await self.state.http.close_channel(self.id)
|
||||
|
||||
@property
|
||||
def server(self) -> Server:
|
||||
""":class:`Server` The server this voice channel belongs too"""
|
||||
return self.state.get_server(self.server_id)
|
||||
|
||||
class SavedMessageChannel(Channel, Messageable):
|
||||
"""The Saved Message Channel"""
|
||||
def __init__(self, data: SavedMessagesPayload, state: State):
|
||||
@@ -58,26 +83,54 @@ class DMChannel(Channel, Messageable):
|
||||
def __init__(self, data: DMChannelPayload, state: State):
|
||||
super().__init__(data, state)
|
||||
|
||||
class GroupDMChannel(Channel, Messageable):
|
||||
__slots__ = ("recipients", "name", "owner", "permissions")
|
||||
class GroupDMChannel(Channel, Messageable, EditableChannel):
|
||||
"""A group DM channel
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
recipients: list[:class:`User`]
|
||||
The recipients of the group dm channel
|
||||
name: :class:`str`
|
||||
The name of the group dm channel
|
||||
owner: :class:`User`
|
||||
The user who created the group dm channel
|
||||
icon: Optional[:class:`Asset`]
|
||||
The icon of the group dm channel
|
||||
permissions: :class:`ChannelPermissions`
|
||||
The permissions of the users inside the group dm channel
|
||||
description: Optional[:class:`str`]
|
||||
The description of the channel, if any
|
||||
"""
|
||||
|
||||
__slots__ = ("recipients", "name", "owner", "permissions", "icon", "description")
|
||||
|
||||
"""A group DM channel"""
|
||||
def __init__(self, data: GroupDMChannelPayload, state: State):
|
||||
super().__init__(data, state)
|
||||
self.recipients = [state.get_user(user_id) for user_id in data["recipients"]]
|
||||
self.name = data["name"]
|
||||
self.owner = state.get_user(data["owner"])
|
||||
self.description = data.get("description")
|
||||
|
||||
if icon := data.get("icon"):
|
||||
self.icon = Asset(icon, state)
|
||||
else:
|
||||
self.icon = None
|
||||
|
||||
if perms := data.get("permissions"):
|
||||
self.permissions = ChannelPermissions._from_value(perms)
|
||||
else:
|
||||
self.permissions = ChannelPermissions._from_value(0)
|
||||
|
||||
def _update(self, *, name: Optional[str] = None, recipients: Optional[list[str]] = None):
|
||||
def _update(self, *, name: Optional[str] = None, recipients: Optional[list[str]] = None, description: Optional[str] = None):
|
||||
if name:
|
||||
self.name = name
|
||||
|
||||
if recipients:
|
||||
self.recipients = [self.state.get_user(user_id) for user_id in recipients]
|
||||
|
||||
if description:
|
||||
self.description = description
|
||||
|
||||
async def set_default_permissions(self, permissions: ChannelPermissions) -> None:
|
||||
"""Sets the default permissions for a group.
|
||||
Parameters
|
||||
@@ -87,10 +140,28 @@ class GroupDMChannel(Channel, Messageable):
|
||||
"""
|
||||
await self.state.http.set_channel_default_permissions(self.id, permissions.value)
|
||||
|
||||
class TextChannel(Channel, Messageable):
|
||||
__slots__ = ("name", "description", "last_message_id", "server_id", "default_permissions", "role_permissions")
|
||||
class TextChannel(Channel, Messageable, EditableChannel):
|
||||
__slots__ = ("name", "description", "last_message_id", "server_id", "default_permissions", "role_permissions", "icon")
|
||||
|
||||
"""A text channel"""
|
||||
"""A text channel
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The name of the text channel
|
||||
server_id: :class:`str`
|
||||
The id of the server this text channel belongs to
|
||||
last_message_id: Optional[:class:`str`]
|
||||
The id of the last message in this channel, if any
|
||||
default_permissions: :class:`ChannelPermissions`
|
||||
The default permissions for all users in the text channel
|
||||
role_permissions: dict[:class:`str`, :class:`ChannelPermissions`]
|
||||
A dictionary of role id's to the permissions of that role in the text channel
|
||||
icon: Optional[:class:`Asset`]
|
||||
The icon of the text channel, if any
|
||||
description: Optional[:class:`str`]
|
||||
The description of the channel, if any
|
||||
"""
|
||||
def __init__(self, data: TextChannelPayload, state: State):
|
||||
super().__init__(data, state)
|
||||
|
||||
@@ -101,17 +172,29 @@ class TextChannel(Channel, Messageable):
|
||||
last_message_id = data.get("last_message")
|
||||
self.last_message_id = last_message_id
|
||||
|
||||
if perms := data.get("default_permissions"):
|
||||
self.default_permissions = ChannelPermissions._from_value(perms)
|
||||
self.default_permissions = ChannelPermissions._from_value(data.get("default_permissions", 0))
|
||||
self.role_permissions = {role_id: ChannelPermissions._from_value(perms) for role_id, perms in data.get("role_permissions", {}).items()}
|
||||
|
||||
if role_perms := data.get("role_permissions"):
|
||||
self.role_permissions = {role_id: ChannelPermissions._from_value(perms) for role_id, perms in role_perms.items()}
|
||||
if icon := data.get("icon"):
|
||||
self.icon = Asset(icon, state)
|
||||
else:
|
||||
self.icon = None
|
||||
|
||||
def _get_channel_id(self) -> str:
|
||||
return self.id
|
||||
|
||||
@property
|
||||
def last_message(self) -> Message:
|
||||
def last_message(self) -> Optional[Message]:
|
||||
"""Gets the last message from the channel, shorthand for `client.get_message(channel.last_message_id)`
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Message` the last message in the channel
|
||||
"""
|
||||
|
||||
if not self.last_message_id:
|
||||
return
|
||||
|
||||
return self.state.get_message(self.last_message_id)
|
||||
|
||||
def _update(self, *, name: Optional[str] = None, description: Optional[str] = None):
|
||||
@@ -139,8 +222,26 @@ class TextChannel(Channel, Messageable):
|
||||
"""
|
||||
await self.state.http.set_channel_role_permissions(self.id, role.id, permissions.value)
|
||||
|
||||
class VoiceChannel(Channel):
|
||||
"""A voice channel"""
|
||||
class VoiceChannel(Channel, EditableChannel):
|
||||
"""A voice channel
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The name of the voice channel
|
||||
server_id: :class:`str`
|
||||
The id of the server this voice channel belongs to
|
||||
last_message_id: Optional[:class:`str`]
|
||||
The id of the last message in this channel, if any
|
||||
default_permissions: :class:`ChannelPermissions`
|
||||
The default permissions for all users in the voice channel
|
||||
role_permissions: dict[:class:`str`, :class:`ChannelPermissions`]
|
||||
A dictionary of role id's to the permissions of that role in the voice channel
|
||||
icon: Optional[:class:`Asset`]
|
||||
The icon of the voice channel, if any
|
||||
description: Optional[:class:`str`]
|
||||
The description of the channel, if any
|
||||
"""
|
||||
def __init__(self, data: VoiceChannelPayload, state: State):
|
||||
super().__init__(data, state)
|
||||
|
||||
@@ -150,9 +251,18 @@ class VoiceChannel(Channel):
|
||||
|
||||
if perms := data.get("default_permissions"):
|
||||
self.default_permissions = ChannelPermissions._from_value(perms)
|
||||
else:
|
||||
self.default_permissions = ChannelPermissions._from_value(0)
|
||||
|
||||
if role_perms := data.get("role_permissions"):
|
||||
self.role_permissions = {role_id: ChannelPermissions._from_value(perms) for role_id, perms in role_perms.items()}
|
||||
else:
|
||||
self.role_permissions = {}
|
||||
|
||||
if icon := data.get("icon"):
|
||||
self.icon = Asset(icon, state)
|
||||
else:
|
||||
self.icon = None
|
||||
|
||||
def _update(self, *, name: Optional[str] = None, description: Optional[str] = None):
|
||||
if name:
|
||||
@@ -179,7 +289,7 @@ class VoiceChannel(Channel):
|
||||
"""
|
||||
await self.state.http.set_channel_role_permissions(self.id, role.id, permissions.value)
|
||||
|
||||
def channel_factory(data: ChannelPayload, state: State) -> Channel:
|
||||
def channel_factory(data: ChannelPayload, state: State) -> Union[DMChannel, GroupDMChannel, SavedMessageChannel, TextChannel, VoiceChannel]:
|
||||
if data["channel_type"] == "SavedMessage":
|
||||
return SavedMessageChannel(data, state)
|
||||
elif data["channel_type"] == "DirectMessage":
|
||||
|
||||
+109
-1
@@ -2,11 +2,15 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Optional, TypeVar
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional, Union, cast
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .channel import (DMChannel, GroupDMChannel, SavedMessageChannel,
|
||||
TextChannel, VoiceChannel, channel_factory)
|
||||
from .http import HttpClient
|
||||
from .invite import Invite
|
||||
from .message import Message
|
||||
from .state import State
|
||||
from .websocket import WebsocketHandler
|
||||
|
||||
@@ -168,6 +172,7 @@ class Client:
|
||||
|
||||
@property
|
||||
def user(self) -> User:
|
||||
""":class:`User` the user corrasponding to the client"""
|
||||
user = self.websocket.user
|
||||
|
||||
assert user
|
||||
@@ -175,4 +180,107 @@ class Client:
|
||||
|
||||
@property
|
||||
def users(self) -> list[User]:
|
||||
"""list[:class:`User`] All users the client can see"""
|
||||
return list(self.state.users.values())
|
||||
|
||||
async def fetch_user(self, user_id: str) -> User:
|
||||
"""Fetchs a user
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
user_id: :class:`str`
|
||||
The id of the user you are fetching
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`User`
|
||||
The user with the matching id
|
||||
"""
|
||||
payload = await self.http.fetch_user(user_id)
|
||||
return User(payload, self.state)
|
||||
|
||||
async def fetch_dm_channels(self) -> list[Union[DMChannel, GroupDMChannel]]:
|
||||
"""Fetchs all dm channels the client has made
|
||||
|
||||
Returns
|
||||
--------
|
||||
list[Union[:class:`DMChanel`, :class:`GroupDMChannel`]]
|
||||
A list of :class:`DMChannel` or :class`GroupDMChannel`
|
||||
"""
|
||||
channel_payloads = await self.http.fetch_dm_channels()
|
||||
return cast(list[Union[DMChannel, GroupDMChannel]], [channel_factory(payload, self.state) for payload in channel_payloads])
|
||||
|
||||
async def fetch_channel(self, channel_id: str) -> Union[DMChannel, GroupDMChannel, SavedMessageChannel, TextChannel, VoiceChannel]:
|
||||
"""Fetches a channel
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
channel_id: :class:`str`
|
||||
The id of the channel
|
||||
|
||||
Returns
|
||||
--------
|
||||
Union[:class:`DMChannel`, :class:`GroupDMChannel`, :class:`SavedMessageChannel`, :class:`TextChannel`, :class:`VoiceChannel`]
|
||||
The channel with the matching id
|
||||
"""
|
||||
payload = await self.http.fetch_channel(channel_id)
|
||||
|
||||
return channel_factory(payload, self.state)
|
||||
|
||||
async def fetch_server(self, server_id: str) -> Server:
|
||||
"""Fetchs a server
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
server_id: :class:`str`
|
||||
The id of the server you are fetching
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Server`
|
||||
The server with the matching id
|
||||
"""
|
||||
payload = await self.http.fetch_server(server_id)
|
||||
|
||||
return Server(payload, self.state)
|
||||
|
||||
async def fetch_invite(self, code: str) -> Invite:
|
||||
"""Fetchs an invite
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
code: :class:`str`
|
||||
The code of the invite you are fetching
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Invite`
|
||||
The invite with the matching code
|
||||
"""
|
||||
payload = await self.http.fetch_invite(code)
|
||||
|
||||
return Invite(payload, code, self.state)
|
||||
|
||||
def get_message(self, message_id: str) -> Message:
|
||||
"""Gets a message from the cache
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
message_id: :class:`str`
|
||||
The id of the message you are getting
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Message`
|
||||
The message with the matching id
|
||||
|
||||
Raises
|
||||
-------
|
||||
LookupError
|
||||
This raises if the message is not found in the cache
|
||||
"""
|
||||
for message in self.state.messages:
|
||||
if message.id == message_id:
|
||||
return message
|
||||
|
||||
raise LookupError
|
||||
|
||||
+97
-8
@@ -1,17 +1,106 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from .enums import EmbedType
|
||||
from .asset import Asset
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .state import State
|
||||
from .types import Embed as EmbedPayload
|
||||
from .types import SendableEmbed as SendableEmbedPayload
|
||||
from .types import WebsiteEmbed as WebsiteEmbedPayload
|
||||
from .types import ImageEmbed as ImageEmbedPayload
|
||||
from .types import TextEmbed as TextEmbedPayload
|
||||
from .types import NoneEmbed as NoneEmbedPayload
|
||||
|
||||
__all__ = ("Embed", "WebsiteEmbed", "ImageEmbed", "TextEmbed", "NoneEmbed", "to_embed", "SendableEmbed")
|
||||
|
||||
__all__ = ("Embed",)
|
||||
class WebsiteEmbed:
|
||||
type = EmbedType.website
|
||||
|
||||
class Embed:
|
||||
@classmethod
|
||||
def from_dict(cls, data: EmbedPayload):
|
||||
...
|
||||
def __init__(self, embed: WebsiteEmbedPayload):
|
||||
self.url = embed.get("url")
|
||||
self.special = embed.get("special")
|
||||
self.title = embed.get("title")
|
||||
self.description = embed.get("description")
|
||||
self.image = embed.get("image")
|
||||
self.video = embed.get("video")
|
||||
self.site_name = embed.get("site_name")
|
||||
self.icon_url = embed.get("icon_url")
|
||||
self.colour = embed.get("colour")
|
||||
|
||||
def to_dict(self) -> EmbedPayload:
|
||||
...
|
||||
class ImageEmbed:
|
||||
type = EmbedType.image
|
||||
|
||||
def __init__(self, image: ImageEmbedPayload):
|
||||
self.url = image.get("url")
|
||||
self.width = image.get("width")
|
||||
self.height = image.get("height")
|
||||
self.size = image.get("size")
|
||||
|
||||
class TextEmbed:
|
||||
type = EmbedType.text
|
||||
|
||||
def __init__(self, embed: TextEmbedPayload, state: State):
|
||||
self.icon_url = embed.get("icon_url")
|
||||
self.url = embed.get("url")
|
||||
self.title = embed.get("title")
|
||||
self.description = embed.get("description")
|
||||
|
||||
if media := embed.get("media"):
|
||||
self.media = Asset(media, state)
|
||||
else:
|
||||
self.media = None
|
||||
|
||||
self.colour = embed.get("colour")
|
||||
|
||||
class NoneEmbed:
|
||||
type = EmbedType.none
|
||||
|
||||
Embed = Union[WebsiteEmbed, ImageEmbed, TextEmbed, NoneEmbed]
|
||||
|
||||
def to_embed(payload: EmbedPayload, state: State) -> Embed:
|
||||
if payload["type"] == "Website":
|
||||
return WebsiteEmbed(payload)
|
||||
elif payload["type"] == "Image":
|
||||
return ImageEmbed(payload)
|
||||
elif payload["type"] == "Text":
|
||||
return TextEmbed(payload, state)
|
||||
else:
|
||||
return NoneEmbed()
|
||||
|
||||
class SendableEmbed:
|
||||
def __init__(self, **attrs):
|
||||
self.title: Optional[str] = None
|
||||
self.description: Optional[str] = None
|
||||
self.media: Optional[str] = None
|
||||
self.icon_url: Optional[str] = None
|
||||
self.colour: Optional[str] = None
|
||||
self.url: Optional[str] = None
|
||||
|
||||
for key, value in attrs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def to_dict(self) -> SendableEmbedPayload:
|
||||
output: SendableEmbedPayload = {"type": "Text"}
|
||||
|
||||
if title := self.title:
|
||||
output["title"] = title
|
||||
|
||||
if description := self.description:
|
||||
output["description"] = description
|
||||
|
||||
if media := self.media:
|
||||
output["media"] = media
|
||||
|
||||
if icon_url := self.icon_url:
|
||||
output["icon_url"] = icon_url
|
||||
|
||||
if colour := self.colour:
|
||||
output["colour"] = colour
|
||||
|
||||
if url := self.url:
|
||||
output["url"] = url
|
||||
|
||||
return output
|
||||
|
||||
@@ -14,6 +14,7 @@ __all__ = (
|
||||
"RelationshipType",
|
||||
"AssetType",
|
||||
"SortType",
|
||||
"EmbedType"
|
||||
)
|
||||
|
||||
class ChannelType(enum.Enum):
|
||||
@@ -49,3 +50,9 @@ class SortType(enum.Enum):
|
||||
latest = "Latest"
|
||||
oldest = "Oldest"
|
||||
relevance = "Relevance"
|
||||
|
||||
class EmbedType(enum.Enum):
|
||||
website = "Website"
|
||||
image = "Image"
|
||||
text = "Text"
|
||||
none = "None"
|
||||
|
||||
@@ -2,5 +2,7 @@ from .checks import *
|
||||
from .client import *
|
||||
from .command import *
|
||||
from .context import *
|
||||
from .converters import *
|
||||
from .errors import *
|
||||
from .converters import IntConverter, BoolConverter, CategoryConverter, UserConverter, MemberConverter, ChannelConverter
|
||||
from .group import *
|
||||
from .cog import *
|
||||
|
||||
@@ -29,7 +29,7 @@ def check(check: Check):
|
||||
else:
|
||||
checks = getattr(func, "_checks", [])
|
||||
checks.append(check)
|
||||
func._checks = checks
|
||||
func._checks = checks # type: ignore
|
||||
|
||||
return func
|
||||
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import traceback
|
||||
from typing import Any, Callable, Union
|
||||
import sys
|
||||
from typing import Any, Union, Protocol, runtime_checkable
|
||||
from importlib import import_module
|
||||
|
||||
import revolt
|
||||
|
||||
from .command import Command
|
||||
from .context import Context
|
||||
from .errors import CheckError, CommandNotFound
|
||||
from .errors import CheckError, CommandNotFound, MissingSetup
|
||||
from .view import StringView
|
||||
from .cog import Cog
|
||||
|
||||
__all__ = (
|
||||
"CommandsMeta",
|
||||
"CommandsClient"
|
||||
)
|
||||
|
||||
quote_regex = re.compile(r"[\"']")
|
||||
chunk_regex = re.compile(r"\S+")
|
||||
|
||||
@runtime_checkable
|
||||
class ExtensionProtocol(Protocol):
|
||||
@staticmethod
|
||||
def setup(client: CommandsClient):
|
||||
raise NotImplementedError
|
||||
|
||||
class CommandsMeta(type):
|
||||
_commands: list[Command]
|
||||
@@ -26,7 +30,6 @@ class CommandsMeta(type):
|
||||
def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]):
|
||||
commands: list[Command] = []
|
||||
self = super().__new__(cls, name, bases, attrs)
|
||||
|
||||
for base in reversed(self.__mro__):
|
||||
for value in base.__dict__.values():
|
||||
if isinstance(value, Command):
|
||||
@@ -43,6 +46,8 @@ class CommandsClient(revolt.Client, metaclass=CommandsMeta):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.all_commands: dict[str, Command] = {}
|
||||
self.cogs: dict[str, Cog] = {}
|
||||
self.extensions: dict[str, ExtensionProtocol] = {}
|
||||
|
||||
for command in self._commands:
|
||||
self.all_commands[command.name] = command
|
||||
@@ -98,6 +103,9 @@ class CommandsClient(revolt.Client, metaclass=CommandsMeta):
|
||||
"""
|
||||
self.all_commands[name] = command
|
||||
|
||||
for alias in command.aliases:
|
||||
self.all_commands[alias] = command
|
||||
|
||||
def get_view(self, message: revolt.Message) -> type[StringView]:
|
||||
return StringView
|
||||
|
||||
@@ -168,6 +176,7 @@ class CommandsClient(revolt.Client, metaclass=CommandsMeta):
|
||||
|
||||
return output
|
||||
except Exception as e:
|
||||
await command._error_handler(command.cog or self, context, e)
|
||||
self.dispatch("command_error", context, e)
|
||||
|
||||
@staticmethod
|
||||
@@ -190,3 +199,33 @@ class CommandsClient(revolt.Client, metaclass=CommandsMeta):
|
||||
"""
|
||||
|
||||
return True
|
||||
|
||||
def add_cog(self, cog: Cog):
|
||||
cog._inject(self)
|
||||
|
||||
def remove_cog(self, cog_name: str) -> Cog:
|
||||
cog = self.cogs.pop(cog_name)
|
||||
cog._uninject(self)
|
||||
|
||||
return cog
|
||||
|
||||
def load_extension(self, name: str):
|
||||
extension = import_module(name)
|
||||
|
||||
if not isinstance(extension, ExtensionProtocol):
|
||||
raise MissingSetup(f"'{extension}' is missing a setup function")
|
||||
|
||||
self.extensions[name] = extension
|
||||
extension.setup(self)
|
||||
|
||||
def unload_extension(self, name: str):
|
||||
extension = self.extensions.pop(name)
|
||||
|
||||
del sys.modules[name]
|
||||
|
||||
if teardown := getattr(extension, "teardown", None):
|
||||
teardown(self)
|
||||
|
||||
def reload_extension(self, name: str):
|
||||
self.unload_extension(name)
|
||||
self.load_extension(name)
|
||||
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
from __future__ import annotations
|
||||
from distutils import command
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from .command import Command
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .client import CommandsClient
|
||||
|
||||
__all__ = ("Cog", "CogMeta")
|
||||
|
||||
class CogMeta(type):
|
||||
_commands: list[Command]
|
||||
|
||||
def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]):
|
||||
commands: list[Command] = []
|
||||
self = super().__new__(cls, name, bases, attrs)
|
||||
|
||||
for base in reversed(self.__mro__):
|
||||
for value in base.__dict__.values():
|
||||
if isinstance(value, Command):
|
||||
value.cog = self # type: ignore
|
||||
commands.append(value)
|
||||
|
||||
|
||||
self._commands = commands
|
||||
|
||||
return self
|
||||
|
||||
class Cog(metaclass=CogMeta):
|
||||
_commands: list[Command]
|
||||
|
||||
def _inject(self, client: CommandsClient):
|
||||
client.cogs[type(self).__name__] = self
|
||||
|
||||
for command in self._commands:
|
||||
client.add_command(command.name, command)
|
||||
|
||||
def _uninject(self, client: CommandsClient):
|
||||
for name, command in client.all_commands.copy().items():
|
||||
if command in self._commands:
|
||||
del client.all_commands[name]
|
||||
@@ -4,7 +4,7 @@ import inspect
|
||||
import traceback
|
||||
from contextlib import suppress
|
||||
from typing import (TYPE_CHECKING, Annotated, Any, Callable, Coroutine,
|
||||
Literal, Optional, Union, get_args, get_origin, cast)
|
||||
Literal, Optional, Union, cast, get_args, get_origin)
|
||||
|
||||
import revolt
|
||||
from revolt.utils import copy_doc, maybe_coroutine
|
||||
@@ -14,6 +14,7 @@ from .errors import InvalidLiteralArgument, UnionConverterError
|
||||
if TYPE_CHECKING:
|
||||
from .checks import Check
|
||||
from .context import Context
|
||||
from .group import Group
|
||||
|
||||
__all__ = (
|
||||
"Command",
|
||||
@@ -34,8 +35,10 @@ class Command:
|
||||
The name of the command
|
||||
aliases: list[:class:`str`]
|
||||
The aliases of the command
|
||||
parent: Optional[:class:`Group`]
|
||||
The parent of the command if this command is a subcommand
|
||||
"""
|
||||
__slots__ = ("callback", "name", "aliases", "signature", "checks")
|
||||
__slots__ = ("callback", "name", "aliases", "signature", "checks", "parent", "_error_handler", "cog")
|
||||
|
||||
def __init__(self, callback: Callable[..., Coroutine[Any, Any, Any]], name: str, aliases: list[str]):
|
||||
self.callback = callback
|
||||
@@ -43,6 +46,9 @@ class Command:
|
||||
self.aliases = aliases
|
||||
self.signature = inspect.signature(self.callback)
|
||||
self.checks: list[Check] = getattr(callback, "_checks", [])
|
||||
self.parent: Optional[Group] = None
|
||||
self.cog = None
|
||||
self._error_handler: Callable[[Any, Context, Exception], Coroutine[Any, Any, Any]] = type(self)._default_error_handler
|
||||
|
||||
async def invoke(self, context: Context, *args, **kwargs) -> Any:
|
||||
"""Runs the command and calls the error handler if the command errors.
|
||||
@@ -55,9 +61,9 @@ class Command:
|
||||
The arguments for the command
|
||||
"""
|
||||
try:
|
||||
return await self.callback(context.client, context, *args, **kwargs)
|
||||
return await self.callback(self.cog or context.client, context, *args, **kwargs)
|
||||
except Exception as err:
|
||||
return await self._error_handler(context, err)
|
||||
return await self._error_handler(self.cog or context.client, context, err)
|
||||
|
||||
@copy_doc(invoke)
|
||||
def __call__(self, context: Context, *args, **kwargs) -> Any:
|
||||
@@ -80,11 +86,10 @@ class Command:
|
||||
await ctx.send(str(error))
|
||||
|
||||
"""
|
||||
self._error_handler = func # type: ignore
|
||||
self._error_handler = func
|
||||
return func
|
||||
|
||||
@staticmethod
|
||||
async def _error_handler(ctx: Context, error: Exception):
|
||||
async def _default_error_handler(self, ctx: Context, error: Exception):
|
||||
traceback.print_exception(type(error), error, error.__traceback__)
|
||||
|
||||
@staticmethod
|
||||
@@ -97,7 +102,7 @@ class Command:
|
||||
|
||||
@classmethod
|
||||
async def convert_argument(cls, arg: str, annotation: Any, context: Context):
|
||||
if annotation:
|
||||
if annotation is not inspect._empty:
|
||||
if annotation is str: # no converting is needed - its already a string
|
||||
return arg
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import revolt
|
||||
from revolt.utils import maybe_coroutine
|
||||
|
||||
from .command import Command
|
||||
from .group import Group
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .client import CommandsClient
|
||||
@@ -41,7 +42,7 @@ class Context(revolt.Messageable):
|
||||
"""
|
||||
__slots__ = ("command", "invoked_with", "args", "message", "server", "channel", "author", "view", "kwargs", "state", "client")
|
||||
|
||||
def _get_channel_id(self) -> str:
|
||||
async def _get_channel_id(self) -> str:
|
||||
return self.channel.id
|
||||
|
||||
def __init__(self, command: Optional[Command], invoked_with: str, view: StringView, message: revolt.Message, client: CommandsClient):
|
||||
@@ -58,7 +59,7 @@ class Context(revolt.Messageable):
|
||||
self.state = message.state
|
||||
|
||||
async def invoke(self) -> Any:
|
||||
"""Invokes the command, this is equal to `await command.invoke(context, context.args)`.
|
||||
"""Invokes the command.
|
||||
|
||||
.. note:: If the command is `None`, this function will do nothing.
|
||||
|
||||
@@ -69,11 +70,24 @@ class Context(revolt.Messageable):
|
||||
"""
|
||||
|
||||
if command := self.command:
|
||||
await command.parse_arguments(self)
|
||||
if isinstance(command, Group):
|
||||
try:
|
||||
subcommand_name = self.view.get_next_word()
|
||||
except StopIteration:
|
||||
pass
|
||||
else:
|
||||
if subcommand := command.subcommands.get(subcommand_name):
|
||||
self.command = command = subcommand
|
||||
return await self.invoke()
|
||||
|
||||
self.view.undo()
|
||||
|
||||
await command.parse_arguments(self)
|
||||
return await command.invoke(self, *self.args, **self.kwargs)
|
||||
|
||||
async def can_run(self) -> bool:
|
||||
"""Runs all of the commands checks, and returns true if all of them pass"""
|
||||
return all([await maybe_coroutine(check, self) for check in (self.command.checks if self.command else [])])
|
||||
|
||||
async def can_run(self, command: Optional[Command] = None) -> bool:
|
||||
"""Runs all of the commands checks, and returns true if all of them pass"""
|
||||
command = command or self.command
|
||||
|
||||
return all([await maybe_coroutine(check, self) for check in (command.checks if command else [])])
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from typing import Annotated
|
||||
import re
|
||||
from typing import Annotated
|
||||
|
||||
from revolt import Category, Channel, Member, User, utils
|
||||
|
||||
from .errors import BadBoolArgument, ServerOnly, CategoryConverterError, ChannelConverterError, UserConverterError, MemberConverterError
|
||||
from .context import Context
|
||||
|
||||
from revolt import Category, Channel, utils, User, Member
|
||||
from .errors import (BadBoolArgument, CategoryConverterError,
|
||||
ChannelConverterError, MemberConverterError, ServerOnly,
|
||||
UserConverterError)
|
||||
|
||||
__all__ = ("bool_converter", "category_converter", "channel_converter", "user_converter", "member_converter", "IntConverter", "BoolConverter", "CategoryConverter", "UserConverter", "MemberConverter", "ChannelConverter")
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ __all__ = (
|
||||
"ChannelConverterError",
|
||||
"UserConverterError",
|
||||
"MemberConverterError",
|
||||
"MissingSetup",
|
||||
)
|
||||
|
||||
class CommandError(RevoltError):
|
||||
@@ -81,3 +82,6 @@ class UnionConverterError(ConverterError):
|
||||
"""Raised when all converters in a union fails"""
|
||||
def __init__(self, argument: str):
|
||||
self.argument = argument
|
||||
|
||||
class MissingSetup(CommandError):
|
||||
"""Raised when an extension is missing the `setup` function"""
|
||||
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, Callable, Coroutine, Any
|
||||
|
||||
from .command import Command
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .context import Context
|
||||
|
||||
__all__ = (
|
||||
"Group",
|
||||
"group"
|
||||
)
|
||||
|
||||
|
||||
class Group(Command):
|
||||
"""Class for holding info about a group command.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
callback: Callable[..., Coroutine[Any, Any, Any]]
|
||||
The callback for the group command
|
||||
name: :class:`str`
|
||||
The name of the command
|
||||
aliases: list[:class:`str`]
|
||||
The aliases of the group command
|
||||
subcommands: dict[:class:`str`, :class:`Command`]
|
||||
The group's subcommands.
|
||||
"""
|
||||
|
||||
__slots__ = ("subcommands",)
|
||||
|
||||
def __init__(self, callback: Callable[..., Coroutine[Any, Any, Any]], name: str, aliases: list[str]):
|
||||
self.subcommands: dict[str, Command] = {}
|
||||
super().__init__(callback, name, aliases)
|
||||
|
||||
def command(self, *, name: Optional[str] = None, aliases: Optional[list[str]] = None, cls: type[Command] = Command):
|
||||
"""A decorator that turns a function into a :class:`Command` and registers the command as a subcommand.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: Optional[:class:`str`]
|
||||
The name of the command, this defaults to the functions name
|
||||
aliases: Optional[list[:class:`str`]]
|
||||
The aliases of the command, defaults to no aliases
|
||||
cls: type[:class:`Command`]
|
||||
The class used for creating the command, this defaults to :class:`Command` but can be used to use a custom command subclass
|
||||
|
||||
Returns
|
||||
--------
|
||||
Callable[Callable[..., Coroutine], :class:`Command`]
|
||||
A function that takes the command callback and returns a :class:`Command`
|
||||
"""
|
||||
def inner(func: Callable[..., Coroutine[Any, Any, Any]]):
|
||||
command = cls(func, name or func.__name__, aliases or [])
|
||||
command.parent = self
|
||||
self.subcommands[command.name] = command
|
||||
return command
|
||||
|
||||
return inner
|
||||
|
||||
def group(self, *, name: Optional[str] = None, aliases: Optional[list[str]] = None, cls: Optional[type["Group"]] = None):
|
||||
"""A decorator that turns a function into a :class:`Group` and registers the command as a subcommand
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: Optional[:class:`str`]
|
||||
The name of the group command, this defaults to the functions name
|
||||
aliases: Optional[list[:class:`str`]]
|
||||
The aliases of the group command, defaults to no aliases
|
||||
cls: type[:class:`Group`]
|
||||
The class used for creating the command, this defaults to :class:`Group` but can be used to use a custom group subclass
|
||||
|
||||
Returns
|
||||
--------
|
||||
Callable[Callable[..., Coroutine], :class:`Group`]
|
||||
A function that takes the command callback and returns a :class:`Group`
|
||||
"""
|
||||
cls = cls or type(self)
|
||||
|
||||
def inner(func: Callable[..., Coroutine[Any, Any, Any]]):
|
||||
command = cls(func, name or func.__name__, aliases or [])
|
||||
command.parent = self
|
||||
self.subcommands[command.name] = command
|
||||
return command
|
||||
|
||||
return inner
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Group name=\"{self.name}\">"
|
||||
|
||||
def group(*, name: Optional[str] = None, aliases: Optional[list[str]] = None, cls: type[Group] = Group):
|
||||
"""A decorator that turns a function into a :class:`Group`
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: Optional[:class:`str`]
|
||||
The name of the group command, this defaults to the functions name
|
||||
aliases: Optional[list[:class:`str`]]
|
||||
The aliases of the group command, defaults to no aliases
|
||||
cls: type[:class:`Group`]
|
||||
The class used for creating the command, this defaults to :class:`Group` but can be used to use a custom group subclass
|
||||
|
||||
Returns
|
||||
--------
|
||||
Callable[Callable[..., Coroutine], :class:`Group`]
|
||||
A function that takes the command callback and returns a :class:`Group`
|
||||
"""
|
||||
|
||||
def inner(func: Callable[..., Coroutine[Any, Any, Any]]):
|
||||
return cls(func, name or func.__name__, aliases or [])
|
||||
|
||||
return inner
|
||||
+34
-9
@@ -23,15 +23,14 @@ if TYPE_CHECKING:
|
||||
from .types import Autumn as AutumnPayload
|
||||
from .types import Channel, DMChannel
|
||||
from .types import Embed as EmbedPayload
|
||||
from .types import GetServerMembers
|
||||
from .types import GetServerMembers, Invite
|
||||
from .types import Masquerade as MasqueradePayload
|
||||
from .types import Member
|
||||
from .types import Message as MessagePayload
|
||||
from .types import (MessageReplyPayload, MessageWithUserData, Role, Server,
|
||||
ServerBans, ServerInvite, TextChannel)
|
||||
from .types import (MessageReplyPayload, MessageWithUserData,
|
||||
PartialInvite, Role, Server, ServerBans, TextChannel)
|
||||
from .types import User as UserPayload
|
||||
from .types import UserProfile, VoiceChannel
|
||||
|
||||
from .types import UserProfile, VoiceChannel, GroupDMChannel, Member
|
||||
from .types import SendableEmbed as SendableEmbedPayload
|
||||
|
||||
__all__ = ("HttpClient",)
|
||||
|
||||
@@ -106,7 +105,7 @@ class HttpClient:
|
||||
else:
|
||||
return response
|
||||
|
||||
async def send_message(self, channel: str, content: Optional[str], embeds: Optional[list[EmbedPayload]], attachments: Optional[list[File]], replies: Optional[list[MessageReplyPayload]], masquerade: Optional[MasqueradePayload]) -> MessagePayload:
|
||||
async def send_message(self, channel: str, content: Optional[str], embeds: Optional[list[SendableEmbedPayload]], attachments: Optional[list[File]], replies: Optional[list[MessageReplyPayload]], masquerade: Optional[MasqueradePayload]) -> MessagePayload:
|
||||
json: dict[str, Any] = {}
|
||||
|
||||
if content:
|
||||
@@ -267,7 +266,7 @@ class HttpClient:
|
||||
def fetch_default_avatar(self, user_id: str) -> Request[bytes]:
|
||||
return self.request_file(f"{self.api_url}/users/{user_id}/default_avatar")
|
||||
|
||||
def fetch_dm_channels(self) -> Request[list[Channel]]:
|
||||
def fetch_dm_channels(self) -> Request[list[Union[DMChannel, GroupDMChannel]]]:
|
||||
return self.request("GET", "/users/dms")
|
||||
|
||||
def open_dm(self, user_id: str) -> Request[DMChannel]:
|
||||
@@ -293,6 +292,14 @@ class HttpClient:
|
||||
def delete_leave_server(self, server_id: str) -> Request[None]:
|
||||
return self.request("DELETE", f"/servers/{server_id}")
|
||||
|
||||
@overload
|
||||
def create_channel(self, server_id: str, channel_type: Literal["Text"], name: str, description: Optional[str]) -> Request[TextChannel]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def create_channel(self, server_id: str, channel_type: Literal["Voice"], name: str, description: Optional[str]) -> Request[VoiceChannel]:
|
||||
...
|
||||
|
||||
def create_channel(self, server_id: str, channel_type: Literal["Text", "Voice"], name: str, description: Optional[str]) -> Request[Union[TextChannel, VoiceChannel]]:
|
||||
payload = {
|
||||
"type": channel_type,
|
||||
@@ -304,7 +311,7 @@ class HttpClient:
|
||||
|
||||
return self.request("POST", f"/servers/{server_id}/channels", json=payload)
|
||||
|
||||
def fetch_server_invites(self, server_id: str) -> Request[list[ServerInvite]]:
|
||||
def fetch_server_invites(self, server_id: str) -> Request[list[PartialInvite]]:
|
||||
return self.request("GET", f"/servers/{server_id}/invites")
|
||||
|
||||
def fetch_member(self, server_id: str, member_id: str) -> Request[Member]:
|
||||
@@ -352,3 +359,21 @@ class HttpClient:
|
||||
|
||||
def delete_role(self, server_id: str, role_id: str) -> Request[None]:
|
||||
return self.request("DELETE", f"/servers/{server_id}/roles/{role_id}")
|
||||
|
||||
def fetch_invite(self, code: str) -> Request[Invite]:
|
||||
return self.request("GET", f"/invites/{code}")
|
||||
|
||||
def delete_invite(self, code: str) -> Request[None]:
|
||||
return self.request("DELETE", f"/invites/{code}")
|
||||
|
||||
def edit_channel(self, channel_id: str, remove: list[str], values: dict[str, Any]):
|
||||
if remove:
|
||||
values["remove"] = remove
|
||||
|
||||
return self.request("PATCH", f"/channels/{channel_id}", json=values)
|
||||
|
||||
def edit_role(self, server_id: str, role_id: str, remove: list[str], values: dict[str, Any]):
|
||||
if remove:
|
||||
values["remove"] = remove
|
||||
|
||||
return self.request("PATCH", f"/servers/{server_id}/roles/{role_id}", json=values)
|
||||
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .asset import Asset
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .state import State
|
||||
from .types import Invite as InvitePayload
|
||||
|
||||
__all__ = ("Invite",)
|
||||
|
||||
class Invite:
|
||||
"""Represents a server invite.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
code: :class:`str`
|
||||
The code for the invite
|
||||
server: :class:`Server`
|
||||
The server this invite is for
|
||||
channel: :class:`Channel`
|
||||
The channel this invite is for
|
||||
user_name: :class:`str`
|
||||
The name of the user who made the invite
|
||||
user: Optional[:class:`User`]
|
||||
The user who made the invite, this is only set if this was fetched via :meth:`Server.fetch_invites`
|
||||
user_avatar: Optional[:class:`Asset`]
|
||||
The invite creator's avatar, if any
|
||||
member_count: :class:`int`
|
||||
The member count of the server this invite is for
|
||||
"""
|
||||
def __init__(self, data: InvitePayload, code: str, state: State):
|
||||
self.state = state
|
||||
|
||||
self.code = code
|
||||
self.server = state.get_server(data["server_id"])
|
||||
self.channel = self.server.get_channel(data["channel_id"])
|
||||
|
||||
self.user_name = data["user_name"]
|
||||
self.user = None
|
||||
|
||||
if avatar := data.get("user_avatar"):
|
||||
self.user_avatar = Asset(avatar, state)
|
||||
else:
|
||||
self.user_avatar = None
|
||||
|
||||
self.member_count = data["member_count"]
|
||||
|
||||
@staticmethod
|
||||
def _from_partial(code: str, server: str, creator: str, channel: str, state: State) -> Invite:
|
||||
invite = Invite.__new__(Invite)
|
||||
|
||||
invite.state = state
|
||||
invite.code = code
|
||||
invite.server = state.get_server(server)
|
||||
invite.channel = state.get_channel(channel)
|
||||
invite.user = state.get_user(creator)
|
||||
invite.user_name = invite.user.name
|
||||
invite.user_avatar = invite.user.avatar
|
||||
invite.member_count = len(invite.server.members)
|
||||
|
||||
return invite
|
||||
|
||||
async def delete(self):
|
||||
"""Deletes the invite"""
|
||||
await self.state.http.delete_invite(self.code)
|
||||
@@ -65,3 +65,21 @@ class Member(User):
|
||||
if roles:
|
||||
member_roles = [self.server.get_role(role_id) for role_id in roles]
|
||||
self.roles = sorted(member_roles, key=lambda role: role.rank, reverse=True)
|
||||
|
||||
async def kick(self):
|
||||
"""Kicks the member from the server"""
|
||||
await self.state.http.kick_member(self.server.id, self.id)
|
||||
|
||||
async def ban(self, *, reason: Optional[str] = None):
|
||||
"""Bans the member from the server
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
reason: Optional[:class:`str`]
|
||||
The reason for the ban
|
||||
"""
|
||||
await self.state.http.ban_member(self.server.id, self.id, reason)
|
||||
|
||||
async def unban(self):
|
||||
"""Unbans the member from the server"""
|
||||
await self.state.http.unban_member(self.server.id, self.id)
|
||||
|
||||
+3
-3
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, NamedTuple, Optional
|
||||
|
||||
from .asset import Asset, PartialAsset
|
||||
from .channel import Messageable
|
||||
from .embed import Embed
|
||||
from .embed import to_embed
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .state import State
|
||||
@@ -31,7 +31,7 @@ class Message:
|
||||
The content of the message, this will not include system message's content
|
||||
attachments: list[:class:`Asset`]
|
||||
The attachments of the message
|
||||
embeds: list[:class:`Embed`]
|
||||
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
|
||||
@@ -56,7 +56,7 @@ class Message:
|
||||
self.id = data["_id"]
|
||||
self.content = data["content"]
|
||||
self.attachments = [Asset(attachment, state) for attachment in data.get("attachments", [])]
|
||||
self.embeds = [Embed.from_dict(embed) for embed in data.get("embeds", [])]
|
||||
self.embeds = [to_embed(embed, state) for embed in data.get("embeds", [])]
|
||||
|
||||
channel = state.get_channel(data["channel"])
|
||||
assert isinstance(channel, Messageable)
|
||||
|
||||
+75
-6
@@ -2,8 +2,10 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from .enums import SortType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .embed import Embed
|
||||
from .embed import Embed, SendableEmbed
|
||||
from .file import File
|
||||
from .message import Masquerade, Message, MessageReply
|
||||
from .state import State
|
||||
@@ -23,10 +25,10 @@ class Messageable:
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def _get_channel_id(self) -> str:
|
||||
async def _get_channel_id(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
async def send(self, content: Optional[str] = None, *, embeds: Optional[list[Embed]] = None, embed: Optional[Embed] = None, attachments: Optional[list[File]] = None, replies: Optional[list[MessageReply]] = None, reply: Optional[MessageReply] = None, masquerade: Optional[Masquerade] = None) -> Message:
|
||||
async def send(self, content: Optional[str] = None, *, embeds: Optional[list[SendableEmbed]] = None, embed: Optional[SendableEmbed] = None, attachments: Optional[list[File]] = None, replies: Optional[list[MessageReply]] = None, reply: Optional[MessageReply] = None, masquerade: Optional[Masquerade] = None) -> Message:
|
||||
"""Sends a message in a channel, you must send at least one of either `content`, `embeds` or `attachments`
|
||||
|
||||
Parameters
|
||||
@@ -35,8 +37,10 @@ class Messageable:
|
||||
The content of the message, this will not include system message's content
|
||||
attachments: Optional[list[:class:`File`]]
|
||||
The attachments of the message
|
||||
embeds: Optional[list[:class:`Embed`]]
|
||||
The embeds of the message
|
||||
embed: Optional[:class:`SendableEmbed`]
|
||||
The embed to send with the message
|
||||
embeds: Optional[list[:class:`SendableEmbed`]]
|
||||
The embeds to send with the message
|
||||
replies: Optional[list[:class:`MessageReply`]]
|
||||
The list of messages to reply to.
|
||||
|
||||
@@ -55,5 +59,70 @@ class Messageable:
|
||||
reply_payload = [reply.to_dict() for reply in replies] if replies else None
|
||||
masquerade_payload = masquerade.to_dict() if masquerade else None
|
||||
|
||||
message = await self.state.http.send_message(self._get_channel_id(), content, embed_payload, attachments, reply_payload, masquerade_payload)
|
||||
message = await self.state.http.send_message(await self._get_channel_id(), content, embed_payload, attachments, reply_payload, masquerade_payload)
|
||||
return self.state.add_message(message)
|
||||
|
||||
|
||||
async def fetch_message(self, message_id: str) -> Message:
|
||||
"""Fetches a message from the channel
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
message_id: :class:`str`
|
||||
The id of the message you want to fetch
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Message`
|
||||
The message with the matching id
|
||||
"""
|
||||
payload = await self.state.http.fetch_message(await self._get_channel_id(), message_id)
|
||||
return Message(payload, self.state)
|
||||
|
||||
async def history(self, *, sort: SortType = SortType.latest, limit: int = 100, before: Optional[str] = None, after: Optional[str] = None, nearby: Optional[str] = None) -> list[Message]:
|
||||
"""Fetches multiple messages from the channel's history
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
sort: :class:`SortType`
|
||||
The order to sort the messages in
|
||||
limit: :class:`int`
|
||||
How many messages to fetch
|
||||
before: Optional[:class:`str`]
|
||||
The id of the message which should come *before* all the messages to be fetched
|
||||
after: Optional[:class:`str`]
|
||||
The id of the message which should come *after* all the messages to be fetched
|
||||
nearby: Optional[:class:`str`]
|
||||
The id of the message which should be nearby all the messages to be fetched
|
||||
|
||||
Returns
|
||||
--------
|
||||
list[:class:`Message`]
|
||||
The messages found in order of the sort parameter
|
||||
"""
|
||||
payloads = await self.state.http.fetch_messages(await self._get_channel_id(), sort=sort, limit=limit, before=before, after=after, nearby=nearby)
|
||||
return [Message(payload, self.state) for payload in payloads]
|
||||
|
||||
async def search(self, query: str, *, sort: SortType = SortType.latest, limit: int = 100, before: Optional[str] = None, after: Optional[str] = None) -> list[Message]:
|
||||
"""searches the channel for a query
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
query: :class:`str`
|
||||
The query to search for in the channel
|
||||
sort: :class:`SortType`
|
||||
The order to sort the messages in
|
||||
limit: :class:`int`
|
||||
How many messages to fetch
|
||||
before: Optional[:class:`str`]
|
||||
The id of the message which should come *before* all the messages to be fetched
|
||||
after: Optional[:class:`str`]
|
||||
The id of the message which should come *after* all the messages to be fetched
|
||||
|
||||
Returns
|
||||
--------
|
||||
list[:class:`Message`]
|
||||
The messages found in order of the sort parameter
|
||||
"""
|
||||
payloads = await self.state.http.search_messages(await self._get_channel_id(), query, sort=sort, limit=limit, before=before, after=after)
|
||||
return [Message(payload, self.state) for payload in payloads]
|
||||
|
||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
||||
|
||||
from .flags import Flags, flag_value
|
||||
|
||||
|
||||
__all__ = (
|
||||
"ChannelPermissions",
|
||||
"ServerPermissions"
|
||||
@@ -43,7 +42,7 @@ class ChannelPermissions(Flags):
|
||||
|
||||
@classmethod
|
||||
def all(cls) -> ChannelPermissions:
|
||||
return cls._from_value(0b11111011)
|
||||
return cls._from_value(0b11111111)
|
||||
|
||||
@classmethod
|
||||
def view(cls) -> ChannelPermissions:
|
||||
|
||||
+24
-7
@@ -2,8 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from revolt.types import server
|
||||
|
||||
from .utils import Missing
|
||||
from .permissions import ChannelPermissions, ServerPermissions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -23,7 +22,7 @@ class Role:
|
||||
The id of the role
|
||||
name: :class:`str`
|
||||
The name of the role
|
||||
colour: :class:`str`
|
||||
colour: Optional[:class:`str`]
|
||||
The colour of the role
|
||||
hoist: :class:`bool`
|
||||
Whether members with the role will display seperate from everyone else
|
||||
@@ -38,13 +37,13 @@ class Role:
|
||||
"""
|
||||
__slots__ = ("id", "name", "colour", "hoist", "rank", "state", "server", "server_permissions", "channel_permissions")
|
||||
|
||||
def __init__(self, data: RolePayload, role_id: str, state: State, server: Server):
|
||||
def __init__(self, data: RolePayload, role_id: str, server: Server, state: State):
|
||||
self.state = state
|
||||
self.id = role_id
|
||||
self.name = data["name"]
|
||||
self.colour = data.get("colour")
|
||||
self.hoist = data.get("hoist", False)
|
||||
self.rank = data.get("rank", 0)
|
||||
self.colour = None
|
||||
self.hoist = False
|
||||
self.rank = 0
|
||||
self.server = server
|
||||
self.server_permissions = ServerPermissions._from_value(data["permissions"][0])
|
||||
self.channel_permissions = ChannelPermissions._from_value(data["permissions"][1])
|
||||
@@ -83,3 +82,21 @@ class Role:
|
||||
|
||||
if rank:
|
||||
self.rank = rank
|
||||
|
||||
async def delete(self):
|
||||
"""Deletes the role"""
|
||||
await self.state.http.delete_role(self.server.id, self.id)
|
||||
|
||||
async def edit(self, **kwargs):
|
||||
"""Edits the role
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
"""
|
||||
remove = []
|
||||
|
||||
if kwargs.get("colour", Missing) is None:
|
||||
kwargs.pop("colour")
|
||||
remove.append("Colour")
|
||||
|
||||
await self.state.http.edit_role(self.server.id, self.id, remove, kwargs)
|
||||
|
||||
+126
-5
@@ -1,10 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, Optional, cast
|
||||
|
||||
from .asset import Asset
|
||||
from .category import Category
|
||||
from .channel import Channel, channel_factory
|
||||
from .channel import Channel, VoiceChannel
|
||||
from .invite import Invite
|
||||
from .permissions import ChannelPermissions, ServerPermissions
|
||||
from .role import Role
|
||||
|
||||
@@ -16,7 +17,7 @@ if TYPE_CHECKING:
|
||||
from .types import File as FilePayload
|
||||
from .types import Permission as PermissionPayload
|
||||
from .types import Server as ServerPayload
|
||||
from .types import SystemMessagesConfig
|
||||
from .types import SystemMessagesConfig, Ban
|
||||
|
||||
|
||||
__all__ = ("Server", "SystemMessages")
|
||||
@@ -98,7 +99,7 @@ class Server:
|
||||
self.default_channel_permissions = ChannelPermissions._from_value(data["default_permissions"][1])
|
||||
self.description = data.get("description") or None
|
||||
self.nsfw = data.get("nsfw", False)
|
||||
self.system_messages = SystemMessages(data.get("system_messages", {}), state)
|
||||
self.system_messages = SystemMessages(data.get("system_messages", cast("SystemMessagesConfig", {})), state)
|
||||
self._categories = {data["id"]: Category(data, state) for data in data.get("categories", [])}
|
||||
|
||||
if icon := data.get("icon"):
|
||||
@@ -112,7 +113,7 @@ class Server:
|
||||
self.banner = None
|
||||
|
||||
self._members: dict[str, Member] = {}
|
||||
self._roles: dict[str, Role] = {role_id: Role(role, role_id, state, self) for role_id, role in data.get("roles", {}).items()}
|
||||
self._roles: dict[str, Role] = {role_id: Role(role, role_id, self, state) for role_id, role in data.get("roles", {}).items()}
|
||||
|
||||
self._channels: dict[str, Channel] = {channel_id: state.get_channel(channel_id) for channel_id in data.get("channels", [])}
|
||||
|
||||
@@ -235,3 +236,123 @@ class Server:
|
||||
channel_value = (channel_permissions or self.default_channel_permissions).value
|
||||
|
||||
await self.state.http.set_default_permissions(self.id, server_value, channel_value)
|
||||
|
||||
async def leave_server(self):
|
||||
"""Leaves or deletes the server"""
|
||||
await self.state.http.delete_leave_server(self.id)
|
||||
|
||||
async def delete_server(self):
|
||||
"""Leaves or deletes a server, alias to :meth`Server.leave_server`"""
|
||||
await self.leave_server()
|
||||
|
||||
async def create_text_channel(self, *, name: str, description: Optional[str] = None) -> TextChannel:
|
||||
"""Creates a text channel in the server
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The name of the channel
|
||||
description: Optional[:class:`str`]
|
||||
The channel's description
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`TextChannel`
|
||||
The text channel that was just created
|
||||
"""
|
||||
payload = await self.state.http.create_channel(self.id, "Text", name, description)
|
||||
|
||||
channel = TextChannel(payload, self.state)
|
||||
self._channels[channel.id] = channel
|
||||
|
||||
return channel
|
||||
|
||||
async def create_voice_channel(self, *, name: str, description: Optional[str] = None) -> VoiceChannel:
|
||||
"""Creates a voice channel in the server
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The name of the channel
|
||||
description: Optional[:class:`str`]
|
||||
The channel's description
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`VoiceChannel`
|
||||
The voice channel that was just created
|
||||
"""
|
||||
payload = await self.state.http.create_channel(self.id, "Voice", name, description)
|
||||
|
||||
channel = VoiceChannel(payload, self.state)
|
||||
self._channels[channel.id] = channel
|
||||
|
||||
return channel
|
||||
|
||||
async def fetch_invites(self) -> list[Invite]:
|
||||
"""Fetches all invites in the server
|
||||
|
||||
Returns
|
||||
--------
|
||||
list[:class:`Invite`]
|
||||
"""
|
||||
invite_payloads = await self.state.http.fetch_server_invites(self.id)
|
||||
|
||||
return [Invite._from_partial(payload["_id"], payload["server"], payload["creator"], payload["channel"], self.state) for payload in invite_payloads]
|
||||
|
||||
async def fetch_member(self, member_id: str) -> Member:
|
||||
"""Fetches a member from this server
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
member_id: :class:`str`
|
||||
The id of the member you are fetching
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Member`
|
||||
The member with the matching id
|
||||
"""
|
||||
payload = await self.state.http.fetch_member(self.id, member_id)
|
||||
|
||||
return Member(payload, self, self.state)
|
||||
|
||||
async def fetch_bans(self) -> list[ServerBan]:
|
||||
"""Fetches all invites in the server
|
||||
|
||||
Returns
|
||||
--------
|
||||
list[:class:`Invite`]
|
||||
"""
|
||||
payload = await self.state.http.fetch_bans(self.id)
|
||||
|
||||
return [ServerBan(ban, self.state) for ban in payload["bans"]]
|
||||
|
||||
async def create_role(self, name: str) -> Role:
|
||||
"""Creates a role in the server
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The name of the role
|
||||
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Role`
|
||||
The role that was just created
|
||||
"""
|
||||
payload = await self.state.http.create_role(self.id, name)
|
||||
|
||||
return Role(payload, name, self, self.state)
|
||||
|
||||
class ServerBan:
|
||||
def __init__(self, ban: Ban, state: State):
|
||||
self.reason = ban.get("reason")
|
||||
self.server = state.get_server(ban["_id"]["server"])
|
||||
self.user = state.get_user(ban["_id"]["user"])
|
||||
self.state = state
|
||||
|
||||
async def unban(self):
|
||||
"""Unbans the user"""
|
||||
await self.state.http.unban_member(self.server.id, self.user.id)
|
||||
|
||||
@@ -10,7 +10,7 @@ if TYPE_CHECKING:
|
||||
__all__ = (
|
||||
"SavedMessages",
|
||||
"DMChannel",
|
||||
"Group",
|
||||
"GroupDMChannel",
|
||||
"TextChannel",
|
||||
"VoiceChannel",
|
||||
"Channel",
|
||||
@@ -37,7 +37,7 @@ class _GroupOptional(TypedDict):
|
||||
permissions: int
|
||||
description: str
|
||||
|
||||
class Group(_NonceChannel, _GroupOptional, BaseChannel):
|
||||
class GroupDMChannel(_NonceChannel, _GroupOptional, BaseChannel):
|
||||
recipients: list[str]
|
||||
name: str
|
||||
owner: str
|
||||
@@ -47,12 +47,12 @@ class _TextChannelOptional(TypedDict, total=False):
|
||||
icon: File
|
||||
default_permissions: int
|
||||
role_permissions: dict[str, int]
|
||||
last_message: str
|
||||
|
||||
class TextChannel(_NonceChannel, _TextChannelOptional, BaseChannel):
|
||||
server: str
|
||||
name: str
|
||||
description: str
|
||||
last_message: str
|
||||
channel_type: Literal["TextChannel"]
|
||||
|
||||
class _VoiceChannelOptional(TypedDict, total=False):
|
||||
@@ -66,4 +66,4 @@ class VoiceChannel(_NonceChannel, _TextChannelOptional, BaseChannel):
|
||||
description: str
|
||||
channel_type: Literal["VoiceChannel"]
|
||||
|
||||
Channel = Union[SavedMessages, DMChannel, Group, TextChannel, VoiceChannel]
|
||||
Channel = Union[SavedMessages, DMChannel, GroupDMChannel, TextChannel, VoiceChannel]
|
||||
|
||||
+80
-4
@@ -1,8 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, TypedDict
|
||||
from typing import TYPE_CHECKING, Literal, TypedDict, Union
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
__all__ = ("Embed",)
|
||||
if TYPE_CHECKING:
|
||||
from .file import File
|
||||
|
||||
__all__ = ("Embed", "SendableEmbed", "WebsiteEmbed", "ImageEmbed", "TextEmbed", "NoneEmbed", "YoutubeSpecial", "TwitchSpecial", "SpotifySpecial", "SoundcloudSpecial", "BandcampSpecial", "WebsiteSpecial", "JanuaryImage", "JanuaryVideo")
|
||||
|
||||
class YoutubeSpecial(TypedDict):
|
||||
type: Literal["Youtube"]
|
||||
id: str
|
||||
timestamp: NotRequired[str]
|
||||
|
||||
class TwitchSpecial(TypedDict):
|
||||
type: Literal["Twitch"]
|
||||
content_type: Literal["Channel", "Video", "Clip"]
|
||||
id: str
|
||||
|
||||
class SpotifySpecial(TypedDict):
|
||||
type: Literal["Spotify"]
|
||||
content_type: str
|
||||
id: str
|
||||
|
||||
class SoundcloudSpecial(TypedDict):
|
||||
type: Literal["Soundcloud"]
|
||||
|
||||
class BandcampSpecial(TypedDict):
|
||||
type: Literal["Bandcamp"]
|
||||
content_type: Literal["Album", "Track"]
|
||||
id: str
|
||||
|
||||
WebsiteSpecial = Union[YoutubeSpecial, TwitchSpecial, SpotifySpecial, SoundcloudSpecial, BandcampSpecial]
|
||||
|
||||
class JanuaryImage(TypedDict):
|
||||
url: str
|
||||
width: int
|
||||
height: int
|
||||
size: Literal["Large", "Preview"]
|
||||
|
||||
class JanuaryVideo(TypedDict):
|
||||
url: str
|
||||
width: int
|
||||
height: int
|
||||
|
||||
class WebsiteEmbed(TypedDict):
|
||||
type: Literal["Website"]
|
||||
url: NotRequired[str]
|
||||
special: NotRequired[WebsiteSpecial]
|
||||
title: NotRequired[str]
|
||||
description: NotRequired[str]
|
||||
image: NotRequired[JanuaryImage]
|
||||
video: NotRequired[JanuaryVideo]
|
||||
site_name: NotRequired[str]
|
||||
icon_url: NotRequired[str]
|
||||
colour: NotRequired[str]
|
||||
|
||||
class ImageEmbed(JanuaryImage):
|
||||
type: Literal["Image"]
|
||||
|
||||
class TextEmbed(TypedDict):
|
||||
type: Literal["Text"]
|
||||
icon_url: NotRequired[str]
|
||||
url: NotRequired[str]
|
||||
title: NotRequired[str]
|
||||
description: NotRequired[str]
|
||||
media: NotRequired[File]
|
||||
colour: NotRequired[str]
|
||||
|
||||
class NoneEmbed(TypedDict):
|
||||
type: Literal["None"]
|
||||
|
||||
Embed = Union[WebsiteEmbed, ImageEmbed, TextEmbed, NoneEmbed]
|
||||
|
||||
class SendableEmbed(TypedDict):
|
||||
type: Literal["Text"]
|
||||
icon_url: NotRequired[str]
|
||||
url: NotRequired[str]
|
||||
title: NotRequired[str]
|
||||
description: NotRequired[str]
|
||||
media: NotRequired[str]
|
||||
colour: NotRequired[str]
|
||||
|
||||
class Embed(TypedDict):
|
||||
pass # TODO
|
||||
|
||||
@@ -2,8 +2,8 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Literal, TypedDict, Union
|
||||
|
||||
from .channel import (Channel, DMChannel, Group, SavedMessages, TextChannel,
|
||||
VoiceChannel)
|
||||
from .channel import (Channel, DMChannel, GroupDMChannel, SavedMessages,
|
||||
TextChannel, VoiceChannel)
|
||||
from .message import Message
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -69,7 +69,7 @@ class MessageDeleteEventPayload(BasePayload):
|
||||
class ChannelCreateEventPayload_SavedMessages(BasePayload, SavedMessages):
|
||||
pass
|
||||
|
||||
class ChannelCreateEventPayload_Group(BasePayload, Group):
|
||||
class ChannelCreateEventPayload_Group(BasePayload, GroupDMChannel):
|
||||
pass
|
||||
|
||||
class ChannelCreateEventPayload_TextChannel(BasePayload, TextChannel):
|
||||
|
||||
+23
-15
@@ -1,22 +1,30 @@
|
||||
from typing import Literal, TypedDict, Union
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = (
|
||||
"GroupInvite",
|
||||
"ServerInvite",
|
||||
"Invite",
|
||||
)
|
||||
from typing import TYPE_CHECKING, Literal, TypedDict
|
||||
|
||||
class GroupInvite(TypedDict):
|
||||
type: Literal["Group"]
|
||||
_id: str
|
||||
creator: str
|
||||
channel: str
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
class ServerInvite(TypedDict):
|
||||
if TYPE_CHECKING:
|
||||
from .file import File
|
||||
|
||||
__all__ = ("Invite", "PartialInvite")
|
||||
|
||||
|
||||
class Invite(TypedDict):
|
||||
type: Literal["Server"]
|
||||
server_id: str
|
||||
server_name: str
|
||||
server_icon: NotRequired[str]
|
||||
server_banner: NotRequired[str]
|
||||
channel_id: str
|
||||
channel_name: str
|
||||
channel_description: NotRequired[str]
|
||||
user_name: str
|
||||
user_avatar: NotRequired[File]
|
||||
member_count: int
|
||||
|
||||
class PartialInvite(TypedDict):
|
||||
_id: str
|
||||
server: str
|
||||
creator: str
|
||||
channel: str
|
||||
|
||||
Invite = Union[ServerInvite, GroupInvite]
|
||||
creator: str
|
||||
|
||||
@@ -9,11 +9,6 @@ __all__ = (
|
||||
|
||||
Permission = tuple[int, int]
|
||||
|
||||
class _RoleOptional(TypedDict, total=False):
|
||||
colour: str
|
||||
hoist: bool
|
||||
rank: int
|
||||
|
||||
class Role(_RoleOptional):
|
||||
class Role(TypedDict):
|
||||
name: str
|
||||
permissions: Permission
|
||||
|
||||
@@ -27,7 +27,7 @@ class _ServerOptional(TypedDict, total=False):
|
||||
description: str
|
||||
categories: list[Category]
|
||||
system_messages: SystemMessagesConfig
|
||||
roles: list[Role]
|
||||
roles: dict[str, Role]
|
||||
icon: File
|
||||
banner: File
|
||||
nsfw: bool
|
||||
@@ -46,11 +46,15 @@ class BannedUser(_OptionalBannedUser):
|
||||
_id: str
|
||||
username: str
|
||||
|
||||
class BanId(TypedDict):
|
||||
server: str
|
||||
user: str
|
||||
|
||||
class _OptionalBan(TypedDict, total=False):
|
||||
reason: str
|
||||
|
||||
class Ban(_OptionalBan):
|
||||
_id: str
|
||||
_id: BanId
|
||||
|
||||
class ServerBans(TypedDict):
|
||||
users: list[BannedUser]
|
||||
|
||||
+45
-2
@@ -3,8 +3,10 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, NamedTuple, Optional, Union
|
||||
|
||||
from .asset import Asset, PartialAsset
|
||||
from .channel import DMChannel
|
||||
from .enums import PresenceType, RelationshipType
|
||||
from .flags import UserBadges
|
||||
from .messageable import Messageable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .state import State
|
||||
@@ -30,7 +32,7 @@ class UserProfile(NamedTuple):
|
||||
content: Optional[str]
|
||||
background: Optional[Asset]
|
||||
|
||||
class User:
|
||||
class User(Messageable):
|
||||
"""Represents a user
|
||||
|
||||
Attributes
|
||||
@@ -53,14 +55,17 @@ class User:
|
||||
The relationship between the user and the bot
|
||||
status: Optional[:class:`Status`]
|
||||
The users status
|
||||
dm_channel: Optional[:class:`DMChannel`]
|
||||
The dm channel between the client and the user, this will only be set if the client has dm'ed the user or :meth:`User.open_dm` was run
|
||||
"""
|
||||
__flattern_attributes__ = ("id", "bot", "owner_id", "badges", "online", "flags", "relations", "relationship", "status", "masquerade_avatar", "masquerade_name", "original_name", "original_avatar", "profile")
|
||||
__flattern_attributes__ = ("id", "bot", "owner_id", "badges", "online", "flags", "relations", "relationship", "status", "masquerade_avatar", "masquerade_name", "original_name", "original_avatar", "profile", "dm_channel")
|
||||
__slots__ = (*__flattern_attributes__, "state")
|
||||
|
||||
def __init__(self, data: UserPayload, state: State):
|
||||
self.state = state
|
||||
self.id = data["_id"]
|
||||
self.original_name = data["username"]
|
||||
self.dm_channel = None
|
||||
|
||||
bot = data.get("bot")
|
||||
if bot:
|
||||
@@ -100,6 +105,13 @@ class User:
|
||||
self.masquerade_avatar: Optional[PartialAsset] = None
|
||||
self.masquerade_name: Optional[str] = None
|
||||
|
||||
async def _get_channel_id(self):
|
||||
if not self.dm_channel:
|
||||
payload = await self.state.http.open_dm(self.id)
|
||||
self.dm_channel = DMChannel(payload, self.state)
|
||||
|
||||
return self.id
|
||||
|
||||
@property
|
||||
def owner(self) -> Optional[User]:
|
||||
owner_id = self.owner_id
|
||||
@@ -135,3 +147,34 @@ class User:
|
||||
|
||||
if online:
|
||||
self.online = online
|
||||
|
||||
async def default_avatar(self) -> bytes:
|
||||
"""Returns the default avatar for this user
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`bytes`
|
||||
The bytes of the image
|
||||
"""
|
||||
return await self.state.http.fetch_default_avatar(self.id)
|
||||
|
||||
async def fetch_profile(self) -> UserProfile:
|
||||
"""Fetches the user's profile
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`UserProfile`
|
||||
The user's profile
|
||||
"""
|
||||
if profile := self.profile:
|
||||
return profile
|
||||
|
||||
payload = await self.state.http.fetch_profile(self.id)
|
||||
|
||||
if file := payload.get("background"):
|
||||
background = Asset(file, self.state)
|
||||
else:
|
||||
background = None
|
||||
|
||||
self.profile = UserProfile(payload.get("content"), background)
|
||||
return self.profile
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
import inspect
|
||||
from typing import Any, Callable, Coroutine, TypeVar, Union, Iterable
|
||||
from operator import attrgetter
|
||||
from typing import Any, Callable, Coroutine, Iterable, TypeVar, Union
|
||||
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
@@ -18,7 +18,7 @@ def copy_doc(from_t: T) -> Callable[[T], T]:
|
||||
def inner(to_t: T) -> T:
|
||||
to_t.__doc__ = from_t.__doc__
|
||||
return to_t
|
||||
|
||||
|
||||
return inner
|
||||
|
||||
R_T = TypeVar("R_T")
|
||||
@@ -38,13 +38,13 @@ async def maybe_coroutine(func: Callable[P, Union[R_T, Coroutine[Any, Any, R_T]]
|
||||
|
||||
|
||||
def get(iterable: Iterable[T], **attrs: Any) -> T:
|
||||
"""A convenaince function to help get a value from an iterable with a specific attribute
|
||||
"""A convenience function to help get a value from an iterable with a specific attribute
|
||||
|
||||
Examples
|
||||
---------
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 4
|
||||
:emphasize-lines: 3
|
||||
|
||||
from revolt import utils
|
||||
|
||||
|
||||
+15
-9
@@ -6,9 +6,6 @@ from copy import copy
|
||||
from typing import TYPE_CHECKING, Callable, cast
|
||||
|
||||
from .enums import RelationshipType
|
||||
from .types import (ChannelCreateEventPayload, ChannelDeleteEventPayload,
|
||||
ChannelDeleteTypingEventPayload,
|
||||
ChannelStartTypingEventPayload, ChannelUpdateEventPayload)
|
||||
from .types import Message as MessagePayload
|
||||
from .types import (MessageDeleteEventPayload, MessageUpdateEventPayload,
|
||||
ServerDeleteEventPayload, ServerMemberJoinEventPayload,
|
||||
@@ -16,8 +13,11 @@ from .types import (MessageDeleteEventPayload, MessageUpdateEventPayload,
|
||||
ServerMemberUpdateEventPayload,
|
||||
ServerRoleDeleteEventPayload, ServerRoleUpdateEventPayload,
|
||||
ServerUpdateEventPayload, UserRelationshipEventPayload,
|
||||
UserUpdateEventPayload)
|
||||
from .user import Status
|
||||
UserUpdateEventPayload, ChannelCreateEventPayload, ChannelDeleteEventPayload,
|
||||
ChannelDeleteTypingEventPayload,
|
||||
ChannelStartTypingEventPayload, ChannelUpdateEventPayload)
|
||||
from .user import Status, UserProfile
|
||||
from .channel import TextChannel, GroupDMChannel, VoiceChannel
|
||||
|
||||
try:
|
||||
import ujson as json
|
||||
@@ -163,10 +163,12 @@ class WebsocketHandler:
|
||||
|
||||
if clear := payload.get("clear"):
|
||||
if clear == "Icon":
|
||||
pass # TODO
|
||||
if isinstance(channel, (TextChannel, VoiceChannel, GroupDMChannel)):
|
||||
channel.icon = None
|
||||
|
||||
elif clear == "Description":
|
||||
channel.description = None # type: ignore
|
||||
if isinstance(channel, (TextChannel, VoiceChannel, GroupDMChannel)):
|
||||
channel.description = None
|
||||
|
||||
self.dispatch("channel_update", old_channel, channel)
|
||||
|
||||
@@ -263,9 +265,13 @@ class WebsocketHandler:
|
||||
|
||||
if clear := payload.get("clear"):
|
||||
if clear == "ProfileContent":
|
||||
...
|
||||
if profile := user.profile:
|
||||
user.profile = UserProfile(None, profile.background)
|
||||
|
||||
elif clear == "ProfileBackground":
|
||||
...
|
||||
if profile := user.profile:
|
||||
user.profile = UserProfile(profile.content, None)
|
||||
|
||||
elif clear == "StatusText":
|
||||
# user.status will never be none because they are trying to remove the text
|
||||
if user.status.presence is None: # type: ignore
|
||||
|
||||
@@ -5,16 +5,12 @@ from setuptools import find_packages, setup
|
||||
here = pathlib.Path(__file__).parent.resolve()
|
||||
|
||||
long_description = (here / "README.md").read_text(encoding="utf-8")
|
||||
|
||||
requirements = []
|
||||
|
||||
with open('requirements.txt') as f:
|
||||
requirements = f.read().splitlines()
|
||||
requirements = (here / "requirements.txt").read_text(encoding="utf-8").splitlines()
|
||||
|
||||
|
||||
setup(
|
||||
name="revolt.py",
|
||||
version="0.1.3",
|
||||
version="0.1.6",
|
||||
description="Python wrapper for the revolt.chat API",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
@@ -37,7 +33,11 @@ setup(
|
||||
packages=find_packages(),
|
||||
python_requires=">=3.9",
|
||||
extras_require={
|
||||
"speedups": ["ujson", "aiohttp[speedups]==3.7.4.post0", "msgpack==1.0.2"],
|
||||
"speedups": [
|
||||
"ujson",
|
||||
"aiohttp[speedups]==3.7.4.post0",
|
||||
"msgpack==1.0.2"
|
||||
],
|
||||
},
|
||||
install_requires=requirements,
|
||||
project_urls={
|
||||
|
||||
Reference in New Issue
Block a user