mirror of
https://github.com/stoatchat/python-client-sdk.git
synced 2026-07-25 16:35:33 -04:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8aafa8b7f2 | |||
| 3754357812 | |||
| 5b757bd5df | |||
| e938136b2e | |||
| 18e8366651 | |||
| 85fbe30c32 | |||
| 02a839a903 | |||
| 9fc5ee1c84 | |||
| 3200b06019 | |||
| df8815e68a | |||
| ccd379b24b | |||
| 1c5ceffc58 | |||
| fd6ef5fc30 | |||
| 7ccd651e4b | |||
| 35566fff42 | |||
| abbbc8c1eb | |||
| 67af8b5bf0 | |||
| 39098692bc | |||
| a16bac6207 | |||
| 23d8ced599 | |||
| e5d65b9400 | |||
| c874017a6b | |||
| 21d27bd44a | |||
| 91fde98739 | |||
| 34a80ea5e2 | |||
| 7d6f33f95c | |||
| 1310f3a31a | |||
| 611939ed6f | |||
| 5514d03b63 | |||
| bd984090c7 | |||
| b2558c1d37 | |||
| 0c7cc738fc | |||
| d7175d0079 | |||
| b5727c818e | |||
| 89986731c1 | |||
| 2a3071712e | |||
| 77fa908ad5 | |||
| b94c8cd58e | |||
| c5a67335d2 | |||
| 287b2d42ea | |||
| a2168e35e9 | |||
| 497d666311 | |||
| c648eda8e5 | |||
| 34a6a9cdbc |
@@ -5,3 +5,5 @@ dist
|
||||
*.egg-info
|
||||
docs/_build
|
||||
.vscode
|
||||
.env
|
||||
.mypy_cache
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
set dotenv-load := true
|
||||
|
||||
activate:
|
||||
source .venv/bin/activate
|
||||
|
||||
venv:
|
||||
python -m venv .venv
|
||||
just activate
|
||||
python -m pip install -r requirements.txt
|
||||
python -m pip install -r docs_requirements.txt
|
||||
|
||||
test: venv
|
||||
python test.py
|
||||
|
||||
build: venv
|
||||
python -m build
|
||||
|
||||
upload: venv
|
||||
python -m twine upload dist/* -u $PYPI_USERNAME -p $PYPI_PASSWORD
|
||||
|
||||
lint:
|
||||
pyright . --venv-path .venv
|
||||
|
||||
coverage:
|
||||
pyright --lib --ignoreexternal --verifytypes revolt
|
||||
|
||||
docs:
|
||||
cd docs && make html
|
||||
@@ -1,16 +1,12 @@
|
||||
# Revolt.py
|
||||
|
||||
An async library to interact with the https://revolt.chat api.
|
||||
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.
|
||||
|
||||
This library will be focused on making bots and i will not implement anything only for user accounts.
|
||||
|
||||
Support server: https://app.revolt.chat/invite/FDXER6hr
|
||||
|
||||
Documentation is [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/).
|
||||
|
||||
## Example
|
||||
|
||||
More examples in the [examples folder](https://github.com/zomatree/revolt.py/blob/master/examples)
|
||||
More examples can be found in the [examples folder](https://github.com/revoltchat/revolt.py/blob/master/examples).
|
||||
|
||||
```py
|
||||
import revolt
|
||||
|
||||
@@ -78,6 +78,9 @@ Message
|
||||
.. autoclass:: Message
|
||||
:members:
|
||||
|
||||
.. autoclass:: MessageReply
|
||||
:members:
|
||||
|
||||
Messageable
|
||||
~~~~~~~~~~~~
|
||||
|
||||
@@ -105,6 +108,20 @@ Server
|
||||
.. autoclass:: Server
|
||||
:members:
|
||||
|
||||
Category
|
||||
~~~~~~~~~
|
||||
|
||||
.. autoclass:: Category
|
||||
:members:
|
||||
|
||||
|
||||
SystemMessages
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: SystemMessages
|
||||
:members:
|
||||
|
||||
|
||||
User
|
||||
~~~~~
|
||||
|
||||
@@ -122,6 +139,12 @@ Status
|
||||
|
||||
.. autonamedtuple:: Status
|
||||
|
||||
UserBadges
|
||||
~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: UserBadges
|
||||
:members:
|
||||
|
||||
|
||||
Enums
|
||||
======
|
||||
@@ -197,3 +220,12 @@ All enums subclass `aenum.Enum`.
|
||||
.. attribute:: user
|
||||
|
||||
That user is yourself
|
||||
|
||||
Utils
|
||||
======
|
||||
|
||||
.. currentmodule:: revolt.utils
|
||||
|
||||
A collection a utility functions and classes to aid in making your bot
|
||||
|
||||
.. autofunction:: get
|
||||
|
||||
@@ -4,9 +4,9 @@ API Reference
|
||||
===============
|
||||
|
||||
|
||||
CommandsMixin
|
||||
~~~~~~~~~~~~~
|
||||
.. autoclass:: revolt.ext.commands.CommandsMixin
|
||||
CommandsClient
|
||||
~~~~~~~~~~~~~~~
|
||||
.. autoclass:: revolt.ext.commands.CommandsClient
|
||||
:members:
|
||||
|
||||
Context
|
||||
@@ -19,11 +19,92 @@ Command
|
||||
.. autoclass:: revolt.ext.commands.Command
|
||||
:members:
|
||||
|
||||
command
|
||||
~~~~~~~~
|
||||
.. autodecorator:: revolt.ext.commands.command
|
||||
|
||||
check
|
||||
~~~~~~
|
||||
.. autodecorator:: revolt.ext.commands.check
|
||||
|
||||
is_bot_owner
|
||||
~~~~~~~~~~~~~
|
||||
.. autodecorator:: revolt.ext.commands.is_bot_owner
|
||||
|
||||
is_server_owner
|
||||
~~~~~~~~~~~~~~~~
|
||||
.. autodecorator:: revolt.ext.commands.is_server_owner
|
||||
|
||||
|
||||
Exceptions
|
||||
===========
|
||||
|
||||
CommandError
|
||||
~~~~~~~~~~~~~
|
||||
.. autoexception:: revolt.ext.commands.CommandError
|
||||
:members:
|
||||
|
||||
CommandNotFound
|
||||
~~~~~~~~~~~~~~~~
|
||||
.. autoclass:: revolt.ext.commands.CommandNotFound
|
||||
.. autoexception:: revolt.ext.commands.CommandNotFound
|
||||
:members:
|
||||
|
||||
NoClosingQuote
|
||||
~~~~~~~~~~~~~~~
|
||||
.. autoexception:: revolt.ext.commands.NoClosingQuote
|
||||
:members:
|
||||
|
||||
CheckError
|
||||
~~~~~~~~~~~
|
||||
.. autoexception:: revolt.ext.commands.CheckError
|
||||
:members:
|
||||
|
||||
NotBotOwner
|
||||
~~~~~~~~~~~~
|
||||
.. autoexception:: revolt.ext.commands.NotBotOwner
|
||||
:members:
|
||||
|
||||
NotServerOwner
|
||||
~~~~~~~~~~~~~~~
|
||||
.. autoexception:: revolt.ext.commands.NotServerOwner
|
||||
:members:
|
||||
|
||||
ServerOnly
|
||||
~~~~~~~~~~~
|
||||
.. autoexception:: revolt.ext.commands.ServerOnly
|
||||
:members:
|
||||
|
||||
ConverterError
|
||||
~~~~~~~~~~~~~~~
|
||||
.. autoexception:: revolt.ext.commands.ConverterError
|
||||
:members:
|
||||
|
||||
InvalidLiteralArgument
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. autoexception:: revolt.ext.commands.InvalidLiteralArgument
|
||||
:members:
|
||||
|
||||
BadBoolArgument
|
||||
~~~~~~~~~~~~~~~~
|
||||
.. autoexception:: revolt.ext.commands.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,6 +1,6 @@
|
||||
Sphinx==3.5.4
|
||||
Sphinx==4.3.2
|
||||
sphinx-nameko-theme==0.0.3
|
||||
aiohttp==3.7.4.post0
|
||||
ulid-py==1.1.0
|
||||
sphinx-toolbox==2.13.0
|
||||
sphinx-toolbox==2.15.2
|
||||
aenum==3.1.0
|
||||
|
||||
@@ -6,9 +6,9 @@ import revolt
|
||||
from revolt.ext import commands
|
||||
|
||||
|
||||
class Client(commands.CommandsMixin, revolt.Client):
|
||||
class Client(commands.CommandsClient):
|
||||
async def get_prefix(self, message: revolt.Message):
|
||||
return ">>>"
|
||||
return "!"
|
||||
|
||||
@commands.command()
|
||||
async def ping(self, ctx: commands.Context):
|
||||
|
||||
+6
-3
@@ -1,4 +1,5 @@
|
||||
from .asset import Asset
|
||||
from .category import Category
|
||||
from .channel import (Channel, DMChannel, GroupDMChannel, SavedMessageChannel,
|
||||
TextChannel, VoiceChannel)
|
||||
from .client import Client
|
||||
@@ -7,12 +8,14 @@ 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 Message
|
||||
from .message import Masquerade, Message, MessageReply
|
||||
from .messageable import Messageable
|
||||
from .permissions import ChannelPermissions, ServerPermissions
|
||||
from .role import Role
|
||||
from .server import Server
|
||||
from .server import Server, SystemMessages
|
||||
from .user import Relation, Status, User
|
||||
from . import utils
|
||||
|
||||
__version__ = (0, 1, 0)
|
||||
__version__ = (0, 1, 3)
|
||||
|
||||
+39
-24
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import mimetypes
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .enums import AssetType
|
||||
@@ -35,8 +36,10 @@ class Asset:
|
||||
The content type of the file
|
||||
type: :class:`AssetType`
|
||||
The type of asset it is
|
||||
url: :class:`str`
|
||||
The assets url
|
||||
"""
|
||||
__slots__ = ("state", "id", "tag", "size", "filename", "content_type", "width", "height", "type")
|
||||
__slots__ = ("state", "id", "tag", "size", "filename", "content_type", "width", "height", "type", "url")
|
||||
|
||||
def __init__(self, data: FilePayload, state: State):
|
||||
self.state = state
|
||||
@@ -58,6 +61,9 @@ class Asset:
|
||||
self.content_type = data["content_type"]
|
||||
self.type = AssetType(metadata["type"])
|
||||
|
||||
base_url = self.state.api_info["features"]["autumn"]["url"]
|
||||
self.url = f"{base_url}/{self.tag}/{self.id}"
|
||||
|
||||
async def read(self) -> bytes:
|
||||
"""Reads the files content into bytes"""
|
||||
return await self.state.http.request_file(self.url)
|
||||
@@ -72,28 +78,37 @@ class Asset:
|
||||
"""
|
||||
fp.write(await self.read())
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
"""Returns the url for the asset
|
||||
|
||||
.. note:: This can error if autumn is disabled on the instance of revolt
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`str`
|
||||
The url
|
||||
class PartialAsset(Asset):
|
||||
"""Partial asset for when we get limited data about the asset
|
||||
|
||||
Raises
|
||||
-------
|
||||
:class:`AutumnDisabled`
|
||||
Raises if autumn is disabled
|
||||
|
||||
"""
|
||||
enabled = self.state.api_info["features"]["autumn"]["enabled"]
|
||||
|
||||
if not enabled:
|
||||
raise AutumnDisabled
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`str`
|
||||
The id of the asset, this will always be ``"0"``
|
||||
tag: Optional[:class:`str`]
|
||||
The tag of the asset, this corrasponds to where the asset is used, this will always be ``None``
|
||||
size: :class:`int`
|
||||
Amount of bytes in the file, this will always be ``0``
|
||||
filename: :class:`str`
|
||||
The name of the file, this be always be ``""``
|
||||
height: Optional[:class:`int`]
|
||||
The height of the file if it is an image or video, this will always be ``None``
|
||||
width: Optional[:class:`int`]
|
||||
The width of the file if it is an image or video, this will always be ``None``
|
||||
content_type: Optional[:class:`str`]
|
||||
The content type of the file, this is guessed from the url's file extension if it has one
|
||||
type: :class:`AssetType`
|
||||
The type of asset it is, this always be ``AssetType.file``
|
||||
"""
|
||||
|
||||
base_url = self.state.api_info["features"]["autumn"]["url"]
|
||||
|
||||
return f"{base_url}/{self.tag}/{self.id}"
|
||||
def __init__(self, url: str, state: State):
|
||||
self.state = state
|
||||
self.id = "0"
|
||||
self.tag = None
|
||||
self.size = 0
|
||||
self.filename = ""
|
||||
self.height = None
|
||||
self.width = None
|
||||
self.content_type = mimetypes.guess_extension(url)
|
||||
self.type = AssetType.file
|
||||
self.url = url
|
||||
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .channel import Channel
|
||||
from .state import State
|
||||
from .types import Category as CategoryPayload
|
||||
|
||||
__all__ = ("Category",)
|
||||
|
||||
class Category:
|
||||
"""Represents a category in a server that stores channels.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The name of the category
|
||||
id: :class:`str`
|
||||
The id of the category
|
||||
channel_ids: list[:class:`str`]
|
||||
The ids of channels that are inside the category
|
||||
"""
|
||||
|
||||
def __init__(self, data: CategoryPayload, state: State):
|
||||
self.state = state
|
||||
self.name = data["title"]
|
||||
self.id = data["id"]
|
||||
self.channel_ids = data["channels"]
|
||||
|
||||
@property
|
||||
def channels(self) -> list[Channel]:
|
||||
"""Returns a list of channels that the category contains"""
|
||||
return [self.state.get_channel(channel_id) for channel_id in self.channel_ids]
|
||||
+13
-10
@@ -42,8 +42,8 @@ class Channel:
|
||||
self.server_id = ""
|
||||
|
||||
@property
|
||||
def server(self) -> Server:
|
||||
return self.state.get_server(self.server_id)
|
||||
def server(self) -> Optional[Server]:
|
||||
return self.state.get_server(self.server_id) if self.server_id else None
|
||||
|
||||
def _update(self):
|
||||
pass
|
||||
@@ -69,12 +69,12 @@ class GroupDMChannel(Channel, Messageable):
|
||||
self.owner = state.get_user(data["owner"])
|
||||
|
||||
if perms := data.get("permissions"):
|
||||
self.permissions = ChannelPermissions(perms)
|
||||
self.permissions = ChannelPermissions._from_value(perms)
|
||||
|
||||
def _update(self, *, name: Optional[str] = None, recipients: Optional[list[str]] = None):
|
||||
if name:
|
||||
self.name = name
|
||||
|
||||
|
||||
if recipients:
|
||||
self.recipients = [self.state.get_user(user_id) for user_id in recipients]
|
||||
|
||||
@@ -93,19 +93,22 @@ class TextChannel(Channel, Messageable):
|
||||
"""A text channel"""
|
||||
def __init__(self, data: TextChannelPayload, state: State):
|
||||
super().__init__(data, state)
|
||||
|
||||
|
||||
self.server_id = data["server"]
|
||||
self.name = data["name"]
|
||||
self.description = data.get("description")
|
||||
|
||||
|
||||
last_message_id = data.get("last_message")
|
||||
self.last_message_id = last_message_id
|
||||
|
||||
if perms := data.get("default_permissions"):
|
||||
self.default_permissions = ChannelPermissions(perms)
|
||||
self.default_permissions = ChannelPermissions._from_value(perms)
|
||||
|
||||
if role_perms := data.get("role_permissions"):
|
||||
self.role_permissions = {role_id: ChannelPermissions(perms) for role_id, perms in role_perms.items()}
|
||||
self.role_permissions = {role_id: ChannelPermissions._from_value(perms) for role_id, perms in role_perms.items()}
|
||||
|
||||
def _get_channel_id(self) -> str:
|
||||
return self.id
|
||||
|
||||
@property
|
||||
def last_message(self) -> Message:
|
||||
@@ -146,10 +149,10 @@ class VoiceChannel(Channel):
|
||||
self.description = data.get("description")
|
||||
|
||||
if perms := data.get("default_permissions"):
|
||||
self.default_permissions = ChannelPermissions(perms)
|
||||
self.default_permissions = ChannelPermissions._from_value(perms)
|
||||
|
||||
if role_perms := data.get("role_permissions"):
|
||||
self.role_permissions = {role_id: ChannelPermissions(perms) for role_id, perms in role_perms.items()}
|
||||
self.role_permissions = {role_id: ChannelPermissions._from_value(perms) for role_id, perms in role_perms.items()}
|
||||
|
||||
def _update(self, *, name: Optional[str] = None, description: Optional[str] = None):
|
||||
if name:
|
||||
|
||||
+20
-10
@@ -40,7 +40,6 @@ class Client:
|
||||
max_messages: :class:`int`
|
||||
The max amount of messages stored in the cache, by default this is 5k
|
||||
"""
|
||||
__slots__ = ("session", "token", "api_url", "max_messages", "api_info", "http", "state", "websocket", "listeners")
|
||||
|
||||
def __init__(self, session: aiohttp.ClientSession, token: str, api_url: str = "https://api.revolt.chat", max_messages: int = 5000):
|
||||
self.session = session
|
||||
@@ -92,7 +91,7 @@ class Client:
|
||||
self.websocket = WebsocketHandler(self.session, self.token, api_info["ws"], self.dispatch, self.state)
|
||||
await self.websocket.start()
|
||||
|
||||
def get_user(self, id: str) -> Optional[User]:
|
||||
def get_user(self, id: str) -> User:
|
||||
"""Gets a user from the cache
|
||||
|
||||
Parameters
|
||||
@@ -102,12 +101,12 @@ class Client:
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[:class:`User`]
|
||||
The user if found
|
||||
:class:`User`
|
||||
The user
|
||||
"""
|
||||
return self.state.get_user(id)
|
||||
|
||||
def get_channel(self, id: str) -> Optional[Channel]:
|
||||
def get_channel(self, id: str) -> Channel:
|
||||
"""Gets a channel from the cache
|
||||
|
||||
Parameters
|
||||
@@ -117,12 +116,12 @@ class Client:
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[:class:`Channel`]
|
||||
The channel if found
|
||||
:class:`Channel`
|
||||
The channel
|
||||
"""
|
||||
return self.state.get_channel(id)
|
||||
|
||||
def get_server(self, id: str) -> Optional[Server]:
|
||||
def get_server(self, id: str) -> Server:
|
||||
"""Gets a server from the cache
|
||||
|
||||
Parameters
|
||||
@@ -132,8 +131,8 @@ class Client:
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[:class:`Server`]
|
||||
The server if found
|
||||
:class:`Server`
|
||||
The server
|
||||
"""
|
||||
return self.state.get_server(id)
|
||||
|
||||
@@ -166,3 +165,14 @@ class Client:
|
||||
self.listeners.setdefault(event, []).append((check, future))
|
||||
|
||||
return await asyncio.wait_for(future, timeout)
|
||||
|
||||
@property
|
||||
def user(self) -> User:
|
||||
user = self.websocket.user
|
||||
|
||||
assert user
|
||||
return user
|
||||
|
||||
@property
|
||||
def users(self) -> list[User]:
|
||||
return list(self.state.users.values())
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from .checks import *
|
||||
from .client import *
|
||||
from .command import *
|
||||
from .context import *
|
||||
from .errors import *
|
||||
from .mixin import *
|
||||
from .converters import IntConverter, BoolConverter, CategoryConverter, UserConverter, MemberConverter, ChannelConverter
|
||||
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
from typing import Any, Callable, Coroutine, TypeVar, Union
|
||||
|
||||
from .command import Command
|
||||
from .context import Context
|
||||
from .errors import NotBotOwner, NotServerOwner, ServerOnly
|
||||
|
||||
__all__ = ("check", "Check", "is_bot_owner", "is_server_owner")
|
||||
|
||||
Check = Callable[[Context], Union[Any, Coroutine[Any, Any, Any]]]
|
||||
|
||||
T = TypeVar("T", Callable[..., Any], Command)
|
||||
|
||||
def check(check: Check):
|
||||
"""A decorator for adding command checks
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
check: Callable[[Context], Union[Any, Coroutine[Any, Any, Any]]]
|
||||
The function to be called, must take one parameter, context and optionally be a coroutine
|
||||
|
||||
Returns
|
||||
--------
|
||||
Any
|
||||
The value denoating whether the check should pass or fail
|
||||
"""
|
||||
def inner(func: T) -> T:
|
||||
if isinstance(func, Command):
|
||||
func.checks.append(check)
|
||||
else:
|
||||
checks = getattr(func, "_checks", [])
|
||||
checks.append(check)
|
||||
func._checks = checks
|
||||
|
||||
return func
|
||||
|
||||
return inner
|
||||
|
||||
def is_bot_owner():
|
||||
"""A command check for limiting the command to only the bot's owner"""
|
||||
@check
|
||||
def inner(context: Context):
|
||||
if context.author.id == context.client.user.owner_id:
|
||||
return True
|
||||
|
||||
raise NotBotOwner
|
||||
|
||||
return inner
|
||||
|
||||
def is_server_owner():
|
||||
"""A command check for limiting the command to only a server's owner"""
|
||||
@check
|
||||
def inner(context: Context):
|
||||
if not context.server:
|
||||
raise ServerOnly
|
||||
|
||||
if context.author.id == context.server.owner_id:
|
||||
return True
|
||||
|
||||
raise NotServerOwner
|
||||
|
||||
return inner
|
||||
@@ -1,46 +1,48 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import traceback
|
||||
from typing import Any, Callable, Union
|
||||
|
||||
import revolt
|
||||
|
||||
from .command import Command
|
||||
from .context import Context
|
||||
from .errors import CommandNotFound
|
||||
from .errors import CheckError, CommandNotFound
|
||||
from .view import StringView
|
||||
|
||||
__all__ = (
|
||||
"CommandsMeta",
|
||||
"CommandsMixin"
|
||||
"CommandsClient"
|
||||
)
|
||||
|
||||
quote_regex = re.compile(r"[\"']")
|
||||
chunk_regex = re.compile(r"\S+")
|
||||
|
||||
|
||||
class CommandsMeta(type):
|
||||
_commands: list[Command]
|
||||
|
||||
def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]):
|
||||
commands: list[Command] = []
|
||||
|
||||
for value in attrs.values():
|
||||
if isinstance(value, Command):
|
||||
commands.append(value)
|
||||
|
||||
self = super().__new__(cls, name, bases, attrs)
|
||||
self._commands = commands
|
||||
|
||||
for command in commands:
|
||||
command._client = self # type: ignore
|
||||
for base in reversed(self.__mro__):
|
||||
for value in base.__dict__.values():
|
||||
if isinstance(value, Command):
|
||||
commands.append(value)
|
||||
|
||||
self._commands = commands
|
||||
|
||||
return self
|
||||
|
||||
class CommandsMixin(metaclass=CommandsMeta):
|
||||
class CommandsClient(revolt.Client, metaclass=CommandsMeta):
|
||||
"""Main class that adds commands, this class should be subclassed along with `revolt.Client`."""
|
||||
|
||||
__slots__ = ("_commands", "all_commands")
|
||||
|
||||
_commands: list[Command]
|
||||
dispatch: Callable[..., None]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.all_commands = {}
|
||||
self.all_commands: dict[str, Command] = {}
|
||||
|
||||
for command in self._commands:
|
||||
self.all_commands[command.name] = command
|
||||
@@ -96,42 +98,8 @@ class CommandsMixin(metaclass=CommandsMeta):
|
||||
"""
|
||||
self.all_commands[name] = command
|
||||
|
||||
def split_content(self, content: str) -> list[str]:
|
||||
"""Splits a string into seperate parameters, overwrite this function to change how arguments are split.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
content: :class:`str`
|
||||
The content of the message
|
||||
|
||||
Returns
|
||||
--------
|
||||
list[:class:`str`]
|
||||
The arguments from the content
|
||||
"""
|
||||
args = []
|
||||
|
||||
temp: list[str] = []
|
||||
quoted = False
|
||||
quote_char = ""
|
||||
|
||||
for char in content:
|
||||
if char == " " and not quoted:
|
||||
args.append("".join(temp))
|
||||
temp = []
|
||||
|
||||
elif char in ["\"", "'"] and not quoted:
|
||||
quoted = True
|
||||
quote_char = char
|
||||
|
||||
elif char == quote_char and quoted:
|
||||
quoted = False
|
||||
args.append("".join(temp))
|
||||
|
||||
else:
|
||||
temp.append(char)
|
||||
|
||||
return args
|
||||
def get_view(self, message: revolt.Message) -> type[StringView]:
|
||||
return StringView
|
||||
|
||||
def get_context(self, message: revolt.Message) -> type[Context]:
|
||||
return Context
|
||||
@@ -169,18 +137,56 @@ class CommandsMixin(metaclass=CommandsMeta):
|
||||
if not content:
|
||||
return
|
||||
|
||||
command_name, *args = self.split_content(content)
|
||||
view = self.get_view(message)(content)
|
||||
|
||||
try:
|
||||
command_name = view.get_next_word()
|
||||
except StopIteration:
|
||||
return
|
||||
|
||||
context_cls = self.get_context(message)
|
||||
|
||||
try:
|
||||
command = self.get_command(command_name)
|
||||
except KeyError:
|
||||
return self.dispatch("command_error", Context(None, command_name, message), CommandNotFound(command_name))
|
||||
context = context_cls(None, command_name, view, message, self)
|
||||
return self.dispatch("command_error", context, CommandNotFound(command_name))
|
||||
|
||||
context = self.get_context(message)(command, command_name, message)
|
||||
context = context_cls(command, command_name, view, message, self)
|
||||
|
||||
try:
|
||||
return await context.invoke(args)
|
||||
self.dispatch("command", context)
|
||||
|
||||
if not await self.bot_check(context):
|
||||
raise CheckError(f"the global check for the command failed")
|
||||
|
||||
if not await context.can_run():
|
||||
raise CheckError(f"the check(s) for the command failed")
|
||||
|
||||
output = await context.invoke()
|
||||
self.dispatch("after_command_invoke", context, output)
|
||||
|
||||
return output
|
||||
except Exception as e:
|
||||
self.dispatch("command_error", context, e)
|
||||
|
||||
@staticmethod
|
||||
async def on_command_error(ctx: Context, error: Exception):
|
||||
traceback.print_exception(type(error), error, error.__traceback__)
|
||||
|
||||
on_message = process_commands
|
||||
|
||||
async def bot_check(self, context: Context) -> bool:
|
||||
"""A global check for the bot that stops commands from running on certain criteria.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
context: :class:`Context`
|
||||
The context for the invokation of the command
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`bool` represents if the command should run or not
|
||||
"""
|
||||
|
||||
return True
|
||||
@@ -1,23 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import traceback
|
||||
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Optional
|
||||
from contextlib import suppress
|
||||
from typing import (TYPE_CHECKING, Annotated, Any, Callable, Coroutine,
|
||||
Literal, Optional, Union, get_args, get_origin, cast)
|
||||
|
||||
import revolt
|
||||
from revolt.utils import copy_doc
|
||||
from revolt.utils import copy_doc, maybe_coroutine
|
||||
|
||||
from .errors import InvalidLiteralArgument, UnionConverterError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .checks import Check
|
||||
from .context import Context
|
||||
|
||||
|
||||
__all__ = (
|
||||
"Command",
|
||||
"command"
|
||||
)
|
||||
|
||||
NoneType = type(None)
|
||||
|
||||
|
||||
class Command:
|
||||
"""Class for holding info about a command.
|
||||
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
callback: Callable[..., Coroutine[Any, Any, Any]]
|
||||
@@ -27,16 +35,16 @@ class Command:
|
||||
aliases: list[:class:`str`]
|
||||
The aliases of the command
|
||||
"""
|
||||
__slots__ = ("callback", "name", "aliases", "_client")
|
||||
|
||||
_client: revolt.Client
|
||||
__slots__ = ("callback", "name", "aliases", "signature", "checks")
|
||||
|
||||
def __init__(self, callback: Callable[..., Coroutine[Any, Any, Any]], name: str, aliases: list[str]):
|
||||
self.callback = callback
|
||||
self.name = name
|
||||
self.aliases = aliases
|
||||
self.signature = inspect.signature(self.callback)
|
||||
self.checks: list[Check] = getattr(callback, "_checks", [])
|
||||
|
||||
async def invoke(self, context: Context, args: list[Any]) -> Any:
|
||||
async def invoke(self, context: Context, *args, **kwargs) -> Any:
|
||||
"""Runs the command and calls the error handler if the command errors.
|
||||
|
||||
Parameters
|
||||
@@ -47,17 +55,17 @@ class Command:
|
||||
The arguments for the command
|
||||
"""
|
||||
try:
|
||||
return await self.callback(self._client, context, *args)
|
||||
return await self.callback(context.client, context, *args, **kwargs)
|
||||
except Exception as err:
|
||||
return await self._error_handler(context, err)
|
||||
|
||||
@copy_doc(invoke)
|
||||
def __call__(self, context: Context, args: list[Any]) -> Any:
|
||||
return self.invoke(context, args)
|
||||
def __call__(self, context: Context, *args, **kwargs) -> Any:
|
||||
return self.invoke(context, *args, **kwargs)
|
||||
|
||||
def error(self, func: Callable[..., Coroutine[Any, Any, Any]]):
|
||||
"""Sets the error handler for the command.
|
||||
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
func: Callable[..., Coroutine[Any, Any, Any]]
|
||||
@@ -79,22 +87,80 @@ class Command:
|
||||
async def _error_handler(ctx: Context, error: Exception):
|
||||
traceback.print_exception(type(error), error, error.__traceback__)
|
||||
|
||||
@staticmethod
|
||||
def extract_type(t):
|
||||
if origin := get_origin(t):
|
||||
if origin is Annotated:
|
||||
return get_args(t)[1]
|
||||
|
||||
return t
|
||||
|
||||
@classmethod
|
||||
async def convert_argument(cls, arg: str, annotation: Any, context: Context):
|
||||
if annotation:
|
||||
if annotation is str: # no converting is needed - its already a string
|
||||
return arg
|
||||
|
||||
if origin := get_origin(annotation):
|
||||
if origin is Union:
|
||||
for converter in get_args(annotation):
|
||||
try:
|
||||
return await cls.convert_argument(arg, converter, context)
|
||||
except:
|
||||
if converter is NoneType:
|
||||
context.view.undo()
|
||||
return None
|
||||
|
||||
raise UnionConverterError(arg)
|
||||
|
||||
elif origin is Annotated:
|
||||
converter: Callable[[str, Context], Any] = get_args(annotation)[1] # the typehint affects the other if statement somehow
|
||||
return await maybe_coroutine(converter, arg, context)
|
||||
|
||||
elif origin is Literal:
|
||||
if arg in get_args(annotation):
|
||||
return arg
|
||||
else:
|
||||
raise InvalidLiteralArgument(arg)
|
||||
else:
|
||||
return await maybe_coroutine(cast(Callable, annotation), arg, context)
|
||||
else:
|
||||
return arg
|
||||
|
||||
async def parse_arguments(self, context: Context):
|
||||
for name, parameter in list(self.signature.parameters.items())[2:]:
|
||||
if parameter.kind == parameter.KEYWORD_ONLY:
|
||||
context.kwargs[name] = await self.convert_argument(context.view.get_rest(), parameter.annotation, context)
|
||||
|
||||
elif parameter.kind == parameter.VAR_POSITIONAL:
|
||||
with suppress(StopIteration):
|
||||
while True:
|
||||
context.args.append(await self.convert_argument(context.view.get_next_word(), parameter.annotation, context))
|
||||
|
||||
elif parameter.kind == parameter.POSITIONAL_OR_KEYWORD:
|
||||
context.args.append(await self.convert_argument(context.view.get_next_word(), parameter.annotation, context))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Command name=\"{self.name}\">"
|
||||
|
||||
def command(*, name: Optional[str] = None, aliases: Optional[list[str]] = None, cls: type[Command] = Command):
|
||||
"""Turns a function into a :class:`Command`.
|
||||
|
||||
"""A decorator that turns a function into a :class:`Command`.
|
||||
|
||||
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
|
||||
--------
|
||||
:class:`Command`
|
||||
The command
|
||||
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]]):
|
||||
return cls(func, name or func.__name__, aliases or [])
|
||||
|
||||
|
||||
return inner
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
import revolt
|
||||
from revolt.utils import copy_doc
|
||||
from revolt.utils import maybe_coroutine
|
||||
|
||||
from .command import Command
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .client import CommandsClient
|
||||
from .view import StringView
|
||||
|
||||
__all__ = (
|
||||
"Context",
|
||||
)
|
||||
|
||||
class Context:
|
||||
class Context(revolt.Messageable):
|
||||
"""Stores metadata the commands execution.
|
||||
|
||||
Parameters
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
command: Optional[:class:`Command`]
|
||||
The command, this can be `None` when no command was found and the error handler is being executed
|
||||
@@ -22,16 +26,39 @@ class Context:
|
||||
The command name that was used, this can be an alias, the commands name or a command that doesnt exist
|
||||
message: :class:`Message`
|
||||
The message that was sent to invoke the command
|
||||
channel: :class:`Messageable`
|
||||
The channel the command was invoked in
|
||||
server: :class:`Server`
|
||||
The server the command was invoked in
|
||||
author: Union[:class:`Member`, :class:`User`]
|
||||
The user or member that invoked the commad, will be :class:`User` in DMs
|
||||
args: list[:class:`str`]
|
||||
The positional arguments being passed to the command
|
||||
kwargs: dict[:class:`str`, Any]
|
||||
The keyword arguments being passed to the command
|
||||
client: :class:`CommandsClient`
|
||||
The revolt client
|
||||
"""
|
||||
__slots__ = ("command", "invoked_with", "message")
|
||||
__slots__ = ("command", "invoked_with", "args", "message", "server", "channel", "author", "view", "kwargs", "state", "client")
|
||||
|
||||
def __init__(self, command: Optional[Command], invoked_with: str, message: revolt.Message):
|
||||
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):
|
||||
self.command = command
|
||||
self.invoked_with = invoked_with
|
||||
self.view = view
|
||||
self.message = message
|
||||
self.client = client
|
||||
self.args = []
|
||||
self.kwargs = {}
|
||||
self.server = message.server
|
||||
self.channel = message.channel
|
||||
self.author = message.author
|
||||
self.state = message.state
|
||||
|
||||
async def invoke(self, args: list[str]) -> Any:
|
||||
"""Invokes the command, this is equal to `await context.command.invoke(context, args)`.
|
||||
async def invoke(self) -> Any:
|
||||
"""Invokes the command, this is equal to `await command.invoke(context, context.args)`.
|
||||
|
||||
.. note:: If the command is `None`, this function will do nothing.
|
||||
|
||||
@@ -40,9 +67,13 @@ class Context:
|
||||
args: list[:class:`str`]
|
||||
The args being passed to the command
|
||||
"""
|
||||
if command := self.command:
|
||||
return await command.invoke(self, args)
|
||||
|
||||
@copy_doc(revolt.Messageable.send)
|
||||
async def send(self, content: Optional[str] = None, *, embeds: Optional[list[revolt.Embed]] = None, embed: Optional[revolt.Embed] = None, attachments: Optional[list[revolt.File]] = None) -> revolt.Message:
|
||||
return await self.message.channel.send(content, embeds=embeds, embed=embed, attachments=attachments)
|
||||
if command := self.command:
|
||||
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 [])])
|
||||
|
||||
|
||||
Executable
+82
@@ -0,0 +1,82 @@
|
||||
from typing import Annotated
|
||||
import re
|
||||
|
||||
from .errors import BadBoolArgument, ServerOnly, CategoryConverterError, ChannelConverterError, UserConverterError, MemberConverterError
|
||||
from .context import Context
|
||||
|
||||
from revolt import Category, Channel, utils, User, Member
|
||||
|
||||
__all__ = ("bool_converter", "category_converter", "channel_converter", "user_converter", "member_converter", "IntConverter", "BoolConverter", "CategoryConverter", "UserConverter", "MemberConverter", "ChannelConverter")
|
||||
|
||||
channel_regex = re.compile("<#([A-z0-9]{26})>")
|
||||
user_regex = re.compile("<@([A-z0-9]{26})>")
|
||||
|
||||
def bool_converter(arg: str, _):
|
||||
lowered = arg.lower()
|
||||
if lowered in ["yes", "true", "ye", "y", "1", "on", "enable"]:
|
||||
return True
|
||||
elif lowered in ('no', 'n', 'false', 'f', '0', 'disable', 'off'):
|
||||
return False
|
||||
else:
|
||||
raise BadBoolArgument(lowered)
|
||||
|
||||
def category_converter(arg: str, context: Context) -> Category:
|
||||
if not (server := context.server):
|
||||
raise ServerOnly
|
||||
|
||||
try:
|
||||
return server.get_category(arg)
|
||||
except KeyError:
|
||||
try:
|
||||
return utils.get(server.categories, name=arg)
|
||||
except LookupError:
|
||||
raise CategoryConverterError(arg)
|
||||
|
||||
def channel_converter(arg: str, context: Context) -> Channel:
|
||||
if not (server := context.server):
|
||||
raise ServerOnly
|
||||
|
||||
if (match := channel_regex.match(arg)):
|
||||
arg = match.group(1)
|
||||
|
||||
try:
|
||||
return server.get_channel(arg)
|
||||
except KeyError:
|
||||
try:
|
||||
return utils.get(server.channels, name=arg)
|
||||
except LookupError:
|
||||
raise ChannelConverterError(arg)
|
||||
|
||||
def user_converter(arg: str, context: Context) -> User:
|
||||
if (match := user_regex.match(arg)):
|
||||
arg = match.group(1)
|
||||
|
||||
try:
|
||||
return context.client.get_user(arg)
|
||||
except KeyError:
|
||||
try:
|
||||
return utils.get(context.client.users, name=arg)
|
||||
except LookupError:
|
||||
raise UserConverterError(arg)
|
||||
|
||||
def member_converter(arg: str, context: Context) -> Member:
|
||||
if not (server := context.server):
|
||||
raise ServerOnly
|
||||
|
||||
if (match := user_regex.match(arg)):
|
||||
arg = match.group(1)
|
||||
|
||||
try:
|
||||
return server.get_member(arg)
|
||||
except KeyError:
|
||||
try:
|
||||
return utils.get(server.members, name=arg)
|
||||
except LookupError:
|
||||
raise MemberConverterError(arg)
|
||||
|
||||
IntConverter = Annotated[int, lambda arg, _: int(arg)]
|
||||
BoolConverter = Annotated[bool, bool_converter]
|
||||
CategoryConverter = Annotated[Category, category_converter]
|
||||
UserConverter = Annotated[User, user_converter]
|
||||
MemberConverter = Annotated[Member, member_converter]
|
||||
ChannelConverter = Annotated[Channel, channel_converter]
|
||||
@@ -1,10 +1,28 @@
|
||||
from revolt import RevoltError
|
||||
|
||||
__all__ = (
|
||||
"CommandError",
|
||||
"CommandNotFound",
|
||||
"NoClosingQuote",
|
||||
"CheckError",
|
||||
"NotBotOwner",
|
||||
"NotServerOwner",
|
||||
"ServerOnly",
|
||||
"ConverterError",
|
||||
"InvalidLiteralArgument",
|
||||
"BadBoolArgument",
|
||||
"CategoryConverterError",
|
||||
"ChannelConverterError",
|
||||
"UserConverterError",
|
||||
"MemberConverterError",
|
||||
)
|
||||
|
||||
class CommandNotFound(Exception):
|
||||
class CommandError(RevoltError):
|
||||
"""base error for all command's related errors"""
|
||||
|
||||
class CommandNotFound(CommandError):
|
||||
"""Raised when a command isnt found.
|
||||
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
command_name: :class:`str`
|
||||
@@ -14,3 +32,52 @@ class CommandNotFound(Exception):
|
||||
|
||||
def __init__(self, command_name: str):
|
||||
self.command_name = command_name
|
||||
|
||||
class NoClosingQuote(CommandError):
|
||||
"""Raised when there is no closing quote for a command argument"""
|
||||
|
||||
class CheckError(CommandError):
|
||||
"""Raised when a check fails for a command"""
|
||||
|
||||
class NotBotOwner(CheckError):
|
||||
"""Raised when the `is_bot_owner` check fails"""
|
||||
|
||||
class NotServerOwner(CheckError):
|
||||
"""Raised when the `is_server_owner` check fails"""
|
||||
|
||||
class ServerOnly(CheckError):
|
||||
"""Raised when a check requires the command to be ran in a server"""
|
||||
|
||||
class ConverterError(CommandError):
|
||||
"""Base class for all converter errors"""
|
||||
|
||||
class InvalidLiteralArgument(ConverterError):
|
||||
"""Raised when the argument is not a valid literal argument"""
|
||||
|
||||
class BadBoolArgument(ConverterError):
|
||||
"""Raised when the bool converter fails"""
|
||||
|
||||
class CategoryConverterError(ConverterError):
|
||||
"""Raised when the Category conveter fails"""
|
||||
def __init__(self, argument: str):
|
||||
self.argument = argument
|
||||
|
||||
class ChannelConverterError(ConverterError):
|
||||
"""Raised when the Channel conveter fails"""
|
||||
def __init__(self, argument: str):
|
||||
self.argument = argument
|
||||
|
||||
class UserConverterError(ConverterError):
|
||||
"""Raised when the Category conveter fails"""
|
||||
def __init__(self, argument: str):
|
||||
self.argument = argument
|
||||
|
||||
class MemberConverterError(ConverterError):
|
||||
"""Raised when the Category conveter fails"""
|
||||
def __init__(self, argument: str):
|
||||
self.argument = argument
|
||||
|
||||
class UnionConverterError(ConverterError):
|
||||
"""Raised when all converters in a union fails"""
|
||||
def __init__(self, argument: str):
|
||||
self.argument = argument
|
||||
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
from .errors import NoClosingQuote
|
||||
|
||||
|
||||
class StringView:
|
||||
def __init__(self, string: str):
|
||||
self.value = iter(string)
|
||||
self.temp = ""
|
||||
self.should_undo = False
|
||||
|
||||
def undo(self):
|
||||
self.should_undo = True
|
||||
|
||||
def next_char(self) -> str:
|
||||
return next(self.value)
|
||||
|
||||
def get_rest(self) -> str:
|
||||
if self.should_undo:
|
||||
return f"{self.temp} {''.join(self.value)}"
|
||||
|
||||
return "".join(self.value)
|
||||
|
||||
def get_next_word(self) -> str:
|
||||
if self.should_undo:
|
||||
self.should_undo = False
|
||||
return self.temp
|
||||
|
||||
char = self.next_char()
|
||||
temp = []
|
||||
|
||||
while char == " ":
|
||||
char = self.next_char()
|
||||
|
||||
if char in ["\"", "'"]:
|
||||
quote = char
|
||||
try:
|
||||
while (char := self.next_char()) != quote:
|
||||
temp.append(char)
|
||||
except StopIteration:
|
||||
raise NoClosingQuote
|
||||
|
||||
else:
|
||||
temp.append(char)
|
||||
try:
|
||||
while (char := self.next_char()) not in " \n":
|
||||
temp.append(char)
|
||||
except StopIteration:
|
||||
pass
|
||||
|
||||
output = "".join(temp)
|
||||
self.temp = output
|
||||
|
||||
return output
|
||||
Executable
+146
@@ -0,0 +1,146 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Iterator, Optional, TypeVar, Union, overload
|
||||
|
||||
__all__ = ("flag_value", "Flags", "UserBadges")
|
||||
|
||||
F_T = TypeVar("F_T", bound="Flags")
|
||||
F_V = TypeVar("F_V", bound="flag_value")
|
||||
|
||||
|
||||
class flag_value:
|
||||
__slots__ = ("flag", "__doc__")
|
||||
|
||||
def __init__(self, func: Callable[[], int]):
|
||||
self.flag = func()
|
||||
self.__doc__ = func.__doc__
|
||||
|
||||
@overload
|
||||
def __get__(self: F_V, instance: None, owner: type[F_T]) -> F_V:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __get__(self, instance: F_T, owner: type[F_T]) -> bool:
|
||||
...
|
||||
|
||||
def __get__(self: F_V, instance: Optional[F_T], owner: type[F_T]) -> Union[F_V, bool]:
|
||||
if instance is None:
|
||||
return self
|
||||
|
||||
return instance._check_flag(self.flag)
|
||||
|
||||
def __set__(self, instance: Flags, value: bool):
|
||||
instance._set_flag(self.flag, value)
|
||||
|
||||
class Flags:
|
||||
def __init__(self, **kwargs: bool):
|
||||
self.value = 0
|
||||
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
@classmethod
|
||||
def _from_value(cls: type[F_T], value: int) -> F_T:
|
||||
self = cls.__new__(cls)
|
||||
self.value = value
|
||||
return self
|
||||
|
||||
def _check_flag(self, flag: int) -> bool:
|
||||
return (self.value & flag) == flag
|
||||
|
||||
def _set_flag(self, flag: int, value: bool):
|
||||
if value:
|
||||
self.value |= flag
|
||||
else:
|
||||
self.value &= ~flag
|
||||
|
||||
def __eq__(self: F_T, other: F_T) -> bool:
|
||||
return self.value == other.value
|
||||
|
||||
def __ne__(self: F_T, other: F_T) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __or__(self: F_T, other: F_T) -> F_T:
|
||||
return self.__class__._from_value(self.value | other.value)
|
||||
|
||||
def __and__(self: F_T, other: F_T) -> F_T:
|
||||
return self.__class__._from_value(self.value & other.value)
|
||||
|
||||
def __invert__(self: F_T) -> F_T:
|
||||
return self.__class__._from_value(~self.value)
|
||||
|
||||
def __add__(self: F_T, other: F_T) -> F_T:
|
||||
return self | other
|
||||
|
||||
def __sub__(self: F_T, other: F_T) -> F_T:
|
||||
return self & ~other
|
||||
|
||||
def __lt__(self: F_T, other: F_T) -> bool:
|
||||
return self.value < other.value
|
||||
|
||||
def __gt__(self: F_T, other: F_T) -> bool:
|
||||
return self.value > other.value
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} value={self.value}>"
|
||||
|
||||
def __iter__(self) -> Iterator[tuple[str, bool]]:
|
||||
for name, value in self.__class__.__dict__.items():
|
||||
if isinstance(value, flag_value):
|
||||
yield name, value.__get__(self, self.__class__)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.value)
|
||||
|
||||
class UserBadges(Flags):
|
||||
"""Contains all user badges"""
|
||||
|
||||
@flag_value
|
||||
def developer():
|
||||
""":class:`bool` The developer badge."""
|
||||
return 1 << 0
|
||||
|
||||
@flag_value
|
||||
def translator():
|
||||
""":class:`bool` The translator badge."""
|
||||
return 1 << 1
|
||||
|
||||
@flag_value
|
||||
def supporter():
|
||||
""":class:`bool` The supporter badge."""
|
||||
return 1 << 2
|
||||
|
||||
@flag_value
|
||||
def responsible_disclosure():
|
||||
""":class:`bool` The responsible disclosure badge."""
|
||||
return 1 << 3
|
||||
|
||||
@flag_value
|
||||
def founder():
|
||||
""":class:`bool` The founder badge."""
|
||||
return 1 << 4
|
||||
|
||||
@flag_value
|
||||
def platform_moderation():
|
||||
""":class:`bool` The platform moderation badge."""
|
||||
return 1 << 5
|
||||
|
||||
@flag_value
|
||||
def active_supporter():
|
||||
""":class:`bool` The active supporter badge."""
|
||||
return 1 << 6
|
||||
|
||||
@flag_value
|
||||
def paw():
|
||||
""":class:`bool` The paw badge."""
|
||||
return 1 << 7
|
||||
|
||||
@flag_value
|
||||
def early_adopter():
|
||||
""":class:`bool` The early adopter badge."""
|
||||
return 1 << 8
|
||||
|
||||
@flag_value
|
||||
def reserved_relevant_joke_badge_1():
|
||||
""":class:`bool` The reserved relevant joke badge 1 badge."""
|
||||
return 1 << 9
|
||||
+19
-11
@@ -23,10 +23,12 @@ if TYPE_CHECKING:
|
||||
from .types import Autumn as AutumnPayload
|
||||
from .types import Channel, DMChannel
|
||||
from .types import Embed as EmbedPayload
|
||||
from .types import GetServerMembers, Member
|
||||
from .types import GetServerMembers
|
||||
from .types import Masquerade as MasqueradePayload
|
||||
from .types import Member
|
||||
from .types import Message as MessagePayload
|
||||
from .types import (MessageWithUserData, Role, Server, ServerBans,
|
||||
ServerInvite, TextChannel)
|
||||
from .types import (MessageReplyPayload, MessageWithUserData, Role, Server,
|
||||
ServerBans, ServerInvite, TextChannel)
|
||||
from .types import User as UserPayload
|
||||
from .types import UserProfile, VoiceChannel
|
||||
|
||||
@@ -51,7 +53,7 @@ class HttpClient:
|
||||
kwargs = {}
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Revolt.py https://github.com/Zomatree/revolt.py",
|
||||
"User-Agent": "Revolt.py (https://github.com/revoltchat/revolt.py)",
|
||||
"x-bot-token": self.token
|
||||
}
|
||||
|
||||
@@ -79,14 +81,14 @@ class HttpClient:
|
||||
|
||||
if 200 <= resp_code <= 300:
|
||||
return response
|
||||
elif resp_code == 400:
|
||||
raise HTTPError
|
||||
else:
|
||||
raise HTTPError(resp_code)
|
||||
|
||||
async def upload_file(self, file: File, tag: str) -> AutumnPayload:
|
||||
url = f"{self.api_info['features']['autumn']['url']}/{tag}"
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Revolt.py https://github.com/Zomatree/revolt.py"
|
||||
"User-Agent": "Revolt.py (https://github.com/revoltchat/revolt.py)"
|
||||
}
|
||||
|
||||
form = aiohttp.FormData()
|
||||
@@ -104,12 +106,12 @@ class HttpClient:
|
||||
else:
|
||||
return response
|
||||
|
||||
async def send_message(self, channel: str, content: Optional[str], embeds: Optional[list[EmbedPayload]], attachments: Optional[list[File]]) -> MessagePayload:
|
||||
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:
|
||||
json: dict[str, Any] = {}
|
||||
|
||||
|
||||
if content:
|
||||
json["content"] = content
|
||||
|
||||
|
||||
if embeds:
|
||||
json["embeds"] = embeds
|
||||
|
||||
@@ -119,9 +121,15 @@ class HttpClient:
|
||||
for attachment in attachments:
|
||||
data = await self.upload_file(attachment, "attachments")
|
||||
attachment_ids.append(data["id"])
|
||||
|
||||
|
||||
json["attachments"] = attachment_ids
|
||||
|
||||
if replies:
|
||||
json["replies"] = replies
|
||||
|
||||
if masquerade:
|
||||
json["masquerade"] = masquerade
|
||||
|
||||
return await self.request("POST", f"/channels/{channel}/messages", json=json)
|
||||
|
||||
def edit_message(self, channel: str, message: str, content: str) -> Request[None]:
|
||||
|
||||
+23
-13
@@ -2,14 +2,15 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from .asset import Asset
|
||||
from .user import User
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .server import Server
|
||||
from .state import State
|
||||
from .types import File
|
||||
from .types import Member as MemberPayload
|
||||
|
||||
|
||||
__all__ = ("Member",)
|
||||
|
||||
def flattern_user(member: Member, user: User):
|
||||
@@ -27,31 +28,40 @@ class Member(User):
|
||||
The roles of the member, ordered by the role's rank in decending order
|
||||
server: :class:`Server`
|
||||
The server the member belongs to
|
||||
guild_avatar: Optional[:class:`Asset`]
|
||||
The member's guild avatar if any
|
||||
"""
|
||||
__slots__ = ("_state", "nickname", "roles", "server")
|
||||
__slots__ = ("_state", "nickname", "roles", "server", "guild_avatar")
|
||||
|
||||
def __init__(self, data: MemberPayload, server: Server, state: State):
|
||||
user = state.get_user(data["_id"]["user"])
|
||||
assert user
|
||||
flattern_user(self, user)
|
||||
|
||||
self._state = state
|
||||
self.nickname = data.get("nickname")
|
||||
roles = []
|
||||
|
||||
for role in (server.get_role(role_id) for role_id in data.get("roles", [])):
|
||||
if role:
|
||||
roles.append(role)
|
||||
if avatar := data.get("avatar"):
|
||||
self.guild_avatar = Asset(avatar, state)
|
||||
else:
|
||||
self.guild_avatar = None
|
||||
|
||||
roles = [server.get_role(role_id) for role_id in data.get("roles", [])]
|
||||
self.roles = sorted(roles, key=lambda role: role.rank, reverse=True)
|
||||
|
||||
|
||||
self.server = server
|
||||
|
||||
@property
|
||||
def owner(self) -> Optional[User]:
|
||||
owner_id = self.owner_id
|
||||
def avatar(self) -> Optional[Asset]:
|
||||
"""Optional[:class:`Asset`] The avatar the member is displaying, this includes guild avatars and masqueraded avatar"""
|
||||
return self.masquerade_avatar or self.guild_avatar or self.original_avatar
|
||||
|
||||
if not owner_id:
|
||||
return
|
||||
def _update(self, *, nickname: Optional[str] = None, avatar: Optional[File] = None, roles: Optional[list[str]] = None):
|
||||
if nickname:
|
||||
self.nickname = nickname
|
||||
|
||||
return self.state.get_user(owner_id)
|
||||
if avatar:
|
||||
self.guild_avatar = Asset(avatar, self.state)
|
||||
|
||||
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)
|
||||
|
||||
+86
-10
@@ -1,22 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, NamedTuple, Optional
|
||||
|
||||
from .asset import Asset
|
||||
from .asset import Asset, PartialAsset
|
||||
from .channel import Messageable
|
||||
from .embed import Embed
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .state import State
|
||||
from .types import Masquerade as MasqueradePayload
|
||||
from .types import Message as MessagePayload
|
||||
from .types import MessageReplyPayload
|
||||
|
||||
|
||||
__all__ = ("Message",)
|
||||
__all__ = (
|
||||
"Message",
|
||||
"MessageReply",
|
||||
"Masquerade"
|
||||
)
|
||||
|
||||
class Message:
|
||||
"""Represents a message
|
||||
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`str`
|
||||
@@ -35,12 +41,18 @@ class Message:
|
||||
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
|
||||
"""
|
||||
__slots__ = ("state", "id", "content", "attachments", "embeds", "channel", "server", "author", "edited_at")
|
||||
|
||||
__slots__ = ("state", "id", "content", "attachments", "embeds", "channel", "server", "author", "edited_at", "mentions", "replies", "reply_ids")
|
||||
|
||||
def __init__(self, data: MessagePayload, state: State):
|
||||
self.state = state
|
||||
|
||||
|
||||
self.id = data["_id"]
|
||||
self.content = data["content"]
|
||||
self.attachments = [Asset(attachment, state) for attachment in data.get("attachments", [])]
|
||||
@@ -51,16 +63,40 @@ class Message:
|
||||
self.channel = channel
|
||||
|
||||
self.server = self.channel and self.channel.server
|
||||
|
||||
|
||||
if self.server:
|
||||
author = state.get_member(self.server.id, data["author"])
|
||||
else:
|
||||
author = state.get_user(data["author"])
|
||||
|
||||
assert author
|
||||
self.author = author
|
||||
|
||||
self.edited_at: Optional[datetime.datetime] = None
|
||||
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["$date"], "%Y-%m-%dT%H:%M:%S.%f%z")
|
||||
|
||||
if self.server:
|
||||
self.mentions = [self.server.get_member(member_id) for member_id in data.get("mentions", [])]
|
||||
else:
|
||||
self.mentions = [state.get_user(member_id) for member_id in data.get("mentions", [])]
|
||||
|
||||
self.replies = []
|
||||
self.reply_ids = []
|
||||
|
||||
for reply in data.get("replies", []):
|
||||
try:
|
||||
message = state.get_message(reply)
|
||||
self.replies.append(message)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
self.reply_ids.append(reply)
|
||||
|
||||
def _update(self, *, content: Optional[str] = None, edited_at: Optional[str] = None) -> Message:
|
||||
if content:
|
||||
@@ -84,3 +120,43 @@ class Message:
|
||||
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)
|
||||
|
||||
class MessageReply(NamedTuple):
|
||||
"""A namedtuple which 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.
|
||||
"""
|
||||
message: Message
|
||||
mention: bool = False
|
||||
|
||||
def to_dict(self) -> MessageReplyPayload:
|
||||
return { "id": self.message.id, "mention": self.mention }
|
||||
|
||||
class Masquerade(NamedTuple):
|
||||
"""A namedtuple which 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
|
||||
"""
|
||||
name: Optional[str] = None
|
||||
avatar: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> MasqueradePayload:
|
||||
output: MasqueradePayload = {}
|
||||
|
||||
if name := self.name:
|
||||
output["name"] = name
|
||||
|
||||
if avatar := self.avatar:
|
||||
output["avatar"] = avatar
|
||||
|
||||
return output
|
||||
|
||||
+15
-6
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
if TYPE_CHECKING:
|
||||
from .embed import Embed
|
||||
from .file import File
|
||||
from .message import Message
|
||||
from .message import Masquerade, Message, MessageReply
|
||||
from .state import State
|
||||
|
||||
|
||||
@@ -13,18 +13,20 @@ __all__ = ("Messageable",)
|
||||
|
||||
class Messageable:
|
||||
"""Base class for all channels that you can send messages in
|
||||
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`str`
|
||||
The id of the channel
|
||||
"""
|
||||
id: str
|
||||
state: State
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
async def send(self, content: Optional[str] = None, *, embeds: Optional[list[Embed]] = None, embed: Optional[Embed] = None, attachments: Optional[list[File]] = None) -> Message:
|
||||
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:
|
||||
"""Sends a message in a channel, you must send at least one of either `content`, `embeds` or `attachments`
|
||||
|
||||
Parameters
|
||||
@@ -35,6 +37,8 @@ class Messageable:
|
||||
The attachments of the message
|
||||
embeds: Optional[list[:class:`Embed`]]
|
||||
The embeds of the message
|
||||
replies: Optional[list[:class:`MessageReply`]]
|
||||
The list of messages to reply to.
|
||||
|
||||
Returns
|
||||
--------
|
||||
@@ -44,7 +48,12 @@ class Messageable:
|
||||
if embed:
|
||||
embeds = [embed]
|
||||
|
||||
embed_payload = [embed.to_dict() for embed in embeds] if embeds else None
|
||||
if reply:
|
||||
replies = [reply]
|
||||
|
||||
message = await self.state.http.send_message(self.id, content, embed_payload, attachments)
|
||||
embed_payload = [embed.to_dict() for embed in embeds] if embeds else None
|
||||
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)
|
||||
return self.state.add_message(message)
|
||||
|
||||
+66
-264
@@ -1,15 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types import Permission as PermissionTuple
|
||||
from .flags import Flags, flag_value
|
||||
|
||||
|
||||
__all__ = (
|
||||
"ChannelPermissions",
|
||||
"ServerPermissions"
|
||||
)
|
||||
)
|
||||
|
||||
# Channel permissions
|
||||
#
|
||||
@@ -37,315 +34,120 @@ __all__ = (
|
||||
# ChangeAvatar = 0b00000000000000000100000000000000 // 16382
|
||||
# RemoveAvatars = 0b00000000000000001000000000000000 // 32768
|
||||
|
||||
class _Permission:
|
||||
|
||||
__slots__ = ("_value",)
|
||||
|
||||
def __init__(self, value: int):
|
||||
self._value = value
|
||||
|
||||
def __or__(self, p: _Permission):
|
||||
self._value |= p._value
|
||||
return self
|
||||
|
||||
def __and__(self, p: _Permission):
|
||||
self._value &= p._value
|
||||
return self
|
||||
|
||||
def __invert__(self):
|
||||
self._value = ~self._value
|
||||
return self
|
||||
|
||||
def __add__(self, p: _Permission):
|
||||
# For example we have a bit array `0011` representing permissions
|
||||
# To allow a permission we would convert the bit for that permission from 0 to 1
|
||||
# Let's say we want to allow permissions with bit array `0101`
|
||||
# Our desired result is `0111` which can be achieved by simple OR (A+B)
|
||||
return self | p
|
||||
|
||||
def __sub__(self, p: _Permission):
|
||||
# For example we have a bit array `0011` representing permissions
|
||||
# To deny a permission we would convert the bit for that permission from 1 to 0
|
||||
# Let's say we want to deny permissions with bit array `0101`
|
||||
# Our desired result is `0010` which can be achieved by Selective Clear Algorithm (A.B')
|
||||
return self & ~p
|
||||
|
||||
def __lt__(self, p: _Permission):
|
||||
return self._value < p._value
|
||||
|
||||
def __gt__(self, p: _Permission):
|
||||
return self._value > p._value
|
||||
|
||||
def __eq__(self, p: _Permission):
|
||||
return self._value == p._value
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}<{self._value:032b}>"
|
||||
|
||||
def _check(self, bit_pos: int):
|
||||
# To check if a permission is present we would need to compare the bit for that position in our bit array
|
||||
# For example if we have permission `0100` and we need to check if it is present in our bit array `0101`
|
||||
# We need to compare the 3rd bit from the left, so we shift it 2 places (n-1) and perform AND with 1
|
||||
# If the result is 1 the bit was 1 and the permission is present
|
||||
return (self._value >> bit_pos) & 1 == 1
|
||||
|
||||
class ChannelPermissions(_Permission):
|
||||
class ChannelPermissions(Flags):
|
||||
"""Represents the channel permissions for a role as seen in channel settings."""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@property
|
||||
def value(self) -> int:
|
||||
return self._value
|
||||
|
||||
@classmethod
|
||||
def none(cls) -> ChannelPermissions:
|
||||
return cls(0)
|
||||
return cls._from_value(0)
|
||||
|
||||
@classmethod
|
||||
def all(cls) -> ChannelPermissions:
|
||||
# channel permissions here do not have manage messages permission
|
||||
return cls(0b00000000000000000000000011111011)
|
||||
return cls._from_value(0b11111011)
|
||||
|
||||
@classmethod
|
||||
def view(cls) -> ChannelPermissions:
|
||||
return cls(0b00000000000000000000000000000001)
|
||||
|
||||
# view permission is always present and therefore is included in every permission below
|
||||
return cls._from_value(0b1)
|
||||
|
||||
@classmethod
|
||||
def send_message(cls) -> ChannelPermissions:
|
||||
return cls(0b00000000000000000000000000000011)
|
||||
return cls._from_value(0b11)
|
||||
|
||||
@classmethod
|
||||
def manage_channel(cls) -> ChannelPermissions:
|
||||
return cls(0b00000000000000000000000000001001)
|
||||
return cls._from_value(0b1001)
|
||||
|
||||
@classmethod
|
||||
def voice_call(cls) -> ChannelPermissions:
|
||||
return cls(0b00000000000000000000000000010001)
|
||||
return cls._from_value(0b10001)
|
||||
|
||||
@classmethod
|
||||
def invite_others(cls) -> ChannelPermissions:
|
||||
return cls(0b00000000000000000000000000100001)
|
||||
return cls._from_value(0b100001)
|
||||
|
||||
@classmethod
|
||||
def embed_links(cls) -> ChannelPermissions:
|
||||
return cls(0b00000000000000000000000001000001)
|
||||
return cls._from_value(0b1000001)
|
||||
|
||||
@classmethod
|
||||
def upload_files(cls) -> ChannelPermissions:
|
||||
return cls(0b00000000000000000000000010000001)
|
||||
return cls._from_value(0b10000001)
|
||||
|
||||
@property
|
||||
def can_view(self) -> bool:
|
||||
return self._check(0)
|
||||
@flag_value
|
||||
def can_view() -> int:
|
||||
return 1 << 0
|
||||
|
||||
@property
|
||||
def can_send_message(self) -> bool:
|
||||
return self._check(1)
|
||||
@flag_value
|
||||
def can_send_message() -> int:
|
||||
return 1 << 1
|
||||
|
||||
@property
|
||||
def can_manage_channel(self) -> bool:
|
||||
return self._check(3)
|
||||
@flag_value
|
||||
def can_manage_channel() -> int:
|
||||
return 1 << 3
|
||||
|
||||
@property
|
||||
def can_voice_call(self) -> bool:
|
||||
return self._check(4)
|
||||
@flag_value
|
||||
def can_voice_call() -> int:
|
||||
return 1 << 4
|
||||
|
||||
@property
|
||||
def can_invite_others(self) -> bool:
|
||||
return self._check(5)
|
||||
@flag_value
|
||||
def can_invite_others() -> int:
|
||||
return 1 << 5
|
||||
|
||||
@property
|
||||
def can_embed_links(self) -> bool:
|
||||
return self._check(6)
|
||||
@flag_value
|
||||
def can_embed_links() -> int:
|
||||
return 1 << 6
|
||||
|
||||
@property
|
||||
def can_upload_files(self) -> bool:
|
||||
return self._check(7)
|
||||
@flag_value
|
||||
def can_upload_files() -> int:
|
||||
return 1 << 7
|
||||
|
||||
class ServerPermissions:
|
||||
class ServerPermissions(Flags):
|
||||
"""Represents the server permissions for a role as seen in server settings."""
|
||||
|
||||
__slots__ = ("_server_perms", "_channel_perms", )
|
||||
|
||||
def __init__(self, server_perms: int, channel_perms: int) -> None:
|
||||
self._server_perms = _Permission(server_perms)
|
||||
self._channel_perms = _Permission(channel_perms)
|
||||
|
||||
def __add__(self, p: ServerPermissions) -> ServerPermissions:
|
||||
self._server_perms += p._server_perms
|
||||
self._channel_perms += p._channel_perms
|
||||
return self
|
||||
|
||||
def __sub__(self, p: ServerPermissions) -> ServerPermissions:
|
||||
self._server_perms -= p._server_perms
|
||||
self._channel_perms -= p._channel_perms
|
||||
return self
|
||||
|
||||
def __lt__(self, p: ServerPermissions) -> bool:
|
||||
return self._server_perms < p._server_perms and self._channel_perms < p._channel_perms
|
||||
|
||||
def __gt__(self, p: ServerPermissions) -> bool:
|
||||
return self._server_perms > p._server_perms and self._channel_perms > p._channel_perms
|
||||
|
||||
def __eq__(self, p: ServerPermissions) -> bool:
|
||||
return self._server_perms == p._server_perms and self._channel_perms == p._channel_perms
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}<Server: {self._server_perms._value:032b} Channel: {self._channel_perms._value:032b}>"
|
||||
|
||||
@property
|
||||
def value(self) -> PermissionTuple:
|
||||
return self._server_perms._value, self._channel_perms._value
|
||||
|
||||
@classmethod
|
||||
def none(cls) -> ServerPermissions:
|
||||
return cls(0, 0)
|
||||
return cls._from_value(0)
|
||||
|
||||
@classmethod
|
||||
def all(cls) -> ServerPermissions:
|
||||
# channel permissions here do not have manage channel permission
|
||||
return cls(0b00000000000000001111000000111111, 0b00000000000000000000000011110111)
|
||||
return cls._from_value(0b1111000000111111)
|
||||
|
||||
# view server and view channel are always present and therefore are included in every permission below
|
||||
# server permissions
|
||||
@flag_value
|
||||
def view_server() -> int:
|
||||
return 1 << 0
|
||||
|
||||
@classmethod
|
||||
def view_server(cls) -> ServerPermissions:
|
||||
return cls(0b00000000000000000000000000000001, 1)
|
||||
@flag_value
|
||||
def manage_roles() -> int:
|
||||
return 1 << 1
|
||||
|
||||
@classmethod
|
||||
def manage_roles(cls) -> ServerPermissions:
|
||||
return cls(0b00000000000000000000000000000011, 1)
|
||||
@flag_value
|
||||
def manage_channels() -> int:
|
||||
return 1 << 2
|
||||
|
||||
@classmethod
|
||||
def manage_channels(cls) -> ServerPermissions:
|
||||
return cls(0b00000000000000000000000000000101, 1)
|
||||
@flag_value
|
||||
def manage_server() -> int:
|
||||
return 1 << 3
|
||||
|
||||
@classmethod
|
||||
def manage_server(cls) -> ServerPermissions:
|
||||
return cls(0b00000000000000000000000000001001, 1)
|
||||
@flag_value
|
||||
def kick_members() -> int:
|
||||
return 1 << 4
|
||||
|
||||
@classmethod
|
||||
def kick_members(cls) -> ServerPermissions:
|
||||
return cls(0b00000000000000000000000000010001, 1)
|
||||
@flag_value
|
||||
def ban_members() -> int:
|
||||
return 1 << 5
|
||||
|
||||
@classmethod
|
||||
def ban_members(cls) -> ServerPermissions:
|
||||
return cls(0b00000000000000000000000000100001, 1)
|
||||
@flag_value
|
||||
def change_nicknames() -> int:
|
||||
return 1 << 12
|
||||
|
||||
@classmethod
|
||||
def change_nicknames(cls) -> ServerPermissions:
|
||||
return cls(0b00000000000000000001000000000001, 1)
|
||||
@flag_value
|
||||
def manage_nicknames() -> int:
|
||||
return 1 << 13
|
||||
|
||||
@classmethod
|
||||
def manage_nicknames(cls) -> ServerPermissions:
|
||||
return cls(0b00000000000000000010000000000001, 1)
|
||||
@flag_value
|
||||
def change_avatar() -> int:
|
||||
return 1 << 14
|
||||
|
||||
@classmethod
|
||||
def change_avatar(cls) -> ServerPermissions:
|
||||
return cls(0b00000000000000000100000000000001, 1)
|
||||
|
||||
@classmethod
|
||||
def remove_avatars(cls) -> ServerPermissions:
|
||||
return cls(0b00000000000000001000000000000001, 1)
|
||||
|
||||
@property
|
||||
def can_view_server(self) -> bool:
|
||||
return self._server_perms._check(0)
|
||||
|
||||
@property
|
||||
def can_manage_roles(self) -> bool:
|
||||
return self._server_perms._check(1)
|
||||
|
||||
@property
|
||||
def can_manage_channels(self) -> bool:
|
||||
return self._server_perms._check(2)
|
||||
|
||||
@property
|
||||
def can_manage_server(self) -> bool:
|
||||
return self._server_perms._check(3)
|
||||
|
||||
@property
|
||||
def can_kick_members(self) -> bool:
|
||||
return self._server_perms._check(4)
|
||||
|
||||
@property
|
||||
def can_ban_members(self) -> bool:
|
||||
return self._server_perms._check(5)
|
||||
|
||||
@property
|
||||
def can_change_nicknames(self) -> bool:
|
||||
return self._server_perms._check(12)
|
||||
|
||||
@property
|
||||
def can_manage_nicknames(self) -> bool:
|
||||
return self._server_perms._check(13)
|
||||
|
||||
@property
|
||||
def can_change_avatar(self) -> bool:
|
||||
return self._server_perms._check(14)
|
||||
|
||||
@property
|
||||
def can_remove_avatars(self) -> bool:
|
||||
return self._server_perms._check(15)
|
||||
|
||||
# channel permissions
|
||||
|
||||
@classmethod
|
||||
def view_channels(cls) -> ServerPermissions:
|
||||
return cls(1, 0b00000000000000000000000000000001)
|
||||
|
||||
@classmethod
|
||||
def send_message(cls) -> ServerPermissions:
|
||||
return cls(1, 0b00000000000000000000000000000011)
|
||||
|
||||
@classmethod
|
||||
def manage_messages(cls) -> ServerPermissions:
|
||||
return cls(1, 0b00000000000000000000000000000101)
|
||||
|
||||
@classmethod
|
||||
def voice_call(cls) -> ServerPermissions:
|
||||
return cls(1, 0b00000000000000000000000000010001)
|
||||
|
||||
@classmethod
|
||||
def invite_others(cls) -> ServerPermissions:
|
||||
return cls(1, 0b00000000000000000000000000100001)
|
||||
|
||||
@classmethod
|
||||
def embed_links(cls) -> ServerPermissions:
|
||||
return cls(1, 0b00000000000000000000000001000001)
|
||||
|
||||
@classmethod
|
||||
def upload_files(cls) -> ServerPermissions:
|
||||
return cls(1, 0b00000000000000000000000010000001)
|
||||
|
||||
@property
|
||||
def can_view_channel(self) -> bool:
|
||||
return self._channel_perms._check(0)
|
||||
|
||||
@property
|
||||
def can_send_message(self) -> bool:
|
||||
return self._channel_perms._check(1)
|
||||
|
||||
@property
|
||||
def can_manage_messages(self) -> bool:
|
||||
return self._channel_perms._check(2)
|
||||
|
||||
@property
|
||||
def can_voice_call(self) -> bool:
|
||||
return self._channel_perms._check(4)
|
||||
|
||||
@property
|
||||
def can_invite_others(self) -> bool:
|
||||
return self._channel_perms._check(5)
|
||||
|
||||
@property
|
||||
def can_embed_links(self) -> bool:
|
||||
return self._channel_perms._check(6)
|
||||
|
||||
@property
|
||||
def can_upload_files(self) -> bool:
|
||||
return self._channel_perms._check(7)
|
||||
@flag_value
|
||||
def remove_avatars() -> int:
|
||||
return 1 << 15
|
||||
|
||||
Executable
+38
-11
@@ -1,8 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from .permissions import ServerPermissions
|
||||
from revolt.types import server
|
||||
|
||||
from .permissions import ChannelPermissions, ServerPermissions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .server import Server
|
||||
@@ -14,7 +16,7 @@ __all__ = ("Role",)
|
||||
|
||||
class Role:
|
||||
"""Represents a role
|
||||
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`str`
|
||||
@@ -29,11 +31,13 @@ class Role:
|
||||
The position of the role in the role heirarchy
|
||||
server: :class:`Server`
|
||||
The server the role belongs to
|
||||
permissions: :class:`ServerPermissions`
|
||||
server_permissions: :class:`ServerPermissions`
|
||||
The server permissions for the role
|
||||
channel_permissions: :class:`ChannelPermissions`
|
||||
The channel permissions for the role
|
||||
"""
|
||||
__slots__ = ("id", "name", "colour", "hoist", "rank", "state", "server", "permissions")
|
||||
|
||||
__slots__ = ("id", "name", "colour", "hoist", "rank", "state", "server", "server_permissions", "channel_permissions")
|
||||
|
||||
def __init__(self, data: RolePayload, role_id: str, state: State, server: Server):
|
||||
self.state = state
|
||||
self.id = role_id
|
||||
@@ -42,17 +46,40 @@ class Role:
|
||||
self.hoist = data.get("hoist", False)
|
||||
self.rank = data.get("rank", 0)
|
||||
self.server = server
|
||||
self.permissions = ServerPermissions(*data.get("permissions"))
|
||||
self.server_permissions = ServerPermissions._from_value(data["permissions"][0])
|
||||
self.channel_permissions = ChannelPermissions._from_value(data["permissions"][1])
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
return self.colour
|
||||
|
||||
async def set_permissions(self, permissions: ServerPermissions) -> None:
|
||||
async def set_permissions(self, *, server_permissions: Optional[ServerPermissions] = None, channel_permissions: Optional[ChannelPermissions] = None) -> None:
|
||||
"""Sets the permissions for a role in a server.
|
||||
Parameters
|
||||
-----------
|
||||
permissions: :class:`ServerPermissions`
|
||||
The new permissions for the role
|
||||
server_permissions: Optional[:class:`ServerPermissions`]
|
||||
The new server permissions for the role
|
||||
channel_permissions: Optional[:class:`ChannelPermissions`]
|
||||
The new channel permissions for the role
|
||||
"""
|
||||
await self.state.http.set_role_permissions(self.server.id, self.id, *permissions.value)
|
||||
|
||||
if not server_permissions and not channel_permissions:
|
||||
return
|
||||
|
||||
server_value = (server_permissions or self.server_permissions).value
|
||||
channel_value = (channel_permissions or self.channel_permissions).value
|
||||
|
||||
await self.state.http.set_role_permissions(self.server.id, self.id, server_value, channel_value)
|
||||
|
||||
def _update(self, *, name: Optional[str] = None, colour: Optional[str] = None, hoist: Optional[bool] = None, rank: Optional[int] = None):
|
||||
if name:
|
||||
self.name = name
|
||||
|
||||
if colour:
|
||||
self.colour = colour
|
||||
|
||||
if hoist:
|
||||
self.hoist = hoist
|
||||
|
||||
if rank:
|
||||
self.rank = rank
|
||||
|
||||
+142
-24
@@ -1,50 +1,147 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, cast
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from .channel import Channel
|
||||
from .permissions import ServerPermissions
|
||||
from .asset import Asset
|
||||
from .category import Category
|
||||
from .channel import Channel, channel_factory
|
||||
from .permissions import ChannelPermissions, ServerPermissions
|
||||
from .role import Role
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .channel import TextChannel
|
||||
from .member import Member
|
||||
from .state import State
|
||||
from .types import Category as CategoryPayload
|
||||
from .types import File as FilePayload
|
||||
from .types import Permission as PermissionPayload
|
||||
from .types import Server as ServerPayload
|
||||
from .types import SystemMessagesConfig
|
||||
|
||||
|
||||
__all__ = ("Server",)
|
||||
__all__ = ("Server", "SystemMessages")
|
||||
|
||||
class SystemMessages:
|
||||
def __init__(self, data: SystemMessagesConfig, state: State):
|
||||
self.state = state
|
||||
self.user_joined_id = data.get("user_joined")
|
||||
self.user_left_id = data.get("user_left")
|
||||
self.user_kicked_id = data.get("user_kicked")
|
||||
self.user_banned_id = data.get("user_banned")
|
||||
|
||||
@property
|
||||
def user_joined(self) -> Optional[TextChannel]:
|
||||
if not self.user_joined_id:
|
||||
return
|
||||
|
||||
channel = self.state.get_channel(self.user_joined_id)
|
||||
assert isinstance(channel, TextChannel)
|
||||
return channel
|
||||
|
||||
@property
|
||||
def user_left(self) -> Optional[TextChannel]:
|
||||
if not self.user_left_id:
|
||||
return
|
||||
|
||||
channel = self.state.get_channel(self.user_left_id)
|
||||
assert isinstance(channel, TextChannel)
|
||||
return channel
|
||||
|
||||
@property
|
||||
def user_kicked(self) -> Optional[TextChannel]:
|
||||
if not self.user_kicked_id:
|
||||
return
|
||||
|
||||
channel = self.state.get_channel(self.user_kicked_id)
|
||||
assert isinstance(channel, TextChannel)
|
||||
return channel
|
||||
|
||||
@property
|
||||
def user_banned(self) -> Optional[TextChannel]:
|
||||
if not self.user_banned_id:
|
||||
return
|
||||
|
||||
channel = self.state.get_channel(self.user_banned_id)
|
||||
assert isinstance(channel, TextChannel)
|
||||
return channel
|
||||
|
||||
class Server:
|
||||
"""Represents a server
|
||||
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`str`
|
||||
The id of the server
|
||||
name: :class:`str`
|
||||
The name of the server
|
||||
owner: Optional[:class:`Member`]
|
||||
The owner of the server
|
||||
owner_id: :class:`str`
|
||||
The owner's id of the server
|
||||
description: Optional[:class:`str`]
|
||||
The servers description
|
||||
nsfw: :class:`bool`
|
||||
Whether the server is nsfw or not
|
||||
system_messages: :class:`SystemMessages`
|
||||
The system message config for the server
|
||||
icon: Optional[:class:`Asset`]
|
||||
The servers icon
|
||||
banner: Optional[:class:`Asset`]
|
||||
The servers banner
|
||||
"""
|
||||
__slots__ = ("state", "id", "name", "owner_id", "default_permissions", "_members", "_roles", "_channels")
|
||||
|
||||
__slots__ = ("state", "id", "name", "owner_id", "default_server_permissions", "default_channel_permissions", "_members", "_roles", "_channels", "description", "icon", "banner", "nsfw", "system_messages", "_categories")
|
||||
|
||||
def __init__(self, data: ServerPayload, state: State):
|
||||
self.state = state
|
||||
self.id = data["_id"]
|
||||
self.name = data["name"]
|
||||
self.owner_id = data["owner"]
|
||||
self.default_permissions = ServerPermissions(*data["default_permissions"])
|
||||
|
||||
self.default_server_permissions = ServerPermissions._from_value(data["default_permissions"][0])
|
||||
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._categories = {data["id"]: Category(data, state) for data in data.get("categories", [])}
|
||||
|
||||
if icon := data.get("icon"):
|
||||
self.icon = Asset(icon, state)
|
||||
else:
|
||||
self.icon = None
|
||||
|
||||
if banner := data.get("banner"):
|
||||
self.banner = Asset(banner, state)
|
||||
else:
|
||||
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()}
|
||||
channels = cast(list[Channel], list(filter(bool, [state.get_channel(channel_id) for channel_id in data["channels"]])))
|
||||
self._channels: dict[str, Channel] = {channel.id: channel for channel in channels}
|
||||
|
||||
self._channels: dict[str, Channel] = {channel_id: state.get_channel(channel_id) for channel_id in data.get("channels", [])}
|
||||
|
||||
def _update(self, *, owner: Optional[str] = None, name: Optional[str] = None, description: Optional[str] = None, icon: Optional[FilePayload] = None, banner: Optional[FilePayload] = None, default_permissions: Optional[PermissionPayload] = None, nsfw: Optional[bool] = None, system_messages: Optional[SystemMessagesConfig] = None, categories: Optional[list[CategoryPayload]] = None):
|
||||
if owner:
|
||||
self.owner_id = owner
|
||||
if name:
|
||||
self.name = name
|
||||
if description is not None:
|
||||
self.description = description or None
|
||||
if icon:
|
||||
self.icon = Asset(icon, self.state)
|
||||
if banner:
|
||||
self.banner = Asset(banner, self.state)
|
||||
if default_permissions:
|
||||
self.default_server_permissions = ServerPermissions._from_value(default_permissions[0])
|
||||
self.default_channel_permissions = ChannelPermissions._from_value(default_permissions[1])
|
||||
if nsfw is not None:
|
||||
self.nsfw = nsfw
|
||||
if system_messages is not None:
|
||||
self.system_messages = SystemMessages(system_messages, self.state)
|
||||
if categories is not None:
|
||||
self._categories = {data["id"]: Category(data, self.state) for data in categories}
|
||||
|
||||
@property
|
||||
def roles(self) -> list[Role]:
|
||||
"""list[:class:`Role`] Gets all roles in the server in decending order"""
|
||||
return list(self._roles.values())
|
||||
|
||||
|
||||
@property
|
||||
def members(self) -> list[Member]:
|
||||
"""list[:class:`Member`] Gets all members in the server"""
|
||||
@@ -55,6 +152,11 @@ class Server:
|
||||
"""list[:class:`Member`] Gets all channels in the server"""
|
||||
return list(self._channels.values())
|
||||
|
||||
@property
|
||||
def categories(self) -> list[Category]:
|
||||
"""list[:class:`Category`] Gets all categories in the server"""
|
||||
return list(self._categories.values())
|
||||
|
||||
def get_role(self, role_id: str) -> Role:
|
||||
"""Gets a role from the cache
|
||||
|
||||
@@ -100,20 +202,36 @@ class Server:
|
||||
"""
|
||||
return self._channels[channel_id]
|
||||
|
||||
def get_category(self, category_id: str) -> Category:
|
||||
"""Gets a category from the cache
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
id: :class:`str`
|
||||
The id of the category
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Category`
|
||||
The category
|
||||
"""
|
||||
return self._categories[category_id]
|
||||
|
||||
@property
|
||||
def owner(self) -> Optional[Member]:
|
||||
owner_id = self.owner_id
|
||||
def owner(self) -> Member:
|
||||
""":class:`Member` The owner of the server"""
|
||||
return self.get_member(self.owner_id)
|
||||
|
||||
if not owner_id:
|
||||
return
|
||||
|
||||
return self.get_member(owner_id)
|
||||
|
||||
async def set_default_permissions(self, permissions: ServerPermissions) -> None:
|
||||
async def set_default_permissions(self, *, server_permissions: Optional[ServerPermissions] = None, channel_permissions: Optional[ChannelPermissions] = None) -> None:
|
||||
"""Sets the default server permissions.
|
||||
Parameters
|
||||
-----------
|
||||
permissions: :class:`ServerPermissions`
|
||||
server_permissions: Optional[:class:`ServerPermissions`]
|
||||
The new default server permissions
|
||||
channel_permissions: Optional[:class:`ChannelPermissions`]
|
||||
the new default channel permissions
|
||||
"""
|
||||
await self.state.http.set_default_permissions(self.id, *permissions.value)
|
||||
server_value = (server_permissions or self.default_server_permissions).value
|
||||
channel_value = (channel_permissions or self.default_channel_permissions).value
|
||||
|
||||
await self.state.http.set_default_permissions(self.id, server_value, channel_value)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from .category import *
|
||||
from .channel import *
|
||||
from .embed import *
|
||||
from .file import *
|
||||
|
||||
@@ -9,4 +9,4 @@ __all__ = ("Category",)
|
||||
class Category(TypedDict):
|
||||
id: str
|
||||
title: str
|
||||
channels: list[Channel]
|
||||
channels: list[str]
|
||||
|
||||
+53
-6
@@ -1,16 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, TypedDict, Union
|
||||
from typing import TYPE_CHECKING, Literal, TypedDict, Union
|
||||
|
||||
from .channel import (Channel, DMChannel, Group, SavedMessages, TextChannel,
|
||||
VoiceChannel)
|
||||
from .message import Message
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .member import Member
|
||||
from .member import Member, MemberID
|
||||
from .server import Server
|
||||
from .user import User
|
||||
|
||||
from .user import Status, User
|
||||
|
||||
__all__ = (
|
||||
"BasePayload",
|
||||
@@ -25,7 +24,16 @@ __all__ = (
|
||||
"ChannelUpdateEventPayload",
|
||||
"ChannelDeleteEventPayload",
|
||||
"ChannelStartTypingEventPayload",
|
||||
"ChannelDeleteTypingEventPayload"
|
||||
"ChannelDeleteTypingEventPayload",
|
||||
"ServerUpdateEventPayload",
|
||||
"ServerDeleteEventPayload",
|
||||
"ServerMemberUpdateEventPayload",
|
||||
"ServerMemberJoinEventPayload",
|
||||
"ServerMemberLeaveEventPayload",
|
||||
"ServerRoleUpdateEventPayload",
|
||||
"ServerRoleDeleteEventPayload",
|
||||
"UserUpdateEventPayload",
|
||||
"UserRelationshipEventPayload"
|
||||
)
|
||||
|
||||
class BasePayload(TypedDict):
|
||||
@@ -78,7 +86,7 @@ ChannelCreateEventPayload = Union[ChannelCreateEventPayload_Group, ChannelCreate
|
||||
class ChannelUpdateEventPayload(BasePayload):
|
||||
id: str
|
||||
data: ...
|
||||
clear: ...
|
||||
clear: Literal["Icon", "Description"]
|
||||
|
||||
class ChannelDeleteEventPayload(BasePayload):
|
||||
id: str
|
||||
@@ -88,3 +96,42 @@ class ChannelStartTypingEventPayload(BasePayload):
|
||||
user: str
|
||||
|
||||
ChannelDeleteTypingEventPayload = ChannelStartTypingEventPayload
|
||||
|
||||
class ServerUpdateEventPayload(BasePayload):
|
||||
id: str
|
||||
data: dict
|
||||
clear: Literal["Icon", "Banner", "Description"]
|
||||
|
||||
class ServerDeleteEventPayload(BasePayload):
|
||||
id: str
|
||||
|
||||
class ServerMemberUpdateEventPayload(BasePayload):
|
||||
id: MemberID
|
||||
data: dict
|
||||
clear: Literal["Nickname", "Avatar"]
|
||||
|
||||
class ServerMemberJoinEventPayload(BasePayload):
|
||||
id: str
|
||||
user: str
|
||||
|
||||
ServerMemberLeaveEventPayload = ServerMemberJoinEventPayload
|
||||
|
||||
class ServerRoleUpdateEventPayload(BasePayload):
|
||||
id: str
|
||||
role_id: str
|
||||
data: dict
|
||||
clear: Literal["Color"]
|
||||
|
||||
class ServerRoleDeleteEventPayload(BasePayload):
|
||||
id: str
|
||||
role_id: str
|
||||
|
||||
class UserUpdateEventPayload(BasePayload):
|
||||
id: str
|
||||
data: dict
|
||||
clear: Literal["ProfileContent", "ProfileBackground", "StatusText", "Avatar"]
|
||||
|
||||
class UserRelationshipEventPayload(BasePayload):
|
||||
id: str
|
||||
user: str
|
||||
status: Status
|
||||
|
||||
+19
-1
@@ -7,7 +7,11 @@ if TYPE_CHECKING:
|
||||
from .file import File
|
||||
|
||||
|
||||
__all__ = ("Message",)
|
||||
__all__ = (
|
||||
"Message",
|
||||
"MessageReplyPayload",
|
||||
"Masquerade"
|
||||
)
|
||||
|
||||
class UserAddContent(TypedDict):
|
||||
id: str
|
||||
@@ -40,12 +44,26 @@ class ChannelDescriptionChangeContent(TypedDict):
|
||||
class ChannelIconChangeContent(TypedDict):
|
||||
by: str
|
||||
|
||||
MessageEdited = TypedDict("MessageEdited", {"$date": str})
|
||||
|
||||
class Masquerade(TypedDict, total=False):
|
||||
name: str
|
||||
avatar: str
|
||||
|
||||
class _OptionalMessage(TypedDict):
|
||||
attachments: list[File]
|
||||
embeds: list[Embed]
|
||||
mentions: list[str]
|
||||
replies: list[str]
|
||||
edited: MessageEdited
|
||||
masquerade: Masquerade
|
||||
|
||||
class Message(_OptionalMessage):
|
||||
_id: str
|
||||
channel: str
|
||||
author: str
|
||||
content: Union[str, UserAddContent, UserRemoveContent, UserJoinedContent, UserLeftContent, UserKickedContent, UserBannedContent, ChannelRenameContent, ChannelDescriptionChangeContent, ChannelIconChangeContent]
|
||||
|
||||
class MessageReplyPayload(TypedDict):
|
||||
id: str
|
||||
mention: bool
|
||||
|
||||
@@ -13,6 +13,7 @@ __all__ = (
|
||||
"BannedUser",
|
||||
"Ban",
|
||||
"ServerBans",
|
||||
"SystemMessagesConfig"
|
||||
)
|
||||
|
||||
class SystemMessagesConfig(TypedDict, total=False):
|
||||
@@ -29,8 +30,9 @@ class _ServerOptional(TypedDict, total=False):
|
||||
roles: list[Role]
|
||||
icon: File
|
||||
banner: File
|
||||
nsfw: bool
|
||||
|
||||
class Server(TypedDict):
|
||||
class Server(_ServerOptional):
|
||||
_id: str
|
||||
owner: str
|
||||
name: str
|
||||
|
||||
+48
-13
@@ -1,14 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, NamedTuple, Optional
|
||||
from typing import TYPE_CHECKING, NamedTuple, Optional, Union
|
||||
|
||||
from .asset import Asset
|
||||
from .asset import Asset, PartialAsset
|
||||
from .enums import PresenceType, RelationshipType
|
||||
from .flags import UserBadges
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .state import State
|
||||
from .types import File
|
||||
from .types import Status as StatusPayload
|
||||
from .types import User as UserPayload
|
||||
from .types import UserRelation
|
||||
|
||||
|
||||
__all__ = ("User",)
|
||||
@@ -23,6 +25,11 @@ class Status(NamedTuple):
|
||||
text: Optional[str]
|
||||
presence: Optional[PresenceType]
|
||||
|
||||
class UserProfile(NamedTuple):
|
||||
"""A namedtuple representing a users profile"""
|
||||
content: Optional[str]
|
||||
background: Optional[Asset]
|
||||
|
||||
class User:
|
||||
"""Represents a user
|
||||
|
||||
@@ -30,20 +37,16 @@ class User:
|
||||
-----------
|
||||
id: :class:`str`
|
||||
The users id
|
||||
name: :class:`str`
|
||||
The users name
|
||||
bot: :class:`bool`
|
||||
Whether or not the user is a bot
|
||||
owner: Optional[:class:`User`]
|
||||
The bot's owner if the user is a bot
|
||||
badges: :class:`int`
|
||||
badges: :class:`UserBadges`
|
||||
The users badges
|
||||
online: :class:`bool`
|
||||
Whether or not the user is online
|
||||
flags: :class:`int`
|
||||
The user flags
|
||||
avatar: :class:`Asset`
|
||||
The users avatar if they have one
|
||||
relations: list[:class:`Relation`]
|
||||
A list of the users relations
|
||||
relationship: Optional[:class:`RelationshipType`]
|
||||
@@ -51,14 +54,14 @@ class User:
|
||||
status: Optional[:class:`Status`]
|
||||
The users status
|
||||
"""
|
||||
__flattern_attributes__ = ("id", "name", "bot", "owner_id", "badges", "online", "flags", "avatar", "relations", "relationship", "status")
|
||||
__flattern_attributes__ = ("id", "bot", "owner_id", "badges", "online", "flags", "relations", "relationship", "status", "masquerade_avatar", "masquerade_name", "original_name", "original_avatar", "profile")
|
||||
__slots__ = (*__flattern_attributes__, "state")
|
||||
|
||||
def __init__(self, data: UserPayload, state: State):
|
||||
self.state = state
|
||||
self.id = data["_id"]
|
||||
self.name = data["username"]
|
||||
|
||||
self.original_name = data["username"]
|
||||
|
||||
bot = data.get("bot")
|
||||
if bot:
|
||||
self.bot = True
|
||||
@@ -67,12 +70,12 @@ class User:
|
||||
self.bot = False
|
||||
self.owner_id = None
|
||||
|
||||
self.badges = data.get("badges", 0)
|
||||
self.badges = UserBadges._from_value(data.get("badges", 0))
|
||||
self.online = data.get("online", False)
|
||||
self.flags = data.get("flags", 0)
|
||||
|
||||
avatar = data.get("avatar")
|
||||
self.avatar = Asset(avatar, state) if avatar else None
|
||||
self.original_avatar = Asset(avatar, state) if avatar else None
|
||||
|
||||
relations = []
|
||||
|
||||
@@ -92,6 +95,11 @@ class User:
|
||||
else:
|
||||
self.status = None
|
||||
|
||||
self.profile: Optional[UserProfile] = None
|
||||
|
||||
self.masquerade_avatar: Optional[PartialAsset] = None
|
||||
self.masquerade_name: Optional[str] = None
|
||||
|
||||
@property
|
||||
def owner(self) -> Optional[User]:
|
||||
owner_id = self.owner_id
|
||||
@@ -100,3 +108,30 @@ class User:
|
||||
return
|
||||
|
||||
return self.state.get_user(owner_id)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
""":class:`str` The name the user is displaying, this includes there orginal name and masqueraded name"""
|
||||
return self.masquerade_name or self.original_name
|
||||
|
||||
@property
|
||||
def avatar(self) -> Union[Asset, PartialAsset, None]:
|
||||
"""Optional[:class:`Asset`] The avatar the member is displaying, this includes there orginal avatar and masqueraded avatar"""
|
||||
return self.masquerade_avatar or self.original_avatar
|
||||
|
||||
def _update(self, *, status: Optional[StatusPayload] = None, profile_content: Optional[str] = None, profile_background: Optional[File] = None, avatar: Optional[File] = None, online: Optional[bool] = None):
|
||||
if status:
|
||||
presence = status.get("presence")
|
||||
self.status = Status(status.get("text"), PresenceType(presence) if presence else None)
|
||||
|
||||
if profile_background:
|
||||
self.profile = UserProfile(self.profile.content if self.profile else None, Asset(profile_background, self.state))
|
||||
|
||||
if profile_content:
|
||||
self.profile = UserProfile(profile_content, self.profile.background if self.profile else None)
|
||||
|
||||
if avatar:
|
||||
self.original_avatar = Asset(avatar, self.state)
|
||||
|
||||
if online:
|
||||
self.online = online
|
||||
|
||||
+62
-2
@@ -1,6 +1,10 @@
|
||||
from typing import Callable, TypeVar
|
||||
import inspect
|
||||
from typing import Any, Callable, Coroutine, TypeVar, Union, Iterable
|
||||
from operator import attrgetter
|
||||
|
||||
__all__ = ("Missing", "copy_doc")
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
__all__ = ("Missing", "copy_doc", "maybe_coroutine", "get")
|
||||
|
||||
class _Missing:
|
||||
def __repr__(self):
|
||||
@@ -16,3 +20,59 @@ def copy_doc(from_t: T) -> Callable[[T], T]:
|
||||
return to_t
|
||||
|
||||
return inner
|
||||
|
||||
R_T = TypeVar("R_T")
|
||||
P = ParamSpec("P")
|
||||
|
||||
# its impossible to type this function correctly for a couple reasons:
|
||||
# 1. isawaitable doesnt narrow while keeping typevars - there is an open pr for this (typeshed#5658) but it cant be merged because mypy doesnt support the feature fully
|
||||
# 2. typeguard doesnt narrow for the negative case which is dumb imo, so `value` would stay being a union even after the if statement (PEP 647 - "The type is not narrowed in the negative case")
|
||||
|
||||
async def maybe_coroutine(func: Callable[P, Union[R_T, Coroutine[Any, Any, R_T]]], *args: P.args, **kwargs: P.kwargs) -> R_T:
|
||||
value = func(*args, **kwargs)
|
||||
|
||||
if inspect.isawaitable(value):
|
||||
value = await value
|
||||
|
||||
return value # type: ignore
|
||||
|
||||
|
||||
def get(iterable: Iterable[T], **attrs: Any) -> T:
|
||||
"""A convenaince function to help get a value from an iterable with a specific attribute
|
||||
|
||||
Examples
|
||||
---------
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 4
|
||||
|
||||
from revolt import utils
|
||||
|
||||
channel = utils.get(server.channels, name="General")
|
||||
await channel.send("Hello general chat.")
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
iterable: Iterable
|
||||
The values to search though
|
||||
**attrs: Any
|
||||
The attributes to check
|
||||
|
||||
Returns
|
||||
--------
|
||||
Any
|
||||
The value from the iterable with the met attributes
|
||||
|
||||
Raises
|
||||
-------
|
||||
LookupError
|
||||
Raises when none of the values in the iterable matches the attributes
|
||||
|
||||
"""
|
||||
converted = [(attrgetter(attr.replace('__', '.')), value) for attr, value in attrs.items()]
|
||||
|
||||
for elem in iterable:
|
||||
if all(pred(elem) == value for pred, value in converted):
|
||||
return elem
|
||||
|
||||
raise LookupError
|
||||
|
||||
+136
-10
@@ -5,11 +5,19 @@ import logging
|
||||
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
|
||||
from .types import (MessageDeleteEventPayload, MessageUpdateEventPayload,
|
||||
ServerDeleteEventPayload, ServerMemberJoinEventPayload,
|
||||
ServerMemberLeaveEventPayload,
|
||||
ServerMemberUpdateEventPayload,
|
||||
ServerRoleDeleteEventPayload, ServerRoleUpdateEventPayload,
|
||||
ServerUpdateEventPayload, UserRelationshipEventPayload,
|
||||
UserUpdateEventPayload)
|
||||
from .user import Status
|
||||
|
||||
try:
|
||||
import ujson as json
|
||||
@@ -36,7 +44,7 @@ __all__ = ("WebsocketHandler",)
|
||||
logger = logging.getLogger("revolt")
|
||||
|
||||
class WebsocketHandler:
|
||||
__slots__ = ("session", "token", "ws_url", "dispatch", "state", "websocket", "loop")
|
||||
__slots__ = ("session", "token", "ws_url", "dispatch", "state", "websocket", "loop", "user", "ready")
|
||||
|
||||
def __init__(self, session: aiohttp.ClientSession, token: str, ws_url: str, dispatch: Callable[..., None], state: State):
|
||||
self.session = session
|
||||
@@ -46,6 +54,8 @@ class WebsocketHandler:
|
||||
self.state = state
|
||||
self.websocket: aiohttp.ClientWebSocketResponse
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.user = None
|
||||
self.ready = asyncio.Event()
|
||||
|
||||
async def send_payload(self, payload: BasePayload):
|
||||
if use_msgpack:
|
||||
@@ -71,6 +81,9 @@ class WebsocketHandler:
|
||||
event_type = payload["type"].lower()
|
||||
logger.debug("Recieved event %s %s", event_type, payload)
|
||||
try:
|
||||
if event_type != "ready":
|
||||
await self.ready.wait()
|
||||
|
||||
func = getattr(self, f"handle_{event_type}")
|
||||
except:
|
||||
logger.debug("Unknown event '%s'", event_type)
|
||||
@@ -82,8 +95,11 @@ class WebsocketHandler:
|
||||
logger.info("Successfully authenticated")
|
||||
|
||||
async def handle_ready(self, payload: ReadyEventPayload):
|
||||
for user in payload["users"]:
|
||||
self.state.add_user(user)
|
||||
for user_payload in payload["users"]:
|
||||
user = self.state.add_user(user_payload)
|
||||
|
||||
if user.relationship == RelationshipType.user:
|
||||
self.user = user
|
||||
|
||||
for channel in payload["channels"]:
|
||||
self.state.add_channel(channel)
|
||||
@@ -97,6 +113,7 @@ class WebsocketHandler:
|
||||
|
||||
await self.state.fetch_all_server_members()
|
||||
|
||||
self.ready.set()
|
||||
self.dispatch("ready")
|
||||
|
||||
async def handle_message(self, payload: MessageEventPayload):
|
||||
@@ -124,10 +141,13 @@ class WebsocketHandler:
|
||||
async def handle_messagedelete(self, payload: MessageDeleteEventPayload):
|
||||
self.dispatch("raw_message_delete", payload)
|
||||
|
||||
message = self.state.get_message(payload["id"])
|
||||
if message:
|
||||
self.state.messages.remove(message)
|
||||
self.dispatch("message_delete", message)
|
||||
try:
|
||||
message = self.state.get_message(payload["id"])
|
||||
except:
|
||||
return
|
||||
|
||||
self.state.messages.remove(message)
|
||||
self.dispatch("message_delete", message)
|
||||
|
||||
async def handle_channelcreate(self, payload: ChannelCreateEventPayload):
|
||||
channel = self.state.add_channel(payload)
|
||||
@@ -161,11 +181,117 @@ class WebsocketHandler:
|
||||
|
||||
self.dispatch("typing_start", channel, user)
|
||||
|
||||
async def handle_channeldeletetyping(self, payload: ChannelDeleteTypingEventPayload):
|
||||
async def handle_channelstoptyping(self, payload: ChannelDeleteTypingEventPayload):
|
||||
channel = self.state.get_channel(payload["id"])
|
||||
user = self.state.get_user(payload["user"])
|
||||
|
||||
self.dispatch("typing_delete", channel, user)
|
||||
self.dispatch("typing_stop", channel, user)
|
||||
|
||||
async def handle_serverupdate(self, payload: ServerUpdateEventPayload):
|
||||
server = self.state.get_server(payload["id"])
|
||||
|
||||
old_server = copy(server)
|
||||
|
||||
server._update(**payload["data"])
|
||||
|
||||
if clear := payload.get("clear"):
|
||||
if clear == "Icon":
|
||||
server.icon = None
|
||||
|
||||
elif clear == "Banner":
|
||||
server.banner = None
|
||||
|
||||
elif clear == "Description":
|
||||
server.description = None
|
||||
|
||||
self.dispatch("server_update", old_server, server)
|
||||
|
||||
async def handle_serverdelete(self, payload: ServerDeleteEventPayload):
|
||||
server = self.state.servers.pop(payload["id"])
|
||||
|
||||
for channel in server.channels:
|
||||
del self.state.channels[channel.id]
|
||||
|
||||
self.dispatch("server_delete", server)
|
||||
|
||||
async def handle_servermemberupdate(self, payload: ServerMemberUpdateEventPayload):
|
||||
member = self.state.get_member(payload["id"]["server"], payload["id"]["user"])
|
||||
old_member = copy(member)
|
||||
|
||||
if clear := payload.get("clear"):
|
||||
if clear == "Nickname":
|
||||
member.nickname = None
|
||||
elif clear == "Avatar":
|
||||
member.guild_avatar = None
|
||||
|
||||
member._update(**payload["data"])
|
||||
|
||||
self.dispatch("member_update", old_member, member)
|
||||
|
||||
async def handle_servermemberjoin(self, payload: ServerMemberJoinEventPayload):
|
||||
member = self.state.add_member(payload["id"], {"_id": {"server": payload["id"], "user": payload["user"]}})
|
||||
self.dispatch("member_join", member)
|
||||
|
||||
async def handle_memberleave(self, payload: ServerMemberLeaveEventPayload):
|
||||
server = self.state.get_server(payload["id"])
|
||||
member = server._members.pop(payload["user"])
|
||||
|
||||
self.dispatch("member_leave", member)
|
||||
|
||||
async def handle_serveroleupdate(self, payload: ServerRoleUpdateEventPayload):
|
||||
server = self.state.get_server(payload["id"])
|
||||
role = server.get_role(payload["role_id"])
|
||||
old_role = copy(role)
|
||||
|
||||
if clear := payload.get("clear"):
|
||||
if clear == "Colour":
|
||||
role.colour = None
|
||||
|
||||
role._update(**payload["data"])
|
||||
|
||||
self.dispatch("role_update", old_role, role)
|
||||
|
||||
async def handle_serverroledelete(self, payload: ServerRoleDeleteEventPayload):
|
||||
server = self.state.get_server(payload["id"])
|
||||
role = server._roles.pop(payload["role_id"])
|
||||
|
||||
self.dispatch("role_delete", role)
|
||||
|
||||
async def handle_userupdate(self, payload: UserUpdateEventPayload):
|
||||
user = self.state.get_user(payload["id"])
|
||||
old_user = copy(user)
|
||||
|
||||
if clear := payload.get("clear"):
|
||||
if clear == "ProfileContent":
|
||||
...
|
||||
elif clear == "ProfileBackground":
|
||||
...
|
||||
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
|
||||
user.status = None
|
||||
else:
|
||||
user.status = Status(None, user.status.presence) # type: ignore
|
||||
|
||||
elif clear == "Avatar":
|
||||
user.original_avatar = None
|
||||
|
||||
# the keys have . in them so i need to replace with _
|
||||
|
||||
data = payload["data"]
|
||||
data["profile_content"] = data.pop("profile.content", None)
|
||||
data["profile_background"] = data.pop("profile.background", None)
|
||||
|
||||
user._update(**data)
|
||||
|
||||
self.dispatch("user_update", old_user, user)
|
||||
|
||||
async def handle_userrelationship(self, payload: UserRelationshipEventPayload):
|
||||
user = self.state.get_user(payload["user"])
|
||||
old_relationship = user.relationship
|
||||
user.relationship = RelationshipType(payload["status"])
|
||||
|
||||
self.dispatch("user_relationship_update", user, old_relationship, user.relationship)
|
||||
|
||||
async def start(self):
|
||||
if use_msgpack:
|
||||
|
||||
@@ -6,16 +6,22 @@ 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()
|
||||
|
||||
|
||||
setup(
|
||||
name="revolt.py",
|
||||
version="0.1.0",
|
||||
description="Python wrapper around revolt.chat",
|
||||
version="0.1.3",
|
||||
description="Python wrapper for the revolt.chat API",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/Zomatree/revolt.py",
|
||||
url="https://github.com/revoltchat/revolt.py",
|
||||
author="Zomatree",
|
||||
classifiers=[
|
||||
"Development Status :: 3 - Beta",
|
||||
"Development Status :: 4 - Beta",
|
||||
|
||||
"Intended Audience :: Developers",
|
||||
|
||||
@@ -23,6 +29,7 @@ setup(
|
||||
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
],
|
||||
|
||||
@@ -32,8 +39,9 @@ setup(
|
||||
extras_require={
|
||||
"speedups": ["ujson", "aiohttp[speedups]==3.7.4.post0", "msgpack==1.0.2"],
|
||||
},
|
||||
install_requires=requirements,
|
||||
project_urls={
|
||||
"Bug Reports": "https://github.com/Zomatree/revolt.py/issues",
|
||||
"Source": "https://github.com/Zomatree/revolt.py/",
|
||||
"Bug Reports": "https://github.com/revoltchat/revolt.py/issues",
|
||||
"Source": "https://github.com/revoltchat/revolt.py/",
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user