This commit is contained in:
Zomatree
2021-08-29 21:19:09 +01:00
parent 96922d021e
commit 3380222054
23 changed files with 580 additions and 30 deletions
+2
View File
@@ -3,3 +3,5 @@
test.py
dist
*.egg-info
docs/_build
.vscode
+1
View File
@@ -0,0 +1 @@
requirements_file: docs_requirements.txt
+20
View File
@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+90
View File
@@ -0,0 +1,90 @@
.. currentmodule:: revolt
API Reference
===============
Client
~~~~~~~
.. autoclass:: Client
:members:
Asset
~~~~~~
.. autoclass:: Asset
:members:
Channel
~~~~~~~~
.. autoclass:: Channel
:members:
SavedMessageChannel
~~~~~~~~~~~~~~~~~~~~
.. autoclass:: SavedMessageChannel
:members:
DMChannel
~~~~~~~~~~
.. autoclass:: DMChannel
:members:
Embed
~~~~~~~~~~
.. autoclass:: Embed
:members:
File
~~~~~
.. autoclass:: File
:members:
Member
~~~~~~~
.. autoclass:: Member
:members:
Message
~~~~~~~
.. autoclass:: Message
:members:
Messageable
~~~~~~~~~~~~
.. autoclass:: Messageable
:members:
Permissions
~~~~~~~~~~~~
.. autoclass:: Permissions
:members:
Role
~~~~~
.. autoclass:: Role
:members:
Server
~~~~~~~~~~
.. autoclass:: Server
:members:
User
~~~~~~~~~~
.. autoclass:: User
:members:
+58
View File
@@ -0,0 +1,58 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
import os
import sys
import sphinx_nameko_theme
sys.path.insert(0, os.path.abspath('..'))
# -- Project information -----------------------------------------------------
project = 'Revolt.py'
copyright = '2021-present, Zomatree'
author = 'Zomatree'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx.ext.napoleon",
"sphinx.ext.autodoc",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'nameko'
html_theme_path = [sphinx_nameko_theme.get_html_theme_path()]
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
+7
View File
@@ -0,0 +1,7 @@
Welcome to Revolt.py's documentation!
====================================
.. toctree::
:maxdepth: 1
api
+35
View File
@@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd
+2
View File
@@ -0,0 +1,2 @@
Sphinx==4.1.2
sphinx-nameko-theme==0.0.3
+10 -2
View File
@@ -1,5 +1,13 @@
from .channel import TextChannel, VoiceChannel
from .asset import Asset
from .channel import DMChannel, Channel, GroupDMChannel, PartialTextChannel, SavedMessageChannel, VoiceChannel
from .client import Client
from .embed import Embed
from .errors import HTTPError, RevoltError, ServerError
from .file import File
from .member import Member
from .message import Message
from .messageable import Messageable
from .permissions import Permissions
from .role import Role
from .server import Server
from .user import User
from .message import Message
+29 -1
View File
@@ -1,12 +1,32 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Union
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .types import File as FilePayload
from .state import State
from io import IOBase
class Asset:
"""Represents a file on revolt
Attributes
-----------
id: str
The id of the asset
tag: str
The tag of the asset, this corrasponds to where the asset is used
size: int
Amount of bytes in the file
filename: str
The name of the file
height: Optional[int]
The height of the file if it is an image or video
width: Optional[int]
The width of the file if it is an image or video
content_type: str
The content type of the file
"""
def __init__(self, data: FilePayload, state: State):
self.id = data['_id']
self.tag = data['tag']
@@ -25,7 +45,15 @@ class Asset:
self.content_type = data["content_type"]
async def read(self) -> bytes:
"""Reads the files content into bytes"""
...
async def save(self, fp: IOBase):
"""Reads the files content and saves it to a file
Parameters
-----------
fp: IOBase
The file to write too.
"""
fp.write(await self.read())
+28 -13
View File
@@ -9,6 +9,17 @@ if TYPE_CHECKING:
from .state import State
class Channel:
"""Base class for all channels
Attributes
-----------
id: :class:`str`
The id of the channel
channel_type: Literal['SavedMessage', 'DirectMessage', 'Group', 'TextChannel', 'VoiceChannel']
The type of the channel
server: Optional[:class:`Server`]
The server the channel is part of
"""
def __init__(self, data: ChannelPayload, state: State):
self.state = state
self.id = data["_id"]
@@ -16,23 +27,28 @@ class Channel:
self.server = None
class SavedMessageChannel(Channel, Messageable):
"""The Saved Message Channel"""
def __init__(self, data: ChannelPayload, state: State):
super().__init__(data, state)
class DMChannel(Channel, Messageable):
"""A DM channel"""
def __init__(self, data: ChannelPayload, state: State):
super().__init__(data, state)
class GroupDMChannel(Channel, Messageable):
"""A group DM channel"""
def __init__(self, data: ChannelPayload, state: State):
super().__init__(data, state)
class TextChannel(Channel, Messageable):
"""A text channel"""
def __init__(self, data: ChannelPayload, state: State):
super().__init__(data, state)
Messageable.__init__(self, state)
class PartialTextChannel(Messageable):
"""A partial text channel, this will appear when the channel isnt cached"""
def __init__(self, channel_id: str, state: State):
super().__init__(state)
@@ -42,21 +58,20 @@ class PartialTextChannel(Messageable):
self.server = None
class VoiceChannel(Channel):
"""A voice channel"""
def __init__(self, data: ChannelPayload, state: State):
super().__init__(data, state)
def channel_factory(data: ChannelPayload) -> type[Channel]:
channel_type = data["channel_type"]
if channel_type == "SavedMessage":
return SavedMessageChannel
elif channel_type == "DirectMessage":
return DMChannel
elif channel_type == "Group":
return GroupDMChannel
elif channel_type == "TextChannel":
return TextChannel
elif channel_type == "VoiceChannel":
return VoiceChannel
def channel_factory(data: ChannelPayload, state) -> Channel:
if data["channel_type"] == "SavedMessage":
return SavedMessageChannel(data, state)
elif data["channel_type"] == "DirectMessage":
return DMChannel(data, state)
elif data["channel_type"] == "Group":
return GroupDMChannel(data, state)
elif data["channel_type"] == "TextChannel":
return TextChannel(data, state)
elif data["channel_type"] == "VoiceChannel":
return VoiceChannel(data, state)
else:
raise Exception
+75 -2
View File
@@ -23,6 +23,19 @@ if TYPE_CHECKING:
logger = logging.getLogger("revolt")
class Client:
"""The client for interacting with revolt
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
"""
def __init__(self, session: aiohttp.ClientSession, token: str, api_url: str = "https://api.revolt.chat", max_messages: int = 5000):
self.session = session
self.token = token
@@ -37,6 +50,15 @@ class Client:
self.listeners: dict[str, list[tuple[Callable[..., bool], asyncio.Future[Any]]]] = {}
def dispatch(self, event: str, *args: Any):
"""Dispatch an event, this is typically used for testing and internals.
Parameters
----------
event: class:`str`
The name of the event to dispatch, not including `on_`
args: :class:`Any`
The arguments passed to the event
"""
for check, future in self.listeners.pop(event, []):
if check(*args):
if len(args) == 1:
@@ -53,6 +75,7 @@ class Client:
return json.loads(await resp.text())
async def start(self):
"""Starts the client"""
api_info = await self.get_api_info()
self.api_info = api_info
@@ -62,19 +85,69 @@ class Client:
await self.websocket.start()
def get_user(self, id: str) -> Optional[User]:
"""Gets a user from the cache
Parameters
-----------
id: :class:`str`
The id of the user
Returns
--------
Optional[:class:`User`]
The user if found
"""
self.state.get_user(id)
def get_channel(self, id: str) -> Optional[Channel]:
"""Gets a channel from the cache
Parameters
-----------
id: :class:`str`
The id of the channel
Returns
--------
Optional[:class:`Channel`]
The channel if found
"""
self.state.get_channel(id)
def get_server(self, id: str) -> Optional[Server]:
"""Gets a server from the cache
Parameters
-----------
id: :class:`str`
The id of the server
Returns
--------
Optional[:class:`Server`]
The server if found
"""
self.state.get_server(id)
def wait_for(self, event: str, *, check: Optional[Callable[..., bool]] = None) -> asyncio.Future[Any]:
async def wait_for(self, event: str, *, check: Optional[Callable[..., bool]] = None) -> Any:
"""Waits for an event
Parameters
-----------
event: :class:`str`
The name of the event to wait for, without the `on_`
check: Optional[Callable[..., :class:`bool`]]
A function that says what event to wait_for, this function takes the same parameters as the event you are waiting for and should return a bool saying if that is the event you want
Returns
--------
Any
The parameters of the event
"""
if not check:
check = lambda *_: True
future = asyncio.get_running_loop().create_future()
self.listeners.setdefault(event, []).append((check, future))
return future
return await future
+11
View File
@@ -4,6 +4,17 @@ import io
import os
class File:
"""Respresents a file about to be uploaded to revolt
Parameters
-----------
file: Union[str, bytes]
The name of the file or the content of the file in bytes, text files will be need to be encoded
filename: Optional[str]
The filename of the file when being uploaded, this will default to the name of the file if one exists
spoiler: bool
Determines if the file will be a spoiler, this prefexes the filename with `SPOILER_`
"""
def __init__(self, file: Union[str, bytes], *, filename: Optional[str] = None, spoiler: bool = False):
if isinstance(file, str):
self.f = open(file, "rb")
+19 -2
View File
@@ -14,13 +14,30 @@ def flattern_user(member: Member, user: User):
setattr(member, attr, getattr(user, attr))
class Member(User):
"""Represents a member of a server, subclasses :class:`User`
Attributes
-----------
nickname: Optional[:class:`str`]
The nickname of the member if any
roles: list[:class:`Role`]
The roles of the member, ordered by the role's rank in decending order
server: :class:`Server`
The server the member belongs to
"""
def __init__(self, data: MemberPayload, server: Server, state: State):
user = state.get_user(data["_id"]["user"])
assert user
flattern_user(self, user)
self._state = state
self.id = data["_id"]["user"]
self.nickname = data.get("nickname")
self.roles = [server.get_role(role_id) for role_id in data.get("roles", [])]
roles = []
for role in (server.get_role(role_id) for role_id in data.get("roles", [])):
if role:
roles.append(role)
self.roles = sorted(roles, key=lambda role: role.rank, reverse=True)
self.server = server
+19
View File
@@ -11,6 +11,25 @@ if TYPE_CHECKING:
from .types import Message as MessagePayload
class Message:
"""Represents a message
Attributes
-----------
id: :class:`str`
The id of the message
content: :class:`str`
The content of the message, this will not include system message's content
attachments: list[:class:`Asset`]
The attachments of the message
embeds: list[:class:`Embed`]
The embeds of the message
channel: :class:`Messageable`
The channel the message was sent in
server: :class:`Server`
The server the message was sent in
author: Union[:class:`Member`, :class:`User`]
The author of the message, will be :class:`User` in DMs
"""
def __init__(self, data: MessagePayload, state: State):
self.state = state
+23
View File
@@ -10,12 +10,35 @@ if TYPE_CHECKING:
from .file import File
class Messageable:
"""Base class for all channels that you can send messages in
Attributes
-----------
id: :class:`str`
The id of the channel
"""
id: str
def __init__(self, state: State):
self.state = state
async def send(self, content: Optional[str] = None, embeds: Optional[list[Embed]] = None, embed: Optional[Embed] = None, attachments: Optional[list[File]] = None) -> Message:
"""Sends a message in a channel, you must send at least one of either `content`, `embeds` or `attachments`
Parameters
-----------
content: Optional[:class:`str`]
The content of the message, this will not include system message's content
attachments: Optional[list[:class:`File`]]
The attachments of the message
embeds: Optional[list[:class:`Embed`]]
The embeds of the message
Returns
--------
:class:`Message`
The message that was just sent
"""
if embed:
embeds = [embed]
+2
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
+17
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
from typing import TYPE_CHECKING
@@ -6,6 +8,21 @@ if TYPE_CHECKING:
from .types import Role as RolePayload
class Role:
"""Represents a role
Attributes
-----------
id: :class:`str`
The id of the role
name: :class:`str`
The name of the role
colour: :class:`str`
The colour of the role
hoist: :class:`bool`
Whether members with the role will display seperate from everyone else
rank: :class:`int`
The position of the role in the role heirarchy
"""
def __init__(self, data: RolePayload, role_id: str, state: State):
self.id = role_id
self.name = data["name"]
+76 -4
View File
@@ -2,6 +2,9 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from .permissions import Permissions
from .channel import channel_factory
if TYPE_CHECKING:
from .types import Server as ServerPayload
from .state import State
@@ -10,16 +13,85 @@ if TYPE_CHECKING:
from .channel import Channel
class Server:
"""Represents a server
Attributes
-----------
id: :class:`str`
The id of the server
name: :class:`str`
The name of the server
owner: Optional[:class:`Member`]
The owner of the server
"""
def __init__(self, data: ServerPayload, state: State):
self.state = state
self.id = data["_id"]
self.name = data["name"]
self.owner = state.get_member(self.id, data["owner"])
self.default_permissions = Permissions(data["default_permissions"])
self._members: dict[str, Member] = {}
self._roles: dict[str, Role] = {}
self._channels: dict[str, Channel] = {}
self._roles: dict[str, Role] = {role_id: Role(role, role_id, state) for role_id, role in data.get("roles", {}).items()}
channels = [channel_factory(channel, state) for channel in data.get("channels", [])]
self._channels: dict[str, Channel] = {channel.id: channel for channel in channels}
def get_role(self, role_id: str, /) -> Optional[Role]:
@property
def roles(self) -> list[Role]:
"""list[:class:`Role`] Gets all roles in the server in decending order"""
return list(self._roles.values())
@property
def members(self) -> list[Member]:
"""list[:class:`Member`] Gets all members in the server"""
return list(self._members.values())
@property
def channels(self) -> list[Channel]:
"""list[:class:`Member`] Gets all channels in the server"""
return list(self._channels.values())
def get_role(self, role_id: str) -> Optional[Role]:
"""Gets a role from the cache
Parameters
-----------
id: :class:`str`
The id of the role
Returns
--------
Optional[:class:`Role`]
The role if found
"""
return self._roles.get(role_id)
def get_member(self, member_id: str, /) -> Optional[Member]:
def get_member(self, member_id: str) -> Optional[Member]:
"""Gets a member from the cache
Parameters
-----------
id: :class:`str`
The id of the member
Returns
--------
Optional[:class:`Member`]
The member if found
"""
return self._members.get(member_id)
def get_channel(self, channel_id: str) -> Optional[Channel]:
"""Gets a channel from the cache
Parameters
-----------
id: :class:`str`
The id of the channel
Returns
--------
Optional[:class:`Channel`]
The channel if found
"""
self._channels.get(channel_id)
+2 -3
View File
@@ -56,10 +56,9 @@ class State:
server._members[member.id] = member
return member
def add_channel(self, payload: ChannelPayload) -> Channel:
cls = channel_factory(payload)
channel = cls(payload, self)
channel = channel_factory(payload, self)
self.channels[channel.id] = channel
return channel
+9
View File
@@ -0,0 +1,9 @@
from typing import TYPE_CHECKING, TypedDict
if TYPE_CHECKING:
from .channel import Channel
class Category(TypedDict):
id: str
title: str
channels: list[Channel]
+24 -1
View File
@@ -1,9 +1,32 @@
from __future__ import annotations
from typing import TYPE_CHECKING, TypedDict
if TYPE_CHECKING:
pass
from .role import Role, Permission
from .file import File
from .category import Category
from .channel import Channel
class SystemMessagesConfig(TypedDict, total=False):
user_joined: str
user_left: str
user_kicked: str
user_banned: str
class _ServerOptional(TypedDict, total=False):
nonce: str
description: str
categories: list[Category]
system_messages: SystemMessagesConfig
roles: list[Role]
icon: File
banner: File
class Server(TypedDict):
_id: str
owner: str
name: str
channels: list[Channel]
default_permissions: Permission
+21 -2
View File
@@ -7,7 +7,26 @@ if TYPE_CHECKING:
from .types import User as UserPayload
class User:
__flattern_attributes__ = ("name", "bot", "owner", "badges", "online", "flags")
"""Represents a user
Attributes
-----------
id: :class:`str`
The users id
name: :class:`str`
The users name
bot: :class:`bool`
Whether or not the user is a bot
owner: Optional[:class:`User`]
The bot's owner if the user is a bot
badges: :class:`int`
The users badges
online: :class:`bool`
Whether or not the user is online
flags: :class:`int`
The user flags
"""
__flattern_attributes__ = ("id", "name", "bot", "owner", "badges", "online", "flags")
def __init__(self, data: UserPayload, state: State):
self.state = state
@@ -17,7 +36,7 @@ class User:
bot = data.get("bot")
if bot:
self.bot = True
self.owner = bot["owner"]
self.owner = state.get_user(bot["owner"])
else:
self.bot = False
self.owner = None