8 Commits

Author SHA1 Message Date
Zomatree efce2aedac add archived notice 2025-12-11 01:22:41 +00:00
green. ca37846df7 fix: dont compare offset-naive and offset-aware datetimes (#89) 2024-11-29 21:54:44 +00:00
Zomatree 86c07a6d6b fix: cleanup docs to include missing things 2024-10-29 17:02:40 +00:00
Zomatree a8be358339 fix: various bugs
Closes #85
Closes #82
2024-10-29 15:57:26 +00:00
Zomatree e34ca0afda Greedy support, closes #77 2024-06-11 21:33:11 +01:00
IAmTomahawkx 060e6ea32a fix: prevent new space from appearing if buffer is depleted 2024-05-29 21:00:20 +01:00
Zomatree 2dabb0dbb8 fix patch for older python versions 2024-05-29 19:11:56 +01:00
Zomatree 48e0f0fb2e better handling of optional args 2024-05-29 00:18:00 +01:00
24 changed files with 262 additions and 49 deletions
+2
View File
@@ -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/).
View File
+8 -2
View File
@@ -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:
+91 -1
View File
@@ -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
View File
@@ -16,7 +16,7 @@ classifiers = [
"Programming Language :: Python :: 3 :: Only",
]
dependencies = [
"aiohttp==3.9.*",
"aiohttp==3.10.*",
"ulid-py==1.1.*",
"aenum==3.1.*",
"typing_extensions>=4.4.0"
+7 -2
View File
@@ -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"):
@@ -290,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()
@@ -300,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
"""
+2
View File
@@ -52,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):
+1 -1
View File
@@ -34,7 +34,7 @@ def check(check: Check[ClientT_D]) -> Callable[[T], T]:
checks.append(check)
func._checks = checks # type: ignore
return func
return func # type: ignore
return inner
+36 -6
View File
@@ -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
@@ -42,8 +44,8 @@ class CommandsMeta(type):
for base in reversed(self.__mro__):
for value in base.__dict__.values():
if isinstance(value, Command) and value.parent is None:
commands.append(value)
if isinstance(value, Command) and value.parent is None: # type: ignore
commands.append(value) # type: ignore
self._commands = commands
@@ -79,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] = {}
@@ -104,7 +134,7 @@ 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]]:
+24 -12
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from typing import Any, Callable, Coroutine, Generic, Optional, TypeVar
from typing import Any, Callable, Coroutine, Generic, Optional, TypeVar
from typing_extensions import ParamSpec
from revolt.errors import RevoltError
@@ -29,9 +29,9 @@ class CogMeta(type):
for key, value in base.__dict__.items():
if isinstance(value, Command):
for extra_key, extra_value in extras.items():
setattr(value, extra_key, extra_value)
setattr(value, extra_key, extra_value) # type: ignore
commands.append(value)
commands.append(value) # type: ignore
elif event_name := getattr(value, "__listener_name", None):
listeners.setdefault(event_name, []).append(key)
@@ -57,26 +57,38 @@ class Cog(Generic[ClientT_D], metaclass=CogMeta):
def _inject(self, client: ClientT_D) -> None:
client.cogs[self.qualified_name] = self
for command in self._cog_commands:
command.cog = self
try:
for command in self._cog_commands:
command.cog = self
if command.parent is None:
client.add_command(command)
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))
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_D) -> None:
for name, command in client.all_commands.copy().items():
if command in self._cog_commands:
del client.all_commands[name]
try:
del client.all_commands[name]
except KeyError:
pass
for key, listeners in self._cog_listeners.items():
for listener_name in listeners:
client.listeners[key].remove(getattr(self, listener_name))
try:
client.listeners[key].remove(getattr(self, listener_name))
except ValueError:
pass
self.cog_unload()
+35 -5
View File
@@ -6,8 +6,16 @@ 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 maybe_coroutine
if sys.version_info >= (3, 10):
from types import UnionType
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
@@ -126,7 +134,7 @@ class Command(Generic[ClientT_Co_D]):
@classmethod
async def handle_origin(cls, context: Context[ClientT_Co_D], origin: Any, annotation: Any, arg: str) -> Any:
if origin is Union:
if origin in UnionTypes:
for converter in get_args(annotation):
try:
return await cls.convert_argument(arg, converter, context)
@@ -140,8 +148,20 @@ class Command(Generic[ClientT_Co_D]):
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)
@@ -175,6 +195,10 @@ class Command(Generic[ClientT_Co_D]):
except StopIteration:
if parameter.default is not parameter.empty:
arg = parameter.default
elif is_optional(parameter.annotation):
arg = None
else:
raise
@@ -192,7 +216,10 @@ class Command(Generic[ClientT_Co_D]):
except StopIteration:
if parameter.default is not parameter.empty:
arg = parameter.default
context.view.undo()
elif is_optional(parameter.annotation):
arg = None
else:
raise
@@ -251,6 +278,9 @@ class Command(Generic[ClientT_Co_D]):
return f"{' '.join(parents[::-1])} {self.name} {' '.join(parameters)}"
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,
+5 -1
View File
@@ -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})>")
@@ -120,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"]
+1 -1
View File
@@ -139,6 +139,6 @@ def cooldown(rate: int, per: int, *, bucket: BucketType | Callable[[Context[Clie
func._cooldown = CooldownMapping(rate, per) # type: ignore
func._bucket = bucket # type: ignore
return func
return func # type: ignore
return inner
+2
View File
@@ -8,6 +8,7 @@ __all__ = (
"NotBotOwner",
"NotServerOwner",
"ServerOnly",
"MissingPermissionsError",
"ConverterError",
"InvalidLiteralArgument",
"BadBoolArgument",
@@ -15,6 +16,7 @@ __all__ = (
"ChannelConverterError",
"UserConverterError",
"MemberConverterError",
"UnionConverterError",
"MissingSetup",
"CommandOnCooldown"
)
-1
View File
@@ -186,7 +186,6 @@ async def help_command_impl(client: ClientT_D, context: Context[ClientT_D], *arg
break
if isinstance(command, Group):
command = cast(Group[ClientT_D], command)
parent = command
else:
payload = await help_command.create_command_help(context, command)
+10 -1
View File
@@ -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()
+3 -1
View File
@@ -31,6 +31,8 @@ if TYPE_CHECKING:
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",)
T = TypeVar("T")
@@ -49,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)",
+2 -2
View File
@@ -149,14 +149,14 @@ class Message(Ulid):
if self.server_id:
for mention in self.raw_mentions:
try:
self.mentions.append(self.server.get_member(mention))
mentions.append(self.server.get_member(mention))
except LookupError:
pass
else:
for mention in self.raw_mentions:
try:
self.mentions.append(self.state.get_user(mention))
mentions.append(self.state.get_user(mention))
except LookupError:
pass
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from datetime import datetime
from datetime import datetime, timezone
from typing import TYPE_CHECKING, cast
from revolt.enums import ChannelType
@@ -28,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
+3 -4
View File
@@ -203,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
@@ -310,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)
-2
View File
@@ -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":
+24 -1
View File
@@ -47,14 +47,37 @@ 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"""
"""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
+1 -1
View File
@@ -318,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"])