mirror of
https://github.com/stoatchat/python-client-sdk.git
synced 2026-07-25 16:35:33 -04:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| efce2aedac | |||
| ca37846df7 | |||
| 86c07a6d6b | |||
| a8be358339 | |||
| e34ca0afda | |||
| 060e6ea32a | |||
| 2dabb0dbb8 | |||
| 48e0f0fb2e | |||
| bc8d650659 | |||
| d8fde3d0a6 | |||
| d4568c02ea | |||
| ff49f8e3e5 | |||
| eed960b9de | |||
| 185bd00eb9 | |||
| 41be16477d | |||
| 5a22d6063d | |||
| 7b95ec1b6b | |||
| dffbc1f665 | |||
| 1cfc21d4ff | |||
| 9bf443e36b | |||
| 5d3250bcce | |||
| 1a697457b7 | |||
| 51c7e45821 | |||
| 1c041d1e9f | |||
| 04b2bfbc40 | |||
| 02cefeab46 | |||
| 5aa3c2c00a | |||
| 11663d493a | |||
| 411a0f8203 | |||
| 7ae2a56610 | |||
| 04e63e9468 | |||
| c8c11b5394 | |||
| 2749822782 | |||
| 71a33c16b4 | |||
| 5ee3c60ba5 | |||
| 8524ff4036 | |||
| c97085cd55 | |||
| 85910a703a | |||
| 042a7da85e | |||
| 1841d21bee | |||
| 69602fc812 | |||
| a52febcb4e | |||
| da6fb64b89 | |||
| 072a4b1f2d | |||
| 826e0f8c85 | |||
| a23615a5ae | |||
| 2c30502b50 | |||
| e23effd605 |
@@ -1,5 +1,7 @@
|
||||
# Revolt.py
|
||||
|
||||
> # This project is archived and is no longer receiving updates.
|
||||
|
||||
An async library to interact with the https://revolt.chat API.
|
||||
|
||||
You can join the support server [here](https://rvlt.gg/FDXER6hr) and find the library's documentation [here](https://revoltpy.readthedocs.io/en/latest/).
|
||||
|
||||
Vendored
+8
-2
@@ -148,7 +148,7 @@ API Reference
|
||||
:members:
|
||||
|
||||
Enums
|
||||
======
|
||||
------
|
||||
|
||||
The api uses enums to say what variant of something is,
|
||||
these represent those enums
|
||||
@@ -278,7 +278,7 @@ All enums subclass `aenum.Enum`.
|
||||
The embed is unknown
|
||||
|
||||
Utils
|
||||
======
|
||||
------
|
||||
|
||||
.. currentmodule:: revolt.utils
|
||||
|
||||
@@ -287,3 +287,9 @@ A collection a utility functions and classes to aid in making your bot
|
||||
.. autofunction:: get
|
||||
|
||||
.. autofunction:: client_session
|
||||
|
||||
.. autoclass:: Ulid
|
||||
:members:
|
||||
|
||||
.. autoclass:: Object
|
||||
:members:
|
||||
@@ -19,6 +19,11 @@ Command
|
||||
.. autoclass:: revolt.ext.commands.Command
|
||||
:members:
|
||||
|
||||
Group
|
||||
~~~~~~~~
|
||||
.. autoclass:: revolt.ext.commands.Group
|
||||
:members:
|
||||
|
||||
Cog
|
||||
~~~~
|
||||
.. autoclass:: revolt.ext.commands.Cog
|
||||
@@ -28,6 +33,13 @@ command
|
||||
~~~~~~~~
|
||||
.. autodecorator:: revolt.ext.commands.command
|
||||
|
||||
group
|
||||
~~~~~~~~
|
||||
.. autodecorator:: revolt.ext.commands.group
|
||||
|
||||
Checks
|
||||
-------
|
||||
|
||||
check
|
||||
~~~~~~
|
||||
.. autodecorator:: revolt.ext.commands.check
|
||||
@@ -40,9 +52,62 @@ is_server_owner
|
||||
~~~~~~~~~~~~~~~~
|
||||
.. autodecorator:: revolt.ext.commands.is_server_owner
|
||||
|
||||
has_permissions
|
||||
~~~~~~~~~~~~~~~~
|
||||
.. autodecorator:: revolt.ext.commands.has_permissions
|
||||
|
||||
has_channel_permissions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. autodecorator:: revolt.ext.commands.has_channel_permissions
|
||||
|
||||
Converters
|
||||
-----------
|
||||
|
||||
IntConverter
|
||||
~~~~~~~~~~~~~
|
||||
Converts the parameter to an int
|
||||
|
||||
BoolConverter
|
||||
~~~~~~~~~~~~~~
|
||||
Converts the parameter to a bool
|
||||
|
||||
CategoryConverter
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
Converts the parameter to a category
|
||||
|
||||
UserConverter
|
||||
~~~~~~~~~~~~~~
|
||||
Converts the parameter to a category
|
||||
|
||||
MemberConverter
|
||||
~~~~~~~~~~~~~~~~
|
||||
Converts the parameter to a category
|
||||
|
||||
ChannelConverter
|
||||
~~~~~~~~~~~~~~~~~
|
||||
Converts the parameter to a category
|
||||
|
||||
Greedy
|
||||
~~~~~~
|
||||
Converts the parameter to a greedy parameter which will take as many arguments which convert successfully.
|
||||
|
||||
Allows you to have var-args in the middle of a signature.
|
||||
|
||||
Help Commands
|
||||
--------------
|
||||
|
||||
HelpCommand
|
||||
~~~~~~~~~~~~
|
||||
.. autoclass:: revolt.ext.commands.HelpCommand
|
||||
:members:
|
||||
|
||||
DefaultHelpCommand
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
.. autoclass:: revolt.ext.commands.DefaultHelpCommand
|
||||
:members:
|
||||
|
||||
Exceptions
|
||||
===========
|
||||
-----------
|
||||
|
||||
CommandError
|
||||
~~~~~~~~~~~~~
|
||||
@@ -79,6 +144,11 @@ ServerOnly
|
||||
.. autoexception:: revolt.ext.commands.ServerOnly
|
||||
:members:
|
||||
|
||||
MissingPermissionsError
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. autoexception:: revolt.ext.commands.MissingPermissionsError
|
||||
:members:
|
||||
|
||||
ConverterError
|
||||
~~~~~~~~~~~~~~~
|
||||
.. autoexception:: revolt.ext.commands.ConverterError
|
||||
@@ -99,6 +169,11 @@ CategoryConverterError
|
||||
.. autoexception:: revolt.ext.commands.CategoryConverterError
|
||||
:members:
|
||||
|
||||
ChannelConverterError
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. autoexception:: revolt.ext.commands.ChannelConverterError
|
||||
:members:
|
||||
|
||||
UserConverterError
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
.. autoexception:: revolt.ext.commands.UserConverterError
|
||||
@@ -108,3 +183,18 @@ MemberConverterError
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
.. autoexception:: revolt.ext.commands.MemberConverterError
|
||||
:members:
|
||||
|
||||
UnionConverterError
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
.. autoexception:: revolt.ext.commands.UnionConverterError
|
||||
:members:
|
||||
|
||||
MissingSetup
|
||||
~~~~~~~~~~~~~
|
||||
.. autoexception:: revolt.ext.commands.MissingSetup
|
||||
:members:
|
||||
|
||||
CommandOnCooldown
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
.. autoexception:: revolt.ext.commands.CommandOnCooldown
|
||||
:members:
|
||||
+1
-1
@@ -16,7 +16,7 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
]
|
||||
dependencies = [
|
||||
"aiohttp==3.8.*",
|
||||
"aiohttp==3.10.*",
|
||||
"ulid-py==1.1.*",
|
||||
"aenum==3.1.*",
|
||||
"typing_extensions>=4.4.0"
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
from . import utils as utils
|
||||
from . import types as types
|
||||
from .asset import *
|
||||
from .category import *
|
||||
from .channel import *
|
||||
@@ -18,4 +19,4 @@ from .role import *
|
||||
from .server import *
|
||||
from .user import *
|
||||
|
||||
__version__ = "0.1.11"
|
||||
__version__ = "0.2.0"
|
||||
|
||||
+3
-3
@@ -23,7 +23,7 @@ class Asset(Ulid):
|
||||
id: :class:`str`
|
||||
The id of the asset
|
||||
tag: :class:`str`
|
||||
The tag of the asset, this corrasponds to where the asset is used
|
||||
The tag of the asset, this corresponds to where the asset is used
|
||||
size: :class:`int`
|
||||
Amount of bytes in the file
|
||||
filename: :class:`str`
|
||||
@@ -37,7 +37,7 @@ class Asset(Ulid):
|
||||
type: :class:`AssetType`
|
||||
The type of asset it is
|
||||
url: :class:`str`
|
||||
The assets url
|
||||
The asset's url
|
||||
"""
|
||||
__slots__ = ("state", "id", "tag", "size", "filename", "content_type", "width", "height", "type", "url")
|
||||
|
||||
@@ -76,7 +76,7 @@ class Asset(Ulid):
|
||||
Parameters
|
||||
-----------
|
||||
fp: IOBase
|
||||
The file to write too.
|
||||
The file to write to
|
||||
"""
|
||||
fp.write(await self.read())
|
||||
|
||||
|
||||
+10
-4
@@ -53,7 +53,8 @@ class EditableChannel:
|
||||
|
||||
if kwargs.get("icon", Missing) == None:
|
||||
remove.append("Icon")
|
||||
elif kwargs.get("description", Missing) == None:
|
||||
|
||||
if kwargs.get("description", Missing) == None:
|
||||
remove.append("Description")
|
||||
|
||||
if icon := kwargs.get("icon"):
|
||||
@@ -133,7 +134,8 @@ class DMChannel(Channel, Messageable):
|
||||
|
||||
def __init__(self, data: DMChannelPayload, state: State):
|
||||
super().__init__(data, state)
|
||||
self.recipient_ids: tuple[str, str] = tuple(data["recipients"])
|
||||
|
||||
self.recipient_ids: list[str] = data["recipients"]
|
||||
self.last_message_id: str | None = data.get("last_message_id")
|
||||
|
||||
@property
|
||||
@@ -265,7 +267,7 @@ class ServerChannel(Channel):
|
||||
def __init__(self, data: ServerChannelPayload, state: State):
|
||||
super().__init__(data, state)
|
||||
|
||||
self.server_id: str = data["server"]
|
||||
self.server_id: Optional[str] = data["server"]
|
||||
self.name: str = data["name"]
|
||||
self.description: Optional[str] = data.get("description")
|
||||
self.nsfw: bool = data.get("nsfw", False)
|
||||
@@ -289,9 +291,10 @@ class ServerChannel(Channel):
|
||||
|
||||
async def set_default_permissions(self, permissions: PermissionsOverwrite) -> None:
|
||||
"""Sets the default permissions for the channel.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
permissions: :class:`ChannelPermissions`
|
||||
permissions: :class:`PermissionsOverwrite`
|
||||
The new default channel permissions
|
||||
"""
|
||||
allow, deny = permissions.to_pair()
|
||||
@@ -299,8 +302,11 @@ class ServerChannel(Channel):
|
||||
|
||||
async def set_role_permissions(self, role: Role, permissions: PermissionsOverwrite) -> None:
|
||||
"""Sets the permissions for a role in the channel.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
role: :class:`Role`
|
||||
The role to set permissions for
|
||||
permissions: :class:`ChannelPermissions`
|
||||
The new channel permissions
|
||||
"""
|
||||
|
||||
+185
-17
@@ -2,10 +2,12 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, Union, cast
|
||||
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Literal, Optional, TypeVar, Union, cast, overload
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .errors import RevoltError
|
||||
from .channel import (DMChannel, GroupDMChannel, SavedMessageChannel,
|
||||
TextChannel, VoiceChannel, channel_factory)
|
||||
from .http import HttpClient
|
||||
@@ -14,6 +16,9 @@ from .message import Message
|
||||
from .state import State
|
||||
from .utils import Missing, Ulid
|
||||
from .websocket import WebsocketHandler
|
||||
from .emoji import Emoji
|
||||
from .server import Server
|
||||
from .user import User
|
||||
|
||||
try:
|
||||
import ujson as json
|
||||
@@ -22,16 +27,18 @@ except ImportError:
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .channel import Channel
|
||||
from .emoji import Emoji
|
||||
from .file import File
|
||||
from .server import Server
|
||||
from .types import ApiInfo
|
||||
from .user import User
|
||||
|
||||
import revolt
|
||||
|
||||
__all__ = ("Client",)
|
||||
|
||||
logger: logging.Logger = logging.getLogger("revolt")
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
|
||||
class Client:
|
||||
"""The client for interacting with revolt
|
||||
|
||||
@@ -45,6 +52,8 @@ class Client:
|
||||
The api url for the revolt instance you are connecting to, by default it uses the offical instance hosted at revolt.chat
|
||||
max_messages: :class:`int`
|
||||
The max amount of messages stored in the cache, by default this is 5k
|
||||
bot: :class:`bool`
|
||||
Denotes whether the account used is a bot account or user account, by default this it assumes a bot account
|
||||
"""
|
||||
|
||||
def __init__(self, session: aiohttp.ClientSession, token: str, *, api_url: str = "https://api.revolt.chat", max_messages: int = 5000, bot: bool = True):
|
||||
@@ -59,7 +68,8 @@ class Client:
|
||||
self.state: State
|
||||
self.websocket: WebsocketHandler
|
||||
|
||||
self.listeners: dict[str, list[tuple[Callable[..., bool], asyncio.Future[Any]]]] = {}
|
||||
self.temp_listeners: dict[str, list[tuple[Callable[..., bool], asyncio.Future[Any]]]] = {}
|
||||
self.listeners: dict[str, list[Callable[..., Coroutine[Any, Any, Any]]]] = {}
|
||||
|
||||
super().__init__()
|
||||
|
||||
@@ -73,22 +83,33 @@ class Client:
|
||||
args: :class:`Any`
|
||||
The arguments passed to the event
|
||||
"""
|
||||
for check, future in self.listeners.pop(event, []):
|
||||
if check(*args):
|
||||
if len(args) == 1:
|
||||
future.set_result(args[0])
|
||||
else:
|
||||
future.set_result(args)
|
||||
|
||||
func = getattr(self, f"on_{event}", None)
|
||||
if func:
|
||||
if temp_listeners := self.temp_listeners.get(event, None):
|
||||
for check, future in temp_listeners:
|
||||
if check(*args):
|
||||
if len(args) == 1:
|
||||
future.set_result(args[0])
|
||||
else:
|
||||
future.set_result(args)
|
||||
|
||||
self.temp_listeners[event] = [(c, f) for c, f in temp_listeners if not f.done()]
|
||||
|
||||
for listener in self.listeners.get(event, []):
|
||||
asyncio.create_task(listener(*args))
|
||||
|
||||
if func := getattr(self, f"on_{event}", None):
|
||||
asyncio.create_task(func(*args))
|
||||
|
||||
async def get_api_info(self) -> ApiInfo:
|
||||
async with self.session.get(self.api_url) as resp:
|
||||
return json.loads(await resp.text())
|
||||
text = await resp.text()
|
||||
|
||||
async def start(self) -> None:
|
||||
try:
|
||||
return json.loads(text)
|
||||
except:
|
||||
raise RevoltError(f"Cant fetch api info:\n{text}")
|
||||
|
||||
async def start(self, *, reconnect: bool = True) -> None:
|
||||
"""Starts the client"""
|
||||
api_info = await self.get_api_info()
|
||||
|
||||
@@ -96,7 +117,8 @@ class Client:
|
||||
self.http = HttpClient(self.session, self.token, self.api_url, self.api_info, self.bot)
|
||||
self.state = State(self.http, api_info, self.max_messages)
|
||||
self.websocket = WebsocketHandler(self.session, self.token, api_info["ws"], self.dispatch, self.state)
|
||||
await self.websocket.start()
|
||||
|
||||
await self.websocket.start(reconnect)
|
||||
|
||||
async def stop(self) -> None:
|
||||
await self.websocket.websocket.close()
|
||||
@@ -172,10 +194,64 @@ class Client:
|
||||
check = lambda *_: True
|
||||
|
||||
future = asyncio.get_running_loop().create_future()
|
||||
self.listeners.setdefault(event, []).append((check, future))
|
||||
self.temp_listeners.setdefault(event, []).append((check, future))
|
||||
|
||||
return await asyncio.wait_for(future, timeout)
|
||||
|
||||
def listen(self, name: str | None = None) -> Callable[[Callable[P, Coroutine[Any, Any, R]]], Callable[P, Coroutine[Any, Any, R]]]:
|
||||
"""Registers a listener for an event, multiple listeners can be registered to the same event without conflict
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: Optional[:class:`str`]
|
||||
The name of the event to register this under, this defaults to the function's name
|
||||
"""
|
||||
def inner(func: Callable[P, Coroutine[Any, Any, R]]) -> Callable[P, Coroutine[Any, Any, R]]:
|
||||
nonlocal name
|
||||
|
||||
if not name:
|
||||
if not func.__name__.startswith("on_"):
|
||||
raise RevoltError("listener name must begin with `on_`")
|
||||
|
||||
name = func.__name__[3:]
|
||||
|
||||
self.listeners.setdefault(name, []).append(func)
|
||||
return func
|
||||
|
||||
return inner
|
||||
|
||||
@overload
|
||||
def remove_listener(self, func: Callable[P, Coroutine[Any, Any, R]], *, event: str = ...) -> Callable[..., Coroutine[Any, Any, R]] | None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def remove_listener(self, func: Callable[P, Coroutine[Any, Any, Any]], *, event: None = ...) -> None:
|
||||
...
|
||||
|
||||
def remove_listener(self, func: Callable[P, Coroutine[Any, Any, R]], *, event: str | None = None) -> Callable[..., Coroutine[Any, Any, R]] | None:
|
||||
"""Removes a listener registered, if the `event` parameter is passed, the listener will only be removed from that event, this can be used if the same listener is registed to multiple events at once.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
func: Callable
|
||||
The function for the listener to be removed
|
||||
event: Optional[:class:`str`]
|
||||
The name of the event to remove this from, passing `None` will make this remove the listener from all events this is registered under
|
||||
"""
|
||||
if event is None:
|
||||
for listeners in self.listeners.values():
|
||||
try:
|
||||
listeners.remove(func)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
else:
|
||||
try:
|
||||
self.listeners[event].remove(func)
|
||||
return func
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
@property
|
||||
def user(self) -> User:
|
||||
""":class:`User` the user corrasponding to the client"""
|
||||
@@ -397,3 +473,95 @@ class Client:
|
||||
ulid.id = asset["id"]
|
||||
|
||||
return ulid
|
||||
|
||||
# events
|
||||
|
||||
async def on_ready(self) -> None:
|
||||
pass
|
||||
|
||||
async def on_message(self, message: revolt.Message) -> None:
|
||||
pass
|
||||
|
||||
async def on_raw_message_update(self, payload: revolt.types.MessageUpdateEventPayload) -> None:
|
||||
pass
|
||||
|
||||
async def on_message_update(self, before: revolt.Message, after: revolt.Message) -> None:
|
||||
pass
|
||||
|
||||
async def on_raw_message_delete(self, payload: revolt.types.MessageDeleteEventPayload) -> None:
|
||||
pass
|
||||
|
||||
async def on_message_delete(self, message: revolt.Message) -> None:
|
||||
pass
|
||||
|
||||
async def on_channel_create(self, channel: revolt.Channel) -> None:
|
||||
pass
|
||||
|
||||
async def on_channel_update(self, before: revolt.Channel, after: revolt.Channel) -> None:
|
||||
pass
|
||||
|
||||
async def on_channel_delete(self, channel: revolt.Channel) -> None:
|
||||
pass
|
||||
|
||||
async def on_typing_start(self, channel: revolt.Channel, user: revolt.User) -> None:
|
||||
pass
|
||||
|
||||
async def on_typing_stop(self, channel: revolt.Channel, user: revolt.User) -> None:
|
||||
pass
|
||||
|
||||
async def on_server_update(self, before: revolt.Server, after: revolt.Server) -> None:
|
||||
pass
|
||||
|
||||
async def on_server_delete(self, server: revolt.Server) -> None:
|
||||
pass
|
||||
|
||||
async def on_server_join(self, server: revolt.Server) -> None:
|
||||
pass
|
||||
|
||||
async def on_member_update(self, before: revolt.Member, after: revolt.Member) -> None:
|
||||
pass
|
||||
|
||||
async def on_member_join(self, member: revolt.Member) -> None:
|
||||
pass
|
||||
|
||||
async def on_member_leave(self, member: revolt.Member) -> None:
|
||||
pass
|
||||
|
||||
async def on_role_create(self, role: revolt.Role) -> None:
|
||||
pass
|
||||
|
||||
async def on_role_update(self, before: revolt.Role, after: revolt.Role) -> None:
|
||||
pass
|
||||
|
||||
async def on_role_delete(self, role: revolt.Role) -> None:
|
||||
pass
|
||||
|
||||
async def on_user_update(self, before: revolt.User, after: revolt.User) -> None:
|
||||
pass
|
||||
|
||||
async def on_user_relationship_update(self, user: revolt.User, before: revolt.RelationshipType, after: revolt.RelationshipType) -> None:
|
||||
pass
|
||||
|
||||
async def on_raw_reaction_add(self, payload: revolt.types.MessageReactEventPayload) -> None:
|
||||
pass
|
||||
|
||||
async def on_reaction_add(self, message: revolt.Message, user: revolt.User, emoji_id: str) -> None:
|
||||
pass
|
||||
|
||||
async def on_raw_reaction_remove(self, payload: revolt.types.MessageUnreactEventPayload) -> None:
|
||||
pass
|
||||
|
||||
async def on_reaction_remove(self, message: revolt.Message, user: revolt.User, emoji_id: str) -> None:
|
||||
pass
|
||||
|
||||
async def on_raw_reaction_clear(self, payload: revolt.types.MessageRemoveReactionEventPayload) -> None:
|
||||
pass
|
||||
|
||||
async def on_reaction_clear(self, message: revolt.Message, user: revolt.User, emoji_id: str) -> None:
|
||||
pass
|
||||
|
||||
async def raw_bulk_message_delete(self, payload: revolt.types.BulkMessageDeleteEventPayload) -> None:
|
||||
pass
|
||||
|
||||
async def bulk_message_delete(self, messages: list[revolt.Message]) -> None:
|
||||
pass
|
||||
|
||||
@@ -4,6 +4,7 @@ from .cog import *
|
||||
from .command import *
|
||||
from .context import *
|
||||
from .converters import *
|
||||
from .cooldown import *
|
||||
from .errors import *
|
||||
from .group import *
|
||||
from .help import *
|
||||
|
||||
@@ -9,15 +9,15 @@ from .command import Command
|
||||
from .context import Context
|
||||
from .errors import (MissingPermissionsError, NotBotOwner, NotServerOwner,
|
||||
ServerOnly)
|
||||
from .utils import ClientT
|
||||
from .utils import ClientT_D
|
||||
|
||||
__all__ = ("check", "Check", "is_bot_owner", "is_server_owner", "has_permissions", "has_channel_permissions")
|
||||
|
||||
T = TypeVar("T", Callable[..., Any], Command, default=Command)
|
||||
|
||||
Check = Callable[[Context[ClientT]], Union[Any, Coroutine[Any, Any, Any]]]
|
||||
Check = Callable[[Context[ClientT_D]], Union[Any, Coroutine[Any, Any, Any]]]
|
||||
|
||||
def check(check: Check[ClientT]) -> Callable[[T], T]:
|
||||
def check(check: Check[ClientT_D]) -> Callable[[T], T]:
|
||||
"""A decorator for adding command checks
|
||||
|
||||
Parameters
|
||||
@@ -27,23 +27,27 @@ def check(check: Check[ClientT]) -> Callable[[T], T]:
|
||||
"""
|
||||
def inner(func: T) -> T:
|
||||
if isinstance(func, Command):
|
||||
command = cast(Command[ClientT], func) # cant verify generic at runtime so must cast
|
||||
command = cast(Command[ClientT_D], func) # cant verify generic at runtime so must cast
|
||||
command.checks.append(check)
|
||||
else:
|
||||
checks = getattr(func, "_checks", [])
|
||||
checks.append(check)
|
||||
func._checks = checks # type: ignore
|
||||
|
||||
return func
|
||||
return func # type: ignore
|
||||
|
||||
return inner
|
||||
|
||||
def is_bot_owner() -> Callable[[T], T]:
|
||||
"""A command check for limiting the command to only the bot's owner"""
|
||||
@check
|
||||
def inner(context: Context[ClientT]):
|
||||
if context.author.id == context.client.user.owner_id:
|
||||
return True
|
||||
def inner(context: Context[ClientT_D]):
|
||||
if user_id := context.client.user.owner_id:
|
||||
if context.author.id == user_id:
|
||||
return True
|
||||
else:
|
||||
if context.author.id == context.client.user.id:
|
||||
return True
|
||||
|
||||
raise NotBotOwner
|
||||
|
||||
@@ -52,7 +56,7 @@ def is_bot_owner() -> Callable[[T], T]:
|
||||
def is_server_owner() -> Callable[[T], T]:
|
||||
"""A command check for limiting the command to only a server's owner"""
|
||||
@check
|
||||
def inner(context: Context[ClientT]) -> bool:
|
||||
def inner(context: Context[ClientT_D]) -> bool:
|
||||
if not context.server_id:
|
||||
raise ServerOnly
|
||||
|
||||
@@ -65,7 +69,7 @@ def is_server_owner() -> Callable[[T], T]:
|
||||
|
||||
def has_permissions(**permissions: bool) -> Callable[[T], T]:
|
||||
@check
|
||||
def inner(context: Context[ClientT]) -> bool:
|
||||
def inner(context: Context[ClientT_D]) -> bool:
|
||||
author = context.author
|
||||
|
||||
if not author.has_permissions(**permissions):
|
||||
@@ -77,7 +81,7 @@ def has_permissions(**permissions: bool) -> Callable[[T], T]:
|
||||
|
||||
def has_channel_permissions(**permissions: bool) -> Callable[[T], T]:
|
||||
@check
|
||||
def inner(context: Context[ClientT]) -> bool:
|
||||
def inner(context: Context[ClientT_D]) -> bool:
|
||||
author = context.author
|
||||
|
||||
if not isinstance(author, revolt.Member):
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import sys
|
||||
import traceback
|
||||
from importlib import import_module
|
||||
from typing import (TYPE_CHECKING, Any, Optional, Protocol, TypeVar, Union,
|
||||
from typing import (TYPE_CHECKING, Any, Coroutine, Optional, Protocol, TypeVar, Union,
|
||||
overload, runtime_checkable)
|
||||
|
||||
from typing_extensions import Self
|
||||
@@ -13,6 +13,8 @@ import revolt
|
||||
if TYPE_CHECKING:
|
||||
from .help import HelpCommand
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .cog import Cog
|
||||
from .command import Command
|
||||
from .context import Context
|
||||
@@ -36,13 +38,14 @@ class ExtensionProtocol(Protocol):
|
||||
class CommandsMeta(type):
|
||||
_commands: list[Command[Any]]
|
||||
|
||||
def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> Self:
|
||||
def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> Any:
|
||||
commands: list[Command[Any]] = []
|
||||
self = super().__new__(cls, name, bases, attrs)
|
||||
|
||||
for base in reversed(self.__mro__):
|
||||
for value in base.__dict__.values():
|
||||
if isinstance(value, Command):
|
||||
commands.append(value)
|
||||
if isinstance(value, Command) and value.parent is None: # type: ignore
|
||||
commands.append(value) # type: ignore
|
||||
|
||||
self._commands = commands
|
||||
|
||||
@@ -78,14 +81,42 @@ class CaseInsensitiveDict(dict[str, V]):
|
||||
|
||||
|
||||
class CommandsClient(revolt.Client, metaclass=CommandsMeta):
|
||||
"""Main class that adds commands, this class should be subclassed along with `revolt.Client`."""
|
||||
"""A subclass of :class:`~revolt.Client` which has support for commands.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
session: :class:`~aiohttp.ClientSession`
|
||||
The aiohttp session to use for http request and the websocket
|
||||
token: :class:`str`
|
||||
The bots token
|
||||
api_url: :class:`str`
|
||||
The api url for the revolt instance you are connecting to, by default it uses the offical instance hosted at revolt.chat
|
||||
max_messages: :class:`int`
|
||||
The max amount of messages stored in the cache, by default this is 5k
|
||||
bot: :class:`bool`
|
||||
Denotes whether the account used is a bot account or user account, by default this it assumes a bot account
|
||||
help_command: Optional[:class:`~revolt.ext.commands.HelpCommand`]
|
||||
Sets the custom help command, or remove it if passed ``None``
|
||||
case_insensitive: :class:`bool`
|
||||
Whether or not commands should be case insensitive
|
||||
"""
|
||||
|
||||
_commands: list[Command[Self]]
|
||||
|
||||
def __init__(self, *args: Any, help_command: Union[HelpCommand[Self], None, revolt.utils._Missing] = revolt.utils.Missing, case_insensitive: bool = False, **kwargs: Any):
|
||||
def __init__(
|
||||
self,
|
||||
session: aiohttp.ClientSession,
|
||||
token: str,
|
||||
*,
|
||||
api_url: str = "https://api.revolt.chat",
|
||||
max_messages: int = 5000,
|
||||
bot: bool = True,
|
||||
help_command: Union[HelpCommand[Self], None, revolt.utils._Missing] = revolt.utils.Missing,
|
||||
case_insensitive: bool = False
|
||||
):
|
||||
from .help import DefaultHelpCommand, HelpCommandImpl
|
||||
|
||||
self.all_commands: dict[str, Command[Self]] = {} if not case_insensitive else CaseInsensitiveDict()
|
||||
self.all_commands: dict[str, Command[Self]] | CaseInsensitiveDict[Command[Self]] = {} if not case_insensitive else CaseInsensitiveDict()
|
||||
self.cogs: dict[str, Cog[Self]] = {}
|
||||
self.extensions: dict[str, ExtensionProtocol] = {}
|
||||
|
||||
@@ -103,10 +134,17 @@ class CommandsClient(revolt.Client, metaclass=CommandsMeta):
|
||||
else:
|
||||
self.help_command = None
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
super().__init__(session, token, api_url=api_url, max_messages=max_messages, bot=bot)
|
||||
|
||||
@property
|
||||
def commands(self) -> list[Command[Self]]:
|
||||
"""Gets all commands registered
|
||||
|
||||
Returns
|
||||
--------
|
||||
list[:class:`Command`]
|
||||
The registered commands
|
||||
"""
|
||||
return list(set(self.all_commands.values()))
|
||||
|
||||
async def get_prefix(self, message: revolt.Message) -> Union[str, list[str]]:
|
||||
@@ -176,9 +214,23 @@ class CommandsClient(revolt.Client, metaclass=CommandsMeta):
|
||||
return command
|
||||
|
||||
def get_view(self, message: revolt.Message) -> type[StringView]:
|
||||
"""Returns the StringView class to use, this can be overwritten to customize how arguments are parsed
|
||||
|
||||
Returns
|
||||
--------
|
||||
type[:class:`StringView`]
|
||||
The string view class to use
|
||||
"""
|
||||
return StringView
|
||||
|
||||
def get_context(self, message: revolt.Message) -> type[Context[Self]]:
|
||||
"""Returns the Context class to use, this can be overwritten to add extra features to context
|
||||
|
||||
Returns
|
||||
--------
|
||||
type[:class:`Context`]
|
||||
The context class to use
|
||||
"""
|
||||
return Context[Self]
|
||||
|
||||
async def process_commands(self, message: revolt.Message) -> Any:
|
||||
@@ -231,7 +283,7 @@ class CommandsClient(revolt.Client, metaclass=CommandsMeta):
|
||||
try:
|
||||
self.dispatch("command", context)
|
||||
|
||||
if not await self.bot_check(context):
|
||||
if not await self.global_check(context):
|
||||
raise CheckError(f"the global check for the command failed")
|
||||
|
||||
if not await context.can_run():
|
||||
@@ -248,10 +300,11 @@ class CommandsClient(revolt.Client, metaclass=CommandsMeta):
|
||||
async def on_command_error(self, ctx: Context[Self], error: Exception, /) -> None:
|
||||
traceback.print_exception(type(error), error, error.__traceback__)
|
||||
|
||||
on_message = process_commands
|
||||
def on_message(self, message: revolt.Message) -> Coroutine[Any, Any, Any]:
|
||||
return self.process_commands(message)
|
||||
|
||||
async def bot_check(self, context: Context[Self]) -> bool:
|
||||
"""A global check for the bot that stops commands from running on certain criteria.
|
||||
async def global_check(self, context: Context[Self]) -> bool:
|
||||
"""A global check that stops commands from running on certain criteria.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
@@ -266,7 +319,7 @@ class CommandsClient(revolt.Client, metaclass=CommandsMeta):
|
||||
return True
|
||||
|
||||
def add_cog(self, cog: Cog[Self]) -> None:
|
||||
"""Adds a cog to the bot, this cog must subclass `Cog`.
|
||||
"""Adds a cog, this cog must subclass `Cog`.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
@@ -276,7 +329,7 @@ class CommandsClient(revolt.Client, metaclass=CommandsMeta):
|
||||
cog._inject(self)
|
||||
|
||||
def remove_cog(self, cog_name: str) -> Cog[Self]:
|
||||
"""Removes a cog from the bot.
|
||||
"""Removes a cog.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
@@ -336,7 +389,7 @@ class CommandsClient(revolt.Client, metaclass=CommandsMeta):
|
||||
self.load_extension(name)
|
||||
|
||||
def get_cog(self, name: str) -> Cog[Self]:
|
||||
"""Gets a cog from the bot.
|
||||
"""Gets a cog.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
@@ -351,7 +404,7 @@ class CommandsClient(revolt.Client, metaclass=CommandsMeta):
|
||||
return self.cogs[name]
|
||||
|
||||
def get_extension(self, name: str) -> ExtensionProtocol:
|
||||
"""Gets an extension from the bot.
|
||||
"""Gets an extension.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
|
||||
+70
-21
@@ -1,33 +1,49 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Generic, Optional, cast
|
||||
from typing_extensions import Self
|
||||
from typing import Any, Callable, Coroutine, Generic, Optional, TypeVar
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
from revolt.errors import RevoltError
|
||||
|
||||
from .command import Command
|
||||
from .utils import ClientT
|
||||
from .utils import ClientT_D
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
|
||||
__all__ = ("Cog", "CogMeta")
|
||||
|
||||
class CogMeta(type, Generic[ClientT]):
|
||||
_commands: list[Command[ClientT]]
|
||||
class CogMeta(type):
|
||||
_cog_commands: list[Command[Any]]
|
||||
_cog_listeners: dict[str, list[str]]
|
||||
qualified_name: str
|
||||
|
||||
def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any], *, qualified_name: Optional[str] = None) -> Self:
|
||||
commands: list[Command[ClientT]] = []
|
||||
def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any], *, qualified_name: Optional[str] = None, extras: dict[str, Any] | None = None) -> Any:
|
||||
commands: list[Command[Any]] = []
|
||||
listeners: dict[str, list[str]] = {}
|
||||
|
||||
self = super().__new__(cls, name, bases, attrs)
|
||||
extras = extras or {}
|
||||
|
||||
for base in reversed(self.__mro__):
|
||||
for value in base.__dict__.values():
|
||||
for key, value in base.__dict__.items():
|
||||
if isinstance(value, Command):
|
||||
commands.append(cast(Command[ClientT], value)) # cant verify generic at runtime so must cast
|
||||
for extra_key, extra_value in extras.items():
|
||||
setattr(value, extra_key, extra_value) # type: ignore
|
||||
|
||||
commands.append(value) # type: ignore
|
||||
|
||||
self._commands = commands
|
||||
elif event_name := getattr(value, "__listener_name", None):
|
||||
listeners.setdefault(event_name, []).append(key)
|
||||
|
||||
self._cog_commands = commands
|
||||
self._cog_listeners = listeners
|
||||
self.qualified_name = qualified_name or name
|
||||
return self
|
||||
|
||||
class Cog(Generic[ClientT], metaclass=CogMeta):
|
||||
_commands: list[Command[ClientT]]
|
||||
class Cog(Generic[ClientT_D], metaclass=CogMeta):
|
||||
_cog_commands: list[Command[ClientT_D]]
|
||||
_cog_listeners: dict[str, list[str]]
|
||||
qualified_name: str
|
||||
|
||||
def cog_load(self) -> None:
|
||||
@@ -38,22 +54,55 @@ class Cog(Generic[ClientT], metaclass=CogMeta):
|
||||
"""A special method that is called when the cog gets removed."""
|
||||
pass
|
||||
|
||||
def _inject(self, client: ClientT) -> None:
|
||||
def _inject(self, client: ClientT_D) -> None:
|
||||
client.cogs[self.qualified_name] = self
|
||||
|
||||
for command in self._commands:
|
||||
command.cog = self
|
||||
client.add_command(command)
|
||||
try:
|
||||
for command in self._cog_commands:
|
||||
command.cog = self
|
||||
|
||||
if command.parent is None:
|
||||
client.add_command(command)
|
||||
|
||||
for key, listeners in self._cog_listeners.items():
|
||||
for listener_name in listeners:
|
||||
client.listeners.setdefault(key, []).append(getattr(self, listener_name))
|
||||
|
||||
except Exception as e:
|
||||
self._uninject(client)
|
||||
|
||||
raise e
|
||||
|
||||
self.cog_load()
|
||||
|
||||
def _uninject(self, client: ClientT) -> None:
|
||||
def _uninject(self, client: ClientT_D) -> None:
|
||||
for name, command in client.all_commands.copy().items():
|
||||
if command in self._commands:
|
||||
del client.all_commands[name]
|
||||
if command in self._cog_commands:
|
||||
try:
|
||||
del client.all_commands[name]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
for key, listeners in self._cog_listeners.items():
|
||||
for listener_name in listeners:
|
||||
try:
|
||||
client.listeners[key].remove(getattr(self, listener_name))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self.cog_unload()
|
||||
|
||||
@property
|
||||
def commands(self) -> list[Command[ClientT]]:
|
||||
return self._commands
|
||||
def commands(self) -> list[Command[ClientT_D]]:
|
||||
return self._cog_commands
|
||||
|
||||
@staticmethod
|
||||
def listen(name: str | None = None) -> Callable[[Callable[P, Coroutine[Any, Any, R]]], Callable[P, Coroutine[Any, Any, R]]]:
|
||||
def inner(func: Callable[P, Coroutine[Any, Any, R]]) -> Callable[P, Coroutine[Any, Any, R]]:
|
||||
if not func.__name__.startswith("on_"):
|
||||
raise RevoltError("event name must start with `on_`")
|
||||
|
||||
setattr(func, "__listener_name", name or func.__name__[3:])
|
||||
return func
|
||||
|
||||
return inner
|
||||
|
||||
@@ -6,11 +6,20 @@ from contextlib import suppress
|
||||
from typing import (TYPE_CHECKING, Annotated, Any, Callable, Coroutine,
|
||||
Generic, Literal, Optional, Union, get_args, get_origin)
|
||||
from typing_extensions import ParamSpec
|
||||
import sys
|
||||
|
||||
from revolt.utils import copy_doc, maybe_coroutine
|
||||
if sys.version_info >= (3, 10):
|
||||
from types import UnionType
|
||||
|
||||
from .errors import InvalidLiteralArgument, UnionConverterError
|
||||
from .utils import ClientCoT, evaluate_parameters
|
||||
UnionTypes: tuple[Any, ...] = (Union, UnionType)
|
||||
else:
|
||||
UnionTypes = (Union,)
|
||||
|
||||
from ...utils import maybe_coroutine
|
||||
|
||||
from .errors import CommandOnCooldown, InvalidLiteralArgument, UnionConverterError
|
||||
from .utils import ClientT_Co_D, evaluate_parameters, ClientT_Co
|
||||
from .cooldown import BucketType, CooldownMapping
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .checks import Check
|
||||
@@ -26,7 +35,7 @@ __all__: tuple[str, ...] = (
|
||||
NoneType: type[None] = type(None)
|
||||
P = ParamSpec("P")
|
||||
|
||||
class Command(Generic[ClientCoT]):
|
||||
class Command(Generic[ClientT_Co_D]):
|
||||
"""Class for holding info about a command.
|
||||
|
||||
Parameters
|
||||
@@ -43,23 +52,46 @@ class Command(Generic[ClientCoT]):
|
||||
The cog the command is apart of.
|
||||
usage: Optional[:class:`str`]
|
||||
The usage string for the command
|
||||
checks: Optional[list[Callable]]
|
||||
The list of checks the command has
|
||||
cooldown: Optional[:class:`Cooldown`]
|
||||
The cooldown for the command to restrict how often the command can be used
|
||||
description: Optional[:class:`str`]
|
||||
The commands description if it has one
|
||||
hidden: :class:`bool`
|
||||
Whether or not the command should be hidden from the help command
|
||||
"""
|
||||
__slots__ = ("callback", "name", "aliases", "signature", "checks", "parent", "_error_handler", "cog", "description", "usage", "parameters")
|
||||
__slots__ = ("callback", "name", "aliases", "signature", "checks", "parent", "_error_handler", "cog", "description", "usage", "parameters", "hidden", "cooldown", "cooldown_bucket")
|
||||
|
||||
def __init__(self, callback: Callable[..., Coroutine[Any, Any, Any]], name: str, aliases: list[str], usage: Optional[str] = None):
|
||||
def __init__(
|
||||
self,
|
||||
callback: Callable[..., Coroutine[Any, Any, Any]],
|
||||
name: str,
|
||||
*,
|
||||
aliases: list[str] | None = None,
|
||||
usage: Optional[str] = None,
|
||||
checks: list[Check[ClientT_Co_D]] | None = None,
|
||||
cooldown: Optional[CooldownMapping] | None = None,
|
||||
bucket: Optional[BucketType | Callable[[Context[ClientT_Co_D]], Coroutine[Any, Any, str]]] = None,
|
||||
description: str | None = None,
|
||||
hidden: bool = False,
|
||||
):
|
||||
self.callback: Callable[..., Coroutine[Any, Any, Any]] = callback
|
||||
self.name: str = name
|
||||
self.aliases: list[str] = aliases
|
||||
self.aliases: list[str] = aliases or []
|
||||
self.usage: str | None = usage
|
||||
self.signature: inspect.Signature = inspect.signature(self.callback)
|
||||
self.parameters: list[inspect.Parameter] = evaluate_parameters(self.signature.parameters.values(), getattr(callback, "__globals__", {}))
|
||||
self.checks: list[Check[ClientCoT]] = getattr(callback, "_checks", [])
|
||||
self.parent: Optional[Group[ClientCoT]] = None
|
||||
self.cog: Optional[Cog[ClientCoT]] = None
|
||||
self._error_handler: Callable[[Any, Context[ClientCoT], Exception], Coroutine[Any, Any, Any]] = type(self)._default_error_handler
|
||||
self.description: str | None = callback.__doc__
|
||||
self.checks: list[Check[ClientT_Co_D]] = checks or getattr(callback, "_checks", [])
|
||||
self.cooldown: CooldownMapping | None = cooldown or getattr(callback, "_cooldown", None)
|
||||
self.cooldown_bucket: BucketType | Callable[[Context[ClientT_Co_D]], Coroutine[Any, Any, str]] = bucket or getattr(callback, "_bucket", BucketType.default)
|
||||
self.parent: Optional[Group[ClientT_Co_D]] = None
|
||||
self.cog: Optional[Cog[ClientT_Co_D]] = None
|
||||
self._error_handler: Callable[[Any, Context[ClientT_Co_D], Exception], Coroutine[Any, Any, Any]] = type(self)._default_error_handler
|
||||
self.description: str | None = description or callback.__doc__
|
||||
self.hidden: bool = hidden
|
||||
|
||||
async def invoke(self, context: Context[ClientCoT], *args: Any, **kwargs: Any) -> Any:
|
||||
async def invoke(self, context: Context[ClientT_Co_D], *args: Any, **kwargs: Any) -> Any:
|
||||
"""Runs the command and calls the error handler if the command errors.
|
||||
|
||||
Parameters
|
||||
@@ -74,8 +106,7 @@ class Command(Generic[ClientCoT]):
|
||||
except Exception as err:
|
||||
return await self._error_handler(self.cog or context.client, context, err)
|
||||
|
||||
@copy_doc(invoke)
|
||||
def __call__(self, context: Context[ClientCoT], *args: Any, **kwargs: Any) -> Any:
|
||||
def __call__(self, context: Context[ClientT_Co_D], *args: Any, **kwargs: Any) -> Any:
|
||||
return self.invoke(context, *args, **kwargs)
|
||||
|
||||
def error(self, func: Callable[..., Coroutine[Any, Any, Any]]) -> Callable[..., Coroutine[Any, Any, Any]]:
|
||||
@@ -98,12 +129,12 @@ class Command(Generic[ClientCoT]):
|
||||
self._error_handler = func
|
||||
return func
|
||||
|
||||
async def _default_error_handler(self, ctx: Context[ClientCoT], error: Exception):
|
||||
async def _default_error_handler(self, ctx: Context[ClientT_Co_D], error: Exception):
|
||||
traceback.print_exception(type(error), error, error.__traceback__)
|
||||
|
||||
@classmethod
|
||||
async def handle_origin(cls, context: Context[ClientCoT], origin: Any, annotation: Any, arg: str) -> Any:
|
||||
if origin is Union:
|
||||
async def handle_origin(cls, context: Context[ClientT_Co_D], origin: Any, annotation: Any, arg: str) -> Any:
|
||||
if origin in UnionTypes:
|
||||
for converter in get_args(annotation):
|
||||
try:
|
||||
return await cls.convert_argument(arg, converter, context)
|
||||
@@ -117,8 +148,20 @@ class Command(Generic[ClientCoT]):
|
||||
elif origin is Annotated:
|
||||
annotated_args = get_args(annotation)
|
||||
|
||||
if origin := get_origin(annotated_args[0]):
|
||||
return await cls.handle_origin(context, origin, annotated_args[1], arg)
|
||||
if annotated_args[1] == "_revolt_greedy_marker":
|
||||
real_annotation = get_args(annotated_args[0])[0]
|
||||
converted_args: list[Any] = []
|
||||
|
||||
converted_args.append(await cls.convert_argument(arg, real_annotation, context))
|
||||
|
||||
for arg in context.view:
|
||||
try:
|
||||
converted_args.append(await cls.convert_argument(arg, real_annotation, context))
|
||||
except:
|
||||
context.view.undo()
|
||||
break
|
||||
|
||||
return converted_args
|
||||
else:
|
||||
return await cls.convert_argument(arg, annotated_args[1], context)
|
||||
|
||||
@@ -129,7 +172,7 @@ class Command(Generic[ClientCoT]):
|
||||
raise InvalidLiteralArgument(arg)
|
||||
|
||||
@classmethod
|
||||
async def convert_argument(cls, arg: str, annotation: Any, context: Context[ClientCoT]) -> Any:
|
||||
async def convert_argument(cls, arg: str, annotation: Any, context: Context[ClientT_Co_D]) -> Any:
|
||||
if annotation is not inspect.Signature.empty:
|
||||
if annotation is str: # no converting is needed - its already a string
|
||||
return arg
|
||||
@@ -142,7 +185,7 @@ class Command(Generic[ClientCoT]):
|
||||
else:
|
||||
return arg
|
||||
|
||||
async def parse_arguments(self, context: Context[ClientCoT]) -> None:
|
||||
async def parse_arguments(self, context: Context[ClientT_Co_D]) -> None:
|
||||
# please pr if you can think of a better way to do this
|
||||
|
||||
for parameter in self.parameters[2:]:
|
||||
@@ -152,6 +195,10 @@ class Command(Generic[ClientCoT]):
|
||||
except StopIteration:
|
||||
if parameter.default is not parameter.empty:
|
||||
arg = parameter.default
|
||||
|
||||
elif is_optional(parameter.annotation):
|
||||
arg = None
|
||||
|
||||
else:
|
||||
raise
|
||||
|
||||
@@ -169,11 +216,27 @@ class Command(Generic[ClientCoT]):
|
||||
except StopIteration:
|
||||
if parameter.default is not parameter.empty:
|
||||
arg = parameter.default
|
||||
|
||||
elif is_optional(parameter.annotation):
|
||||
arg = None
|
||||
|
||||
else:
|
||||
raise
|
||||
|
||||
context.args.append(arg)
|
||||
|
||||
async def run_cooldown(self, context: Context[ClientT_Co_D]) -> None:
|
||||
if mapping := self.cooldown:
|
||||
if isinstance(self.cooldown_bucket, BucketType):
|
||||
key = self.cooldown_bucket.resolve(context)
|
||||
else:
|
||||
key = await self.cooldown_bucket(context)
|
||||
|
||||
cooldown = mapping.get_bucket(key)
|
||||
|
||||
if retry_after := cooldown.update_cooldown():
|
||||
raise CommandOnCooldown(retry_after)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__} name=\"{self.name}\">"
|
||||
|
||||
@@ -215,7 +278,16 @@ class Command(Generic[ClientCoT]):
|
||||
|
||||
return f"{' '.join(parents[::-1])} {self.name} {' '.join(parameters)}"
|
||||
|
||||
def command(*, name: Optional[str] = None, aliases: Optional[list[str]] = None, cls: type[Command[ClientCoT]] = Command, usage: Optional[str] = None) -> Callable[[Callable[..., Coroutine[Any, Any, Any]]], Command[ClientCoT]]:
|
||||
def is_optional(arg: Any) -> bool:
|
||||
return get_origin(arg) in UnionTypes and any(arg is NoneType for arg in get_args(arg))
|
||||
|
||||
def command(
|
||||
*,
|
||||
name: Optional[str] = None,
|
||||
aliases: Optional[list[str]] = None,
|
||||
cls: type[Command[ClientT_Co]] = Command,
|
||||
usage: Optional[str] = None
|
||||
) -> Callable[[Callable[..., Coroutine[Any, Any, Any]]], Command[ClientT_Co]]:
|
||||
"""A decorator that turns a function into a :class:`Command`.n
|
||||
|
||||
Parameters
|
||||
@@ -226,13 +298,15 @@ def command(*, name: Optional[str] = None, aliases: Optional[list[str]] = None,
|
||||
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
|
||||
usage: Optional[:class:`str`]
|
||||
The signature for how the command should be called
|
||||
|
||||
Returns
|
||||
--------
|
||||
Callable[Callable[..., Coroutine], :class:`Command`]
|
||||
A function that takes the command callback and returns a :class:`Command`
|
||||
"""
|
||||
def inner(func: Callable[..., Coroutine[Any, Any, Any]]):
|
||||
return cls(func, name or func.__name__, aliases or [], usage)
|
||||
def inner(func: Callable[..., Coroutine[Any, Any, Any]]) -> Command[ClientT_Co]:
|
||||
return cls(func, name or func.__name__, aliases=aliases or [], usage=usage)
|
||||
|
||||
return inner
|
||||
|
||||
@@ -7,7 +7,7 @@ from revolt.utils import maybe_coroutine
|
||||
|
||||
from .command import Command
|
||||
from .group import Group
|
||||
from .utils import ClientCoT
|
||||
from .utils import ClientT_Co_D
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .view import StringView
|
||||
@@ -17,7 +17,7 @@ __all__ = (
|
||||
"Context",
|
||||
)
|
||||
|
||||
class Context(revolt.Messageable, Generic[ClientCoT]):
|
||||
class Context(revolt.Messageable, Generic[ClientT_Co_D]):
|
||||
"""Stores metadata the commands execution.
|
||||
|
||||
Attributes
|
||||
@@ -46,27 +46,27 @@ class Context(revolt.Messageable, Generic[ClientCoT]):
|
||||
async def _get_channel_id(self) -> str:
|
||||
return self.channel.id
|
||||
|
||||
def __init__(self, command: Optional[Command[ClientCoT]], invoked_with: str, view: StringView, message: revolt.Message, client: ClientCoT):
|
||||
self.command: Command[ClientCoT] | None = command
|
||||
def __init__(self, command: Optional[Command[ClientT_Co_D]], invoked_with: str, view: StringView, message: revolt.Message, client: ClientT_Co_D):
|
||||
self.command: Command[ClientT_Co_D] | None = command
|
||||
self.invoked_with: str = invoked_with
|
||||
self.view: StringView = view
|
||||
self.message: revolt.Message = message
|
||||
self.client: ClientCoT = client
|
||||
self.client: ClientT_Co_D = client
|
||||
self.args: list[Any] = []
|
||||
self.kwargs: dict[str, Any] = {}
|
||||
self.server_id: str | None = message.server_id
|
||||
self.channel: revolt.TextChannel | revolt.GroupDMChannel | revolt.DMChannel = message.channel
|
||||
self.channel: revolt.TextChannel | revolt.GroupDMChannel | revolt.DMChannel | revolt.SavedMessageChannel = message.channel
|
||||
self.author: revolt.Member | revolt.User = message.author
|
||||
self.state: State = message.state
|
||||
|
||||
@property
|
||||
def server(self) -> revolt.Server:
|
||||
""":class:`Server` The server this voice channel belongs too
|
||||
""":class:`Server` The server this context belongs too
|
||||
|
||||
Raises
|
||||
-------
|
||||
:class:`LookupError`
|
||||
Raises if the channel is not part of a server
|
||||
Raises if the context is not from a server
|
||||
"""
|
||||
if not self.server_id:
|
||||
raise LookupError
|
||||
@@ -97,16 +97,17 @@ class Context(revolt.Messageable, Generic[ClientCoT]):
|
||||
|
||||
self.view.undo()
|
||||
|
||||
await command.run_cooldown(self)
|
||||
await command.parse_arguments(self)
|
||||
return await command.invoke(self, *self.args, **self.kwargs)
|
||||
|
||||
async def can_run(self, command: Optional[Command[ClientCoT]] = None) -> bool:
|
||||
async def can_run(self, command: Optional[Command[ClientT_Co_D]] = None) -> bool:
|
||||
"""Runs all of the commands checks, and returns true if all of them pass"""
|
||||
command = command or self.command
|
||||
|
||||
return all([await maybe_coroutine(check, self) for check in (command.checks if command else [])])
|
||||
|
||||
async def send_help(self, argument: Command[Any] | Group[Any] | ClientCoT | None = None) -> None:
|
||||
async def send_help(self, argument: Command[Any] | Group[Any] | ClientT_Co_D | None = None) -> None:
|
||||
argument = argument or self.client
|
||||
|
||||
command = self.client.get_command("help")
|
||||
|
||||
@@ -13,7 +13,9 @@ from .errors import (BadBoolArgument, CategoryConverterError,
|
||||
if TYPE_CHECKING:
|
||||
from .client import CommandsClient
|
||||
|
||||
__all__: tuple[str, ...] = ("bool_converter", "category_converter", "channel_converter", "user_converter", "member_converter", "IntConverter", "BoolConverter", "CategoryConverter", "UserConverter", "MemberConverter", "ChannelConverter")
|
||||
T = TypeVar("T")
|
||||
|
||||
__all__: tuple[str, ...] = ("bool_converter", "category_converter", "channel_converter", "user_converter", "member_converter", "IntConverter", "BoolConverter", "CategoryConverter", "UserConverter", "MemberConverter", "ChannelConverter", "Greedy")
|
||||
|
||||
channel_regex: re.Pattern[str] = re.compile("<#([A-z0-9]{26})>")
|
||||
user_regex: re.Pattern[str] = re.compile("<@([A-z0-9]{26})>")
|
||||
@@ -35,7 +37,7 @@ def category_converter(arg: str, context: Context[ClientT]) -> Category:
|
||||
|
||||
try:
|
||||
return context.server.get_category(arg)
|
||||
except KeyError:
|
||||
except LookupError:
|
||||
try:
|
||||
return utils.get(context.server.categories, name=arg)
|
||||
except LookupError:
|
||||
@@ -50,7 +52,7 @@ def channel_converter(arg: str, context: Context[ClientT]) -> Channel:
|
||||
|
||||
try:
|
||||
return context.server.get_channel(arg)
|
||||
except KeyError:
|
||||
except LookupError:
|
||||
try:
|
||||
return utils.get(context.server.channels, name=arg)
|
||||
except LookupError:
|
||||
@@ -62,9 +64,23 @@ def user_converter(arg: str, context: Context[ClientT]) -> User:
|
||||
|
||||
try:
|
||||
return context.client.get_user(arg)
|
||||
except KeyError:
|
||||
except LookupError:
|
||||
try:
|
||||
return utils.get(context.client.users, name=arg)
|
||||
parts = arg.split("#")
|
||||
|
||||
if len(parts) == 1:
|
||||
return (
|
||||
utils.get(context.client.users, original_name=arg)
|
||||
or utils.get(context.client.users, display_name=arg)
|
||||
)
|
||||
elif len(parts) == 2:
|
||||
return (
|
||||
utils.get(context.client.users, original_name=parts[0], discriminator=parts[1])
|
||||
or utils.get(context.client.users, display_name=parts[0], discriminator=parts[1])
|
||||
)
|
||||
else:
|
||||
raise LookupError
|
||||
|
||||
except LookupError:
|
||||
raise UserConverterError(arg)
|
||||
|
||||
@@ -77,9 +93,23 @@ def member_converter(arg: str, context: Context[ClientT]) -> Member:
|
||||
|
||||
try:
|
||||
return context.server.get_member(arg)
|
||||
except KeyError:
|
||||
except LookupError:
|
||||
try:
|
||||
return utils.get(context.server.members, name=arg)
|
||||
parts = arg.split("#")
|
||||
|
||||
if len(parts) == 1:
|
||||
return (
|
||||
utils.get(context.server.members, original_name=arg)
|
||||
or utils.get(context.server.members, display_name=arg)
|
||||
)
|
||||
elif len(parts) == 2:
|
||||
return (
|
||||
utils.get(context.server.members, original_name=parts[0], discriminator=parts[1])
|
||||
or utils.get(context.server.members, display_name=parts[0], discriminator=parts[1])
|
||||
)
|
||||
else:
|
||||
raise LookupError
|
||||
|
||||
except LookupError:
|
||||
raise MemberConverterError(arg)
|
||||
|
||||
@@ -92,3 +122,5 @@ CategoryConverter = Annotated[Category, category_converter]
|
||||
UserConverter = Annotated[User, user_converter]
|
||||
MemberConverter = Annotated[Member, member_converter]
|
||||
ChannelConverter = Annotated[Channel, channel_converter]
|
||||
|
||||
Greedy = Annotated[list[T], "_revolt_greedy_marker"]
|
||||
@@ -0,0 +1,144 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, Callable, Coroutine, TypeVar, cast
|
||||
|
||||
from .errors import ServerOnly
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from enum import Enum
|
||||
|
||||
from .context import Context
|
||||
from .utils import ClientT_Co_D, ClientT_Co
|
||||
else:
|
||||
from aenum import Enum
|
||||
|
||||
__all__ = ("Cooldown", "CooldownMapping", "BucketType", "cooldown")
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class Cooldown:
|
||||
"""Represent a single cooldown for a single key
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
rate: :class:`int`
|
||||
How many times it can be used
|
||||
per: :class:`int`
|
||||
How long the window is before the ratelimit resets
|
||||
"""
|
||||
|
||||
def __init__(self, rate: int, per: int):
|
||||
self.rate: int = rate
|
||||
self.per: int = per
|
||||
self.window: float = 0.0
|
||||
self.tokens: int = rate
|
||||
self.last: float = 0.0
|
||||
|
||||
def get_tokens(self, current: float | None) -> int:
|
||||
current = current or time.time()
|
||||
|
||||
if current > (self.window + self.per):
|
||||
return self.rate
|
||||
else:
|
||||
return self.tokens
|
||||
|
||||
def update_cooldown(self) -> float | None:
|
||||
current = time.time()
|
||||
|
||||
self.last = current
|
||||
|
||||
self.tokens = self.get_tokens(current)
|
||||
|
||||
if self.tokens == 0:
|
||||
return self.per - (current - self.window)
|
||||
|
||||
self.tokens -= 1
|
||||
|
||||
if self.tokens == 0:
|
||||
self.window = current
|
||||
|
||||
return None
|
||||
|
||||
class CooldownMapping:
|
||||
"""Holds all cooldowns for every key"""
|
||||
def __init__(self, rate: int, per: int):
|
||||
self.rate = rate
|
||||
self.per = per
|
||||
self.cache: dict[str, Cooldown] = {}
|
||||
|
||||
def verify_cache(self) -> None:
|
||||
current = time.time()
|
||||
self.cache = {k: v for k, v in self.cache.items() if current < (v.last + v.per)}
|
||||
|
||||
def get_bucket(self, key: str) -> Cooldown:
|
||||
self.verify_cache()
|
||||
|
||||
if not (rl := self.cache.get(key)):
|
||||
self.cache[key] = rl = Cooldown(self.rate, self.per)
|
||||
|
||||
return rl
|
||||
|
||||
class BucketType(Enum):
|
||||
default = 0
|
||||
user = 1
|
||||
server = 2
|
||||
channel = 3
|
||||
member = 4
|
||||
|
||||
def resolve(self, context: Context[ClientT_Co_D]) -> str:
|
||||
if self == BucketType.default:
|
||||
return f"{context.author.id}{context.channel.id}"
|
||||
|
||||
elif self == BucketType.user:
|
||||
return context.author.id
|
||||
|
||||
elif self == BucketType.server:
|
||||
if id := context.server_id:
|
||||
return id
|
||||
|
||||
raise ServerOnly
|
||||
|
||||
elif self == BucketType.channel:
|
||||
return context.channel.id
|
||||
|
||||
else: # BucketType.member
|
||||
if server_id := context.server_id:
|
||||
return f"{context.author.id}{server_id}"
|
||||
|
||||
raise ServerOnly
|
||||
|
||||
def cooldown(rate: int, per: int, *, bucket: BucketType | Callable[[Context[ClientT_Co]], Coroutine[Any, Any, str]] = BucketType.default) -> Callable[[T], T]:
|
||||
"""Adds a cooldown to a command
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
rate: :class:`int`
|
||||
How many times it can be used
|
||||
per: :class:`int`
|
||||
How long the window is before the ratelimit resets
|
||||
bucket: Optional[Union[:class:`BucketType`, Callable[[Context], str]]]
|
||||
Controls how the key is generated for the cooldowns
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. code-block:: python
|
||||
@commands.command()
|
||||
@commands.cooldown(1, 5)
|
||||
async def ping(self, ctx: Context):
|
||||
await ctx.send("Pong")
|
||||
"""
|
||||
def inner(func: T) -> T:
|
||||
from .command import Command
|
||||
|
||||
if isinstance(func, Command):
|
||||
command = cast(Command[ClientT_Co], func) # cant verify generic at runtime so must cast
|
||||
command.cooldown = CooldownMapping(rate, per)
|
||||
command.cooldown_bucket = bucket
|
||||
else:
|
||||
func._cooldown = CooldownMapping(rate, per) # type: ignore
|
||||
func._bucket = bucket # type: ignore
|
||||
|
||||
return func # type: ignore
|
||||
|
||||
return inner
|
||||
@@ -8,6 +8,7 @@ __all__ = (
|
||||
"NotBotOwner",
|
||||
"NotServerOwner",
|
||||
"ServerOnly",
|
||||
"MissingPermissionsError",
|
||||
"ConverterError",
|
||||
"InvalidLiteralArgument",
|
||||
"BadBoolArgument",
|
||||
@@ -15,7 +16,9 @@ __all__ = (
|
||||
"ChannelConverterError",
|
||||
"UserConverterError",
|
||||
"MemberConverterError",
|
||||
"UnionConverterError",
|
||||
"MissingSetup",
|
||||
"CommandOnCooldown"
|
||||
)
|
||||
|
||||
class CommandError(RevoltError):
|
||||
@@ -97,3 +100,17 @@ class UnionConverterError(ConverterError):
|
||||
|
||||
class MissingSetup(CommandError):
|
||||
"""Raised when an extension is missing the `setup` function"""
|
||||
|
||||
class CommandOnCooldown(CommandError):
|
||||
"""Raised when a command is on cooldown
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
retry_after: :class:`float`
|
||||
How long the user must wait until the cooldown resets
|
||||
"""
|
||||
|
||||
__slots__ = ("retry_after",)
|
||||
|
||||
def __init__(self, retry_after: float):
|
||||
self.retry_after: float = retry_after
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from typing import Any, Callable, Coroutine, Optional
|
||||
|
||||
from .command import Command
|
||||
from .utils import ClientCoT, ClientT
|
||||
from .utils import ClientT_Co_D, ClientT_D
|
||||
|
||||
|
||||
__all__ = (
|
||||
@@ -11,7 +11,7 @@ __all__ = (
|
||||
"group"
|
||||
)
|
||||
|
||||
class Group(Command[ClientCoT]):
|
||||
class Group(Command[ClientT_Co_D]):
|
||||
"""Class for holding info about a group command.
|
||||
|
||||
Parameters
|
||||
@@ -29,10 +29,10 @@ class Group(Command[ClientCoT]):
|
||||
__slots__: tuple[str, ...] = ("subcommands",)
|
||||
|
||||
def __init__(self, callback: Callable[..., Coroutine[Any, Any, Any]], name: str, aliases: list[str]):
|
||||
self.subcommands: dict[str, Command[ClientCoT]] = {}
|
||||
super().__init__(callback, name, aliases)
|
||||
self.subcommands: dict[str, Command[ClientT_Co_D]] = {}
|
||||
super().__init__(callback, name, aliases=aliases)
|
||||
|
||||
def command(self, *, name: Optional[str] = None, aliases: Optional[list[str]] = None, cls: type[Command[ClientCoT]] = Command[ClientCoT]) -> Callable[[Callable[..., Coroutine[Any, Any, Any]]], Command[ClientCoT]]:
|
||||
def command(self, *, name: Optional[str] = None, aliases: Optional[list[str]] = None, cls: type[Command[ClientT_Co_D]] = Command[ClientT_Co_D]) -> Callable[[Callable[..., Coroutine[Any, Any, Any]]], Command[ClientT_Co_D]]:
|
||||
"""A decorator that turns a function into a :class:`Command` and registers the command as a subcommand.
|
||||
|
||||
Parameters
|
||||
@@ -50,14 +50,18 @@ class Group(Command[ClientCoT]):
|
||||
A function that takes the command callback and returns a :class:`Command`
|
||||
"""
|
||||
def inner(func: Callable[..., Coroutine[Any, Any, Any]]):
|
||||
command = cls(func, name or func.__name__, aliases or [])
|
||||
command = cls(func, name or func.__name__, aliases=aliases or [])
|
||||
command.parent = self
|
||||
self.subcommands[command.name] = command
|
||||
|
||||
for alias in command.aliases:
|
||||
self.subcommands[alias] = command
|
||||
|
||||
return command
|
||||
|
||||
return inner
|
||||
|
||||
def group(self, *, name: Optional[str] = None, aliases: Optional[list[str]] = None, cls: Optional[type[Group[ClientCoT]]] = None) -> Callable[[Callable[..., Coroutine[Any, Any, Any]]], Group[ClientCoT]]:
|
||||
def group(self, *, name: Optional[str] = None, aliases: Optional[list[str]] = None, cls: Optional[type[Group[ClientT_Co_D]]] = None) -> Callable[[Callable[..., Coroutine[Any, Any, Any]]], Group[ClientT_Co_D]]:
|
||||
"""A decorator that turns a function into a :class:`Group` and registers the command as a subcommand
|
||||
|
||||
Parameters
|
||||
@@ -80,6 +84,10 @@ class Group(Command[ClientCoT]):
|
||||
command = cls(func, name or func.__name__, aliases or [])
|
||||
command.parent = self
|
||||
self.subcommands[command.name] = command
|
||||
|
||||
for alias in command.aliases:
|
||||
self.subcommands[alias] = command
|
||||
|
||||
return command
|
||||
|
||||
return inner
|
||||
@@ -88,10 +96,68 @@ class Group(Command[ClientCoT]):
|
||||
return f"<Group name=\"{self.name}\">"
|
||||
|
||||
@property
|
||||
def commands(self) -> list[Command[ClientCoT]]:
|
||||
return list(self.subcommands.values())
|
||||
def commands(self) -> list[Command[ClientT_Co_D]]:
|
||||
"""Gets all commands registered
|
||||
|
||||
def group(*, name: Optional[str] = None, aliases: Optional[list[str]] = None, cls: type[Group[ClientT]] = Group) -> Callable[[Callable[..., Coroutine[Any, Any, Any]]], Group[ClientT]]:
|
||||
Returns
|
||||
--------
|
||||
list[:class:`Command`]
|
||||
The registered commands
|
||||
"""
|
||||
return list(set(self.subcommands.values()))
|
||||
|
||||
def get_command(self, name: str) -> Command[ClientT_Co_D]:
|
||||
"""Gets a command.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The name or alias of the command
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Command`
|
||||
The command with the name
|
||||
"""
|
||||
return self.subcommands[name]
|
||||
|
||||
def add_command(self, command: Command[ClientT_Co_D]) -> None:
|
||||
"""Adds a command, this is typically only used for dynamic commands, you should use the `commands.command` decorator for most usecases.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The name or alias of the command
|
||||
command: :class:`Command`
|
||||
The command to be added
|
||||
"""
|
||||
self.subcommands[command.name] = command
|
||||
|
||||
for alias in command.aliases:
|
||||
self.subcommands[alias] = command
|
||||
|
||||
def remove_command(self, name: str) -> Optional[Command[ClientT_Co_D]]:
|
||||
"""Removes a command.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The name or alias of the command
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[:class:`Command`]
|
||||
The command that was removed
|
||||
"""
|
||||
command = self.subcommands.pop(name, None)
|
||||
|
||||
if command is not None:
|
||||
for alias in command.aliases:
|
||||
self.subcommands.pop(alias, None)
|
||||
|
||||
return command
|
||||
|
||||
def group(*, name: Optional[str] = None, aliases: Optional[list[str]] = None, cls: type[Group[ClientT_D]] = Group) -> Callable[[Callable[..., Coroutine[Any, Any, Any]]], Group[ClientT_D]]:
|
||||
"""A decorator that turns a function into a :class:`Group`
|
||||
|
||||
Parameters
|
||||
|
||||
+60
-50
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Any, Generic, Optional, TypedDict, Union
|
||||
from typing import TYPE_CHECKING, Generic, Optional, TypedDict, Union, cast
|
||||
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
@@ -9,11 +9,11 @@ from .cog import Cog
|
||||
from .command import Command
|
||||
from .context import Context
|
||||
from .group import Group
|
||||
from .utils import ClientCoT, ClientT
|
||||
from .utils import ClientT_Co_D, ClientT_D
|
||||
|
||||
from revolt import File, Message, Messageable, MessageReply, SendableEmbed
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from revolt import File, Message, Messageable, MessageReply, SendableEmbed
|
||||
|
||||
from .cog import Cog
|
||||
|
||||
__all__ = ("MessagePayload", "HelpCommand", "DefaultHelpCommand", "help_command_impl")
|
||||
@@ -26,30 +26,33 @@ class MessagePayload(TypedDict):
|
||||
attachments: NotRequired[list[File]]
|
||||
replies: NotRequired[list[MessageReply]]
|
||||
|
||||
class HelpCommand(ABC, Generic[ClientCoT]):
|
||||
class HelpCommand(ABC, Generic[ClientT_Co_D]):
|
||||
@abstractmethod
|
||||
async def create_bot_help(self, context: Context[ClientCoT], commands: dict[Optional[Cog[ClientCoT]], list[Command[ClientCoT]]]) -> Union[str, SendableEmbed, MessagePayload]:
|
||||
async def create_global_help(self, context: Context[ClientT_Co_D], commands: dict[Optional[Cog[ClientT_Co_D]], list[Command[ClientT_Co_D]]]) -> Union[str, SendableEmbed, MessagePayload]:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def create_command_help(self, context: Context[ClientCoT], command: Command[ClientCoT]) -> Union[str, SendableEmbed, MessagePayload]:
|
||||
async def create_command_help(self, context: Context[ClientT_Co_D], command: Command[ClientT_Co_D]) -> Union[str, SendableEmbed, MessagePayload]:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def create_group_help(self, context: Context[ClientCoT], group: Group[ClientCoT]) -> Union[str, SendableEmbed, MessagePayload]:
|
||||
async def create_group_help(self, context: Context[ClientT_Co_D], group: Group[ClientT_Co_D]) -> Union[str, SendableEmbed, MessagePayload]:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def create_cog_help(self, context: Context[ClientCoT], cog: Cog[ClientCoT]) -> Union[str, SendableEmbed, MessagePayload]:
|
||||
async def create_cog_help(self, context: Context[ClientT_Co_D], cog: Cog[ClientT_Co_D]) -> Union[str, SendableEmbed, MessagePayload]:
|
||||
raise NotImplementedError
|
||||
|
||||
async def send_help_command(self, context: Context[ClientCoT], message_payload: MessagePayload) -> Message:
|
||||
async def send_help_command(self, context: Context[ClientT_Co_D], message_payload: MessagePayload) -> Message:
|
||||
return await context.send(**message_payload)
|
||||
|
||||
async def filter_commands(self, context: Context[ClientCoT], commands: list[Command[ClientCoT]]) -> list[Command[ClientCoT]]:
|
||||
filtered: list[Command[ClientCoT]] = []
|
||||
async def filter_commands(self, context: Context[ClientT_Co_D], commands: list[Command[ClientT_Co_D]]) -> list[Command[ClientT_Co_D]]:
|
||||
filtered: list[Command[ClientT_Co_D]] = []
|
||||
|
||||
for command in commands:
|
||||
if command.hidden:
|
||||
continue
|
||||
|
||||
try:
|
||||
if await context.can_run(command):
|
||||
filtered.append(command)
|
||||
@@ -58,34 +61,29 @@ class HelpCommand(ABC, Generic[ClientCoT]):
|
||||
|
||||
return filtered
|
||||
|
||||
async def group_commands(self, context: Context[ClientCoT], commands: list[Command[ClientCoT]]) -> dict[Optional[Cog[ClientCoT]], list[Command[ClientCoT]]]:
|
||||
cogs: dict[Optional[Cog[ClientCoT]], list[Command[ClientCoT]]] = {}
|
||||
async def group_commands(self, context: Context[ClientT_Co_D], commands: list[Command[ClientT_Co_D]]) -> dict[Optional[Cog[ClientT_Co_D]], list[Command[ClientT_Co_D]]]:
|
||||
cogs: dict[Optional[Cog[ClientT_Co_D]], list[Command[ClientT_Co_D]]] = {}
|
||||
|
||||
for command in commands:
|
||||
cogs.setdefault(command.cog, []).append(command)
|
||||
|
||||
return cogs
|
||||
|
||||
async def handle_message(self, context: Context[ClientCoT], message: Message) -> None:
|
||||
async def handle_message(self, context: Context[ClientT_Co_D], message: Message) -> None:
|
||||
pass
|
||||
|
||||
async def get_channel(self, context: Context) -> Messageable:
|
||||
return context
|
||||
|
||||
@abstractmethod
|
||||
async def handle_no_command_found(self, context: Context[ClientCoT], name: str) -> Any:
|
||||
async def handle_no_command_found(self, context: Context[ClientT_Co_D], name: str) -> Union[str, SendableEmbed, MessagePayload]:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def handle_no_cog_found(self, context: Context[ClientCoT], name: str) -> Any:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DefaultHelpCommand(HelpCommand[ClientCoT]):
|
||||
class DefaultHelpCommand(HelpCommand[ClientT_Co_D]):
|
||||
def __init__(self, default_cog_name: str = "No Cog"):
|
||||
self.default_cog_name = default_cog_name
|
||||
|
||||
async def create_bot_help(self, context: Context[ClientCoT], commands: dict[Optional[Cog[ClientCoT]], list[Command[ClientCoT]]]) -> Union[str, SendableEmbed, MessagePayload]:
|
||||
async def create_global_help(self, context: Context[ClientT_Co_D], commands: dict[Optional[Cog[ClientT_Co_D]], list[Command[ClientT_Co_D]]]) -> Union[str, SendableEmbed, MessagePayload]:
|
||||
lines = ["```"]
|
||||
|
||||
for cog, cog_commands in commands.items():
|
||||
@@ -100,7 +98,7 @@ class DefaultHelpCommand(HelpCommand[ClientCoT]):
|
||||
lines.append("```")
|
||||
return "\n".join(lines)
|
||||
|
||||
async def create_cog_help(self, context: Context[ClientCoT], cog: Cog[ClientCoT]) -> Union[str, SendableEmbed, MessagePayload]:
|
||||
async def create_cog_help(self, context: Context[ClientT_Co_D], cog: Cog[ClientT_Co_D]) -> Union[str, SendableEmbed, MessagePayload]:
|
||||
lines = ["```"]
|
||||
|
||||
lines.append(f"{cog.qualified_name}:")
|
||||
@@ -111,7 +109,7 @@ class DefaultHelpCommand(HelpCommand[ClientCoT]):
|
||||
lines.append("```")
|
||||
return "\n".join(lines)
|
||||
|
||||
async def create_command_help(self, context: Context[ClientCoT], command: Command[ClientCoT]) -> Union[str, SendableEmbed, MessagePayload]:
|
||||
async def create_command_help(self, context: Context[ClientT_Co_D], command: Command[ClientT_Co_D]) -> Union[str, SendableEmbed, MessagePayload]:
|
||||
lines = ["```"]
|
||||
|
||||
lines.append(f"{command.name}:")
|
||||
@@ -127,7 +125,7 @@ class DefaultHelpCommand(HelpCommand[ClientCoT]):
|
||||
lines.append("```")
|
||||
return "\n".join(lines)
|
||||
|
||||
async def create_group_help(self, context: Context[ClientCoT], group: Group[ClientCoT]) -> Union[str, SendableEmbed, MessagePayload]:
|
||||
async def create_group_help(self, context: Context[ClientT_Co_D], group: Group[ClientT_Co_D]) -> Union[str, SendableEmbed, MessagePayload]:
|
||||
lines = ["```"]
|
||||
|
||||
lines.append(f"{group.name}:")
|
||||
@@ -145,54 +143,66 @@ class DefaultHelpCommand(HelpCommand[ClientCoT]):
|
||||
lines.append("```")
|
||||
return "\n".join(lines)
|
||||
|
||||
async def handle_no_command_found(self, context: Context[ClientCoT], name: str) -> None:
|
||||
channel = await self.get_channel(context)
|
||||
await channel.send(f"Command `{name}` not found.")
|
||||
async def handle_no_command_found(self, context: Context[ClientT_Co_D], name: str) -> str:
|
||||
return f"Command `{name}` not found."
|
||||
|
||||
async def handle_no_cog_found(self, context: Context[ClientCoT], name: str) -> None:
|
||||
channel = await self.get_channel(context)
|
||||
await channel.send(f"Cog `{name}` not found.")
|
||||
|
||||
|
||||
class HelpCommandImpl(Command[ClientCoT]):
|
||||
def __init__(self, client: ClientCoT):
|
||||
class HelpCommandImpl(Command[ClientT_Co_D]):
|
||||
def __init__(self, client: ClientT_Co_D):
|
||||
self.client = client
|
||||
|
||||
async def callback(_: Union[ClientCoT, Cog[ClientCoT]], context: Context[ClientCoT], *args: str) -> None:
|
||||
async def callback(_: Union[ClientT_Co_D, Cog[ClientT_Co_D]], context: Context[ClientT_Co_D], *args: str) -> None:
|
||||
await help_command_impl(context.client, context, *args)
|
||||
|
||||
super().__init__(callback=callback, name="help", aliases=[])
|
||||
self.description: str | None = "Shows help for a command, cog or the entire bot"
|
||||
|
||||
|
||||
async def help_command_impl(self: ClientT, context: Context[ClientT], *arguments: str) -> None:
|
||||
help_command = self.help_command
|
||||
async def help_command_impl(client: ClientT_D, context: Context[ClientT_D], *arguments: str) -> None:
|
||||
help_command = client.help_command
|
||||
|
||||
if not help_command:
|
||||
return
|
||||
|
||||
filtered_commands = await help_command.filter_commands(context, self.commands)
|
||||
filtered_commands = await help_command.filter_commands(context, client.commands)
|
||||
commands = await help_command.group_commands(context, filtered_commands)
|
||||
|
||||
if not arguments:
|
||||
payload = await help_command.create_bot_help(context, commands)
|
||||
else:
|
||||
command_name = arguments[0]
|
||||
payload = await help_command.create_global_help(context, commands)
|
||||
|
||||
try:
|
||||
command = self.get_command(command_name)
|
||||
except KeyError:
|
||||
cog = self.cogs.get(command_name)
|
||||
if cog:
|
||||
payload = await help_command.create_cog_help(context, cog)
|
||||
else:
|
||||
parent: ClientT_D | Group[ClientT_D] = client
|
||||
|
||||
for param in arguments:
|
||||
try:
|
||||
command = parent.get_command(param)
|
||||
except LookupError:
|
||||
try:
|
||||
cog = client.get_cog(param)
|
||||
except LookupError:
|
||||
payload = await help_command.handle_no_command_found(context, param)
|
||||
else:
|
||||
payload = await help_command.create_cog_help(context, cog)
|
||||
finally:
|
||||
break
|
||||
|
||||
if isinstance(command, Group):
|
||||
parent = command
|
||||
else:
|
||||
return await help_command.handle_no_command_found(context, command_name)
|
||||
payload = await help_command.create_command_help(context, command)
|
||||
break
|
||||
else:
|
||||
|
||||
if TYPE_CHECKING:
|
||||
command = cast(Command[ClientT_D], ...)
|
||||
|
||||
if isinstance(command, Group):
|
||||
payload = await help_command.create_group_help(context, command)
|
||||
else:
|
||||
payload = await help_command.create_command_help(context, command)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
payload = cast(MessagePayload, ...)
|
||||
|
||||
msg_payload: MessagePayload
|
||||
|
||||
if isinstance(payload, str):
|
||||
|
||||
@@ -12,9 +12,10 @@ if TYPE_CHECKING:
|
||||
|
||||
__all__ = ("evaluate_parameters",)
|
||||
|
||||
ClientT = TypeVar("ClientT", bound="CommandsClient", default="CommandsClient")
|
||||
ClientCoT = TypeVar("ClientCoT", bound="CommandsClient", default="CommandsClient", covariant=True)
|
||||
ContextT = TypeVar("ContextT", bound="Context")
|
||||
ClientT_Co = TypeVar("ClientT_Co", bound="CommandsClient", covariant=True)
|
||||
ClientT_D = TypeVar("ClientT_D", bound="CommandsClient", default="CommandsClient")
|
||||
ClientT_Co_D = TypeVar("ClientT_Co_D", bound="CommandsClient", default="CommandsClient", covariant=True)
|
||||
ContextT = TypeVar("ContextT", bound="Context", default="Context")
|
||||
|
||||
def evaluate_parameters(parameters: Iterable[Parameter], globals: dict[str, Any]) -> list[Parameter]:
|
||||
new_parameters: list[Parameter] = []
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from typing import Iterator
|
||||
from typing_extensions import Self
|
||||
|
||||
from .errors import NoClosingQuote
|
||||
|
||||
|
||||
@@ -16,7 +18,8 @@ class StringView:
|
||||
|
||||
def get_rest(self) -> str:
|
||||
if self.should_undo:
|
||||
return f"{self.temp} {''.join(self.value)}"
|
||||
return f"{self.temp} {''.join(self.value)}".rstrip()
|
||||
# prevent a new space appearing at end if the buffer is depleted
|
||||
|
||||
return "".join(self.value)
|
||||
|
||||
@@ -51,3 +54,9 @@ class StringView:
|
||||
self.temp = output
|
||||
|
||||
return output
|
||||
|
||||
def __iter__(self) -> Self:
|
||||
return self
|
||||
|
||||
def __next__(self) -> str:
|
||||
return self.get_next_word()
|
||||
+21
-15
@@ -6,6 +6,7 @@ from typing import (TYPE_CHECKING, Any, Coroutine, Literal, Optional, TypeVar,
|
||||
import aiohttp
|
||||
import ulid
|
||||
|
||||
|
||||
from .errors import Forbidden, HTTPError, ServerError
|
||||
from .file import File
|
||||
|
||||
@@ -19,23 +20,18 @@ if TYPE_CHECKING:
|
||||
|
||||
from .enums import SortType
|
||||
from .file import File
|
||||
from .types import ApiInfo
|
||||
from .types import Autumn as AutumnPayload
|
||||
from .types import Channel, DMChannel
|
||||
from .types import Emoji as EmojiPayload
|
||||
from .types import EmojiParent, GetServerMembers, GroupDMChannel
|
||||
from .types import Interactions as InteractionsPayload
|
||||
from .types import Invite
|
||||
from .types import Masquerade as MasqueradePayload
|
||||
from .types import Member
|
||||
from .types import Member as MemberPayload
|
||||
from .types import Message as MessagePayload
|
||||
from .types import (MessageReplyPayload, MessageWithUserData,
|
||||
PartialInvite, Role)
|
||||
from .types import SendableEmbed as SendableEmbedPayload
|
||||
from .types import Server, ServerBans, TextChannel
|
||||
from .types import User as UserPayload
|
||||
from .types import UserProfile, VoiceChannel
|
||||
from .types import (Server, ServerBans, TextChannel, UserProfile, VoiceChannel, Member, Invite, ApiInfo, Channel, SavedMessages,
|
||||
DMChannel, EmojiParent, GetServerMembers, GroupDMChannel, MessageReplyPayload, MessageWithUserData, PartialInvite, CreateRole)
|
||||
|
||||
from aiohttp.client import _RequestOptions
|
||||
|
||||
__all__ = ("HttpClient",)
|
||||
|
||||
@@ -55,7 +51,7 @@ class HttpClient:
|
||||
async def request(self, method: Literal["GET", "POST", "PUT", "DELETE", "PATCH"], route: str, *, json: Optional[dict[str, Any]] = None, nonce: bool = True, params: Optional[dict[str, Any]] = None) -> Any:
|
||||
url = f"{self.api_url}{route}"
|
||||
|
||||
kwargs = {}
|
||||
kwargs: _RequestOptions = {}
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Revolt.py (https://github.com/revoltchat/revolt.py)",
|
||||
@@ -146,7 +142,7 @@ class HttpClient:
|
||||
return await self.request("POST", f"/channels/{channel}/messages", json=json)
|
||||
|
||||
def edit_message(self, channel: str, message: str, content: Optional[str], embeds: Optional[list[SendableEmbedPayload]] = None) -> Request[None]:
|
||||
json = {}
|
||||
json: dict[str, Any] = {}
|
||||
|
||||
if content is not None:
|
||||
json["content"] = content
|
||||
@@ -290,7 +286,7 @@ class HttpClient:
|
||||
def fetch_dm_channels(self) -> Request[list[Union[DMChannel, GroupDMChannel]]]:
|
||||
return self.request("GET", "/users/dms")
|
||||
|
||||
def open_dm(self, user_id: str) -> Request[DMChannel]:
|
||||
def open_dm(self, user_id: str) -> Request[DMChannel | SavedMessages]:
|
||||
return self.request("GET", f"/users/{user_id}/dm")
|
||||
|
||||
def fetch_channel(self, channel_id: str) -> Request[Channel]:
|
||||
@@ -347,7 +343,7 @@ class HttpClient:
|
||||
def fetch_bans(self, server_id: str) -> Request[ServerBans]:
|
||||
return self.request("GET", f"/servers/{server_id}/bans")
|
||||
|
||||
def create_role(self, server_id: str, name: str) -> Request[Role]:
|
||||
def create_role(self, server_id: str, name: str) -> Request[CreateRole]:
|
||||
return self.request("POST", f"/servers/{server_id}/roles", json={"name": name}, nonce=False)
|
||||
|
||||
def delete_role(self, server_id: str, role_id: str) -> Request[None]:
|
||||
@@ -405,7 +401,14 @@ class HttpClient:
|
||||
return self.request("PUT", f"/channels/{channel_id}/messages/{message_id}/reactions/{emoji}")
|
||||
|
||||
def remove_reaction(self, channel_id: str, message_id: str, emoji: str, user_id: Optional[str], remove_all: bool) -> Request[None]:
|
||||
return self.request("PUT", f"/channels/{channel_id}/messages/{message_id}/reactions/{emoji}")
|
||||
parameters: dict[str, str] = {}
|
||||
|
||||
if user_id:
|
||||
parameters["user_id"] = user_id
|
||||
|
||||
parameters["remove_all"] = "true" if remove_all else "false"
|
||||
|
||||
return self.request("DELETE", f"/channels/{channel_id}/messages/{message_id}/reactions/{emoji}", params=parameters)
|
||||
|
||||
def remove_all_reactions(self, channel_id: str, message_id: str) -> Request[None]:
|
||||
return self.request("DELETE", f"/channels/{channel_id}/messages/{message_id}/reactions")
|
||||
@@ -422,7 +425,10 @@ class HttpClient:
|
||||
return await self.request("PUT", f"/custom/emoji/{asset['id']}", json={"name": name, "parent": parent, "nsfw": nsfw})
|
||||
|
||||
def edit_member(self, server_id: str, member_id: str, remove: list[str] | None, values: dict[str, Any]) -> Request[MemberPayload]:
|
||||
return self.request("PATCH", f"/servers/{server_id}/members/{member_id}", json={"remove": remove, **values})
|
||||
if remove:
|
||||
values["remove"] = remove
|
||||
|
||||
return self.request("PATCH", f"/servers/{server_id}/members/{member_id}", json=values)
|
||||
|
||||
def delete_messages(self, channel_id: str, messages: list[str]) -> Request[None]:
|
||||
return self.request("DELETE", f"/channels/{channel_id}/messages/bulk", json={"ids": messages})
|
||||
|
||||
+32
-18
@@ -4,7 +4,7 @@ import datetime
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
|
||||
from .utils import _Missing, Missing
|
||||
from .utils import _Missing, Missing, parse_timestamp
|
||||
|
||||
from .asset import Asset
|
||||
from .permissions import Permissions
|
||||
@@ -63,17 +63,12 @@ class Member(User):
|
||||
|
||||
self.server: Server = server
|
||||
self.nickname: str | None = data.get("nickname")
|
||||
joined_at = data["joined_at"]
|
||||
|
||||
if isinstance(joined_at, int):
|
||||
self.joined_at: datetime.datetime = datetime.datetime.fromtimestamp(joined_at / 1000)
|
||||
else:
|
||||
self.joined_at: datetime.datetime = datetime.datetime.strptime(joined_at, "%Y-%m-%dT%H:%M:%S.%f%z")
|
||||
self.joined_at: datetime.datetime = parse_timestamp(data["joined_at"])
|
||||
|
||||
self.current_timeout: datetime.datetime | None
|
||||
|
||||
if current_timeout := data.get("timeout"):
|
||||
self.current_timeout = datetime.datetime.strptime(current_timeout, "%Y-%m-%dT%H:%M:%S.%f%z")
|
||||
self.current_timeout = parse_timestamp(current_timeout)
|
||||
else:
|
||||
self.current_timeout = None
|
||||
|
||||
@@ -92,7 +87,14 @@ class Member(User):
|
||||
""":class:`str`: Returns a string that allows you to mention the given member."""
|
||||
return f"<@{self.id}>"
|
||||
|
||||
def _update(self, *, nickname: Optional[str] = None, avatar: Optional[FilePayload] = None, roles: Optional[list[str]] = None):
|
||||
def _update(
|
||||
self,
|
||||
*,
|
||||
nickname: Optional[str] = None,
|
||||
avatar: Optional[FilePayload] = None,
|
||||
roles: Optional[list[str]] = None,
|
||||
timeout: Optional[str | int] = None
|
||||
) -> None:
|
||||
if nickname is not None:
|
||||
self.nickname = nickname
|
||||
|
||||
@@ -103,6 +105,9 @@ class Member(User):
|
||||
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)
|
||||
|
||||
if timeout is not None:
|
||||
self.current_timeout = parse_timestamp(timeout)
|
||||
|
||||
async def kick(self) -> None:
|
||||
"""Kicks the member from the server"""
|
||||
await self.state.http.kick_member(self.server.id, self.id)
|
||||
@@ -129,6 +134,19 @@ class Member(User):
|
||||
avatar: File | None | _Missing = Missing,
|
||||
timeout: datetime.timedelta | None | _Missing = Missing
|
||||
) -> None:
|
||||
"""Edits the member
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
nickname: Union[:class:`str`, :class:`None`]
|
||||
The new nickname, or :class:`None` to reset it
|
||||
roles: Union[list[:class:`Role`], :class:`None`]
|
||||
The new roles for the member, or :class:`None` to clear it
|
||||
avatar: Union[:class:`File`, :class:`None`]
|
||||
The new server avatar, or :class:`None` to reset it
|
||||
timeout: Union[:class:`datetime.timedelta`, :class:`None`]
|
||||
The new timeout length for the member, or :class:`None` to reset it
|
||||
"""
|
||||
remove: list[str] = []
|
||||
data: dict[str, Any] = {}
|
||||
|
||||
@@ -139,21 +157,17 @@ class Member(User):
|
||||
|
||||
if roles is None:
|
||||
remove.append("Roles")
|
||||
elif roles is not Missing:
|
||||
data["roles"] = roles
|
||||
elif not isinstance(roles, _Missing):
|
||||
data["roles"] = [role.id for role in roles]
|
||||
|
||||
if avatar is None:
|
||||
remove.append("Avatar")
|
||||
elif avatar is not Missing:
|
||||
# pyright cant understand custom singletons - it doesnt know this will never be an instance of _Missing here because Missing is the only instance
|
||||
assert not isinstance(avatar, _Missing)
|
||||
|
||||
elif not isinstance(avatar, _Missing):
|
||||
data["avatar"] = (await self.state.http.upload_file(avatar, "avatars"))["id"]
|
||||
|
||||
if timeout is None:
|
||||
remove.append("Timeout")
|
||||
elif timeout is not Missing:
|
||||
assert not isinstance(timeout, _Missing)
|
||||
elif not isinstance(timeout, _Missing):
|
||||
data["timeout"] = (datetime.datetime.now(datetime.timezone.utc) + timeout).isoformat()
|
||||
|
||||
await self.state.http.edit_member(self.server.id, self.id, remove, data)
|
||||
@@ -166,7 +180,7 @@ class Member(User):
|
||||
length: :class:`datetime.timedelta`
|
||||
The length of the timeout
|
||||
"""
|
||||
ends_at = datetime.datetime.utcnow() + length
|
||||
ends_at = datetime.datetime.now(tz=datetime.timezone.utc) + length
|
||||
|
||||
await self.state.http.edit_member(self.server.id, self.id, None, {"timeout": ends_at.isoformat()})
|
||||
|
||||
|
||||
+71
-30
@@ -3,12 +3,11 @@ from __future__ import annotations
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING, Any, Coroutine, Optional, Union
|
||||
|
||||
from revolt.types.message import SystemMessageContent
|
||||
|
||||
from .asset import Asset, PartialAsset
|
||||
from .channel import DMChannel, GroupDMChannel, TextChannel
|
||||
from .channel import DMChannel, GroupDMChannel, TextChannel, SavedMessageChannel
|
||||
from .embed import Embed, SendableEmbed, to_embed
|
||||
from .utils import Ulid
|
||||
from .utils import Ulid, parse_timestamp
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .server import Server
|
||||
@@ -17,7 +16,7 @@ if TYPE_CHECKING:
|
||||
from .types import Interactions as InteractionsPayload
|
||||
from .types import Masquerade as MasqueradePayload
|
||||
from .types import Message as MessagePayload
|
||||
from .types import MessageReplyPayload
|
||||
from .types import MessageReplyPayload, SystemMessageContent
|
||||
from .user import User
|
||||
from .member import Member
|
||||
|
||||
@@ -47,8 +46,8 @@ class Message(Ulid):
|
||||
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
|
||||
raw_mentions: list[:class:`str`]
|
||||
A list of ids of the mentions in this 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`]
|
||||
@@ -58,38 +57,37 @@ class Message(Ulid):
|
||||
interactions: Optional[:class:`MessageInteractions`]
|
||||
The interactions on the message, if any
|
||||
"""
|
||||
__slots__ = ("state", "id", "content", "attachments", "embeds", "channel", "author", "edited_at", "mentions", "replies", "reply_ids", "reactions", "interactions")
|
||||
__slots__ = ("state", "id", "content", "attachments", "embeds", "channel", "author", "edited_at", "replies", "reply_ids", "reactions", "interactions")
|
||||
|
||||
def __init__(self, data: MessagePayload, state: State):
|
||||
self.state: State = state
|
||||
|
||||
self.id: str = data["_id"]
|
||||
self.content: str = data.get("content", "")
|
||||
|
||||
content = data.get("content", "")
|
||||
|
||||
if not isinstance(content, str):
|
||||
self.system_content: SystemMessageContent = content
|
||||
self.content: str = ""
|
||||
else:
|
||||
self.content = content
|
||||
self.system_content: SystemMessageContent | None = data.get("system")
|
||||
|
||||
self.attachments: list[Asset] = [Asset(attachment, state) for attachment in data.get("attachments", [])]
|
||||
self.embeds: list[Embed] = [to_embed(embed, state) for embed in data.get("embeds", [])]
|
||||
|
||||
channel = state.get_channel(data["channel"])
|
||||
assert isinstance(channel, Union[TextChannel, GroupDMChannel, DMChannel])
|
||||
self.channel: TextChannel | GroupDMChannel | DMChannel = channel
|
||||
assert isinstance(channel, (TextChannel, GroupDMChannel, DMChannel, SavedMessageChannel))
|
||||
self.channel: TextChannel | GroupDMChannel | DMChannel | SavedMessageChannel = channel
|
||||
|
||||
self.server_id: str | None = self.channel.server_id
|
||||
|
||||
self.mentions: list[Member | User]
|
||||
self.raw_mentions: list[str] = data.get("mentions", [])
|
||||
|
||||
if self.system_content:
|
||||
author_id: str = self.system_content.get("id", data["author"])
|
||||
else:
|
||||
author_id = data["author"]
|
||||
|
||||
if self.server_id:
|
||||
author = state.get_member(self.server_id, data["author"])
|
||||
self.mentions = [self.server.get_member(member_id) for member_id in data.get("mentions", [])]
|
||||
author = state.get_member(self.server_id, author_id)
|
||||
|
||||
else:
|
||||
author = state.get_user(data["author"])
|
||||
self.mentions = [state.get_user(member_id) for member_id in data.get("mentions", [])]
|
||||
author = state.get_user(author_id)
|
||||
|
||||
self.author: Member | User = author
|
||||
|
||||
@@ -101,7 +99,7 @@ class Message(Ulid):
|
||||
self.author.masquerade_avatar = PartialAsset(avatar, state)
|
||||
|
||||
if edited_at := data.get("edited"):
|
||||
self.edited_at: Optional[datetime.datetime] = datetime.datetime.strptime(edited_at, "%Y-%m-%dT%H:%M:%S.%f%z")
|
||||
self.edited_at: Optional[datetime.datetime] = parse_timestamp(edited_at)
|
||||
|
||||
self.replies: list[Message] = []
|
||||
self.reply_ids: list[str] = []
|
||||
@@ -137,17 +135,42 @@ class Message(Ulid):
|
||||
self.embeds = [to_embed(embed, self.state) for embed in embeds]
|
||||
|
||||
if edited is not None:
|
||||
if isinstance(edited, int):
|
||||
self.edited_at = datetime.datetime.fromtimestamp(edited / 1000, tz=datetime.timezone.utc)
|
||||
else:
|
||||
self.edited_at = datetime.datetime.strptime(edited, "%Y-%m-%dT%H:%M:%S.%f%z")
|
||||
self.edited_at = parse_timestamp(edited)
|
||||
|
||||
@property
|
||||
def mentions(self) -> list[User | Member]:
|
||||
"""The users or members that where mentioned in the message
|
||||
|
||||
Returns: list[Union[:class:`Member`, :class:`User`]]
|
||||
"""
|
||||
|
||||
mentions: list[User | Member] = []
|
||||
|
||||
if self.server_id:
|
||||
for mention in self.raw_mentions:
|
||||
try:
|
||||
mentions.append(self.server.get_member(mention))
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
else:
|
||||
for mention in self.raw_mentions:
|
||||
try:
|
||||
mentions.append(self.state.get_user(mention))
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
return mentions
|
||||
|
||||
async def edit(self, *, content: Optional[str] = None, embeds: Optional[list[SendableEmbed]] = None) -> None:
|
||||
"""Edits the message. The bot can only edit its own message
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
content: :class:`str`
|
||||
The new content of the message
|
||||
embeds: list[:class:`SendableEmbed`]
|
||||
The new embeds of the message
|
||||
"""
|
||||
|
||||
new_embeds = [embed.to_dict() for embed in embeds] if embeds else None
|
||||
@@ -169,15 +192,33 @@ class Message(Ulid):
|
||||
return self.channel.send(*args, **kwargs, replies=[MessageReply(self, mention)])
|
||||
|
||||
async def add_reaction(self, emoji: str) -> None:
|
||||
"""Adds a reaction to the message
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
emoji: :class:`str`
|
||||
The emoji to add as a reaction
|
||||
"""
|
||||
await self.state.http.add_reaction(self.channel.id, self.id, emoji)
|
||||
|
||||
async def remove_reaction(self, emoji: str, user: Optional[User] = None, remove_all: bool = False) -> None:
|
||||
"""Removes a reaction from the message, this can remove either a specific users, the current users reaction or all of a specific emoji
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
emoji: :class:`str`
|
||||
The emoji to remove
|
||||
user: Optional[:class:`User`]
|
||||
The user to use for removing a reaction from
|
||||
remove_all: bool
|
||||
Whether or not to remove all reactions for that specific emoji
|
||||
"""
|
||||
await self.state.http.remove_reaction(self.channel.id, self.id, emoji, user.id if user else None, remove_all)
|
||||
|
||||
async def remove_all_reactions(self) -> None:
|
||||
"""Removes all reactions from the message"""
|
||||
await self.state.http.remove_all_reactions(self.channel.id, self.id)
|
||||
|
||||
|
||||
@property
|
||||
def server(self) -> Server:
|
||||
""":class:`Server` The server this voice channel belongs too
|
||||
@@ -201,12 +242,12 @@ class MessageReply:
|
||||
"""
|
||||
__slots__ = ("message", "mention")
|
||||
|
||||
def __init__(self, message: Message, mention: bool = False):
|
||||
self.message: Message = message
|
||||
def __init__(self, message: Ulid, mention: bool = False):
|
||||
self.message: Ulid = message
|
||||
self.mention: bool = mention
|
||||
|
||||
def to_dict(self) -> MessageReplyPayload:
|
||||
return { "id": self.message.id, "mention": self.mention }
|
||||
return {"id": self.message.id, "mention": self.mention}
|
||||
|
||||
class Masquerade:
|
||||
"""represents a message's masquerade.
|
||||
|
||||
+21
-4
@@ -9,6 +9,7 @@ if TYPE_CHECKING:
|
||||
from .file import File
|
||||
from .message import Masquerade, Message, MessageInteractions, MessageReply
|
||||
from .state import State
|
||||
from .types.http import MessageWithUserData
|
||||
|
||||
|
||||
__all__ = ("Messageable",)
|
||||
@@ -86,6 +87,18 @@ class Messageable:
|
||||
payload = await self.state.http.fetch_message(await self._get_channel_id(), message_id)
|
||||
return Message(payload, self.state)
|
||||
|
||||
def _add_missing_users(self, payload: MessageWithUserData):
|
||||
for user in payload["users"]:
|
||||
if user["_id"] not in self.state.users:
|
||||
self.state.add_user(user)
|
||||
|
||||
if members := payload.get("members", []):
|
||||
server = self.state.get_server(members[0]["_id"]["server"])
|
||||
|
||||
for member in members:
|
||||
if member["_id"]["user"] not in server._members:
|
||||
server._add_member(member)
|
||||
|
||||
async def history(self, *, sort: SortType = SortType.latest, limit: int = 100, before: Optional[str] = None, after: Optional[str] = None, nearby: Optional[str] = None) -> list[Message]:
|
||||
"""Fetches multiple messages from the channel's history
|
||||
|
||||
@@ -109,8 +122,10 @@ class Messageable:
|
||||
"""
|
||||
from .message import Message
|
||||
|
||||
payloads = await self.state.http.fetch_messages(await self._get_channel_id(), sort=sort, limit=limit, before=before, after=after, nearby=nearby)
|
||||
return [Message(payload, self.state) for payload in payloads]
|
||||
payload = await self.state.http.fetch_messages(await self._get_channel_id(), sort=sort, limit=limit, before=before, after=after, nearby=nearby, include_users=True)
|
||||
self._add_missing_users(payload)
|
||||
|
||||
return [Message(msg, self.state) for msg in payload["messages"]]
|
||||
|
||||
async def search(self, query: str, *, sort: SortType = SortType.latest, limit: int = 100, before: Optional[str] = None, after: Optional[str] = None) -> list[Message]:
|
||||
"""searches the channel for a query
|
||||
@@ -135,8 +150,10 @@ class Messageable:
|
||||
"""
|
||||
from .message import Message
|
||||
|
||||
payloads = await self.state.http.search_messages(await self._get_channel_id(), query, sort=sort, limit=limit, before=before, after=after)
|
||||
return [Message(payload, self.state) for payload in payloads]
|
||||
payload = await self.state.http.search_messages(await self._get_channel_id(), query, sort=sort, limit=limit, before=before, after=after, include_users=True)
|
||||
self._add_missing_users(payload)
|
||||
|
||||
return [Message(msg, self.state) for msg in payload["messages"]]
|
||||
|
||||
async def delete_messages(self, messages: list[Message]) -> None:
|
||||
"""Bulk deletes messages from the channel
|
||||
|
||||
@@ -178,11 +178,11 @@ class PermissionsOverwrite:
|
||||
|
||||
def __setattr__(self, key: str, value: Any) -> None:
|
||||
if key in Permissions.FLAG_NAMES:
|
||||
if key is True:
|
||||
if value is True:
|
||||
setattr(self._allow, key, True)
|
||||
super().__setattr__(key, True)
|
||||
|
||||
elif key is False:
|
||||
elif value is False:
|
||||
setattr(self._deny, key, True)
|
||||
super().__setattr__(key, False)
|
||||
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from revolt.enums import ChannelType
|
||||
|
||||
from .permissions import Permissions
|
||||
from .server import Server
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .channel import Channel, DMChannel, GroupDMChannel, ServerChannel
|
||||
from .member import Member
|
||||
from .server import Server
|
||||
|
||||
|
||||
def calculate_permissions(member: Member, target: Server | Channel) -> Permissions:
|
||||
if member.privileged:
|
||||
return Permissions.all()
|
||||
|
||||
from .server import Server
|
||||
|
||||
if isinstance(target, Server):
|
||||
if target.owner_id == member.id:
|
||||
return Permissions.all()
|
||||
@@ -26,7 +28,7 @@ def calculate_permissions(member: Member, target: Server | Channel) -> Permissio
|
||||
for role in member.roles:
|
||||
permissions = (permissions | role.permissions._allow) & (~role.permissions._deny)
|
||||
|
||||
if member.current_timeout and member.current_timeout > datetime.now():
|
||||
if member.current_timeout and member.current_timeout > datetime.now(timezone.utc):
|
||||
permissions = permissions & Permissions.default_view_only()
|
||||
|
||||
return permissions
|
||||
|
||||
+47
-11
@@ -8,22 +8,25 @@ from .invite import Invite
|
||||
from .permissions import Permissions
|
||||
from .role import Role
|
||||
from .utils import Ulid
|
||||
from .channel import Channel, TextChannel, VoiceChannel
|
||||
from .member import Member
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .channel import Channel, TextChannel, VoiceChannel
|
||||
from .emoji import Emoji
|
||||
from .file import File
|
||||
from .member import Member
|
||||
from .state import State
|
||||
from .types import Ban
|
||||
from .types import Category as CategoryPayload
|
||||
from .types import File as FilePayload
|
||||
from .types import Server as ServerPayload
|
||||
from .types import SystemMessagesConfig
|
||||
from .types import Member as MemberPayload
|
||||
|
||||
__all__ = ("Server", "SystemMessages", "ServerBan")
|
||||
|
||||
class SystemMessages:
|
||||
"""Holds all the configuration for the server's system message channels"""
|
||||
|
||||
def __init__(self, data: SystemMessagesConfig, state: State):
|
||||
self.state: State = state
|
||||
self.user_joined_id: str | None = data.get("user_joined")
|
||||
@@ -33,6 +36,13 @@ class SystemMessages:
|
||||
|
||||
@property
|
||||
def user_joined(self) -> Optional[TextChannel]:
|
||||
"""The channel which user join messages get sent in
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[:class:`TextChannel`]
|
||||
The channel
|
||||
"""
|
||||
if not self.user_joined_id:
|
||||
return
|
||||
|
||||
@@ -42,6 +52,13 @@ class SystemMessages:
|
||||
|
||||
@property
|
||||
def user_left(self) -> Optional[TextChannel]:
|
||||
"""The channel which user leave messages get sent in
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[:class:`TextChannel`]
|
||||
The channel
|
||||
"""
|
||||
if not self.user_left_id:
|
||||
return
|
||||
|
||||
@@ -51,6 +68,13 @@ class SystemMessages:
|
||||
|
||||
@property
|
||||
def user_kicked(self) -> Optional[TextChannel]:
|
||||
"""The channel which user kick messages get sent in
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[:class:`TextChannel`]
|
||||
The channel
|
||||
"""
|
||||
if not self.user_kicked_id:
|
||||
return
|
||||
|
||||
@@ -60,6 +84,13 @@ class SystemMessages:
|
||||
|
||||
@property
|
||||
def user_banned(self) -> Optional[TextChannel]:
|
||||
"""The channel which user ban messages get sent in
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[:class:`TextChannel`]
|
||||
The channel
|
||||
"""
|
||||
if not self.user_banned_id:
|
||||
return
|
||||
|
||||
@@ -154,6 +185,12 @@ class Server(Ulid):
|
||||
if channels is not None:
|
||||
self._channels = {channel_id: self.state.get_channel(channel_id) for channel_id in channels}
|
||||
|
||||
def _add_member(self, payload: MemberPayload) -> Member:
|
||||
member = Member(payload, self, self.state)
|
||||
self._members[member.id] = member
|
||||
|
||||
return member
|
||||
|
||||
@property
|
||||
def roles(self) -> list[Role]:
|
||||
"""list[:class:`Role`] Gets all roles in the server in decending order"""
|
||||
@@ -166,7 +203,7 @@ class Server(Ulid):
|
||||
|
||||
@property
|
||||
def channels(self) -> list[Channel]:
|
||||
"""list[:class:`Member`] Gets all channels in the server"""
|
||||
"""list[:class:`Channel`] Gets all channels in the server"""
|
||||
return list(self._channels.values())
|
||||
|
||||
@property
|
||||
@@ -273,12 +310,11 @@ class Server(Ulid):
|
||||
|
||||
async def set_default_permissions(self, permissions: Permissions) -> None:
|
||||
"""Sets the default server permissions.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
server_permissions: Optional[:class:`ServerPermissions`]
|
||||
permissions: :class:`Permissions`
|
||||
The new default server permissions
|
||||
channel_permissions: Optional[:class:`ChannelPermissions`]
|
||||
the new default channel permissions
|
||||
"""
|
||||
|
||||
await self.state.http.set_server_default_permissions(self.id, permissions.value)
|
||||
@@ -333,7 +369,7 @@ class Server(Ulid):
|
||||
channel = self.state.add_channel(payload)
|
||||
self._channels[channel.id] = channel
|
||||
|
||||
return cast("VoiceChannel", channel)
|
||||
return cast(VoiceChannel, channel)
|
||||
|
||||
async def fetch_invites(self) -> list[Invite]:
|
||||
"""Fetches all invites in the server
|
||||
@@ -364,11 +400,11 @@ class Server(Ulid):
|
||||
return Member(payload, self, self.state)
|
||||
|
||||
async def fetch_bans(self) -> list[ServerBan]:
|
||||
"""Fetches all invites in the server
|
||||
"""Fetches all bans in the server
|
||||
|
||||
Returns
|
||||
--------
|
||||
list[:class:`Invite`]
|
||||
list[:class:`ServerBan`]
|
||||
"""
|
||||
payload = await self.state.http.fetch_bans(self.id)
|
||||
|
||||
@@ -390,7 +426,7 @@ class Server(Ulid):
|
||||
"""
|
||||
payload = await self.state.http.create_role(self.id, name)
|
||||
|
||||
return Role(payload, name, self, self.state)
|
||||
return Role(payload["role"], payload["id"], self, self.state)
|
||||
|
||||
async def create_emoji(self, name: str, file: File, *, nsfw: bool = False) -> Emoji:
|
||||
"""Creates an emoji
|
||||
@@ -414,7 +450,7 @@ class ServerBan:
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
reason: Optional[:class:str`]
|
||||
reason: Optional[:class:`str`]
|
||||
The reason the user was banned
|
||||
server: :class:`Server`
|
||||
The server the user was banned in
|
||||
|
||||
+1
-5
@@ -61,8 +61,6 @@ class State:
|
||||
raise LookupError from None
|
||||
|
||||
def add_user(self, payload: UserPayload) -> User:
|
||||
|
||||
|
||||
user = User(payload, self)
|
||||
|
||||
if payload.get("relationship") == "User":
|
||||
@@ -73,10 +71,8 @@ class State:
|
||||
|
||||
def add_member(self, server_id: str, payload: MemberPayload) -> Member:
|
||||
server = self.get_server(server_id)
|
||||
member = Member(payload, server, self)
|
||||
server._members[member.id] = member
|
||||
|
||||
return member
|
||||
return server._add_member(payload)
|
||||
|
||||
def add_channel(self, payload: ChannelPayload) -> Channel:
|
||||
channel = channel_factory(payload, self)
|
||||
|
||||
+11
-4
@@ -4,8 +4,7 @@ from typing import TYPE_CHECKING, Literal, TypedDict, Union
|
||||
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
from .channel import (Channel, DMChannel, GroupDMChannel, SavedMessages,
|
||||
TextChannel, VoiceChannel)
|
||||
from .channel import Channel, DMChannel, GroupDMChannel, SavedMessages, TextChannel, VoiceChannel
|
||||
from .message import Message
|
||||
from .permissions import Overwrite
|
||||
|
||||
@@ -16,8 +15,7 @@ if TYPE_CHECKING:
|
||||
from .file import File
|
||||
from .member import Member, MemberID
|
||||
from .server import Server, SystemMessagesConfig
|
||||
from .user import Status, User, UserProfile
|
||||
|
||||
from .user import Status, User, UserProfile, UserRelation
|
||||
|
||||
__all__ = (
|
||||
"BasePayload",
|
||||
@@ -146,6 +144,7 @@ class ServerMemberUpdateEventPayloadData(TypedDict, total=False):
|
||||
nickname: str
|
||||
avatar: File
|
||||
roles: list[str]
|
||||
timeout: str | int
|
||||
|
||||
class ServerMemberUpdateEventPayload(BasePayload):
|
||||
id: MemberID
|
||||
@@ -179,6 +178,14 @@ class UserUpdateEventPayloadData(TypedDict):
|
||||
avatar: NotRequired[File]
|
||||
online: NotRequired[bool]
|
||||
profile: NotRequired[UserProfile]
|
||||
username: NotRequired[str]
|
||||
display_name: NotRequired[str]
|
||||
relations: NotRequired[list[UserRelation]]
|
||||
badges: NotRequired[int]
|
||||
online: NotRequired[bool]
|
||||
flags: NotRequired[int]
|
||||
discriminator: NotRequired[str]
|
||||
privileged: NotRequired[bool]
|
||||
|
||||
class UserUpdateEventPayload(BasePayload):
|
||||
id: str
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, TypedDict
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .member import Member
|
||||
from .message import Message
|
||||
from .user import User
|
||||
from .role import Role
|
||||
|
||||
|
||||
__all__ = (
|
||||
@@ -14,6 +16,7 @@ __all__ = (
|
||||
"Autumn",
|
||||
"GetServerMembers",
|
||||
"MessageWithUserData",
|
||||
"CreateRole",
|
||||
)
|
||||
|
||||
|
||||
@@ -48,5 +51,9 @@ class GetServerMembers(TypedDict):
|
||||
|
||||
class MessageWithUserData(TypedDict):
|
||||
messages: list[Message]
|
||||
members: list[Member]
|
||||
members: NotRequired[list[Member]]
|
||||
users: list[User]
|
||||
|
||||
class CreateRole(TypedDict):
|
||||
id: str
|
||||
role: Role
|
||||
@@ -20,4 +20,4 @@ class Member(TypedDict):
|
||||
avatar: NotRequired[File]
|
||||
roles: NotRequired[list[str]]
|
||||
joined_at: int | str
|
||||
timeout: NotRequired[str]
|
||||
timeout: NotRequired[str | int]
|
||||
|
||||
@@ -23,6 +23,7 @@ __all__ = (
|
||||
"Interactions",
|
||||
"Message",
|
||||
"MessageReplyPayload",
|
||||
"SystemMessageContent",
|
||||
)
|
||||
|
||||
class UserAddContent(TypedDict):
|
||||
@@ -71,12 +72,13 @@ class Message(TypedDict):
|
||||
_id: str
|
||||
channel: str
|
||||
author: str
|
||||
content: Union[str, SystemMessageContent]
|
||||
content: str
|
||||
system: NotRequired[SystemMessageContent]
|
||||
attachments: NotRequired[list[File]]
|
||||
embeds: NotRequired[list[Embed]]
|
||||
mentions: NotRequired[list[str]]
|
||||
replies: NotRequired[list[str]]
|
||||
edited: NotRequired[str]
|
||||
edited: NotRequired[str | int]
|
||||
masquerade: NotRequired[Masquerade]
|
||||
interactions: NotRequired[Interactions]
|
||||
reactions: dict[str, list[str]]
|
||||
|
||||
+85
-8
@@ -3,8 +3,10 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, NamedTuple, Optional, Union
|
||||
from weakref import WeakValueDictionary
|
||||
|
||||
from revolt.types.user import UserRelation
|
||||
|
||||
from .asset import Asset, PartialAsset
|
||||
from .channel import DMChannel, GroupDMChannel
|
||||
from .channel import DMChannel, GroupDMChannel, SavedMessageChannel
|
||||
from .enums import PresenceType, RelationshipType
|
||||
from .flags import UserBadges
|
||||
from .messageable import Messageable
|
||||
@@ -76,10 +78,10 @@ class User(Messageable, Ulid):
|
||||
self.state = state
|
||||
self._members: WeakValueDictionary[str, Member] = WeakValueDictionary() # we store all member versions of this user to avoid having to check every guild when needing to update.
|
||||
self.id: str = data["_id"]
|
||||
self.discriminator = data["discriminator"]
|
||||
self.display_name = data.get("display_name")
|
||||
self.discriminator: str = data["discriminator"]
|
||||
self.display_name: str | None = data.get("display_name")
|
||||
self.original_name: str = data["username"]
|
||||
self.dm_channel: DMChannel | None = None
|
||||
self.dm_channel: DMChannel | SavedMessageChannel | None = None
|
||||
|
||||
bot = data.get("bot")
|
||||
|
||||
@@ -107,6 +109,7 @@ class User(Messageable, Ulid):
|
||||
user = state.get_user(relation["_id"])
|
||||
if user:
|
||||
relations.append(Relation(RelationshipType(relation["status"]), user))
|
||||
|
||||
self.relations: list[Relation] = relations
|
||||
|
||||
relationship = data.get("relationship")
|
||||
@@ -175,9 +178,13 @@ class User(Messageable, Ulid):
|
||||
async def _get_channel_id(self):
|
||||
if not self.dm_channel:
|
||||
payload = await self.state.http.open_dm(self.id)
|
||||
self.dm_channel = DMChannel(payload, self.state)
|
||||
|
||||
return self.id
|
||||
if payload["channel_type"] == "SavedMessages":
|
||||
self.dm_channel = SavedMessageChannel(payload, self.state)
|
||||
else:
|
||||
self.dm_channel = DMChannel(payload, self.state)
|
||||
|
||||
return self.dm_channel.id
|
||||
|
||||
@property
|
||||
def owner(self) -> User:
|
||||
@@ -203,7 +210,21 @@ class User(Messageable, Ulid):
|
||||
""":class:`str`: Returns a string that allows you to mention the given user."""
|
||||
return f"<@{self.id}>"
|
||||
|
||||
def _update(self, *, status: Optional[StatusPayload] = None, profile: Optional[UserProfileData] = None, avatar: Optional[File] = None, online: Optional[bool] = None):
|
||||
def _update(
|
||||
self,
|
||||
*,
|
||||
status: Optional[StatusPayload] = None,
|
||||
profile: Optional[UserProfileData] = None,
|
||||
avatar: Optional[File] = None,
|
||||
online: Optional[bool] = None,
|
||||
display_name: Optional[str] = None,
|
||||
relations: Optional[list[UserRelation]] = None,
|
||||
badges: Optional[int] = None,
|
||||
flags: Optional[int] = None,
|
||||
discriminator: Optional[str] = None,
|
||||
privileged: Optional[bool] = None,
|
||||
username: Optional[str] = None
|
||||
) -> None:
|
||||
if status is not None:
|
||||
presence = status.get("presence")
|
||||
self.status = Status(status.get("text"), PresenceType(presence) if presence else None)
|
||||
@@ -222,11 +243,52 @@ class User(Messageable, Ulid):
|
||||
if online is not None:
|
||||
self.online = online
|
||||
|
||||
if display_name is not None:
|
||||
self.display_name = display_name
|
||||
|
||||
if relations is not None:
|
||||
new_relations: list[Relation] = []
|
||||
|
||||
for relation in relations:
|
||||
user = self.state.get_user(relation["_id"])
|
||||
if user:
|
||||
new_relations.append(Relation(RelationshipType(relation["status"]), user))
|
||||
|
||||
self.relations = new_relations
|
||||
|
||||
if badges is not None:
|
||||
self.badges = UserBadges(badges)
|
||||
|
||||
if flags is not None:
|
||||
self.flags = flags
|
||||
|
||||
if discriminator is not None:
|
||||
self.discriminator = discriminator
|
||||
|
||||
if privileged is not None:
|
||||
self.privileged = privileged
|
||||
|
||||
if username is not None:
|
||||
self.original_name = username
|
||||
|
||||
# update user infomation for all members
|
||||
|
||||
if self.__class__ is User:
|
||||
for member in self._members.values():
|
||||
User._update(member, status=status, profile=profile, avatar=avatar, online=online)
|
||||
User._update(
|
||||
member,
|
||||
status=status,
|
||||
profile=profile,
|
||||
avatar=avatar,
|
||||
online=online,
|
||||
display_name=display_name,
|
||||
relations=relations,
|
||||
badges=badges,
|
||||
flags=flags,
|
||||
discriminator=discriminator,
|
||||
privileged=privileged,
|
||||
username=username
|
||||
)
|
||||
|
||||
async def default_avatar(self) -> bytes:
|
||||
"""Returns the default avatar for this user
|
||||
@@ -288,3 +350,18 @@ class User(Messageable, Ulid):
|
||||
return self._members[server.id]
|
||||
except IndexError:
|
||||
raise LookupError from None
|
||||
|
||||
async def open_dm(self) -> DMChannel | SavedMessageChannel:
|
||||
"""Opens a dm with the user, if this user is the current user this will return :class:`SavedMessageChannel`
|
||||
|
||||
.. note:: using this function is discouraged as :meth:`User.send` does this implicitally.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Union[:class:`DMChannel`, :class:`SavedMessageChannel`]
|
||||
"""
|
||||
|
||||
await self._get_channel_id()
|
||||
|
||||
assert self.dm_channel
|
||||
return self.dm_channel
|
||||
|
||||
+37
-1
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import inspect
|
||||
from contextlib import asynccontextmanager
|
||||
@@ -8,7 +10,7 @@ import ulid
|
||||
from aiohttp import ClientSession
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
__all__ = ("Missing", "copy_doc", "maybe_coroutine", "get", "client_session")
|
||||
__all__ = ("_Missing", "Missing", "copy_doc", "maybe_coroutine", "get", "client_session", "parse_timestamp")
|
||||
|
||||
class _Missing:
|
||||
def __repr__(self) -> str:
|
||||
@@ -45,11 +47,39 @@ async def maybe_coroutine(func: Callable[P, Union[R_T, Coroutine[Any, Any, R_T]]
|
||||
|
||||
|
||||
class Ulid:
|
||||
"""Base class for any revolt object with an id
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`str`
|
||||
The id of the object
|
||||
"""
|
||||
id: str
|
||||
|
||||
@property
|
||||
def created_at(self) -> datetime.datetime:
|
||||
"""Returns a datetime for when the object was created according to the id
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`datetime.datetime`
|
||||
The datetime of the creation date and time
|
||||
"""
|
||||
return ulid.from_str(self.id).timestamp().datetime
|
||||
|
||||
class Object(Ulid):
|
||||
"""Class to mock objects with an id
|
||||
|
||||
.. note::
|
||||
This does not validate or guarantee the id is correct, you must handle this yourself
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
id: :class:`str`
|
||||
The ULID id to mock
|
||||
"""
|
||||
def __init__(self, id: str):
|
||||
self.id = id
|
||||
|
||||
def get(iterable: Iterable[T], **attrs: Any) -> T:
|
||||
"""A convenience function to help get a value from an iterable with a specific attribute
|
||||
@@ -115,3 +145,9 @@ async def client_session():
|
||||
yield session
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
def parse_timestamp(timestamp: int | str) -> datetime.datetime:
|
||||
if isinstance(timestamp, int):
|
||||
return datetime.datetime.fromtimestamp(timestamp / 1000, tz=datetime.timezone.utc)
|
||||
else:
|
||||
return datetime.datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f%z")
|
||||
|
||||
+32
-20
@@ -6,6 +6,7 @@ import time
|
||||
from copy import copy
|
||||
from typing import TYPE_CHECKING, Callable, NamedTuple, cast
|
||||
|
||||
from .errors import RevoltError
|
||||
from . import utils
|
||||
from .channel import GroupDMChannel, TextChannel, VoiceChannel
|
||||
from .enums import RelationshipType
|
||||
@@ -81,7 +82,7 @@ class WebsocketHandler:
|
||||
|
||||
async def send_payload(self, payload: BasePayload) -> None:
|
||||
if use_msgpack:
|
||||
await self.websocket.send_bytes(msgpack.packb(payload))
|
||||
await self.websocket.send_bytes(msgpack.packb(payload)) # type: ignore
|
||||
else:
|
||||
await self.websocket.send_str(json.dumps(payload))
|
||||
|
||||
@@ -102,8 +103,9 @@ class WebsocketHandler:
|
||||
async def handle_event(self, payload: BasePayload) -> None:
|
||||
event_type = payload["type"].lower()
|
||||
logger.debug("Recieved event %s %s", event_type, payload)
|
||||
|
||||
try:
|
||||
if event_type != "ready":
|
||||
if event_type not in ["ready", "notfound"]:
|
||||
await self.ready.wait()
|
||||
|
||||
func = getattr(self, f"handle_{event_type}")
|
||||
@@ -115,6 +117,9 @@ class WebsocketHandler:
|
||||
async def handle_authenticated(self, _: BasePayload) -> None:
|
||||
logger.info("Successfully authenticated")
|
||||
|
||||
async def handle_notfound(self, _: BasePayload) -> None:
|
||||
raise RevoltError("Invalid token")
|
||||
|
||||
async def handle_ready(self, payload: ReadyEventPayload) -> None:
|
||||
for user_payload in payload["users"]:
|
||||
user = self.state.add_user(user_payload)
|
||||
@@ -160,9 +165,10 @@ class WebsocketHandler:
|
||||
if server_id := message.channel.server_id:
|
||||
await self._wait_for_server_ready(server_id)
|
||||
|
||||
before = copy(message)
|
||||
message._update(**payload["data"])
|
||||
|
||||
self.dispatch("message_update", message)
|
||||
self.dispatch("message_update", before, message)
|
||||
|
||||
async def handle_messagedelete(self, payload: MessageDeleteEventPayload) -> None:
|
||||
self.dispatch("raw_message_delete", payload)
|
||||
@@ -312,7 +318,7 @@ class WebsocketHandler:
|
||||
|
||||
self.dispatch("member_join", member)
|
||||
|
||||
async def handle_memberleave(self, payload: ServerMemberLeaveEventPayload) -> None:
|
||||
async def handle_servermemberleave(self, payload: ServerMemberLeaveEventPayload) -> None:
|
||||
await self._wait_for_server_ready(payload["id"])
|
||||
|
||||
server = self.state.get_server(payload["id"])
|
||||
@@ -390,7 +396,7 @@ class WebsocketHandler:
|
||||
if server := self.state.get_channel(payload["channel_id"]).server_id:
|
||||
await self._wait_for_server_ready(server)
|
||||
|
||||
self.dispatch("raw_reaction_add", payload["channel_id"], payload["id"], payload["user_id"], payload["emoji_id"])
|
||||
self.dispatch("raw_reaction_add", payload)
|
||||
|
||||
try:
|
||||
message = utils.get(self.state.messages, id=payload["id"])
|
||||
@@ -407,7 +413,7 @@ class WebsocketHandler:
|
||||
if server := self.state.get_channel(payload["channel_id"]).server_id:
|
||||
await self._wait_for_server_ready(server)
|
||||
|
||||
self.dispatch("raw_reaction_remove", payload["channel_id"], payload["id"], payload["user_id"], payload["emoji_id"])
|
||||
self.dispatch("raw_reaction_remove", payload)
|
||||
|
||||
try:
|
||||
message = utils.get(self.state.messages, id=payload["id"])
|
||||
@@ -423,7 +429,7 @@ class WebsocketHandler:
|
||||
if server := self.state.get_channel(payload["channel_id"]).server_id:
|
||||
await self._wait_for_server_ready(server)
|
||||
|
||||
self.dispatch("raw_reaction_clear", payload["channel_id"], payload["id"], payload["emoji_id"])
|
||||
self.dispatch("raw_reaction_clear", payload)
|
||||
|
||||
try:
|
||||
message = utils.get(self.state.messages, id=payload["id"])
|
||||
@@ -459,26 +465,32 @@ class WebsocketHandler:
|
||||
|
||||
self.dispatch("bulk_message_delete", messages)
|
||||
|
||||
async def start(self) -> None:
|
||||
async def start(self, reconnect: bool) -> None:
|
||||
if use_msgpack:
|
||||
url = f"{self.ws_url}?format=msgpack"
|
||||
else:
|
||||
url = f"{self.ws_url}?format=json"
|
||||
|
||||
self.websocket = await self.session.ws_connect(url) # type: ignore
|
||||
await self.send_authenticate()
|
||||
asyncio.create_task(self.heartbeat())
|
||||
while True:
|
||||
self.websocket = await self.session.ws_connect(url) # type: ignore
|
||||
await self.send_authenticate()
|
||||
hb = asyncio.create_task(self.heartbeat())
|
||||
|
||||
async for msg in self.websocket:
|
||||
msg = cast(WSMessage, msg) # aiohttp doesnt use NamedTuple so the type info is missing
|
||||
async for msg in self.websocket:
|
||||
msg = cast(WSMessage, msg) # aiohttp doesnt use NamedTuple so the type info is missing
|
||||
|
||||
if use_msgpack:
|
||||
data = cast(bytes, msg.data)
|
||||
if use_msgpack:
|
||||
data = cast(bytes, msg.data)
|
||||
|
||||
payload = msgpack.unpackb(data)
|
||||
else:
|
||||
data = cast(str, msg.data)
|
||||
payload = msgpack.unpackb(data) # type: ignore
|
||||
else:
|
||||
data = cast(str, msg.data)
|
||||
|
||||
payload = json.loads(data)
|
||||
payload = json.loads(data)
|
||||
|
||||
self.loop.create_task(self.handle_event(payload))
|
||||
self.loop.create_task(self.handle_event(payload))
|
||||
|
||||
hb.cancel()
|
||||
|
||||
if not reconnect:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user