first commit
This commit is contained in:
commit
988afb33e3
73 changed files with 8407 additions and 0 deletions
26
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
26
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: unconfirmed bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
A small code snippet showing the error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Library version**
|
||||||
|
Access this info via `pip show next.py`
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
38
.github/workflows/pyright.yml
vendored
Normal file
38
.github/workflows/pyright.yml
vendored
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
on: [push, pull_request]
|
||||||
|
name: pyright
|
||||||
|
jobs:
|
||||||
|
pyright-type-checking:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
version: ["3.9", "3.10", "3.11"]
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.version }}
|
||||||
|
- run: pip install .[speedups,docs]
|
||||||
|
- uses: jakebailey/pyright-action@v1
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.version }}
|
||||||
|
working-directory: next
|
||||||
|
|
||||||
|
pyright-type-completeness:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
version: ["3.9", "3.10", "3.11"]
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.version }}
|
||||||
|
- run: pip install .[speedups,docs]
|
||||||
|
- uses: jakebailey/pyright-action@v1
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.version }}
|
||||||
|
working-directory: next
|
||||||
|
verify-types: next
|
||||||
|
ignore-external: true
|
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
.venv
|
||||||
|
**/__pycache__
|
||||||
|
test.py
|
||||||
|
dist
|
||||||
|
*.egg-info
|
||||||
|
docs/_build
|
||||||
|
.vscode
|
||||||
|
.env
|
||||||
|
.mypy_cache
|
||||||
|
build
|
16
.readthedocs.yml
Normal file
16
.readthedocs.yml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
sphinx:
|
||||||
|
configuration: docs/conf.py
|
||||||
|
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- method: pip
|
||||||
|
path: .
|
||||||
|
extra_requirements:
|
||||||
|
- docs
|
||||||
|
|
||||||
|
build:
|
||||||
|
tools:
|
||||||
|
python: "3.9"
|
||||||
|
os: "ubuntu-22.04"
|
20
Justfile
Normal file
20
Justfile
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
set dotenv-load := true
|
||||||
|
|
||||||
|
test:
|
||||||
|
python test.py
|
||||||
|
|
||||||
|
build:
|
||||||
|
rm -rf dist/*
|
||||||
|
python -m build
|
||||||
|
|
||||||
|
upload:
|
||||||
|
python -m twine upload dist/*
|
||||||
|
|
||||||
|
lint:
|
||||||
|
pyright .
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
pyright --ignoreexternal --verifytypes next
|
||||||
|
|
||||||
|
docs:
|
||||||
|
cd docs && make html
|
19
LICENSE
Normal file
19
LICENSE
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2021-present Zomatree
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
40
README.md
Normal file
40
README.md
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# next.py
|
||||||
|
|
||||||
|
An async library to interact with the https://next.avanpost20.ru API.
|
||||||
|
|
||||||
|
You can join the support server [here](https://app.avanpost20.ru/invite/Testers) and find the library's documentation [here](https://nextpy.readthedocs.io/en/latest/).
|
||||||
|
|
||||||
|
## Installing
|
||||||
|
|
||||||
|
You can use `pip` to install next.py. It differs slightly depending on what OS/Distro you use.
|
||||||
|
|
||||||
|
On Windows
|
||||||
|
```
|
||||||
|
py -m pip install -U next-api-py # -U to update
|
||||||
|
```
|
||||||
|
|
||||||
|
On macOS and Linux
|
||||||
|
```
|
||||||
|
python3 -m pip install -U next-api-py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
More examples can be found in the [examples folder](https://github.com/avanpost200/next.py/blob/master/examples).
|
||||||
|
|
||||||
|
```py
|
||||||
|
import next
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
class Client(next.Client):
|
||||||
|
async def on_message(self, message: next.Message):
|
||||||
|
if message.content == "hello":
|
||||||
|
await message.channel.send("hi how are you")
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with next.utils.client_session() as session:
|
||||||
|
client = Client(session, "BOT TOKEN HERE")
|
||||||
|
await client.start()
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
```
|
20
docs/Makefile
Normal file
20
docs/Makefile
Normal 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)
|
289
docs/api.rst
Normal file
289
docs/api.rst
Normal file
|
@ -0,0 +1,289 @@
|
||||||
|
.. currentmodule:: next
|
||||||
|
|
||||||
|
API Reference
|
||||||
|
===============
|
||||||
|
|
||||||
|
|
||||||
|
.. autoclass:: Client
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: Asset
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: PartialAsset
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: Channel
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: ServerChannel
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: SavedMessageChannel
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: DMChannel
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: GroupDMChannel
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: TextChannel
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: VoiceChannel
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: Embed
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: WebsiteEmbed
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: ImageEmbed
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: TextEmbed
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: NoneEmbed
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: SendableEmbed
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: File
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: Member
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: Message
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: MessageReply
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: Masquerade
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: Messageable
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: Permissions
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: UserPermissions
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: PermissionsOverwrite
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: Role
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: Server
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: ServerBan
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: Category
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: SystemMessages
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: User
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autonamedtuple:: Relation
|
||||||
|
|
||||||
|
.. autonamedtuple:: Status
|
||||||
|
|
||||||
|
.. autoclass:: UserBadges
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: UserProfile
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: Invite
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: Emoji
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: MessageInteractions
|
||||||
|
:members:
|
||||||
|
|
||||||
|
Enums
|
||||||
|
======
|
||||||
|
|
||||||
|
The api uses enums to say what variant of something is,
|
||||||
|
these represent those enums
|
||||||
|
|
||||||
|
All enums subclass `aenum.Enum`.
|
||||||
|
|
||||||
|
.. class:: ChannelType
|
||||||
|
|
||||||
|
Specifies the type of channel.
|
||||||
|
|
||||||
|
.. attribute:: saved_message
|
||||||
|
|
||||||
|
A private channel only you can access.
|
||||||
|
.. attribute:: direct_message
|
||||||
|
|
||||||
|
A private direct message channel between you and another user
|
||||||
|
.. attribute:: group
|
||||||
|
|
||||||
|
A private group channel for messages between a group of users
|
||||||
|
.. attribute:: text_channel
|
||||||
|
|
||||||
|
A text channel in a server
|
||||||
|
.. attribute:: voice_channel
|
||||||
|
|
||||||
|
A voice only channel
|
||||||
|
|
||||||
|
.. class:: PresenceType
|
||||||
|
|
||||||
|
Specifies what a users presence is
|
||||||
|
|
||||||
|
.. attribute:: busy
|
||||||
|
|
||||||
|
The user is busy and wont receive notification
|
||||||
|
.. attribute:: idle
|
||||||
|
|
||||||
|
The user is idle
|
||||||
|
.. attribute:: invisible
|
||||||
|
|
||||||
|
The user is invisible, you will never receive this, instead they will appear offline
|
||||||
|
.. attribute:: online
|
||||||
|
|
||||||
|
The user is online
|
||||||
|
|
||||||
|
.. attribute:: offline
|
||||||
|
|
||||||
|
The user is offline or invisible
|
||||||
|
|
||||||
|
.. class:: RelationshipType
|
||||||
|
|
||||||
|
Specifies the relationship between two users
|
||||||
|
|
||||||
|
.. attribute:: blocked
|
||||||
|
|
||||||
|
You have blocked them
|
||||||
|
.. attribute:: blocked_other
|
||||||
|
|
||||||
|
They have blocked you
|
||||||
|
.. attribute:: friend
|
||||||
|
|
||||||
|
You are friends with them
|
||||||
|
.. attribute:: incoming_friend_request
|
||||||
|
|
||||||
|
They are sending you a friend request
|
||||||
|
.. attribute:: none
|
||||||
|
|
||||||
|
You have no relationship with them
|
||||||
|
.. attribute:: outgoing_friend_request
|
||||||
|
|
||||||
|
You are sending them a friend request
|
||||||
|
|
||||||
|
.. attribute:: user
|
||||||
|
|
||||||
|
That user is yourself
|
||||||
|
|
||||||
|
.. class:: AssetType
|
||||||
|
|
||||||
|
Specifies the type of asset
|
||||||
|
|
||||||
|
.. attribute:: image
|
||||||
|
|
||||||
|
The asset is an image
|
||||||
|
.. attribute:: video
|
||||||
|
|
||||||
|
The asset is a video
|
||||||
|
.. attribute:: text
|
||||||
|
|
||||||
|
The asset is a text file
|
||||||
|
.. attribute:: audio
|
||||||
|
|
||||||
|
The asset is an audio file
|
||||||
|
.. attribute:: file
|
||||||
|
|
||||||
|
The asset is a generic file
|
||||||
|
|
||||||
|
.. class:: SortType
|
||||||
|
|
||||||
|
The sort type for a message search
|
||||||
|
|
||||||
|
.. attribute:: latest
|
||||||
|
|
||||||
|
Sort by the latest message
|
||||||
|
.. attribute:: oldest
|
||||||
|
|
||||||
|
Sort by the oldest message
|
||||||
|
.. attribute:: relevance
|
||||||
|
|
||||||
|
Sort by the relevance of the message
|
||||||
|
|
||||||
|
.. class:: EmbedType
|
||||||
|
|
||||||
|
The type of embed
|
||||||
|
|
||||||
|
.. attribute:: website
|
||||||
|
|
||||||
|
The embed is a website
|
||||||
|
.. attribute:: image
|
||||||
|
|
||||||
|
The embed is an image
|
||||||
|
.. attribute:: text
|
||||||
|
|
||||||
|
The embed is text
|
||||||
|
.. attribute:: video
|
||||||
|
|
||||||
|
The embed is a video
|
||||||
|
.. attribute:: unknown
|
||||||
|
|
||||||
|
The embed is unknown
|
||||||
|
|
||||||
|
Utils
|
||||||
|
======
|
||||||
|
|
||||||
|
.. currentmodule:: next.utils
|
||||||
|
|
||||||
|
A collection a utility functions and classes to aid in making your bot
|
||||||
|
|
||||||
|
.. autofunction:: get
|
||||||
|
|
||||||
|
.. autofunction:: client_session
|
65
docs/conf.py
Normal file
65
docs/conf.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
# 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('..'))
|
||||||
|
|
||||||
|
import next
|
||||||
|
|
||||||
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
|
project = 'Next.py'
|
||||||
|
copyright = '2024-present, Avanpost'
|
||||||
|
author = 'Avanpost'
|
||||||
|
version = ".".join(map(str, next.__version__))
|
||||||
|
|
||||||
|
# -- 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",
|
||||||
|
"sphinx_toolbox.installation",
|
||||||
|
"sphinx_toolbox.more_autodoc.autonamedtuple"
|
||||||
|
]
|
||||||
|
|
||||||
|
# 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']
|
||||||
|
|
||||||
|
add_module_names = False
|
||||||
|
|
||||||
|
# -- 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']
|
||||||
|
|
||||||
|
autodoc_typehints = "none"
|
110
docs/ext/commands/api.rst
Normal file
110
docs/ext/commands/api.rst
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
.. currentmodule:: next
|
||||||
|
|
||||||
|
API Reference
|
||||||
|
===============
|
||||||
|
|
||||||
|
|
||||||
|
CommandsClient
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
.. autoclass:: next.ext.commands.CommandsClient
|
||||||
|
:members:
|
||||||
|
|
||||||
|
Context
|
||||||
|
~~~~~~~~
|
||||||
|
.. autoclass:: next.ext.commands.Context
|
||||||
|
:members:
|
||||||
|
|
||||||
|
Command
|
||||||
|
~~~~~~~~
|
||||||
|
.. autoclass:: next.ext.commands.Command
|
||||||
|
:members:
|
||||||
|
|
||||||
|
Cog
|
||||||
|
~~~~
|
||||||
|
.. autoclass:: next.ext.commands.Cog
|
||||||
|
:members:
|
||||||
|
|
||||||
|
command
|
||||||
|
~~~~~~~~
|
||||||
|
.. autodecorator:: next.ext.commands.command
|
||||||
|
|
||||||
|
check
|
||||||
|
~~~~~~
|
||||||
|
.. autodecorator:: next.ext.commands.check
|
||||||
|
|
||||||
|
is_bot_owner
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
.. autodecorator:: next.ext.commands.is_bot_owner
|
||||||
|
|
||||||
|
is_server_owner
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
.. autodecorator:: next.ext.commands.is_server_owner
|
||||||
|
|
||||||
|
|
||||||
|
Exceptions
|
||||||
|
===========
|
||||||
|
|
||||||
|
CommandError
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
.. autoexception:: next.ext.commands.CommandError
|
||||||
|
:members:
|
||||||
|
|
||||||
|
CommandNotFound
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
.. autoexception:: next.ext.commands.CommandNotFound
|
||||||
|
:members:
|
||||||
|
|
||||||
|
NoClosingQuote
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
.. autoexception:: next.ext.commands.NoClosingQuote
|
||||||
|
:members:
|
||||||
|
|
||||||
|
CheckError
|
||||||
|
~~~~~~~~~~~
|
||||||
|
.. autoexception:: next.ext.commands.CheckError
|
||||||
|
:members:
|
||||||
|
|
||||||
|
NotBotOwner
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
.. autoexception:: next.ext.commands.NotBotOwner
|
||||||
|
:members:
|
||||||
|
|
||||||
|
NotServerOwner
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
.. autoexception:: next.ext.commands.NotServerOwner
|
||||||
|
:members:
|
||||||
|
|
||||||
|
ServerOnly
|
||||||
|
~~~~~~~~~~~
|
||||||
|
.. autoexception:: next.ext.commands.ServerOnly
|
||||||
|
:members:
|
||||||
|
|
||||||
|
ConverterError
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
.. autoexception:: next.ext.commands.ConverterError
|
||||||
|
:members:
|
||||||
|
|
||||||
|
InvalidLiteralArgument
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
.. autoexception:: next.ext.commands.InvalidLiteralArgument
|
||||||
|
:members:
|
||||||
|
|
||||||
|
BadBoolArgument
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
.. autoexception:: next.ext.commands.BadBoolArgument
|
||||||
|
:members:
|
||||||
|
|
||||||
|
CategoryConverterError
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
.. autoexception:: next.ext.commands.CategoryConverterError
|
||||||
|
:members:
|
||||||
|
|
||||||
|
UserConverterError
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
.. autoexception:: next.ext.commands.UserConverterError
|
||||||
|
:members:
|
||||||
|
|
||||||
|
MemberConverterError
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
.. autoexception:: next.ext.commands.MemberConverterError
|
||||||
|
:members:
|
9
docs/ext/commands/index.rst
Normal file
9
docs/ext/commands/index.rst
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.. next_ext_commands:
|
||||||
|
|
||||||
|
``next.ext.commands`` - Command Framework
|
||||||
|
============================================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
api
|
14
docs/index.rst
Normal file
14
docs/index.rst
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
Welcome to Next.py's documentation!
|
||||||
|
======================================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
api
|
||||||
|
|
||||||
|
Extensions
|
||||||
|
-----------
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
ext/commands/index.rst
|
35
docs/make.bat
Normal file
35
docs/make.bat
Normal 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
|
16
examples/basic.py
Normal file
16
examples/basic.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
import next
|
||||||
|
|
||||||
|
|
||||||
|
class Client(next.Client):
|
||||||
|
async def on_message(self, message: next.Message):
|
||||||
|
if message.content == "hello":
|
||||||
|
await message.channel.send("hi how are you")
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
client = Client(session, "BOT TOKEN HERE")
|
||||||
|
await client.start()
|
||||||
|
|
||||||
|
asyncio.run(main())
|
22
examples/commands.py
Normal file
22
examples/commands.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
import next
|
||||||
|
from next.ext import commands
|
||||||
|
|
||||||
|
|
||||||
|
class Client(commands.CommandsClient):
|
||||||
|
async def get_prefix(self, message: next.Message):
|
||||||
|
return "!"
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def ping(self, ctx: commands.Context):
|
||||||
|
await ctx.send("pong")
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
client = Client(session, "BOT TOKEN HERE")
|
||||||
|
await client.start()
|
||||||
|
|
||||||
|
asyncio.run(main())
|
22
next/__init__.py
Normal file
22
next/__init__.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
from . import utils as utils
|
||||||
|
from . import types as types
|
||||||
|
from .asset import *
|
||||||
|
from .category import *
|
||||||
|
from .channel import *
|
||||||
|
from .client import *
|
||||||
|
from .embed import *
|
||||||
|
from .emoji import *
|
||||||
|
from .enums import *
|
||||||
|
from .errors import *
|
||||||
|
from .file import *
|
||||||
|
from .flags import *
|
||||||
|
from .invite import *
|
||||||
|
from .member import *
|
||||||
|
from .message import *
|
||||||
|
from .messageable import *
|
||||||
|
from .permissions import *
|
||||||
|
from .role import *
|
||||||
|
from .server import *
|
||||||
|
from .user import *
|
||||||
|
|
||||||
|
__version__ = "0.2.0"
|
113
next/asset.py
Normal file
113
next/asset.py
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import mimetypes
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from .enums import AssetType
|
||||||
|
from .utils import Ulid
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from io import IOBase
|
||||||
|
|
||||||
|
from .state import State
|
||||||
|
from .types import File as FilePayload
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ("Asset", "PartialAsset")
|
||||||
|
|
||||||
|
class Asset(Ulid):
|
||||||
|
"""Represents a file on next
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
id: :class:`str`
|
||||||
|
The id of the asset
|
||||||
|
tag: :class:`str`
|
||||||
|
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`
|
||||||
|
The name of the file
|
||||||
|
height: Optional[:class:`int`]
|
||||||
|
The height of the file if it is an image or video
|
||||||
|
width: Optional[:class:`int`]
|
||||||
|
The width of the file if it is an image or video
|
||||||
|
content_type: :class:`str`
|
||||||
|
The content type of the file
|
||||||
|
type: :class:`AssetType`
|
||||||
|
The type of asset it is
|
||||||
|
url: :class:`str`
|
||||||
|
The asset's url
|
||||||
|
"""
|
||||||
|
__slots__ = ("state", "id", "tag", "size", "filename", "content_type", "width", "height", "type", "url")
|
||||||
|
|
||||||
|
def __init__(self, data: FilePayload, state: State):
|
||||||
|
self.state: State = state
|
||||||
|
|
||||||
|
self.id: str = data['_id']
|
||||||
|
self.tag: str = data['tag']
|
||||||
|
self.size: int = data['size']
|
||||||
|
self.filename: str = data['filename']
|
||||||
|
|
||||||
|
metadata = data['metadata']
|
||||||
|
self.height: int | None
|
||||||
|
self.width: int | None
|
||||||
|
|
||||||
|
if metadata["type"] == "Image" or metadata["type"] == "Video": # cannot use `in` because type narrowing will not happen
|
||||||
|
self.height = metadata["height"]
|
||||||
|
self.width = metadata["width"]
|
||||||
|
else:
|
||||||
|
self.height = None
|
||||||
|
self.width = None
|
||||||
|
|
||||||
|
self.content_type: str | None = data["content_type"]
|
||||||
|
self.type: AssetType = AssetType(metadata["type"])
|
||||||
|
|
||||||
|
base_url = self.state.api_info["features"]["autumn"]["url"]
|
||||||
|
self.url: str = f"{base_url}/{self.tag}/{self.id}"
|
||||||
|
|
||||||
|
async def read(self) -> bytes:
|
||||||
|
"""Reads the files content into bytes"""
|
||||||
|
return await self.state.http.request_file(self.url)
|
||||||
|
|
||||||
|
async def save(self, fp: IOBase) -> None:
|
||||||
|
"""Reads the files content and saves it to a file
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
fp: IOBase
|
||||||
|
The file to write to
|
||||||
|
"""
|
||||||
|
fp.write(await self.read())
|
||||||
|
|
||||||
|
class PartialAsset(Asset):
|
||||||
|
"""Partial asset for when we get limited data about the asset
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
id: :class:`str`
|
||||||
|
The id of the asset, this will always be ``"0"``
|
||||||
|
size: :class:`int`
|
||||||
|
Amount of bytes in the file, this will always be ``0``
|
||||||
|
filename: :class:`str`
|
||||||
|
The name of the file, this be always be ``""``
|
||||||
|
height: Optional[:class:`int`]
|
||||||
|
The height of the file if it is an image or video, this will always be ``None``
|
||||||
|
width: Optional[:class:`int`]
|
||||||
|
The width of the file if it is an image or video, this will always be ``None``
|
||||||
|
content_type: Optional[:class:`str`]
|
||||||
|
The content type of the file, this is guessed from the url's file extension if it has one
|
||||||
|
type: :class:`AssetType`
|
||||||
|
The type of asset it is, this always be ``AssetType.file``
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, url: str, state: State):
|
||||||
|
self.state: State = state
|
||||||
|
self.id: str = "0"
|
||||||
|
self.size: int = 0
|
||||||
|
self.filename: str = ""
|
||||||
|
self.height: int | None = None
|
||||||
|
self.width: int | None = None
|
||||||
|
self.content_type: str | None = mimetypes.guess_extension(url)
|
||||||
|
self.type: AssetType = AssetType.file
|
||||||
|
self.url: str = url
|
36
next/category.py
Normal file
36
next/category.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from .utils import Ulid
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .channel import Channel
|
||||||
|
from .state import State
|
||||||
|
from .types import Category as CategoryPayload
|
||||||
|
|
||||||
|
__all__ = ("Category",)
|
||||||
|
|
||||||
|
class Category(Ulid):
|
||||||
|
"""Represents a category in a server that stores channels.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
name: :class:`str`
|
||||||
|
The name of the category
|
||||||
|
id: :class:`str`
|
||||||
|
The id of the category
|
||||||
|
channel_ids: list[:class:`str`]
|
||||||
|
The ids of channels that are inside the category
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, data: CategoryPayload, state: State):
|
||||||
|
self.state: State = state
|
||||||
|
self.name: str = data["title"]
|
||||||
|
self.id: str = data["id"]
|
||||||
|
self.channel_ids: list[str] = data["channels"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def channels(self) -> list[Channel]:
|
||||||
|
"""Returns a list of channels that the category contains"""
|
||||||
|
return [self.state.get_channel(channel_id) for channel_id in self.channel_ids]
|
422
next/channel.py
Normal file
422
next/channel.py
Normal file
|
@ -0,0 +1,422 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any, Optional, Union
|
||||||
|
|
||||||
|
from .asset import Asset
|
||||||
|
from .enums import ChannelType
|
||||||
|
from .messageable import Messageable
|
||||||
|
from .permissions import Permissions, PermissionsOverwrite
|
||||||
|
from .utils import Missing, Ulid
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .message import Message
|
||||||
|
from .role import Role
|
||||||
|
from .server import Server
|
||||||
|
from .state import State
|
||||||
|
from .types import Channel as ChannelPayload
|
||||||
|
from .types import DMChannel as DMChannelPayload
|
||||||
|
from .types import File as FilePayload
|
||||||
|
from .types import GroupDMChannel as GroupDMChannelPayload
|
||||||
|
from .types import Overwrite as OverwritePayload
|
||||||
|
from .types import SavedMessages as SavedMessagesPayload
|
||||||
|
from .types import ServerChannel as ServerChannelPayload
|
||||||
|
from .types import TextChannel as TextChannelPayload
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
__all__ = ("DMChannel", "GroupDMChannel", "SavedMessageChannel", "TextChannel", "VoiceChannel", "Channel", "ServerChannel")
|
||||||
|
|
||||||
|
class EditableChannel:
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
state: State
|
||||||
|
id: str
|
||||||
|
|
||||||
|
async def edit(self, **kwargs: Any) -> None:
|
||||||
|
"""Edits the channel
|
||||||
|
|
||||||
|
Passing ``None`` to the parameters that accept it will remove them.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
name: str
|
||||||
|
The new name for the channel
|
||||||
|
description: Optional[str]
|
||||||
|
The new description for the channel
|
||||||
|
owner: User
|
||||||
|
The new owner for the group dm channel
|
||||||
|
icon: Optional[File]
|
||||||
|
The new icon for the channel
|
||||||
|
nsfw: bool
|
||||||
|
Sets whether the channel is nsfw or not
|
||||||
|
"""
|
||||||
|
remove: list[str] = []
|
||||||
|
|
||||||
|
if kwargs.get("icon", Missing) == None:
|
||||||
|
remove.append("Icon")
|
||||||
|
elif kwargs.get("description", Missing) == None:
|
||||||
|
remove.append("Description")
|
||||||
|
|
||||||
|
if icon := kwargs.get("icon"):
|
||||||
|
asset = await self.state.http.upload_file(icon, "icons")
|
||||||
|
kwargs["icon"] = asset["id"]
|
||||||
|
|
||||||
|
if owner := kwargs.get("owner"):
|
||||||
|
kwargs["owner"] = owner.id
|
||||||
|
|
||||||
|
await self.state.http.edit_channel(self.id, remove, kwargs)
|
||||||
|
|
||||||
|
class Channel(Ulid):
|
||||||
|
"""Base class for all channels
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
id: :class:`str`
|
||||||
|
The id of the channel
|
||||||
|
channel_type: ChannelType
|
||||||
|
The type of the channel
|
||||||
|
server_id: Optional[:class:`str`]
|
||||||
|
The server id of the chanel, if any
|
||||||
|
"""
|
||||||
|
__slots__ = ("state", "id", "channel_type", "server_id")
|
||||||
|
|
||||||
|
def __init__(self, data: ChannelPayload, state: State):
|
||||||
|
self.state: State = state
|
||||||
|
self.id: str = data["_id"]
|
||||||
|
self.channel_type: ChannelType = ChannelType(data["channel_type"])
|
||||||
|
self.server_id: Optional[str] = None
|
||||||
|
|
||||||
|
async def _get_channel_id(self) -> str:
|
||||||
|
return self.id
|
||||||
|
|
||||||
|
def _update(self, **_: Any) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def delete(self) -> None:
|
||||||
|
"""Deletes or closes the channel"""
|
||||||
|
await self.state.http.close_channel(self.id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server(self) -> Server:
|
||||||
|
""":class:`Server` The server this voice channel belongs too
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
:class:`LookupError`
|
||||||
|
Raises if the channel is not part of a server
|
||||||
|
"""
|
||||||
|
if not self.server_id:
|
||||||
|
raise LookupError
|
||||||
|
|
||||||
|
return self.state.get_server(self.server_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mention(self) -> str:
|
||||||
|
""":class:`str`: Returns a string that allows you to mention the given channel."""
|
||||||
|
return f"<#{self.id}>"
|
||||||
|
|
||||||
|
|
||||||
|
class SavedMessageChannel(Channel, Messageable):
|
||||||
|
"""The Saved Message Channel"""
|
||||||
|
def __init__(self, data: SavedMessagesPayload, state: State):
|
||||||
|
super().__init__(data, state)
|
||||||
|
|
||||||
|
class DMChannel(Channel, Messageable):
|
||||||
|
"""A DM channel
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
last_message_id: Optional[:class:`str`]
|
||||||
|
The id of the last message in this channel, if any
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("last_message_id", "recipient_ids")
|
||||||
|
|
||||||
|
def __init__(self, data: DMChannelPayload, state: State):
|
||||||
|
super().__init__(data, state)
|
||||||
|
|
||||||
|
self.recipient_ids: list[str] = data["recipients"]
|
||||||
|
self.last_message_id: str | None = data.get("last_message_id")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def recipients(self) -> tuple[User, User]:
|
||||||
|
a, b = self.recipient_ids
|
||||||
|
|
||||||
|
return (self.state.get_user(a), self.state.get_user(b))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def recipient(self) -> User:
|
||||||
|
if self.recipient_ids[0] != self.state.user_id:
|
||||||
|
user_id = self.recipient_ids[0]
|
||||||
|
else:
|
||||||
|
user_id = self.recipient_ids[1]
|
||||||
|
|
||||||
|
return self.state.get_user(user_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_message(self) -> Message:
|
||||||
|
"""Gets the last message from the channel, shorthand for `client.get_message(channel.last_message_id)`
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Message` the last message in the channel
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.last_message_id:
|
||||||
|
raise LookupError
|
||||||
|
|
||||||
|
return self.state.get_message(self.last_message_id)
|
||||||
|
|
||||||
|
class GroupDMChannel(Channel, Messageable, EditableChannel):
|
||||||
|
"""A group DM channel
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
recipients: list[:class:`User`]
|
||||||
|
The recipients of the group dm channel
|
||||||
|
name: :class:`str`
|
||||||
|
The name of the group dm channel
|
||||||
|
owner: :class:`User`
|
||||||
|
The user who created the group dm channel
|
||||||
|
icon: Optional[:class:`Asset`]
|
||||||
|
The icon of the group dm channel
|
||||||
|
permissions: :class:`ChannelPermissions`
|
||||||
|
The permissions of the users inside the group dm channel
|
||||||
|
description: Optional[:class:`str`]
|
||||||
|
The description of the channel, if any
|
||||||
|
last_message_id: Optional[:class:`str`]
|
||||||
|
The id of the last message in this channel, if any
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("recipient_ids", "name", "owner_id", "permissions", "icon", "description", "last_message_id")
|
||||||
|
|
||||||
|
def __init__(self, data: GroupDMChannelPayload, state: State):
|
||||||
|
super().__init__(data, state)
|
||||||
|
self.recipient_ids: list[str] = data["recipients"]
|
||||||
|
self.name: str = data["name"]
|
||||||
|
self.owner_id: str = data["owner"]
|
||||||
|
self.description: str | None = data.get("description")
|
||||||
|
self.last_message_id: str | None = data.get("last_message_id")
|
||||||
|
|
||||||
|
self.icon: Asset | None
|
||||||
|
|
||||||
|
if icon := data.get("icon"):
|
||||||
|
self.icon = Asset(icon, state)
|
||||||
|
else:
|
||||||
|
self.icon = None
|
||||||
|
|
||||||
|
self.permissions: Permissions = Permissions(data.get("permissions", 0))
|
||||||
|
|
||||||
|
def _update(self, *, name: Optional[str] = None, recipients: Optional[list[str]] = None, description: Optional[str] = None) -> None:
|
||||||
|
if name is not None:
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
if recipients is not None:
|
||||||
|
self.recipient_ids = recipients
|
||||||
|
|
||||||
|
if description is not None:
|
||||||
|
self.description = description
|
||||||
|
|
||||||
|
@property
|
||||||
|
def recipients(self) -> list[User]:
|
||||||
|
return [self.state.get_user(user_id) for user_id in self.recipient_ids]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def owner(self) -> User:
|
||||||
|
return self.state.get_user(self.owner_id)
|
||||||
|
|
||||||
|
async def set_default_permissions(self, permissions: Permissions) -> None:
|
||||||
|
"""Sets the default permissions for a group.
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
permissions: :class:`ChannelPermissions`
|
||||||
|
The new default group permissions
|
||||||
|
"""
|
||||||
|
await self.state.http.set_group_channel_default_permissions(self.id, permissions.value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_message(self) -> Message:
|
||||||
|
"""Gets the last message from the channel, shorthand for `client.get_message(channel.last_message_id)`
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Message` the last message in the channel
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.last_message_id:
|
||||||
|
raise LookupError
|
||||||
|
|
||||||
|
return self.state.get_message(self.last_message_id)
|
||||||
|
|
||||||
|
class ServerChannel(Channel):
|
||||||
|
"""Base class for all guild channels
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
server_id: :class:`str`
|
||||||
|
The id of the server this text channel belongs to
|
||||||
|
name: :class:`str`
|
||||||
|
The name of the text channel
|
||||||
|
description: Optional[:class:`str`]
|
||||||
|
The description of the channel, if any
|
||||||
|
nsfw: bool
|
||||||
|
Sets whether the channel is nsfw or not
|
||||||
|
default_permissions: :class:`ChannelPermissions`
|
||||||
|
The default permissions for all users in the text channel
|
||||||
|
"""
|
||||||
|
def __init__(self, data: ServerChannelPayload, state: State):
|
||||||
|
super().__init__(data, state)
|
||||||
|
|
||||||
|
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)
|
||||||
|
self.active: bool = False
|
||||||
|
self.default_permissions: PermissionsOverwrite = PermissionsOverwrite._from_overwrite(data.get("default_permissions", {"a": 0, "d": 0}))
|
||||||
|
|
||||||
|
permissions: dict[str, PermissionsOverwrite] = {}
|
||||||
|
|
||||||
|
for role_name, overwrite_data in data.get("role_permissions", {}).items():
|
||||||
|
overwrite = PermissionsOverwrite._from_overwrite(overwrite_data)
|
||||||
|
permissions[role_name] = overwrite
|
||||||
|
|
||||||
|
self.permissions: dict[str, PermissionsOverwrite] = permissions
|
||||||
|
|
||||||
|
self.icon: Asset | None
|
||||||
|
|
||||||
|
if icon := data.get("icon"):
|
||||||
|
self.icon = Asset(icon, state)
|
||||||
|
else:
|
||||||
|
self.icon = None
|
||||||
|
|
||||||
|
async def set_default_permissions(self, permissions: PermissionsOverwrite) -> None:
|
||||||
|
"""Sets the default permissions for the channel.
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
permissions: :class:`ChannelPermissions`
|
||||||
|
The new default channel permissions
|
||||||
|
"""
|
||||||
|
allow, deny = permissions.to_pair()
|
||||||
|
await self.state.http.set_guild_channel_default_permissions(self.id, allow.value, deny.value)
|
||||||
|
|
||||||
|
async def set_role_permissions(self, role: Role, permissions: PermissionsOverwrite) -> None:
|
||||||
|
"""Sets the permissions for a role in the channel.
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
permissions: :class:`ChannelPermissions`
|
||||||
|
The new channel permissions
|
||||||
|
"""
|
||||||
|
allow, deny = permissions.to_pair()
|
||||||
|
|
||||||
|
await self.state.http.set_guild_channel_role_permissions(self.id, role.id, allow.value, deny.value)
|
||||||
|
|
||||||
|
def _update(self, *, name: Optional[str] = None, description: Optional[str] = None, icon: Optional[FilePayload] = None, nsfw: Optional[bool] = None, active: Optional[bool] = None, role_permissions: Optional[dict[str, OverwritePayload]] = None, default_permissions: Optional[OverwritePayload] = None):
|
||||||
|
if name is not None:
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
if description is not None:
|
||||||
|
self.description = description
|
||||||
|
|
||||||
|
if icon is not None:
|
||||||
|
self.icon = Asset(icon, self.state)
|
||||||
|
|
||||||
|
if nsfw is not None:
|
||||||
|
self.nsfw = nsfw
|
||||||
|
|
||||||
|
if active is not None:
|
||||||
|
self.active = active
|
||||||
|
|
||||||
|
if role_permissions is not None:
|
||||||
|
permissions = {}
|
||||||
|
|
||||||
|
for role_name, overwrite_data in role_permissions.items():
|
||||||
|
overwrite = PermissionsOverwrite._from_overwrite(overwrite_data)
|
||||||
|
permissions[role_name] = overwrite
|
||||||
|
|
||||||
|
self.permissions = permissions
|
||||||
|
|
||||||
|
if default_permissions is not None:
|
||||||
|
self.default_permissions = PermissionsOverwrite._from_overwrite(default_permissions)
|
||||||
|
|
||||||
|
class TextChannel(ServerChannel, Messageable, EditableChannel):
|
||||||
|
"""A text channel
|
||||||
|
|
||||||
|
Subclasses :class:`ServerChannel` and :class:`Messageable`
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
name: :class:`str`
|
||||||
|
The name of the text channel
|
||||||
|
server_id: :class:`str`
|
||||||
|
The id of the server this text channel belongs to
|
||||||
|
last_message_id: Optional[:class:`str`]
|
||||||
|
The id of the last message in this channel, if any
|
||||||
|
default_permissions: :class:`ChannelPermissions`
|
||||||
|
The default permissions for all users in the text channel
|
||||||
|
role_permissions: dict[:class:`str`, :class:`ChannelPermissions`]
|
||||||
|
A dictionary of role id's to the permissions of that role in the text channel
|
||||||
|
icon: Optional[:class:`Asset`]
|
||||||
|
The icon of the text channel, if any
|
||||||
|
description: Optional[:class:`str`]
|
||||||
|
The description of the channel, if any
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("name", "description", "last_message_id", "default_permissions", "icon", "overwrites")
|
||||||
|
|
||||||
|
def __init__(self, data: TextChannelPayload, state: State):
|
||||||
|
super().__init__(data, state)
|
||||||
|
|
||||||
|
self.last_message_id: str | None = data.get("last_message_id")
|
||||||
|
|
||||||
|
async def _get_channel_id(self) -> str:
|
||||||
|
return self.id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_message(self) -> Message:
|
||||||
|
"""Gets the last message from the channel, shorthand for `client.get_message(channel.last_message_id)`
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Message` the last message in the channel
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.last_message_id:
|
||||||
|
raise LookupError
|
||||||
|
|
||||||
|
return self.state.get_message(self.last_message_id)
|
||||||
|
|
||||||
|
class VoiceChannel(ServerChannel, EditableChannel):
|
||||||
|
"""A voice channel
|
||||||
|
|
||||||
|
Subclasses :class:`ServerChannel`
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
name: :class:`str`
|
||||||
|
The name of the voice channel
|
||||||
|
server_id: :class:`str`
|
||||||
|
The id of the server this voice channel belongs to
|
||||||
|
last_message_id: Optional[:class:`str`]
|
||||||
|
The id of the last message in this channel, if any
|
||||||
|
default_permissions: :class:`ChannelPermissions`
|
||||||
|
The default permissions for all users in the voice channel
|
||||||
|
role_permissions: dict[:class:`str`, :class:`ChannelPermissions`]
|
||||||
|
A dictionary of role id's to the permissions of that role in the voice channel
|
||||||
|
icon: Optional[:class:`Asset`]
|
||||||
|
The icon of the voice channel, if any
|
||||||
|
description: Optional[:class:`str`]
|
||||||
|
The description of the channel, if any
|
||||||
|
"""
|
||||||
|
|
||||||
|
def channel_factory(data: ChannelPayload, state: State) -> Union[DMChannel, GroupDMChannel, SavedMessageChannel, TextChannel, VoiceChannel]:
|
||||||
|
if data["channel_type"] == "SavedMessages":
|
||||||
|
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
|
565
next/client.py
Normal file
565
next/client.py
Normal file
|
@ -0,0 +1,565 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Literal, Optional, TypeVar, Union, cast, overload
|
||||||
|
from typing_extensions import ParamSpec
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from .errors import NextError
|
||||||
|
from .channel import (DMChannel, GroupDMChannel, SavedMessageChannel,
|
||||||
|
TextChannel, VoiceChannel, channel_factory)
|
||||||
|
from .http import HttpClient
|
||||||
|
from .invite import Invite
|
||||||
|
from .message import Message
|
||||||
|
from .state import State
|
||||||
|
from .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
|
||||||
|
except ImportError:
|
||||||
|
import json
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .channel import Channel
|
||||||
|
from .file import File
|
||||||
|
from .types import ApiInfo
|
||||||
|
|
||||||
|
import next
|
||||||
|
|
||||||
|
__all__ = ("Client",)
|
||||||
|
|
||||||
|
logger: logging.Logger = logging.getLogger("next")
|
||||||
|
|
||||||
|
P = ParamSpec("P")
|
||||||
|
R = TypeVar("R")
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
"""The client for interacting with next
|
||||||
|
|
||||||
|
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 next instance you are connecting to, by default it uses the offical instance hosted at next.avanpost20.ru
|
||||||
|
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.avanpost20.ru", max_messages: int = 5000, bot: bool = True):
|
||||||
|
self.session: aiohttp.ClientSession = session
|
||||||
|
self.token: str = token
|
||||||
|
self.api_url: str = api_url
|
||||||
|
self.max_messages: int = max_messages
|
||||||
|
self.bot: bool = bot
|
||||||
|
|
||||||
|
self.api_info: ApiInfo
|
||||||
|
self.http: HttpClient
|
||||||
|
self.state: State
|
||||||
|
self.websocket: WebsocketHandler
|
||||||
|
|
||||||
|
self.temp_listeners: dict[str, list[tuple[Callable[..., bool], asyncio.Future[Any]]]] = {}
|
||||||
|
self.listeners: dict[str, list[Callable[..., Coroutine[Any, Any, Any]]]] = {}
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def dispatch(self, event: str, *args: Any) -> None:
|
||||||
|
"""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
|
||||||
|
"""
|
||||||
|
|
||||||
|
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:
|
||||||
|
text = await resp.text()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.loads(text)
|
||||||
|
except:
|
||||||
|
raise NextError(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()
|
||||||
|
|
||||||
|
self.api_info = api_info
|
||||||
|
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(reconnect)
|
||||||
|
|
||||||
|
async def stop(self) -> None:
|
||||||
|
await self.websocket.websocket.close()
|
||||||
|
|
||||||
|
def get_user(self, id: str) -> User:
|
||||||
|
"""Gets a user from the cache
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
id: :class:`str`
|
||||||
|
The id of the user
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`User`
|
||||||
|
The user
|
||||||
|
"""
|
||||||
|
return self.state.get_user(id)
|
||||||
|
|
||||||
|
def get_channel(self, id: str) -> Channel:
|
||||||
|
"""Gets a channel from the cache
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
id: :class:`str`
|
||||||
|
The id of the channel
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Channel`
|
||||||
|
The channel
|
||||||
|
"""
|
||||||
|
return self.state.get_channel(id)
|
||||||
|
|
||||||
|
def get_server(self, id: str) -> Server:
|
||||||
|
"""Gets a server from the cache
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
id: :class:`str`
|
||||||
|
The id of the server
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Server`
|
||||||
|
The server
|
||||||
|
"""
|
||||||
|
return self.state.get_server(id)
|
||||||
|
|
||||||
|
async def wait_for(self, event: str, *, check: Optional[Callable[..., bool]] = None, timeout: Optional[float] = 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
|
||||||
|
timeout: Optional[:class:`float`]
|
||||||
|
Time in seconds to wait for the event. By default it waits forever
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
asyncio.TimeoutError
|
||||||
|
If timeout is provided and it was reached
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
Any
|
||||||
|
The parameters of the event
|
||||||
|
"""
|
||||||
|
if not check:
|
||||||
|
check = lambda *_: True
|
||||||
|
|
||||||
|
future = asyncio.get_running_loop().create_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 NextError("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"""
|
||||||
|
user = self.websocket.user
|
||||||
|
|
||||||
|
assert user
|
||||||
|
return user
|
||||||
|
|
||||||
|
@property
|
||||||
|
def users(self) -> list[User]:
|
||||||
|
"""list[:class:`User`] All users the client can see"""
|
||||||
|
return list(self.state.users.values())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def servers(self) -> list[Server]:
|
||||||
|
"""list[:class:'Server'] All servers the client can see"""
|
||||||
|
return list(self.state.servers.values())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def global_emojis(self) -> list[Emoji]:
|
||||||
|
return self.state.global_emojis
|
||||||
|
|
||||||
|
async def fetch_user(self, user_id: str) -> User:
|
||||||
|
"""Fetchs a user
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
user_id: :class:`str`
|
||||||
|
The id of the user you are fetching
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`User`
|
||||||
|
The user with the matching id
|
||||||
|
"""
|
||||||
|
payload = await self.http.fetch_user(user_id)
|
||||||
|
return User(payload, self.state)
|
||||||
|
|
||||||
|
async def fetch_dm_channels(self) -> list[Union[DMChannel, GroupDMChannel]]:
|
||||||
|
"""Fetchs all dm channels the client has made
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
list[Union[:class:`DMChanel`, :class:`GroupDMChannel`]]
|
||||||
|
A list of :class:`DMChannel` or :class`GroupDMChannel`
|
||||||
|
"""
|
||||||
|
channel_payloads = await self.http.fetch_dm_channels()
|
||||||
|
return cast(list[Union[DMChannel, GroupDMChannel]], [channel_factory(payload, self.state) for payload in channel_payloads])
|
||||||
|
|
||||||
|
async def fetch_channel(self, channel_id: str) -> Union[DMChannel, GroupDMChannel, SavedMessageChannel, TextChannel, VoiceChannel]:
|
||||||
|
"""Fetches a channel
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
channel_id: :class:`str`
|
||||||
|
The id of the channel
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
Union[:class:`DMChannel`, :class:`GroupDMChannel`, :class:`SavedMessageChannel`, :class:`TextChannel`, :class:`VoiceChannel`]
|
||||||
|
The channel with the matching id
|
||||||
|
"""
|
||||||
|
payload = await self.http.fetch_channel(channel_id)
|
||||||
|
|
||||||
|
return channel_factory(payload, self.state)
|
||||||
|
|
||||||
|
async def fetch_server(self, server_id: str) -> Server:
|
||||||
|
"""Fetchs a server
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
server_id: :class:`str`
|
||||||
|
The id of the server you are fetching
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Server`
|
||||||
|
The server with the matching id
|
||||||
|
"""
|
||||||
|
payload = await self.http.fetch_server(server_id)
|
||||||
|
|
||||||
|
return Server(payload, self.state)
|
||||||
|
|
||||||
|
async def fetch_invite(self, code: str) -> Invite:
|
||||||
|
"""Fetchs an invite
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
code: :class:`str`
|
||||||
|
The code of the invite you are fetching
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Invite`
|
||||||
|
The invite with the matching code
|
||||||
|
"""
|
||||||
|
payload = await self.http.fetch_invite(code)
|
||||||
|
|
||||||
|
return Invite(payload, code, self.state)
|
||||||
|
|
||||||
|
def get_message(self, message_id: str) -> Message:
|
||||||
|
"""Gets a message from the cache
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
message_id: :class:`str`
|
||||||
|
The id of the message you are getting
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Message`
|
||||||
|
The message with the matching id
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
LookupError
|
||||||
|
This raises if the message is not found in the cache
|
||||||
|
"""
|
||||||
|
for message in self.state.messages:
|
||||||
|
if message.id == message_id:
|
||||||
|
return message
|
||||||
|
|
||||||
|
raise LookupError
|
||||||
|
|
||||||
|
async def edit_self(self, **kwargs: Any) -> None:
|
||||||
|
"""Edits the client's own user
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
avatar: Optional[:class:`File`]
|
||||||
|
The avatar to change to, passing in ``None`` will remove the avatar
|
||||||
|
"""
|
||||||
|
if kwargs.get("avatar", Missing) is None:
|
||||||
|
del kwargs["avatar"]
|
||||||
|
remove = ["Avatar"]
|
||||||
|
else:
|
||||||
|
remove = None
|
||||||
|
|
||||||
|
await self.state.http.edit_self(remove, kwargs)
|
||||||
|
|
||||||
|
async def edit_status(self, **kwargs: Any) -> None:
|
||||||
|
"""Edits the client's own status
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
presence: :class:`PresenceType`
|
||||||
|
The presence to change to
|
||||||
|
text: Optional[:class:`str`]
|
||||||
|
The text to change the status to, passing in ``None`` will remove the status
|
||||||
|
"""
|
||||||
|
if kwargs.get("text", Missing) is None:
|
||||||
|
del kwargs["text"]
|
||||||
|
remove = ["StatusText"]
|
||||||
|
else:
|
||||||
|
remove = None
|
||||||
|
|
||||||
|
if presence := kwargs.get("presence"):
|
||||||
|
kwargs["presence"] = presence.value
|
||||||
|
|
||||||
|
await self.state.http.edit_self(remove, {"status": kwargs})
|
||||||
|
|
||||||
|
async def edit_profile(self, **kwargs: Any) -> None:
|
||||||
|
"""Edits the client's own profile
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
content: Optional[:class:`str`]
|
||||||
|
The new content for the profile, passing in ``None`` will remove the profile content
|
||||||
|
background: Optional[:class:`File`]
|
||||||
|
The new background for the profile, passing in ``None`` will remove the profile background
|
||||||
|
"""
|
||||||
|
remove: list[str] = []
|
||||||
|
|
||||||
|
if kwargs.get("content", Missing) is None:
|
||||||
|
del kwargs["content"]
|
||||||
|
remove.append("ProfileContent")
|
||||||
|
|
||||||
|
if kwargs.get("background", Missing) is None:
|
||||||
|
del kwargs["background"]
|
||||||
|
remove.append("ProfileBackground")
|
||||||
|
|
||||||
|
await self.state.http.edit_self(remove, {"profile": kwargs})
|
||||||
|
|
||||||
|
async def fetch_emoji(self, emoji_id: str) -> Emoji:
|
||||||
|
"""Fetches an emoji
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
emoji_id: str
|
||||||
|
The id of the emoji
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Emoji`
|
||||||
|
The emoji with the corrasponding id
|
||||||
|
"""
|
||||||
|
|
||||||
|
emoji = await self.state.http.fetch_emoji(emoji_id)
|
||||||
|
|
||||||
|
return Emoji(emoji, self.state)
|
||||||
|
|
||||||
|
async def upload_file(self, file: File, tag: Literal['attachments', 'avatars', 'backgrounds', 'icons', 'banners', 'emojis']) -> Ulid:
|
||||||
|
"""Uploads a file to next
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
file: :class:`File`
|
||||||
|
The file to upload
|
||||||
|
tag: :class:`str`
|
||||||
|
The type of file to upload, this should a string of either `'attachments'`, `'avatars'`, `'backgrounds'`, `'icons'`, `'banners'` or `'emojis'`
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Ulid`
|
||||||
|
The id of the file that was uploaded
|
||||||
|
"""
|
||||||
|
asset = await self.http.upload_file(file, tag)
|
||||||
|
|
||||||
|
ulid = Ulid()
|
||||||
|
ulid.id = asset["id"]
|
||||||
|
|
||||||
|
return ulid
|
||||||
|
|
||||||
|
# events
|
||||||
|
|
||||||
|
async def on_ready(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_message(self, message: next.Message) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_raw_message_update(self, payload: next.types.MessageUpdateEventPayload) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_message_update(self, before: next.Message, after: next.Message) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_raw_message_delete(self, payload: next.types.MessageDeleteEventPayload) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_message_delete(self, message: next.Message) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_channel_create(self, channel: next.Channel) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_channel_update(self, before: next.Channel, after: next.Channel) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_channel_delete(self, channel: next.Channel) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_typing_start(self, channel: next.Channel, user: next.User) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_typing_stop(self, channel: next.Channel, user: next.User) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_server_update(self, before: next.Server, after: next.Server) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_server_delete(self, server: next.Server) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_server_join(self, server: next.Server) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_member_update(self, before: next.Member, after: next.Member) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_member_join(self, member: next.Member) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_member_leave(self, member: next.Member) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_role_create(self, role: next.Role) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_role_update(self, before: next.Role, after: next.Role) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_role_delete(self, role: next.Role) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_user_update(self, before: next.User, after: next.User) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_user_relationship_update(self, user: next.User, before: next.RelationshipType, after: next.RelationshipType) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_raw_reaction_add(self, payload: next.types.MessageReactEventPayload) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_reaction_add(self, message: next.Message, user: next.User, emoji_id: str) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_raw_reaction_remove(self, payload: next.types.MessageUnreactEventPayload) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_reaction_remove(self, message: next.Message, user: next.User, emoji_id: str) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_raw_reaction_clear(self, payload: next.types.MessageRemoveReactionEventPayload) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_reaction_clear(self, message: next.Message, user: next.User, emoji_id: str) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def raw_bulk_message_delete(self, payload: next.types.BulkMessageDeleteEventPayload) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def bulk_message_delete(self, messages: list[next.Message]) -> None:
|
||||||
|
pass
|
150
next/embed.py
Normal file
150
next/embed.py
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Optional, TypedDict, Union
|
||||||
|
|
||||||
|
from typing_extensions import NotRequired, Unpack
|
||||||
|
|
||||||
|
from next.types.embed import WebsiteSpecial
|
||||||
|
|
||||||
|
from .asset import Asset
|
||||||
|
from .enums import EmbedType
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .state import State
|
||||||
|
from .types import Embed as EmbedPayload
|
||||||
|
from .types import ImageEmbed as ImageEmbedPayload
|
||||||
|
from .types import SendableEmbed as SendableEmbedPayload
|
||||||
|
from .types import TextEmbed as TextEmbedPayload
|
||||||
|
from .types import WebsiteEmbed as WebsiteEmbedPayload
|
||||||
|
from .types import JanuaryImage, JanuaryVideo
|
||||||
|
|
||||||
|
__all__ = ("Embed", "WebsiteEmbed", "ImageEmbed", "TextEmbed", "NoneEmbed", "to_embed", "SendableEmbed")
|
||||||
|
|
||||||
|
class WebsiteEmbed:
|
||||||
|
type = EmbedType.website
|
||||||
|
|
||||||
|
def __init__(self, embed: WebsiteEmbedPayload):
|
||||||
|
self.url: str | None = embed.get("url")
|
||||||
|
self.special: WebsiteSpecial | None = embed.get("special")
|
||||||
|
self.title: str | None = embed.get("title")
|
||||||
|
self.description: str | None = embed.get("description")
|
||||||
|
self.image: JanuaryImage | None = embed.get("image")
|
||||||
|
self.video: JanuaryVideo | None = embed.get("video")
|
||||||
|
self.site_name: str | None = embed.get("site_name")
|
||||||
|
self.icon_url: str | None = embed.get("icon_url")
|
||||||
|
self.colour: str | None = embed.get("colour")
|
||||||
|
|
||||||
|
class ImageEmbed:
|
||||||
|
type: EmbedType = EmbedType.image
|
||||||
|
|
||||||
|
def __init__(self, image: ImageEmbedPayload):
|
||||||
|
self.url: str = image.get("url")
|
||||||
|
self.width: int = image.get("width")
|
||||||
|
self.height: int = image.get("height")
|
||||||
|
self.size: str = image.get("size")
|
||||||
|
|
||||||
|
class TextEmbed:
|
||||||
|
type: EmbedType = EmbedType.text
|
||||||
|
|
||||||
|
def __init__(self, embed: TextEmbedPayload, state: State):
|
||||||
|
self.icon_url: str | None = embed.get("icon_url")
|
||||||
|
self.url: str | None = embed.get("url")
|
||||||
|
self.title: str | None = embed.get("title")
|
||||||
|
self.description: str | None = embed.get("description")
|
||||||
|
|
||||||
|
self.media: Asset | None
|
||||||
|
|
||||||
|
if media := embed.get("media"):
|
||||||
|
self.media = Asset(media, state)
|
||||||
|
else:
|
||||||
|
self.media = None
|
||||||
|
|
||||||
|
self.colour: str | None = embed.get("colour")
|
||||||
|
|
||||||
|
class NoneEmbed:
|
||||||
|
type: EmbedType = EmbedType.none
|
||||||
|
|
||||||
|
Embed = Union[WebsiteEmbed, ImageEmbed, TextEmbed, NoneEmbed]
|
||||||
|
|
||||||
|
def to_embed(payload: EmbedPayload, state: State) -> Embed:
|
||||||
|
if payload["type"] == "Website":
|
||||||
|
return WebsiteEmbed(payload)
|
||||||
|
elif payload["type"] == "Image":
|
||||||
|
return ImageEmbed(payload)
|
||||||
|
elif payload["type"] == "Text":
|
||||||
|
return TextEmbed(payload, state)
|
||||||
|
else:
|
||||||
|
return NoneEmbed()
|
||||||
|
|
||||||
|
class EmbedParameters(TypedDict):
|
||||||
|
title: NotRequired[str]
|
||||||
|
description: NotRequired[str]
|
||||||
|
media: NotRequired[str]
|
||||||
|
icon_url: NotRequired[str]
|
||||||
|
colour: NotRequired[str]
|
||||||
|
url: NotRequired[str]
|
||||||
|
|
||||||
|
class SendableEmbed:
|
||||||
|
"""
|
||||||
|
Represents an embed that can be sent in a message, you will never receive this, you will receive :class:`Embed`.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
title: Optional[:class:`str`]
|
||||||
|
The title of the embed
|
||||||
|
|
||||||
|
description: Optional[:class:`str`]
|
||||||
|
The description of the embed
|
||||||
|
|
||||||
|
media: Optional[:class:`str`]
|
||||||
|
The file inside the embed, this is the ID of the file, you can use :meth:`Client.upload_file` to get an ID.
|
||||||
|
|
||||||
|
icon_url: Optional[:class:`str`]
|
||||||
|
The url of the icon url
|
||||||
|
|
||||||
|
colour: Optional[:class:`str`]
|
||||||
|
The embed's accent colour, this is any valid `CSS color <https://developer.mozilla.org/en-US/docs/Web/CSS/color_value>`_
|
||||||
|
|
||||||
|
url: Optional[:class:`str`]
|
||||||
|
URL for hyperlinking the embed's title
|
||||||
|
"""
|
||||||
|
def __init__(self, **attrs: Unpack[EmbedParameters]):
|
||||||
|
self.title: Optional[str] = None
|
||||||
|
self.description: Optional[str] = None
|
||||||
|
self.media: Optional[str] = None
|
||||||
|
self.icon_url: Optional[str] = None
|
||||||
|
self.colour: Optional[str] = None
|
||||||
|
self.url: Optional[str] = None
|
||||||
|
|
||||||
|
for key, value in attrs.items():
|
||||||
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
def to_dict(self) -> SendableEmbedPayload:
|
||||||
|
"""Converts the embed to a dictionary which next accepts
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`dict[str, Any]`
|
||||||
|
The embed
|
||||||
|
"""
|
||||||
|
output: SendableEmbedPayload = {"type": "Text"}
|
||||||
|
|
||||||
|
if title := self.title:
|
||||||
|
output["title"] = title
|
||||||
|
|
||||||
|
if description := self.description:
|
||||||
|
output["description"] = description
|
||||||
|
|
||||||
|
if media := self.media:
|
||||||
|
output["media"] = media
|
||||||
|
|
||||||
|
if icon_url := self.icon_url:
|
||||||
|
output["icon_url"] = icon_url
|
||||||
|
|
||||||
|
if colour := self.colour:
|
||||||
|
output["colour"] = colour
|
||||||
|
|
||||||
|
if url := self.url:
|
||||||
|
output["url"] = url
|
||||||
|
|
||||||
|
return output
|
55
next/emoji.py
Normal file
55
next/emoji.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from .utils import Ulid
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .server import Server
|
||||||
|
from .state import State
|
||||||
|
from .types import Emoji as EmojiPayload
|
||||||
|
|
||||||
|
__all__ = ("Emoji",)
|
||||||
|
|
||||||
|
class Emoji(Ulid):
|
||||||
|
"""Represents a custom emoji.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
id: :class:`str`
|
||||||
|
The id of the emoji
|
||||||
|
author_id: :class:`str`
|
||||||
|
The id of the of user who created the emoji
|
||||||
|
name: :class:`str`
|
||||||
|
The name of the emoji
|
||||||
|
animated: :class:`bool`
|
||||||
|
Whether or not the emoji is animated
|
||||||
|
nsfw: :class:`bool`
|
||||||
|
Whether or not the emoji is nsfw
|
||||||
|
server_id: Optional[:class:`str`]
|
||||||
|
The server id this emoji belongs to, if any
|
||||||
|
"""
|
||||||
|
def __init__(self, payload: EmojiPayload, state: State):
|
||||||
|
self.state: State = state
|
||||||
|
|
||||||
|
self.id: str = payload["_id"]
|
||||||
|
self.author_id: str = payload["creator_id"]
|
||||||
|
self.name: str = payload["name"]
|
||||||
|
self.animated: bool = payload.get("animated", False)
|
||||||
|
self.nsfw: bool = payload.get("nsfw", False)
|
||||||
|
self.server_id: str | None = payload["parent"].get("id")
|
||||||
|
|
||||||
|
async def delete(self) -> None:
|
||||||
|
"""Deletes the emoji."""
|
||||||
|
await self.state.http.delete_emoji(self.id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server(self) -> Server:
|
||||||
|
"""Returns the server this emoji is part of
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Server`
|
||||||
|
The Server this emoji is part of
|
||||||
|
"""
|
||||||
|
return self.state.get_server(self.server_id) # type: ignore
|
59
next/enums.py
Normal file
59
next/enums.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
# typing does not understand aenum so I am pretending its stdlib enum while type checking
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import enum
|
||||||
|
else:
|
||||||
|
import aenum as enum
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"ChannelType",
|
||||||
|
"PresenceType",
|
||||||
|
"RelationshipType",
|
||||||
|
"AssetType",
|
||||||
|
"SortType",
|
||||||
|
"EmbedType"
|
||||||
|
)
|
||||||
|
|
||||||
|
class ChannelType(enum.Enum):
|
||||||
|
saved_messages = "SavedMessages"
|
||||||
|
direct_message = "DirectMessage"
|
||||||
|
group = "Group"
|
||||||
|
text_channel = "TextChannel"
|
||||||
|
voice_channel = "VoiceChannel"
|
||||||
|
|
||||||
|
class PresenceType(enum.Enum):
|
||||||
|
busy = "Busy"
|
||||||
|
idle = "Idle"
|
||||||
|
invisible = "Invisible"
|
||||||
|
online = "Online"
|
||||||
|
focus = "Focus"
|
||||||
|
|
||||||
|
class RelationshipType(enum.Enum):
|
||||||
|
blocked = "Blocked"
|
||||||
|
blocked_other = "BlockedOther"
|
||||||
|
friend = "Friend"
|
||||||
|
incoming_friend_request = "Incoming"
|
||||||
|
none = "None"
|
||||||
|
outgoing_friend_request = "Outgoing"
|
||||||
|
user = "User"
|
||||||
|
|
||||||
|
class AssetType(enum.Enum):
|
||||||
|
image = "Image"
|
||||||
|
video = "Video"
|
||||||
|
text = "Text"
|
||||||
|
audio = "Audio"
|
||||||
|
file = "File"
|
||||||
|
|
||||||
|
class SortType(enum.Enum):
|
||||||
|
latest = "Latest"
|
||||||
|
oldest = "Oldest"
|
||||||
|
relevance = "Relevance"
|
||||||
|
|
||||||
|
class EmbedType(enum.Enum):
|
||||||
|
website = "Website"
|
||||||
|
image = "Image"
|
||||||
|
text = "Text"
|
||||||
|
none = "None"
|
26
next/errors.py
Normal file
26
next/errors.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
__all__ = (
|
||||||
|
"NextError",
|
||||||
|
"HTTPError",
|
||||||
|
"ServerError",
|
||||||
|
"FeatureDisabled",
|
||||||
|
"AutumnDisabled",
|
||||||
|
"Forbidden",
|
||||||
|
)
|
||||||
|
|
||||||
|
class NextError(Exception):
|
||||||
|
"Base exception for next"
|
||||||
|
|
||||||
|
class HTTPError(NextError):
|
||||||
|
"Base exception for http errors"
|
||||||
|
|
||||||
|
class ServerError(NextError):
|
||||||
|
"Internal server error"
|
||||||
|
|
||||||
|
class FeatureDisabled(NextError):
|
||||||
|
"Base class for any feature disabled errors"
|
||||||
|
|
||||||
|
class AutumnDisabled(FeatureDisabled):
|
||||||
|
"The autumn feature is disabled"
|
||||||
|
|
||||||
|
class Forbidden(HTTPError):
|
||||||
|
"Missing permissions"
|
0
next/ext/__init__.py
Normal file
0
next/ext/__init__.py
Normal file
10
next/ext/commands/__init__.py
Normal file
10
next/ext/commands/__init__.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from .checks import *
|
||||||
|
from .client import *
|
||||||
|
from .cog import *
|
||||||
|
from .command import *
|
||||||
|
from .context import *
|
||||||
|
from .converters import *
|
||||||
|
from .cooldown import *
|
||||||
|
from .errors import *
|
||||||
|
from .group import *
|
||||||
|
from .help import *
|
95
next/ext/commands/checks.py
Normal file
95
next/ext/commands/checks.py
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Callable, Coroutine, Union, cast
|
||||||
|
from typing_extensions import TypeVar
|
||||||
|
|
||||||
|
import next
|
||||||
|
|
||||||
|
from .command import Command
|
||||||
|
from .context import Context
|
||||||
|
from .errors import (MissingPermissionsError, NotBotOwner, NotServerOwner,
|
||||||
|
ServerOnly)
|
||||||
|
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_D]], Union[Any, Coroutine[Any, Any, Any]]]
|
||||||
|
|
||||||
|
def check(check: Check[ClientT_D]) -> Callable[[T], T]:
|
||||||
|
"""A decorator for adding command checks
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
check: Callable[[Context], Union[Any, Coroutine[Any, Any, Any]]]
|
||||||
|
The function to be called, must take one parameter, context and optionally be a coroutine, the return value denoating whether the check should pass or fail
|
||||||
|
"""
|
||||||
|
def inner(func: T) -> T:
|
||||||
|
if isinstance(func, Command):
|
||||||
|
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 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_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
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
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_D]) -> bool:
|
||||||
|
if not context.server_id:
|
||||||
|
raise ServerOnly
|
||||||
|
|
||||||
|
if context.author.id == context.server.owner_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
raise NotServerOwner
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
def has_permissions(**permissions: bool) -> Callable[[T], T]:
|
||||||
|
@check
|
||||||
|
def inner(context: Context[ClientT_D]) -> bool:
|
||||||
|
author = context.author
|
||||||
|
|
||||||
|
if not author.has_permissions(**permissions):
|
||||||
|
raise MissingPermissionsError(permissions)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
def has_channel_permissions(**permissions: bool) -> Callable[[T], T]:
|
||||||
|
@check
|
||||||
|
def inner(context: Context[ClientT_D]) -> bool:
|
||||||
|
author = context.author
|
||||||
|
|
||||||
|
if not isinstance(author, next.Member):
|
||||||
|
raise ServerOnly
|
||||||
|
|
||||||
|
if not author.has_channel_permissions(context.channel, **permissions):
|
||||||
|
raise MissingPermissionsError(permissions)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
return inner
|
389
next/ext/commands/client.py
Normal file
389
next/ext/commands/client.py
Normal file
|
@ -0,0 +1,389 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
from importlib import import_module
|
||||||
|
from typing import (TYPE_CHECKING, Any, Coroutine, Optional, Protocol, TypeVar, Union,
|
||||||
|
overload, runtime_checkable)
|
||||||
|
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
import next
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .help import HelpCommand
|
||||||
|
|
||||||
|
from .cog import Cog
|
||||||
|
from .command import Command
|
||||||
|
from .context import Context
|
||||||
|
from .errors import CheckError, CommandNotFound, MissingSetup
|
||||||
|
from .view import StringView
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"CommandsMeta",
|
||||||
|
"CommandsClient"
|
||||||
|
)
|
||||||
|
|
||||||
|
V = TypeVar("V")
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class ExtensionProtocol(Protocol):
|
||||||
|
@staticmethod
|
||||||
|
def setup(client: CommandsClient) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
class CommandsMeta(type):
|
||||||
|
_commands: list[Command[Any]]
|
||||||
|
|
||||||
|
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) and value.parent is None:
|
||||||
|
commands.append(value)
|
||||||
|
|
||||||
|
self._commands = commands
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class CaseInsensitiveDict(dict[str, V]):
|
||||||
|
def __setitem__(self, key: str, value: V) -> None:
|
||||||
|
super().__setitem__(key.casefold(), value)
|
||||||
|
|
||||||
|
def __getitem__(self, key: str) -> V:
|
||||||
|
return super().__getitem__(key.casefold())
|
||||||
|
|
||||||
|
def __contains__(self, key: object) -> bool:
|
||||||
|
if isinstance(key, str):
|
||||||
|
return super().__contains__(key.casefold())
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get(self, key: str) -> V | None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get(self, key: str, default: V | T) -> V | T:
|
||||||
|
...
|
||||||
|
|
||||||
|
def get(self, key: str, default: Optional[T] = None) -> V | T | None:
|
||||||
|
return super().get(key.casefold(), default)
|
||||||
|
|
||||||
|
def __delitem__(self, key: str) -> None:
|
||||||
|
super().__delitem__(key.casefold())
|
||||||
|
|
||||||
|
|
||||||
|
class CommandsClient(next.Client, metaclass=CommandsMeta):
|
||||||
|
"""Main class that adds commands, this class should be subclassed along with `next.Client`."""
|
||||||
|
|
||||||
|
_commands: list[Command[Self]]
|
||||||
|
|
||||||
|
def __init__(self, *args: Any, help_command: Union[HelpCommand[Self], None, next.utils._Missing] = next.utils.Missing, case_insensitive: bool = False, **kwargs: Any):
|
||||||
|
from .help import DefaultHelpCommand, HelpCommandImpl
|
||||||
|
|
||||||
|
self.all_commands: dict[str, Command[Self]] = {} if not case_insensitive else CaseInsensitiveDict()
|
||||||
|
self.cogs: dict[str, Cog[Self]] = {}
|
||||||
|
self.extensions: dict[str, ExtensionProtocol] = {}
|
||||||
|
|
||||||
|
for command in self._commands:
|
||||||
|
self.all_commands[command.name] = command
|
||||||
|
|
||||||
|
for alias in command.aliases:
|
||||||
|
self.all_commands[alias] = command
|
||||||
|
|
||||||
|
self.help_command: HelpCommand[Self] | None
|
||||||
|
|
||||||
|
if help_command is not None:
|
||||||
|
self.help_command = help_command or DefaultHelpCommand[Self]()
|
||||||
|
self.add_command(HelpCommandImpl(self))
|
||||||
|
else:
|
||||||
|
self.help_command = None
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
@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: next.Message) -> Union[str, list[str]]:
|
||||||
|
"""Overwrite this function to set the prefix used for commands, this function is called for every message.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
message: :class:`Message`
|
||||||
|
The message that was sent
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
Union[:class:`str`, list[:class:`str`]]
|
||||||
|
The prefix(s) for the commands
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_command(self, name: str) -> Command[Self]:
|
||||||
|
"""Gets a command.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
name: :class:`str`
|
||||||
|
The name or alias of the command
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Command`
|
||||||
|
The command with the name
|
||||||
|
"""
|
||||||
|
return self.all_commands[name]
|
||||||
|
|
||||||
|
def add_command(self, command: Command[Self]) -> 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.all_commands[command.name] = command
|
||||||
|
|
||||||
|
for alias in command.aliases:
|
||||||
|
self.all_commands[alias] = command
|
||||||
|
|
||||||
|
def remove_command(self, name: str) -> Optional[Command[Self]]:
|
||||||
|
"""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.all_commands.pop(name, None)
|
||||||
|
|
||||||
|
if command is not None:
|
||||||
|
for alias in command.aliases:
|
||||||
|
self.all_commands.pop(alias, None)
|
||||||
|
|
||||||
|
return command
|
||||||
|
|
||||||
|
def get_view(self, message: next.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: next.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: next.Message) -> Any:
|
||||||
|
"""Processes commands, if you overwrite `Client.on_message` you should manually call this function inside the event.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
message: :class:`Message`
|
||||||
|
The message to process commands on
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
Any
|
||||||
|
The return of the command, if any
|
||||||
|
"""
|
||||||
|
content = message.content
|
||||||
|
|
||||||
|
prefixes = await self.get_prefix(message)
|
||||||
|
|
||||||
|
if isinstance(prefixes, str):
|
||||||
|
prefixes = [prefixes]
|
||||||
|
|
||||||
|
for prefix in prefixes:
|
||||||
|
if content.startswith(prefix):
|
||||||
|
content = content[len(prefix):]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not content:
|
||||||
|
return
|
||||||
|
|
||||||
|
view = self.get_view(message)(content)
|
||||||
|
|
||||||
|
try:
|
||||||
|
command_name = view.get_next_word()
|
||||||
|
except StopIteration:
|
||||||
|
return
|
||||||
|
|
||||||
|
context_cls = self.get_context(message)
|
||||||
|
|
||||||
|
try:
|
||||||
|
command = self.get_command(command_name)
|
||||||
|
except KeyError:
|
||||||
|
context = context_cls(None, command_name, view, message, self)
|
||||||
|
return self.dispatch("command_error", context, CommandNotFound(command_name))
|
||||||
|
|
||||||
|
context = context_cls(command, command_name, view, message, self)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.dispatch("command", context)
|
||||||
|
|
||||||
|
if not await self.global_check(context):
|
||||||
|
raise CheckError(f"the global check for the command failed")
|
||||||
|
|
||||||
|
if not await context.can_run():
|
||||||
|
raise CheckError(f"the check(s) for the command failed")
|
||||||
|
|
||||||
|
output = await context.invoke()
|
||||||
|
self.dispatch("after_command_invoke", context, output)
|
||||||
|
|
||||||
|
return output
|
||||||
|
except Exception as e:
|
||||||
|
await command._error_handler(command.cog or self, context, e)
|
||||||
|
self.dispatch("command_error", context, e)
|
||||||
|
|
||||||
|
async def on_command_error(self, ctx: Context[Self], error: Exception, /) -> None:
|
||||||
|
traceback.print_exception(type(error), error, error.__traceback__)
|
||||||
|
|
||||||
|
def on_message(self, message: next.Message) -> Coroutine[Any, Any, Any]:
|
||||||
|
return self.process_commands(message)
|
||||||
|
|
||||||
|
async def global_check(self, context: Context[Self]) -> bool:
|
||||||
|
"""A global check that stops commands from running on certain criteria.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
context: :class:`Context`
|
||||||
|
The context for the invokation of the command
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`bool` represents if the command should run or not
|
||||||
|
"""
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def add_cog(self, cog: Cog[Self]) -> None:
|
||||||
|
"""Adds a cog, this cog must subclass `Cog`.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
cog: :class:`Cog`
|
||||||
|
The cog to be added
|
||||||
|
"""
|
||||||
|
cog._inject(self)
|
||||||
|
|
||||||
|
def remove_cog(self, cog_name: str) -> Cog[Self]:
|
||||||
|
"""Removes a cog.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
cog_name: :class:`str`
|
||||||
|
The name of the cog to be removed
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Cog`
|
||||||
|
The cog that was removed
|
||||||
|
"""
|
||||||
|
cog = self.cogs.pop(cog_name)
|
||||||
|
cog._uninject(self)
|
||||||
|
|
||||||
|
return cog
|
||||||
|
|
||||||
|
def load_extension(self, name: str) -> None:
|
||||||
|
"""Loads an extension, this takes a module name and runs the setup function inside of it.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
name: :class:`str`
|
||||||
|
The name of the extension to be loaded
|
||||||
|
"""
|
||||||
|
extension = import_module(name)
|
||||||
|
|
||||||
|
if not isinstance(extension, ExtensionProtocol):
|
||||||
|
raise MissingSetup(f"'{extension}' is missing a setup function")
|
||||||
|
|
||||||
|
self.extensions[name] = extension
|
||||||
|
extension.setup(self)
|
||||||
|
|
||||||
|
def unload_extension(self, name: str) -> None:
|
||||||
|
"""Unloads an extension, this takes a module name and runs the teardown function inside of it.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
name: :class:`str`
|
||||||
|
The name of the extension to be unloaded
|
||||||
|
"""
|
||||||
|
extension = self.extensions.pop(name)
|
||||||
|
|
||||||
|
del sys.modules[name]
|
||||||
|
|
||||||
|
if teardown := getattr(extension, "teardown", None):
|
||||||
|
teardown(self)
|
||||||
|
|
||||||
|
def reload_extension(self, name: str) -> None:
|
||||||
|
"""Reloads an extension, this will unload and reload the extension.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
name: :class:`str`
|
||||||
|
The name of the extension to be reloaded
|
||||||
|
"""
|
||||||
|
self.unload_extension(name)
|
||||||
|
self.load_extension(name)
|
||||||
|
|
||||||
|
def get_cog(self, name: str) -> Cog[Self]:
|
||||||
|
"""Gets a cog.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
name: :class:`str`
|
||||||
|
The name of the cog to get
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Cog`
|
||||||
|
The cog that was requested
|
||||||
|
"""
|
||||||
|
return self.cogs[name]
|
||||||
|
|
||||||
|
def get_extension(self, name: str) -> ExtensionProtocol:
|
||||||
|
"""Gets an extension.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
name: :class:`str`
|
||||||
|
The name of the extension to get
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`ExtensionProtocol`
|
||||||
|
The extension that was requested
|
||||||
|
"""
|
||||||
|
return self.extensions[name]
|
96
next/ext/commands/cog.py
Normal file
96
next/ext/commands/cog.py
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Callable, Coroutine, Generic, Optional, TypeVar
|
||||||
|
from typing_extensions import ParamSpec
|
||||||
|
|
||||||
|
from next.errors import NextError
|
||||||
|
|
||||||
|
from .command import Command
|
||||||
|
from .utils import ClientT_D
|
||||||
|
|
||||||
|
P = ParamSpec("P")
|
||||||
|
R = TypeVar("R")
|
||||||
|
|
||||||
|
__all__ = ("Cog", "CogMeta")
|
||||||
|
|
||||||
|
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, 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 key, value in base.__dict__.items():
|
||||||
|
if isinstance(value, Command):
|
||||||
|
for extra_key, extra_value in extras.items():
|
||||||
|
setattr(value, extra_key, extra_value)
|
||||||
|
|
||||||
|
commands.append(value)
|
||||||
|
|
||||||
|
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_D], metaclass=CogMeta):
|
||||||
|
_cog_commands: list[Command[ClientT_D]]
|
||||||
|
_cog_listeners: dict[str, list[str]]
|
||||||
|
qualified_name: str
|
||||||
|
|
||||||
|
def cog_load(self) -> None:
|
||||||
|
"""A special method that is called when the cog gets loaded."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def cog_unload(self) -> None:
|
||||||
|
"""A special method that is called when the cog gets removed."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _inject(self, client: ClientT_D) -> None:
|
||||||
|
client.cogs[self.qualified_name] = self
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
for key, listeners in self._cog_listeners.items():
|
||||||
|
for listener_name in listeners:
|
||||||
|
client.listeners[key].remove(getattr(self, listener_name))
|
||||||
|
|
||||||
|
self.cog_unload()
|
||||||
|
|
||||||
|
@property
|
||||||
|
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 NextError("event name must start with `on_`")
|
||||||
|
|
||||||
|
setattr(func, "__listener_name", name or func.__name__[3:])
|
||||||
|
return func
|
||||||
|
|
||||||
|
return inner
|
312
next/ext/commands/command.py
Normal file
312
next/ext/commands/command.py
Normal file
|
@ -0,0 +1,312 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import traceback
|
||||||
|
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
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 10):
|
||||||
|
from types import UnionType
|
||||||
|
|
||||||
|
UnionTypes = (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
|
||||||
|
from .cog import Cog
|
||||||
|
from .context import Context
|
||||||
|
from .group import Group
|
||||||
|
|
||||||
|
__all__: tuple[str, ...] = (
|
||||||
|
"Command",
|
||||||
|
"command"
|
||||||
|
)
|
||||||
|
|
||||||
|
NoneType: type[None] = type(None)
|
||||||
|
P = ParamSpec("P")
|
||||||
|
|
||||||
|
class Command(Generic[ClientT_Co_D]):
|
||||||
|
"""Class for holding info about a command.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
callback: Callable[..., Coroutine[Any, Any, Any]]
|
||||||
|
The callback for the command
|
||||||
|
name: :class:`str`
|
||||||
|
The name of the command
|
||||||
|
aliases: list[:class:`str`]
|
||||||
|
The aliases of the command
|
||||||
|
parent: Optional[:class:`Group`]
|
||||||
|
The parent of the command if this command is a subcommand
|
||||||
|
cog: Optional[:class:`Cog`]
|
||||||
|
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", "hidden", "cooldown", "cooldown_bucket")
|
||||||
|
|
||||||
|
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 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[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[ClientT_Co_D], *args: Any, **kwargs: Any) -> Any:
|
||||||
|
"""Runs the command and calls the error handler if the command errors.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
context: :class:`Context`
|
||||||
|
The context for the command
|
||||||
|
args: list[:class:`str`]
|
||||||
|
The arguments for the command
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return await self.callback(self.cog or context.client, context, *args, **kwargs)
|
||||||
|
except Exception as err:
|
||||||
|
return await self._error_handler(self.cog or context.client, context, err)
|
||||||
|
|
||||||
|
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]]:
|
||||||
|
"""Sets the error handler for the command.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
func: Callable[..., Coroutine[Any, Any, Any]]
|
||||||
|
The function for the error handler
|
||||||
|
|
||||||
|
Example
|
||||||
|
--------
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
@mycommand.error
|
||||||
|
async def mycommand_error(self, ctx, error):
|
||||||
|
await ctx.send(str(error))
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._error_handler = func
|
||||||
|
return func
|
||||||
|
|
||||||
|
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[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)
|
||||||
|
except:
|
||||||
|
if converter is NoneType:
|
||||||
|
context.view.undo()
|
||||||
|
return None
|
||||||
|
|
||||||
|
raise UnionConverterError(arg)
|
||||||
|
|
||||||
|
elif origin is Annotated:
|
||||||
|
annotated_args = get_args(annotation)
|
||||||
|
|
||||||
|
if annotated_args[1] == "_next_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)
|
||||||
|
|
||||||
|
elif origin is Literal:
|
||||||
|
if arg in get_args(annotation):
|
||||||
|
return arg
|
||||||
|
else:
|
||||||
|
raise InvalidLiteralArgument(arg)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
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
|
||||||
|
|
||||||
|
origin: Any
|
||||||
|
if origin := get_origin(annotation):
|
||||||
|
return await cls.handle_origin(context, origin, annotation, arg)
|
||||||
|
else:
|
||||||
|
return await maybe_coroutine(annotation, arg, context)
|
||||||
|
else:
|
||||||
|
return arg
|
||||||
|
|
||||||
|
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:]:
|
||||||
|
if parameter.kind == parameter.KEYWORD_ONLY:
|
||||||
|
try:
|
||||||
|
arg = await self.convert_argument(context.view.get_rest(), parameter.annotation, context)
|
||||||
|
except StopIteration:
|
||||||
|
if parameter.default is not parameter.empty:
|
||||||
|
arg = parameter.default
|
||||||
|
|
||||||
|
elif is_optional(parameter.annotation):
|
||||||
|
arg = None
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
context.kwargs[parameter.name] = arg
|
||||||
|
|
||||||
|
elif parameter.kind == parameter.VAR_POSITIONAL:
|
||||||
|
with suppress(StopIteration):
|
||||||
|
while True:
|
||||||
|
context.args.append(await self.convert_argument(context.view.get_next_word(), parameter.annotation, context))
|
||||||
|
|
||||||
|
elif parameter.kind == parameter.POSITIONAL_OR_KEYWORD:
|
||||||
|
try:
|
||||||
|
rest = context.view.get_next_word()
|
||||||
|
arg = await self.convert_argument(rest, parameter.annotation, context)
|
||||||
|
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}\">"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def short_description(self) -> Optional[str]:
|
||||||
|
"""Returns the first line of the description or None if there is no description."""
|
||||||
|
if self.description:
|
||||||
|
return self.description.split("\n")[0]
|
||||||
|
|
||||||
|
def get_usage(self) -> str:
|
||||||
|
"""Returns the usage string for the command."""
|
||||||
|
if self.usage:
|
||||||
|
return self.usage
|
||||||
|
|
||||||
|
parents: list[str] = []
|
||||||
|
|
||||||
|
if self.parent:
|
||||||
|
parent = self.parent
|
||||||
|
|
||||||
|
while parent:
|
||||||
|
parents.append(parent.name)
|
||||||
|
parent = parent.parent
|
||||||
|
|
||||||
|
parameters: list[str] = []
|
||||||
|
|
||||||
|
for parameter in self.parameters[2:]:
|
||||||
|
if parameter.kind == parameter.POSITIONAL_OR_KEYWORD:
|
||||||
|
if parameter.default is not parameter.empty:
|
||||||
|
parameters.append(f"[{parameter.name}]")
|
||||||
|
else:
|
||||||
|
parameters.append(f"<{parameter.name}>")
|
||||||
|
elif parameter.kind == parameter.KEYWORD_ONLY:
|
||||||
|
if parameter.default is not parameter.empty:
|
||||||
|
parameters.append(f"[{parameter.name}]")
|
||||||
|
else:
|
||||||
|
parameters.append(f"<{parameter.name}...>")
|
||||||
|
elif parameter.kind == parameter.VAR_POSITIONAL:
|
||||||
|
parameters.append(f"[{parameter.name}...]")
|
||||||
|
|
||||||
|
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,
|
||||||
|
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
|
||||||
|
-----------
|
||||||
|
name: Optional[:class:`str`]
|
||||||
|
The name of the command, this defaults to the functions name
|
||||||
|
aliases: Optional[list[:class:`str`]]
|
||||||
|
The aliases of the command, defaults to no aliases
|
||||||
|
cls: type[:class:`Command`]
|
||||||
|
The class used for creating the command, this defaults to :class:`Command` but can be used to use a custom command subclass
|
||||||
|
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]]) -> Command[ClientT_Co]:
|
||||||
|
return cls(func, name or func.__name__, aliases=aliases or [], usage=usage)
|
||||||
|
|
||||||
|
return inner
|
114
next/ext/commands/context.py
Normal file
114
next/ext/commands/context.py
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any, Generic, Optional
|
||||||
|
|
||||||
|
import next
|
||||||
|
from next.utils import maybe_coroutine
|
||||||
|
|
||||||
|
from .command import Command
|
||||||
|
from .group import Group
|
||||||
|
from .utils import ClientT_Co_D
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .view import StringView
|
||||||
|
from next.state import State
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"Context",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Context(next.Messageable, Generic[ClientT_Co_D]):
|
||||||
|
"""Stores metadata the commands execution.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
command: Optional[:class:`Command`]
|
||||||
|
The command, this can be `None` when no command was found and the error handler is being executed
|
||||||
|
invoked_with: :class:`str`
|
||||||
|
The command name that was used, this can be an alias, the commands name or a command that doesnt exist
|
||||||
|
message: :class:`Message`
|
||||||
|
The message that was sent to invoke the command
|
||||||
|
channel: :class:`Messageable`
|
||||||
|
The channel the command was invoked in
|
||||||
|
server_id: Optional[:class:`Server`]
|
||||||
|
The server the command was invoked in
|
||||||
|
author: Union[:class:`Member`, :class:`User`]
|
||||||
|
The user or member that invoked the commad, will be :class:`User` in DMs
|
||||||
|
args: list[:class:`str`]
|
||||||
|
The positional arguments being passed to the command
|
||||||
|
kwargs: dict[:class:`str`, Any]
|
||||||
|
The keyword arguments being passed to the command
|
||||||
|
client: :class:`CommandsClient`
|
||||||
|
The next client
|
||||||
|
"""
|
||||||
|
__slots__ = ("command", "invoked_with", "args", "message", "channel", "author", "view", "kwargs", "state", "client", "server_id")
|
||||||
|
|
||||||
|
async def _get_channel_id(self) -> str:
|
||||||
|
return self.channel.id
|
||||||
|
|
||||||
|
def __init__(self, command: Optional[Command[ClientT_Co_D]], invoked_with: str, view: StringView, message: next.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: next.Message = message
|
||||||
|
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: next.TextChannel | next.GroupDMChannel | next.DMChannel | next.SavedMessageChannel = message.channel
|
||||||
|
self.author: next.Member | next.User = message.author
|
||||||
|
self.state: State = message.state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server(self) -> next.Server:
|
||||||
|
""":class:`Server` The server this context belongs too
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
:class:`LookupError`
|
||||||
|
Raises if the context is not from a server
|
||||||
|
"""
|
||||||
|
if not self.server_id:
|
||||||
|
raise LookupError
|
||||||
|
|
||||||
|
return self.state.get_server(self.server_id)
|
||||||
|
|
||||||
|
async def invoke(self) -> Any:
|
||||||
|
"""Invokes the command.
|
||||||
|
|
||||||
|
.. note:: If the command is `None`, this function will do nothing.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
args: list[:class:`str`]
|
||||||
|
The args being passed to the command
|
||||||
|
"""
|
||||||
|
|
||||||
|
if command := self.command:
|
||||||
|
if isinstance(command, Group):
|
||||||
|
try:
|
||||||
|
subcommand_name = self.view.get_next_word()
|
||||||
|
except StopIteration:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if subcommand := command.subcommands.get(subcommand_name):
|
||||||
|
self.command = command = subcommand
|
||||||
|
return await self.invoke()
|
||||||
|
|
||||||
|
self.view.undo()
|
||||||
|
|
||||||
|
await command.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[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] | ClientT_Co_D | None = None) -> None:
|
||||||
|
argument = argument or self.client
|
||||||
|
|
||||||
|
command = self.client.get_command("help")
|
||||||
|
await command.invoke(self, argument)
|
126
next/ext/commands/converters.py
Normal file
126
next/ext/commands/converters.py
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import TYPE_CHECKING, Annotated, TypeVar
|
||||||
|
|
||||||
|
from next import Category, Channel, Member, User, utils
|
||||||
|
|
||||||
|
from .context import Context
|
||||||
|
from .errors import (BadBoolArgument, CategoryConverterError,
|
||||||
|
ChannelConverterError, MemberConverterError, ServerOnly,
|
||||||
|
UserConverterError)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .client import CommandsClient
|
||||||
|
|
||||||
|
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})>")
|
||||||
|
|
||||||
|
ClientT = TypeVar("ClientT", bound="CommandsClient")
|
||||||
|
|
||||||
|
def bool_converter(arg: str, _: Context[ClientT]) -> bool:
|
||||||
|
lowered = arg.lower()
|
||||||
|
if lowered in ("yes", "true", "ye", "y", "1", "on", "enable"):
|
||||||
|
return True
|
||||||
|
elif lowered in ("no", "false", "n", "f", "0", "off", "disabled"):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
raise BadBoolArgument(lowered)
|
||||||
|
|
||||||
|
def category_converter(arg: str, context: Context[ClientT]) -> Category:
|
||||||
|
if not context.server_id:
|
||||||
|
raise ServerOnly
|
||||||
|
|
||||||
|
try:
|
||||||
|
return context.server.get_category(arg)
|
||||||
|
except LookupError:
|
||||||
|
try:
|
||||||
|
return utils.get(context.server.categories, name=arg)
|
||||||
|
except LookupError:
|
||||||
|
raise CategoryConverterError(arg)
|
||||||
|
|
||||||
|
def channel_converter(arg: str, context: Context[ClientT]) -> Channel:
|
||||||
|
if not context.server_id:
|
||||||
|
raise ServerOnly
|
||||||
|
|
||||||
|
if (match := channel_regex.match(arg)):
|
||||||
|
arg = match.group(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return context.server.get_channel(arg)
|
||||||
|
except LookupError:
|
||||||
|
try:
|
||||||
|
return utils.get(context.server.channels, name=arg)
|
||||||
|
except LookupError:
|
||||||
|
raise ChannelConverterError(arg)
|
||||||
|
|
||||||
|
def user_converter(arg: str, context: Context[ClientT]) -> User:
|
||||||
|
if (match := user_regex.match(arg)):
|
||||||
|
arg = match.group(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return context.client.get_user(arg)
|
||||||
|
except LookupError:
|
||||||
|
try:
|
||||||
|
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)
|
||||||
|
|
||||||
|
def member_converter(arg: str, context: Context[ClientT]) -> Member:
|
||||||
|
if not context.server_id:
|
||||||
|
raise ServerOnly
|
||||||
|
|
||||||
|
if (match := user_regex.match(arg)):
|
||||||
|
arg = match.group(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return context.server.get_member(arg)
|
||||||
|
except LookupError:
|
||||||
|
try:
|
||||||
|
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)
|
||||||
|
|
||||||
|
def int_converter(arg: str, context: Context[ClientT]) -> int:
|
||||||
|
return int(arg)
|
||||||
|
|
||||||
|
IntConverter = Annotated[int, int_converter]
|
||||||
|
BoolConverter = Annotated[bool, bool_converter]
|
||||||
|
CategoryConverter = Annotated[Category, category_converter]
|
||||||
|
UserConverter = Annotated[User, user_converter]
|
||||||
|
MemberConverter = Annotated[Member, member_converter]
|
||||||
|
ChannelConverter = Annotated[Channel, channel_converter]
|
||||||
|
|
||||||
|
Greedy = Annotated[list[T], "_next_greedy_marker"]
|
144
next/ext/commands/cooldown.py
Normal file
144
next/ext/commands/cooldown.py
Normal file
|
@ -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
|
||||||
|
|
||||||
|
return inner
|
114
next/ext/commands/errors.py
Normal file
114
next/ext/commands/errors.py
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
from next import NextError
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"CommandError",
|
||||||
|
"CommandNotFound",
|
||||||
|
"NoClosingQuote",
|
||||||
|
"CheckError",
|
||||||
|
"NotBotOwner",
|
||||||
|
"NotServerOwner",
|
||||||
|
"ServerOnly",
|
||||||
|
"ConverterError",
|
||||||
|
"InvalidLiteralArgument",
|
||||||
|
"BadBoolArgument",
|
||||||
|
"CategoryConverterError",
|
||||||
|
"ChannelConverterError",
|
||||||
|
"UserConverterError",
|
||||||
|
"MemberConverterError",
|
||||||
|
"MissingSetup",
|
||||||
|
"CommandOnCooldown"
|
||||||
|
)
|
||||||
|
|
||||||
|
class CommandError(NextError):
|
||||||
|
"""base error for all command's related errors"""
|
||||||
|
|
||||||
|
class CommandNotFound(CommandError):
|
||||||
|
"""Raised when a command isnt found.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
command_name: :class:`str`
|
||||||
|
The name of the command that wasnt found
|
||||||
|
"""
|
||||||
|
__slots__ = ("command_name",)
|
||||||
|
|
||||||
|
def __init__(self, command_name: str):
|
||||||
|
self.command_name: str = command_name
|
||||||
|
|
||||||
|
class NoClosingQuote(CommandError):
|
||||||
|
"""Raised when there is no closing quote for a command argument"""
|
||||||
|
|
||||||
|
class CheckError(CommandError):
|
||||||
|
"""Raised when a check fails for a command"""
|
||||||
|
|
||||||
|
class NotBotOwner(CheckError):
|
||||||
|
"""Raised when the `is_bot_owner` check fails"""
|
||||||
|
|
||||||
|
class NotServerOwner(CheckError):
|
||||||
|
"""Raised when the `is_server_owner` check fails"""
|
||||||
|
|
||||||
|
class ServerOnly(CheckError):
|
||||||
|
"""Raised when a check requires the command to be ran in a server"""
|
||||||
|
|
||||||
|
class MissingPermissionsError(CheckError):
|
||||||
|
"""Raised when a check requires permissions the user does not have
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
permissions: :class:`dict[str, bool]`
|
||||||
|
The permissions which the user did not have
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, permissions: dict[str, bool]):
|
||||||
|
self.permissions = permissions
|
||||||
|
|
||||||
|
class ConverterError(CommandError):
|
||||||
|
"""Base class for all converter errors"""
|
||||||
|
|
||||||
|
class InvalidLiteralArgument(ConverterError):
|
||||||
|
"""Raised when the argument is not a valid literal argument"""
|
||||||
|
|
||||||
|
class BadBoolArgument(ConverterError):
|
||||||
|
"""Raised when the bool converter fails"""
|
||||||
|
|
||||||
|
class CategoryConverterError(ConverterError):
|
||||||
|
"""Raised when the Category conveter fails"""
|
||||||
|
def __init__(self, argument: str):
|
||||||
|
self.argument = argument
|
||||||
|
|
||||||
|
class ChannelConverterError(ConverterError):
|
||||||
|
"""Raised when the Channel conveter fails"""
|
||||||
|
def __init__(self, argument: str):
|
||||||
|
self.argument = argument
|
||||||
|
|
||||||
|
class UserConverterError(ConverterError):
|
||||||
|
"""Raised when the Category conveter fails"""
|
||||||
|
def __init__(self, argument: str):
|
||||||
|
self.argument = argument
|
||||||
|
|
||||||
|
class MemberConverterError(ConverterError):
|
||||||
|
"""Raised when the Category conveter fails"""
|
||||||
|
def __init__(self, argument: str):
|
||||||
|
self.argument = argument
|
||||||
|
|
||||||
|
class UnionConverterError(ConverterError):
|
||||||
|
"""Raised when all converters in a union fails"""
|
||||||
|
def __init__(self, argument: str):
|
||||||
|
self.argument = argument
|
||||||
|
|
||||||
|
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
|
181
next/ext/commands/group.py
Normal file
181
next/ext/commands/group.py
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Callable, Coroutine, Optional
|
||||||
|
|
||||||
|
from .command import Command
|
||||||
|
from .utils import ClientT_Co_D, ClientT_D
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"Group",
|
||||||
|
"group"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Group(Command[ClientT_Co_D]):
|
||||||
|
"""Class for holding info about a group command.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
callback: Callable[..., Coroutine[Any, Any, Any]]
|
||||||
|
The callback for the group command
|
||||||
|
name: :class:`str`
|
||||||
|
The name of the command
|
||||||
|
aliases: list[:class:`str`]
|
||||||
|
The aliases of the group command
|
||||||
|
subcommands: dict[:class:`str`, :class:`Command`]
|
||||||
|
The group's subcommands.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__: tuple[str, ...] = ("subcommands",)
|
||||||
|
|
||||||
|
def __init__(self, callback: Callable[..., Coroutine[Any, Any, Any]], name: str, aliases: list[str]):
|
||||||
|
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[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
|
||||||
|
-----------
|
||||||
|
name: Optional[:class:`str`]
|
||||||
|
The name of the command, this defaults to the functions name
|
||||||
|
aliases: Optional[list[:class:`str`]]
|
||||||
|
The aliases of the command, defaults to no aliases
|
||||||
|
cls: type[:class:`Command`]
|
||||||
|
The class used for creating the command, this defaults to :class:`Command` but can be used to use a custom command subclass
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
Callable[Callable[..., Coroutine], :class:`Command`]
|
||||||
|
A function that takes the command callback and returns a :class:`Command`
|
||||||
|
"""
|
||||||
|
def inner(func: Callable[..., Coroutine[Any, Any, Any]]):
|
||||||
|
command = cls(func, name or func.__name__, aliases=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[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
|
||||||
|
-----------
|
||||||
|
name: Optional[:class:`str`]
|
||||||
|
The name of the group command, this defaults to the functions name
|
||||||
|
aliases: Optional[list[:class:`str`]]
|
||||||
|
The aliases of the group command, defaults to no aliases
|
||||||
|
cls: type[:class:`Group`]
|
||||||
|
The class used for creating the command, this defaults to :class:`Group` but can be used to use a custom group subclass
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
Callable[Callable[..., Coroutine], :class:`Group`]
|
||||||
|
A function that takes the command callback and returns a :class:`Group`
|
||||||
|
"""
|
||||||
|
cls = cls or type(self)
|
||||||
|
|
||||||
|
def inner(func: Callable[..., Coroutine[Any, Any, Any]]):
|
||||||
|
command = cls(func, name or func.__name__, aliases or [])
|
||||||
|
command.parent = self
|
||||||
|
self.subcommands[command.name] = command
|
||||||
|
|
||||||
|
for alias in command.aliases:
|
||||||
|
self.subcommands[alias] = command
|
||||||
|
|
||||||
|
return command
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<Group name=\"{self.name}\">"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def commands(self) -> list[Command[ClientT_Co_D]]:
|
||||||
|
"""Gets all commands registered
|
||||||
|
|
||||||
|
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
|
||||||
|
-----------
|
||||||
|
name: Optional[:class:`str`]
|
||||||
|
The name of the group command, this defaults to the functions name
|
||||||
|
aliases: Optional[list[:class:`str`]]
|
||||||
|
The aliases of the group command, defaults to no aliases
|
||||||
|
cls: type[:class:`Group`]
|
||||||
|
The class used for creating the command, this defaults to :class:`Group` but can be used to use a custom group subclass
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
Callable[Callable[..., Coroutine], :class:`Group`]
|
||||||
|
A function that takes the command callback and returns a :class:`Group`
|
||||||
|
"""
|
||||||
|
|
||||||
|
def inner(func: Callable[..., Coroutine[Any, Any, Any]]):
|
||||||
|
return cls(func, name or func.__name__, aliases or [])
|
||||||
|
|
||||||
|
return inner
|
217
next/ext/commands/help.py
Normal file
217
next/ext/commands/help.py
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import TYPE_CHECKING, Generic, Optional, TypedDict, Union, cast
|
||||||
|
|
||||||
|
from typing_extensions import NotRequired
|
||||||
|
|
||||||
|
from .cog import Cog
|
||||||
|
from .command import Command
|
||||||
|
from .context import Context
|
||||||
|
from .group import Group
|
||||||
|
from .utils import ClientT_Co_D, ClientT_D
|
||||||
|
|
||||||
|
from next import File, Message, Messageable, MessageReply, SendableEmbed
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .cog import Cog
|
||||||
|
|
||||||
|
__all__ = ("MessagePayload", "HelpCommand", "DefaultHelpCommand", "help_command_impl")
|
||||||
|
|
||||||
|
|
||||||
|
class MessagePayload(TypedDict):
|
||||||
|
content: str
|
||||||
|
embed: NotRequired[SendableEmbed]
|
||||||
|
embeds: NotRequired[list[SendableEmbed]]
|
||||||
|
attachments: NotRequired[list[File]]
|
||||||
|
replies: NotRequired[list[MessageReply]]
|
||||||
|
|
||||||
|
class HelpCommand(ABC, Generic[ClientT_Co_D]):
|
||||||
|
@abstractmethod
|
||||||
|
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[ClientT_Co_D], command: Command[ClientT_Co_D]) -> Union[str, SendableEmbed, MessagePayload]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
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[ClientT_Co_D], cog: Cog[ClientT_Co_D]) -> Union[str, SendableEmbed, MessagePayload]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
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[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)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
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[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[ClientT_Co_D], name: str) -> Union[str, SendableEmbed, MessagePayload]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
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_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():
|
||||||
|
cog_lines: list[str] = []
|
||||||
|
cog_lines.append(f"{cog.qualified_name if cog else self.default_cog_name}:")
|
||||||
|
|
||||||
|
for command in cog_commands:
|
||||||
|
cog_lines.append(f" {command.name} - {command.short_description or 'No description'}")
|
||||||
|
|
||||||
|
lines.append("\n".join(cog_lines))
|
||||||
|
|
||||||
|
lines.append("```")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
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}:")
|
||||||
|
|
||||||
|
for command in cog.commands:
|
||||||
|
lines.append(f" {command.name} - {command.short_description or 'No description'}")
|
||||||
|
|
||||||
|
lines.append("```")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
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}:")
|
||||||
|
lines.append(f" Usage: {command.get_usage()}")
|
||||||
|
|
||||||
|
if command.aliases:
|
||||||
|
lines.append(f" Aliases: {', '.join(command.aliases)}")
|
||||||
|
|
||||||
|
|
||||||
|
if command.description:
|
||||||
|
lines.append(command.description)
|
||||||
|
|
||||||
|
lines.append("```")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
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}:")
|
||||||
|
lines.append(f" Usage: {group.get_usage()}")
|
||||||
|
|
||||||
|
if group.aliases:
|
||||||
|
lines.append(f" Aliases: {', '.join(group.aliases)}")
|
||||||
|
|
||||||
|
if group.description:
|
||||||
|
lines.append(group.description)
|
||||||
|
|
||||||
|
for command in group.commands:
|
||||||
|
lines.append(f" {command.name} - {command.short_description or 'No description'}")
|
||||||
|
|
||||||
|
lines.append("```")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
async def handle_no_command_found(self, context: Context[ClientT_Co_D], name: str) -> str:
|
||||||
|
return f"Command `{name}` not found."
|
||||||
|
|
||||||
|
class HelpCommandImpl(Command[ClientT_Co_D]):
|
||||||
|
def __init__(self, client: ClientT_Co_D):
|
||||||
|
self.client = client
|
||||||
|
|
||||||
|
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(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, client.commands)
|
||||||
|
commands = await help_command.group_commands(context, filtered_commands)
|
||||||
|
|
||||||
|
if not arguments:
|
||||||
|
payload = await help_command.create_global_help(context, commands)
|
||||||
|
|
||||||
|
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):
|
||||||
|
command = cast(Group[ClientT_D], command)
|
||||||
|
parent = command
|
||||||
|
else:
|
||||||
|
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):
|
||||||
|
msg_payload = {"content": payload}
|
||||||
|
elif isinstance(payload, SendableEmbed):
|
||||||
|
msg_payload = {"embed": payload, "content": " "}
|
||||||
|
else:
|
||||||
|
msg_payload = payload
|
||||||
|
|
||||||
|
message = await help_command.send_help_command(context, msg_payload)
|
||||||
|
await help_command.handle_message(context, message)
|
30
next/ext/commands/utils.py
Normal file
30
next/ext/commands/utils.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from inspect import Parameter
|
||||||
|
from typing import TYPE_CHECKING, Any, Iterable
|
||||||
|
|
||||||
|
from typing_extensions import TypeVar
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .client import CommandsClient
|
||||||
|
from .context import Context
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ("evaluate_parameters",)
|
||||||
|
|
||||||
|
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] = []
|
||||||
|
|
||||||
|
for parameter in parameters:
|
||||||
|
if parameter.annotation is not parameter.empty:
|
||||||
|
if isinstance(parameter.annotation, str):
|
||||||
|
parameter = parameter.replace(annotation=eval(parameter.annotation, globals))
|
||||||
|
|
||||||
|
new_parameters.append(parameter)
|
||||||
|
|
||||||
|
return new_parameters
|
62
next/ext/commands/view.py
Normal file
62
next/ext/commands/view.py
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
from typing import Iterator
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
from .errors import NoClosingQuote
|
||||||
|
|
||||||
|
|
||||||
|
class StringView:
|
||||||
|
def __init__(self, string: str):
|
||||||
|
self.value: Iterator[str] = iter(string)
|
||||||
|
self.temp: str = ""
|
||||||
|
self.should_undo: bool = False
|
||||||
|
|
||||||
|
def undo(self) -> None:
|
||||||
|
self.should_undo = True
|
||||||
|
|
||||||
|
def next_char(self) -> str:
|
||||||
|
return next(self.value)
|
||||||
|
|
||||||
|
def get_rest(self) -> str:
|
||||||
|
if self.should_undo:
|
||||||
|
return f"{self.temp} {''.join(self.value)}".rstrip()
|
||||||
|
# prevent a new space appearing at end if the buffer is depleted
|
||||||
|
|
||||||
|
return "".join(self.value)
|
||||||
|
|
||||||
|
def get_next_word(self) -> str:
|
||||||
|
if self.should_undo:
|
||||||
|
self.should_undo = False
|
||||||
|
return self.temp
|
||||||
|
|
||||||
|
char = self.next_char()
|
||||||
|
temp: list[str] = []
|
||||||
|
|
||||||
|
while char == " ":
|
||||||
|
char = self.next_char()
|
||||||
|
|
||||||
|
if char in ["\"", "'"]:
|
||||||
|
quote = char
|
||||||
|
try:
|
||||||
|
while (char := self.next_char()) != quote:
|
||||||
|
temp.append(char)
|
||||||
|
except StopIteration:
|
||||||
|
raise NoClosingQuote
|
||||||
|
|
||||||
|
else:
|
||||||
|
temp.append(char)
|
||||||
|
try:
|
||||||
|
while (char := self.next_char()) not in " \n":
|
||||||
|
temp.append(char)
|
||||||
|
except StopIteration:
|
||||||
|
pass
|
||||||
|
|
||||||
|
output = "".join(temp)
|
||||||
|
self.temp = output
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
def __iter__(self) -> Self:
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self) -> str:
|
||||||
|
return self.get_next_word()
|
38
next/file.py
Normal file
38
next/file.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
|
from typing import Optional, Union, cast
|
||||||
|
|
||||||
|
__all__ = ("File",)
|
||||||
|
|
||||||
|
class File:
|
||||||
|
"""Respresents a file about to be uploaded to next
|
||||||
|
|
||||||
|
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_`
|
||||||
|
"""
|
||||||
|
__slots__ = ("f", "spoiler", "filename")
|
||||||
|
|
||||||
|
def __init__(self, file: Union[str, bytes], *, filename: Optional[str] = None, spoiler: bool = False):
|
||||||
|
self.f: io.BufferedIOBase
|
||||||
|
|
||||||
|
if isinstance(file, str):
|
||||||
|
self.f = open(file, "rb")
|
||||||
|
else:
|
||||||
|
self.f = io.BytesIO(file)
|
||||||
|
|
||||||
|
if filename is None and isinstance(file, str):
|
||||||
|
filename = cast(Optional[str], self.f.name)
|
||||||
|
|
||||||
|
self.spoiler: bool = spoiler or (bool(filename) and filename.startswith("SPOILER_"))
|
||||||
|
|
||||||
|
if self.spoiler and (filename and not filename.startswith("SPOILER_")):
|
||||||
|
filename = f"SPOILER_{filename}"
|
||||||
|
|
||||||
|
self.filename: str | None = filename
|
156
next/flags.py
Normal file
156
next/flags.py
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Callable, Iterator, Optional, Union, overload
|
||||||
|
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
__all__ = ("Flag", "Flags", "UserBadges")
|
||||||
|
|
||||||
|
|
||||||
|
class Flag:
|
||||||
|
__slots__ = ("flag", "__doc__")
|
||||||
|
|
||||||
|
def __init__(self, func: Callable[[], int]):
|
||||||
|
self.flag: int = func()
|
||||||
|
self.__doc__: str | None = func.__doc__
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __get__(self: Self, instance: None, owner: type[Flags]) -> Self:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __get__(self, instance: Flags, owner: type[Flags]) -> bool:
|
||||||
|
...
|
||||||
|
|
||||||
|
def __get__(self: Self, instance: Optional[Flags], owner: type[Flags]) -> Union[Self, bool]:
|
||||||
|
if instance is None:
|
||||||
|
return self
|
||||||
|
|
||||||
|
return instance._check_flag(self.flag)
|
||||||
|
|
||||||
|
def __set__(self, instance: Flags, value: bool) -> None:
|
||||||
|
instance._set_flag(self.flag, value)
|
||||||
|
|
||||||
|
class Flags:
|
||||||
|
FLAG_NAMES: list[str]
|
||||||
|
|
||||||
|
def __init_subclass__(cls) -> None:
|
||||||
|
cls.FLAG_NAMES = []
|
||||||
|
|
||||||
|
for name in dir(cls):
|
||||||
|
value = getattr(cls, name)
|
||||||
|
|
||||||
|
if isinstance(value, Flag):
|
||||||
|
cls.FLAG_NAMES.append(name)
|
||||||
|
|
||||||
|
def __init__(self, value: int = 0, **flags: bool):
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
for k, v in flags.items():
|
||||||
|
setattr(self, k, v)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_value(cls, value: int) -> Self:
|
||||||
|
self = cls.__new__(cls)
|
||||||
|
self.value = value
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _check_flag(self, flag: int) -> bool:
|
||||||
|
return (self.value & flag) == flag
|
||||||
|
|
||||||
|
def _set_flag(self, flag: int, value: bool) -> None:
|
||||||
|
if value:
|
||||||
|
self.value |= flag
|
||||||
|
else:
|
||||||
|
self.value &= ~flag
|
||||||
|
|
||||||
|
def __eq__(self, other: Self) -> bool:
|
||||||
|
return self.value == other.value
|
||||||
|
|
||||||
|
def __ne__(self, other: Self) -> bool:
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
def __or__(self, other: Self) -> Self:
|
||||||
|
return self.__class__._from_value(self.value | other.value)
|
||||||
|
|
||||||
|
def __and__(self, other: Self) -> Self:
|
||||||
|
return self.__class__._from_value(self.value & other.value)
|
||||||
|
|
||||||
|
def __invert__(self) -> Self:
|
||||||
|
return self.__class__._from_value(~self.value)
|
||||||
|
|
||||||
|
def __add__(self, other: Self) -> Self:
|
||||||
|
return self | other
|
||||||
|
|
||||||
|
def __sub__(self, other: Self) -> Self:
|
||||||
|
return self & ~other
|
||||||
|
|
||||||
|
def __lt__(self, other: Self) -> bool:
|
||||||
|
return self.value < other.value
|
||||||
|
|
||||||
|
def __gt__(self, other: Self) -> bool:
|
||||||
|
return self.value > other.value
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<{self.__class__.__name__} value={self.value}>"
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[tuple[str, bool]]:
|
||||||
|
for name, value in self.__class__.__dict__.items():
|
||||||
|
if isinstance(value, Flag):
|
||||||
|
yield name, self._check_flag(value.flag)
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash(self.value)
|
||||||
|
|
||||||
|
class UserBadges(Flags):
|
||||||
|
"""Contains all user badges"""
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def developer():
|
||||||
|
""":class:`bool` The developer badge."""
|
||||||
|
return 1 << 0
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def translator():
|
||||||
|
""":class:`bool` The translator badge."""
|
||||||
|
return 1 << 1
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def supporter():
|
||||||
|
""":class:`bool` The supporter badge."""
|
||||||
|
return 1 << 2
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def responsible_disclosure():
|
||||||
|
""":class:`bool` The responsible disclosure badge."""
|
||||||
|
return 1 << 3
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def founder():
|
||||||
|
""":class:`bool` The founder badge."""
|
||||||
|
return 1 << 4
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def platform_moderation():
|
||||||
|
""":class:`bool` The platform moderation badge."""
|
||||||
|
return 1 << 5
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def active_supporter():
|
||||||
|
""":class:`bool` The active supporter badge."""
|
||||||
|
return 1 << 6
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def bug_hunter():
|
||||||
|
""":class:`bool` The bug hunter badge."""
|
||||||
|
return 1 << 7
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def early_adopter():
|
||||||
|
""":class:`bool` The early adopter badge."""
|
||||||
|
return 1 << 8
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def reserved_relevant_joke_badge_1():
|
||||||
|
""":class:`bool` The reserved relevant joke badge 1 badge."""
|
||||||
|
return 1 << 9
|
432
next/http.py
Normal file
432
next/http.py
Normal file
|
@ -0,0 +1,432 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import (TYPE_CHECKING, Any, Coroutine, Literal, Optional, TypeVar,
|
||||||
|
Union, overload)
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import ulid
|
||||||
|
|
||||||
|
|
||||||
|
from .errors import Forbidden, HTTPError, ServerError
|
||||||
|
from .file import File
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ujson as _json
|
||||||
|
except ImportError:
|
||||||
|
import json as _json
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from .enums import SortType
|
||||||
|
from .file import File
|
||||||
|
from .types import Autumn as AutumnPayload
|
||||||
|
from .types import Emoji as EmojiPayload
|
||||||
|
from .types import Interactions as InteractionsPayload
|
||||||
|
from .types import Masquerade as MasqueradePayload
|
||||||
|
from .types import Member as MemberPayload
|
||||||
|
from .types import Message as MessagePayload
|
||||||
|
from .types import SendableEmbed as SendableEmbedPayload
|
||||||
|
from .types import User as UserPayload
|
||||||
|
from .types import (Server, ServerBans, TextChannel, UserProfile, VoiceChannel, Member, Invite, ApiInfo, Channel, SavedMessages,
|
||||||
|
DMChannel, EmojiParent, GetServerMembers, GroupDMChannel, MessageReplyPayload, MessageWithUserData, PartialInvite, CreateRole)
|
||||||
|
|
||||||
|
__all__ = ("HttpClient",)
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
Request = Coroutine[Any, Any, T]
|
||||||
|
|
||||||
|
class HttpClient:
|
||||||
|
__slots__ = ("session", "token", "api_url", "api_info", "auth_header")
|
||||||
|
|
||||||
|
def __init__(self, session: aiohttp.ClientSession, token: str, api_url: str, api_info: ApiInfo, bot: bool = True):
|
||||||
|
self.session: aiohttp.ClientSession = session
|
||||||
|
self.token: str = token
|
||||||
|
self.api_url: str = api_url
|
||||||
|
self.api_info: ApiInfo = api_info
|
||||||
|
self.auth_header: str = "x-bot-token" if bot else "x-session-token"
|
||||||
|
|
||||||
|
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 = {}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"User-Agent": "Next.py (https://github.com/avanpost200/next.py)",
|
||||||
|
self.auth_header: self.token
|
||||||
|
}
|
||||||
|
|
||||||
|
if json:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
|
if nonce:
|
||||||
|
json["nonce"] = ulid.new().str # type: ignore
|
||||||
|
|
||||||
|
kwargs["data"] = _json.dumps(json)
|
||||||
|
|
||||||
|
kwargs["headers"] = headers
|
||||||
|
|
||||||
|
if params:
|
||||||
|
kwargs["params"] = params
|
||||||
|
|
||||||
|
async with self.session.request(method, url, **kwargs) as resp:
|
||||||
|
text = await resp.text()
|
||||||
|
if text:
|
||||||
|
try:
|
||||||
|
response = _json.loads(await resp.text())
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPError(f"Invalid json response:\n{text}") from None
|
||||||
|
else:
|
||||||
|
response = text
|
||||||
|
|
||||||
|
resp_code = resp.status
|
||||||
|
|
||||||
|
if 200 <= resp_code <= 300:
|
||||||
|
return response
|
||||||
|
elif resp_code == 401:
|
||||||
|
raise Forbidden("401: Missing Permissions")
|
||||||
|
else:
|
||||||
|
raise HTTPError(resp_code)
|
||||||
|
|
||||||
|
async def upload_file(self, file: File, tag: Literal["attachments", "avatars", "backgrounds", "icons", "banners", "emojis"]) -> AutumnPayload:
|
||||||
|
url = f"{self.api_info['features']['autumn']['url']}/{tag}"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"User-Agent": "Next.py (https://github.com/avanpost200/next.py)"
|
||||||
|
}
|
||||||
|
|
||||||
|
form = aiohttp.FormData()
|
||||||
|
form.add_field("file", file.f.read(), filename=file.filename)
|
||||||
|
|
||||||
|
async with self.session.post(url, data=form, headers=headers) as resp:
|
||||||
|
response: AutumnPayload = _json.loads(await resp.text())
|
||||||
|
|
||||||
|
resp_code = resp.status
|
||||||
|
|
||||||
|
if resp_code == 400:
|
||||||
|
raise HTTPError(response)
|
||||||
|
elif 500 <= resp_code <= 600:
|
||||||
|
raise ServerError
|
||||||
|
else:
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def send_message(self, channel: str, content: Optional[str], embeds: Optional[list[SendableEmbedPayload]], attachments: Optional[list[File]], replies: Optional[list[MessageReplyPayload]], masquerade: Optional[MasqueradePayload], interactions: Optional[InteractionsPayload]) -> MessagePayload:
|
||||||
|
json: dict[str, Any] = {}
|
||||||
|
|
||||||
|
if content:
|
||||||
|
json["content"] = content
|
||||||
|
|
||||||
|
if embeds:
|
||||||
|
json["embeds"] = embeds
|
||||||
|
|
||||||
|
if attachments:
|
||||||
|
attachment_ids: list[str] = []
|
||||||
|
|
||||||
|
for attachment in attachments:
|
||||||
|
data = await self.upload_file(attachment, "attachments")
|
||||||
|
attachment_ids.append(data["id"])
|
||||||
|
|
||||||
|
json["attachments"] = attachment_ids
|
||||||
|
|
||||||
|
if replies:
|
||||||
|
json["replies"] = replies
|
||||||
|
|
||||||
|
if masquerade:
|
||||||
|
json["masquerade"] = masquerade
|
||||||
|
|
||||||
|
if interactions:
|
||||||
|
json["interactions"] = interactions
|
||||||
|
|
||||||
|
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: dict[str, Any] = {}
|
||||||
|
|
||||||
|
if content is not None:
|
||||||
|
json["content"] = content
|
||||||
|
|
||||||
|
if embeds is not None:
|
||||||
|
json["embeds"] = embeds
|
||||||
|
|
||||||
|
return self.request("PATCH", f"/channels/{channel}/messages/{message}", json=json)
|
||||||
|
|
||||||
|
def delete_message(self, channel: str, message: str) -> Request[None]:
|
||||||
|
return self.request("DELETE", f"/channels/{channel}/messages/{message}")
|
||||||
|
|
||||||
|
def fetch_message(self, channel: str, message: str) -> Request[MessagePayload]:
|
||||||
|
return self.request("GET", f"/channels/{channel}/messages/{message}")
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def fetch_messages(
|
||||||
|
self,
|
||||||
|
channel: str,
|
||||||
|
sort: SortType,
|
||||||
|
*,
|
||||||
|
limit: Optional[int] = ...,
|
||||||
|
before: Optional[str] = ...,
|
||||||
|
after: Optional[str] = ...,
|
||||||
|
nearby: Optional[str] = ...,
|
||||||
|
include_users: Literal[False] = ...
|
||||||
|
) -> Request[list[MessagePayload]]:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def fetch_messages(
|
||||||
|
self,
|
||||||
|
channel: str,
|
||||||
|
sort: SortType,
|
||||||
|
*,
|
||||||
|
limit: Optional[int] = ...,
|
||||||
|
before: Optional[str] = ...,
|
||||||
|
after: Optional[str] = ...,
|
||||||
|
nearby: Optional[str] = ...,
|
||||||
|
include_users: Literal[True] = ...
|
||||||
|
) -> Request[MessageWithUserData]:
|
||||||
|
...
|
||||||
|
|
||||||
|
def fetch_messages(
|
||||||
|
self,
|
||||||
|
channel: str,
|
||||||
|
sort: SortType,
|
||||||
|
*,
|
||||||
|
limit: Optional[int] = None,
|
||||||
|
before: Optional[str] = None,
|
||||||
|
after: Optional[str] = None,
|
||||||
|
nearby: Optional[str] = None,
|
||||||
|
include_users: bool = False
|
||||||
|
) -> Request[Union[list[MessagePayload], MessageWithUserData]]:
|
||||||
|
|
||||||
|
json: dict[str, Any] = {"sort": sort.value, "include_users": str(include_users)}
|
||||||
|
|
||||||
|
if limit:
|
||||||
|
json["limit"] = limit
|
||||||
|
|
||||||
|
if before:
|
||||||
|
json["before"] = before
|
||||||
|
|
||||||
|
if after:
|
||||||
|
json["after"] = after
|
||||||
|
|
||||||
|
if nearby:
|
||||||
|
json["nearby"] = nearby
|
||||||
|
|
||||||
|
return self.request("GET", f"/channels/{channel}/messages", params=json)
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def search_messages(
|
||||||
|
self,
|
||||||
|
channel: str,
|
||||||
|
query: str,
|
||||||
|
*,
|
||||||
|
limit: Optional[int] = ...,
|
||||||
|
before: Optional[str] = ...,
|
||||||
|
after: Optional[str] = ...,
|
||||||
|
sort: Optional[SortType] = ...,
|
||||||
|
include_users: Literal[False] = ...
|
||||||
|
) -> Request[list[MessagePayload]]:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def search_messages(
|
||||||
|
self,
|
||||||
|
channel: str,
|
||||||
|
query: str,
|
||||||
|
*,
|
||||||
|
limit: Optional[int] = ...,
|
||||||
|
before: Optional[str] = ...,
|
||||||
|
after: Optional[str] = ...,
|
||||||
|
sort: Optional[SortType] = ...,
|
||||||
|
include_users: Literal[True] = ...
|
||||||
|
) -> Request[MessageWithUserData]:
|
||||||
|
...
|
||||||
|
|
||||||
|
def search_messages(
|
||||||
|
self,
|
||||||
|
channel: str,
|
||||||
|
query: str,
|
||||||
|
*,
|
||||||
|
limit: Optional[int] = None,
|
||||||
|
before: Optional[str] = None,
|
||||||
|
after: Optional[str] = None,
|
||||||
|
sort: Optional[SortType] = None,
|
||||||
|
include_users: bool = False
|
||||||
|
) -> Request[Union[list[MessagePayload], MessageWithUserData]]:
|
||||||
|
|
||||||
|
json: dict[str, Any] = {"query": query, "include_users": include_users}
|
||||||
|
|
||||||
|
if limit:
|
||||||
|
json["limit"] = limit
|
||||||
|
|
||||||
|
if before:
|
||||||
|
json["before"] = before
|
||||||
|
|
||||||
|
if after:
|
||||||
|
json["after"] = after
|
||||||
|
|
||||||
|
if sort:
|
||||||
|
json["sort"] = sort.value
|
||||||
|
|
||||||
|
return self.request("POST", f"/channels/{channel}/search", json=json)
|
||||||
|
|
||||||
|
async def request_file(self, url: str) -> bytes:
|
||||||
|
async with self.session.get(url) as resp:
|
||||||
|
return await resp.content.read()
|
||||||
|
|
||||||
|
def fetch_user(self, user_id: str) -> Request[UserPayload]:
|
||||||
|
return self.request("GET", f"/users/{user_id}")
|
||||||
|
|
||||||
|
def fetch_profile(self, user_id: str) -> Request[UserProfile]:
|
||||||
|
return self.request("GET", f"/users/{user_id}/profile")
|
||||||
|
|
||||||
|
def fetch_default_avatar(self, user_id: str) -> Request[bytes]:
|
||||||
|
return self.request_file(f"{self.api_url}/users/{user_id}/default_avatar")
|
||||||
|
|
||||||
|
def fetch_dm_channels(self) -> Request[list[Union[DMChannel, GroupDMChannel]]]:
|
||||||
|
return self.request("GET", "/users/dms")
|
||||||
|
|
||||||
|
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]:
|
||||||
|
return self.request("GET", f"/channels/{channel_id}")
|
||||||
|
|
||||||
|
def close_channel(self, channel_id: str) -> Request[None]:
|
||||||
|
return self.request("DELETE", f"/channels/{channel_id}")
|
||||||
|
|
||||||
|
def fetch_server(self, server_id: str) -> Request[Server]:
|
||||||
|
return self.request("GET", f"/servers/{server_id}")
|
||||||
|
|
||||||
|
def delete_leave_server(self, server_id: str) -> Request[None]:
|
||||||
|
return self.request("DELETE", f"/servers/{server_id}")
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def create_channel(self, server_id: str, channel_type: Literal["Text"], name: str, description: Optional[str]) -> Request[TextChannel]:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def create_channel(self, server_id: str, channel_type: Literal["Voice"], name: str, description: Optional[str]) -> Request[VoiceChannel]:
|
||||||
|
...
|
||||||
|
|
||||||
|
def create_channel(self, server_id: str, channel_type: Literal["Text", "Voice"], name: str, description: Optional[str]) -> Request[Union[TextChannel, VoiceChannel]]:
|
||||||
|
payload = {
|
||||||
|
"type": channel_type,
|
||||||
|
"name": name
|
||||||
|
}
|
||||||
|
|
||||||
|
if description:
|
||||||
|
payload["description"] = description
|
||||||
|
|
||||||
|
return self.request("POST", f"/servers/{server_id}/channels", json=payload)
|
||||||
|
|
||||||
|
def fetch_server_invites(self, server_id: str) -> Request[list[PartialInvite]]:
|
||||||
|
return self.request("GET", f"/servers/{server_id}/invites")
|
||||||
|
|
||||||
|
def fetch_member(self, server_id: str, member_id: str) -> Request[Member]:
|
||||||
|
return self.request("GET", f"/servers/{server_id}/members/{member_id}")
|
||||||
|
|
||||||
|
def kick_member(self, server_id: str, member_id: str) -> Request[None]:
|
||||||
|
return self.request("DELETE", f"/servers/{server_id}/members/{member_id}")
|
||||||
|
|
||||||
|
def fetch_members(self, server_id: str) -> Request[GetServerMembers]:
|
||||||
|
return self.request("GET", f"/servers/{server_id}/members")
|
||||||
|
|
||||||
|
def ban_member(self, server_id: str, member_id: str, reason: Optional[str]) -> Request[GetServerMembers]:
|
||||||
|
payload = {"reason": reason} if reason else None
|
||||||
|
|
||||||
|
return self.request("PUT", f"/servers/{server_id}/bans/{member_id}", json=payload, nonce=False)
|
||||||
|
|
||||||
|
def unban_member(self, server_id: str, member_id: str) -> Request[None]:
|
||||||
|
return self.request("DELETE", f"/servers/{server_id}/bans/{member_id}")
|
||||||
|
|
||||||
|
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[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]:
|
||||||
|
return self.request("DELETE", f"/servers/{server_id}/roles/{role_id}")
|
||||||
|
|
||||||
|
def fetch_invite(self, code: str) -> Request[Invite]:
|
||||||
|
return self.request("GET", f"/invites/{code}")
|
||||||
|
|
||||||
|
def delete_invite(self, code: str) -> Request[None]:
|
||||||
|
return self.request("DELETE", f"/invites/{code}")
|
||||||
|
|
||||||
|
def edit_channel(self, channel_id: str, remove: list[str] | None, values: dict[str, Any]) -> Request[None]:
|
||||||
|
if remove:
|
||||||
|
values["remove"] = remove
|
||||||
|
|
||||||
|
return self.request("PATCH", f"/channels/{channel_id}", json=values)
|
||||||
|
|
||||||
|
def edit_role(self, server_id: str, role_id: str, remove: list[str] | None, values: dict[str, Any]) -> Request[None]:
|
||||||
|
if remove:
|
||||||
|
values["remove"] = remove
|
||||||
|
|
||||||
|
return self.request("PATCH", f"/servers/{server_id}/roles/{role_id}", json=values)
|
||||||
|
|
||||||
|
async def edit_self(self, remove: list[str] | None, values: dict[str, Any]) -> Request[None]:
|
||||||
|
if remove:
|
||||||
|
values["remove"] = remove
|
||||||
|
|
||||||
|
if avatar := values.get("avatar"):
|
||||||
|
asset = await self.upload_file(avatar, "avatars")
|
||||||
|
values["avatar"] = asset["id"]
|
||||||
|
|
||||||
|
if profile := values.get("profile"):
|
||||||
|
if background := profile.background():
|
||||||
|
asset = await self.upload_file(background, "backgrounds")
|
||||||
|
profile["background"] = asset["id"]
|
||||||
|
|
||||||
|
return await self.request("PATCH", "/users/@me", json=values)
|
||||||
|
|
||||||
|
def set_guild_channel_default_permissions(self, channel_id: str, allow: int, deny: int) -> Request[None]:
|
||||||
|
return self.request("PUT", f"/channels/{channel_id}/permissions/default", json={"permissions": {"allow": allow, "deny": deny}})
|
||||||
|
|
||||||
|
def set_guild_channel_role_permissions(self, channel_id: str, role_id: str, allow: int, deny: int) -> Request[None]:
|
||||||
|
return self.request("PUT", f"/channels/{channel_id}/permissions/{role_id}", json={"permissions": {"allow": allow, "deny": deny}})
|
||||||
|
|
||||||
|
def set_group_channel_default_permissions(self, channel_id: str, value: int) -> Request[None]:
|
||||||
|
return self.request("PUT", f"/channels/{channel_id}/permissions/default", json={"permissions": value})
|
||||||
|
|
||||||
|
def set_server_role_permissions(self, server_id: str, role_id: str, allow: int, deny: int) -> Request[None]:
|
||||||
|
return self.request("PUT", f"/servers/{server_id}/permissions/{role_id}", json={"permissions": {"allow": allow, "deny": deny}})
|
||||||
|
|
||||||
|
def set_server_default_permissions(self, server_id: str, value: int) -> Request[None]:
|
||||||
|
return self.request("PUT", f"/servers/{server_id}/permissions/default", json={"permissions": value})
|
||||||
|
|
||||||
|
def add_reaction(self, channel_id: str, message_id: str, emoji: str) -> Request[None]:
|
||||||
|
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]:
|
||||||
|
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")
|
||||||
|
|
||||||
|
def delete_emoji(self, emoji_id: str) -> Request[None]:
|
||||||
|
return self.request("DELETE", f"/custom/emoji/{emoji_id}")
|
||||||
|
|
||||||
|
def fetch_emoji(self, emoji_id: str) -> Request[EmojiPayload]:
|
||||||
|
return self.request("GET", f"/custom/emoji/{emoji_id}")
|
||||||
|
|
||||||
|
async def create_emoji(self, name: str, file: File, nsfw: bool, parent: EmojiParent) -> EmojiPayload:
|
||||||
|
asset = await self.upload_file(file, "emojis")
|
||||||
|
|
||||||
|
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]:
|
||||||
|
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})
|
81
next/invite.py
Normal file
81
next/invite.py
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
|
||||||
|
from .asset import Asset
|
||||||
|
from .utils import Ulid
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .state import State
|
||||||
|
from .channel import Channel
|
||||||
|
from .server import Server
|
||||||
|
from .types import Invite as InvitePayload
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ("Invite",)
|
||||||
|
|
||||||
|
class Invite(Ulid):
|
||||||
|
"""Represents a server invite.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
code: :class:`str`
|
||||||
|
The code for the invite
|
||||||
|
id: :class:`str`
|
||||||
|
Alias for :attr:`code`
|
||||||
|
server: :class:`Server`
|
||||||
|
The server this invite is for
|
||||||
|
channel: :class:`Channel`
|
||||||
|
The channel this invite is for
|
||||||
|
user_name: :class:`str`
|
||||||
|
The name of the user who made the invite
|
||||||
|
user: Optional[:class:`User`]
|
||||||
|
The user who made the invite, this is only set if this was fetched via :meth:`Server.fetch_invites`
|
||||||
|
user_avatar: Optional[:class:`Asset`]
|
||||||
|
The invite creator's avatar, if any
|
||||||
|
member_count: :class:`int`
|
||||||
|
The member count of the server this invite is for
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("state", "code", "id", "server", "channel", "user_name", "user_avatar", "user", "member_count")
|
||||||
|
|
||||||
|
def __init__(self, data: InvitePayload, code: str, state: State):
|
||||||
|
self.state: State = state
|
||||||
|
|
||||||
|
self.code: str = code
|
||||||
|
self.id: str = code
|
||||||
|
self.server: Server = state.get_server(data["server_id"])
|
||||||
|
self.channel: Channel = self.server.get_channel(data["channel_id"])
|
||||||
|
|
||||||
|
self.user_name: str = data["user_name"]
|
||||||
|
self.user: User | None = None
|
||||||
|
|
||||||
|
self.user_avatar: Asset | None
|
||||||
|
|
||||||
|
if avatar := data.get("user_avatar"):
|
||||||
|
self.user_avatar = Asset(avatar, state)
|
||||||
|
else:
|
||||||
|
self.user_avatar = None
|
||||||
|
|
||||||
|
self.member_count: int = data["member_count"]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _from_partial(code: str, server: str, creator: str, channel: str, state: State) -> Invite:
|
||||||
|
invite = Invite.__new__(Invite)
|
||||||
|
|
||||||
|
invite.state = state
|
||||||
|
invite.code = code
|
||||||
|
invite.server = state.get_server(server)
|
||||||
|
invite.channel = state.get_channel(channel)
|
||||||
|
invite.user = state.get_user(creator)
|
||||||
|
invite.user_name = invite.user.name
|
||||||
|
invite.user_avatar = invite.user.avatar
|
||||||
|
invite.member_count = len(invite.server.members)
|
||||||
|
|
||||||
|
return invite
|
||||||
|
|
||||||
|
async def delete(self) -> None:
|
||||||
|
"""Deletes the invite"""
|
||||||
|
await self.state.http.delete_invite(self.code)
|
246
next/member.py
Normal file
246
next/member.py
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from typing import TYPE_CHECKING, Any, Optional
|
||||||
|
|
||||||
|
|
||||||
|
from .utils import _Missing, Missing, parse_timestamp
|
||||||
|
|
||||||
|
from .asset import Asset
|
||||||
|
from .permissions import Permissions
|
||||||
|
from .permissions_calculator import calculate_permissions
|
||||||
|
from .user import User
|
||||||
|
from .file import File
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .channel import Channel
|
||||||
|
from .server import Server
|
||||||
|
from .state import State
|
||||||
|
from .types import File as FilePayload
|
||||||
|
from .types import Member as MemberPayload
|
||||||
|
from .role import Role
|
||||||
|
|
||||||
|
__all__ = ("Member",)
|
||||||
|
|
||||||
|
def flattern_user(member: Member, user: User) -> None:
|
||||||
|
for attr in user.__flattern_attributes__:
|
||||||
|
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
|
||||||
|
guild_avatar: Optional[:class:`Asset`]
|
||||||
|
The member's guild avatar if any
|
||||||
|
"""
|
||||||
|
__slots__ = ("state", "nickname", "roles", "server", "guild_avatar", "joined_at", "current_timeout")
|
||||||
|
|
||||||
|
def __init__(self, data: MemberPayload, server: Server, state: State):
|
||||||
|
user = state.get_user(data["_id"]["user"])
|
||||||
|
|
||||||
|
# due to not having a user payload and only a user object we have to manually add all the attributes instead of calling User.__init__
|
||||||
|
flattern_user(self, user)
|
||||||
|
user._members[server.id] = self
|
||||||
|
|
||||||
|
self.state: State = state
|
||||||
|
|
||||||
|
self.guild_avatar: Asset | None
|
||||||
|
|
||||||
|
if avatar := data.get("avatar"):
|
||||||
|
self.guild_avatar = Asset(avatar, state)
|
||||||
|
else:
|
||||||
|
self.guild_avatar = None
|
||||||
|
|
||||||
|
roles = [server.get_role(role_id) for role_id in data.get("roles", [])]
|
||||||
|
self.roles: list[Role] = sorted(roles, key=lambda role: role.rank, reverse=True)
|
||||||
|
|
||||||
|
self.server: Server = server
|
||||||
|
self.nickname: str | None = data.get("nickname")
|
||||||
|
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 = parse_timestamp(current_timeout)
|
||||||
|
else:
|
||||||
|
self.current_timeout = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def avatar(self) -> Optional[Asset]:
|
||||||
|
"""Optional[:class:`Asset`] The avatar the member is displaying, this includes guild avatars and masqueraded avatar"""
|
||||||
|
return self.masquerade_avatar or self.guild_avatar or self.original_avatar
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
""":class:`str` The name the user is displaying, this includes (in order) their masqueraded name, display name and orginal name"""
|
||||||
|
return self.nickname or self.display_name or self.masquerade_name or self.original_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mention(self) -> str:
|
||||||
|
""":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,
|
||||||
|
timeout: Optional[str | int] = None
|
||||||
|
) -> None:
|
||||||
|
if nickname is not None:
|
||||||
|
self.nickname = nickname
|
||||||
|
|
||||||
|
if avatar is not None:
|
||||||
|
self.guild_avatar = Asset(avatar, self.state)
|
||||||
|
|
||||||
|
if roles is not None:
|
||||||
|
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)
|
||||||
|
|
||||||
|
async def ban(self, *, reason: Optional[str] = None) -> None:
|
||||||
|
"""Bans the member from the server
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
reason: Optional[:class:`str`]
|
||||||
|
The reason for the ban
|
||||||
|
"""
|
||||||
|
await self.state.http.ban_member(self.server.id, self.id, reason)
|
||||||
|
|
||||||
|
async def unban(self) -> None:
|
||||||
|
"""Unbans the member from the server"""
|
||||||
|
await self.state.http.unban_member(self.server.id, self.id)
|
||||||
|
|
||||||
|
async def edit(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
nickname: str | None | _Missing = Missing,
|
||||||
|
roles: list[Role] | None | _Missing = Missing,
|
||||||
|
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] = {}
|
||||||
|
|
||||||
|
if nickname is None:
|
||||||
|
remove.append("Nickname")
|
||||||
|
elif nickname is not Missing:
|
||||||
|
data["nickname"] = nickname
|
||||||
|
|
||||||
|
if roles is None:
|
||||||
|
remove.append("Roles")
|
||||||
|
elif not isinstance(roles, _Missing):
|
||||||
|
data["roles"] = [role.id for role in roles]
|
||||||
|
|
||||||
|
if avatar is None:
|
||||||
|
remove.append("Avatar")
|
||||||
|
elif not isinstance(avatar, _Missing):
|
||||||
|
data["avatar"] = (await self.state.http.upload_file(avatar, "avatars"))["id"]
|
||||||
|
|
||||||
|
if timeout is None:
|
||||||
|
remove.append("Timeout")
|
||||||
|
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)
|
||||||
|
|
||||||
|
async def timeout(self, length: datetime.timedelta) -> None:
|
||||||
|
"""Timeouts the member
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
length: :class:`datetime.timedelta`
|
||||||
|
The length of the timeout
|
||||||
|
"""
|
||||||
|
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()})
|
||||||
|
|
||||||
|
def get_permissions(self) -> Permissions:
|
||||||
|
"""Gets the permissions for the member in the server
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Permissions`
|
||||||
|
The members permissions
|
||||||
|
"""
|
||||||
|
return calculate_permissions(self, self.server)
|
||||||
|
|
||||||
|
def get_channel_permissions(self, channel: Channel) -> Permissions:
|
||||||
|
"""Gets the permissions for the member in the server taking into account the channel as well
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
channel: :class:`Channel`
|
||||||
|
The channel to calculate permissions with
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Permissions`
|
||||||
|
The members permissions
|
||||||
|
"""
|
||||||
|
return calculate_permissions(self, channel)
|
||||||
|
|
||||||
|
def has_permissions(self, **permissions: bool) -> bool:
|
||||||
|
"""Computes if the member has the specified permissions
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
permissions: :class:`bool`
|
||||||
|
The permissions to check, this also accepted `False` if you need to check if the member does not have the permission
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`bool`
|
||||||
|
Whether or not they have the permissions
|
||||||
|
"""
|
||||||
|
calculated_perms = self.get_permissions()
|
||||||
|
|
||||||
|
return all([getattr(calculated_perms, key, False) == value for key, value in permissions.items()])
|
||||||
|
|
||||||
|
def has_channel_permissions(self, channel: Channel, **permissions: bool) -> bool:
|
||||||
|
"""Computes if the member has the specified permissions, taking into account the channel as well
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
channel: :class:`Channel`
|
||||||
|
The channel to calculate permissions with
|
||||||
|
permissions: :class:`bool`
|
||||||
|
The permissions to check, this also accepted `False` if you need to check if the member does not have the permission
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`bool`
|
||||||
|
Whether or not they have the permissions
|
||||||
|
"""
|
||||||
|
calculated_perms = self.get_channel_permissions(channel)
|
||||||
|
|
||||||
|
return all([getattr(calculated_perms, key, False) == value for key, value in permissions.items()])
|
310
next/message.py
Normal file
310
next/message.py
Normal file
|
@ -0,0 +1,310 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from typing import TYPE_CHECKING, Any, Coroutine, Optional, Union
|
||||||
|
|
||||||
|
|
||||||
|
from .asset import Asset, PartialAsset
|
||||||
|
from .channel import DMChannel, GroupDMChannel, TextChannel, SavedMessageChannel
|
||||||
|
from .embed import Embed, SendableEmbed, to_embed
|
||||||
|
from .utils import Ulid, parse_timestamp
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .server import Server
|
||||||
|
from .state import State
|
||||||
|
from .types import Embed as EmbedPayload
|
||||||
|
from .types import Interactions as InteractionsPayload
|
||||||
|
from .types import Masquerade as MasqueradePayload
|
||||||
|
from .types import Message as MessagePayload
|
||||||
|
from .types import MessageReplyPayload, SystemMessageContent
|
||||||
|
from .user import User
|
||||||
|
from .member import Member
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"Message",
|
||||||
|
"MessageReply",
|
||||||
|
"Masquerade",
|
||||||
|
"MessageInteractions"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Message(Ulid):
|
||||||
|
"""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[Union[:class:`WebsiteEmbed`, :class:`ImageEmbed`, :class:`TextEmbed`, :class:`NoneEmbed`]]
|
||||||
|
The embeds of the message
|
||||||
|
channel: :class:`Messageable`
|
||||||
|
The channel the message was sent in
|
||||||
|
author: Union[:class:`Member`, :class:`User`]
|
||||||
|
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
|
||||||
|
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`]
|
||||||
|
The message's ids this message has replies to
|
||||||
|
reactions: dict[str, list[:class:`User`]]
|
||||||
|
The reactions on the message
|
||||||
|
interactions: Optional[:class:`MessageInteractions`]
|
||||||
|
The interactions on the message, if any
|
||||||
|
"""
|
||||||
|
__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", "")
|
||||||
|
|
||||||
|
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, (TextChannel, GroupDMChannel, DMChannel, SavedMessageChannel))
|
||||||
|
self.channel: TextChannel | GroupDMChannel | DMChannel | SavedMessageChannel = channel
|
||||||
|
|
||||||
|
self.server_id: str | None = self.channel.server_id
|
||||||
|
|
||||||
|
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, author_id)
|
||||||
|
|
||||||
|
else:
|
||||||
|
author = state.get_user(author_id)
|
||||||
|
|
||||||
|
self.author: Member | User = author
|
||||||
|
|
||||||
|
if masquerade := data.get("masquerade"):
|
||||||
|
if name := masquerade.get("name"):
|
||||||
|
self.author.masquerade_name = name
|
||||||
|
|
||||||
|
if avatar := masquerade.get("avatar"):
|
||||||
|
self.author.masquerade_avatar = PartialAsset(avatar, state)
|
||||||
|
|
||||||
|
if edited_at := data.get("edited"):
|
||||||
|
self.edited_at: Optional[datetime.datetime] = parse_timestamp(edited_at)
|
||||||
|
|
||||||
|
self.replies: list[Message] = []
|
||||||
|
self.reply_ids: list[str] = []
|
||||||
|
|
||||||
|
for reply in data.get("replies", []):
|
||||||
|
try:
|
||||||
|
message = state.get_message(reply)
|
||||||
|
self.replies.append(message)
|
||||||
|
except LookupError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.reply_ids.append(reply)
|
||||||
|
|
||||||
|
reactions = data.get("reactions", {})
|
||||||
|
|
||||||
|
self.reactions: dict[str, list[User]] = {}
|
||||||
|
|
||||||
|
for emoji, users in reactions.items():
|
||||||
|
self.reactions[emoji] = [self.state.get_user(user_id) for user_id in users]
|
||||||
|
|
||||||
|
self.interactions: MessageInteractions | None
|
||||||
|
|
||||||
|
if interactions := data.get("interactions"):
|
||||||
|
self.interactions = MessageInteractions(reactions=interactions.get("reactions"), restrict_reactions=interactions.get("restrict_reactions", False))
|
||||||
|
else:
|
||||||
|
self.interactions = None
|
||||||
|
|
||||||
|
def _update(self, *, content: Optional[str] = None, embeds: Optional[list[EmbedPayload]] = None, edited: Optional[Union[str, int]] = None):
|
||||||
|
if content is not None:
|
||||||
|
self.content = content
|
||||||
|
|
||||||
|
if embeds is not None:
|
||||||
|
self.embeds = [to_embed(embed, self.state) for embed in embeds]
|
||||||
|
|
||||||
|
if edited is not None:
|
||||||
|
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:
|
||||||
|
self.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))
|
||||||
|
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
|
||||||
|
|
||||||
|
await self.state.http.edit_message(self.channel.id, self.id, content, new_embeds)
|
||||||
|
|
||||||
|
async def delete(self) -> None:
|
||||||
|
"""Deletes the message. The bot can only delete its own messages and messages it has permission to delete """
|
||||||
|
await self.state.http.delete_message(self.channel.id, self.id)
|
||||||
|
|
||||||
|
def reply(self, *args: Any, mention: bool = False, **kwargs: Any) -> Coroutine[Any, Any, Message]:
|
||||||
|
"""Replies to this message, equivilant to:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
await channel.send(..., replies=[MessageReply(message, mention)])
|
||||||
|
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
:class:`LookupError`
|
||||||
|
Raises if the channel is not part of a server
|
||||||
|
"""
|
||||||
|
return self.channel.server
|
||||||
|
|
||||||
|
class MessageReply:
|
||||||
|
"""represents a reply to a message.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
message: :class:`Message`
|
||||||
|
The message being replied to.
|
||||||
|
mention: :class:`bool`
|
||||||
|
Whether the reply should mention the author of the message. Defaults to false.
|
||||||
|
"""
|
||||||
|
__slots__ = ("message", "mention")
|
||||||
|
|
||||||
|
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}
|
||||||
|
|
||||||
|
class Masquerade:
|
||||||
|
"""represents a message's masquerade.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
name: Optional[:class:`str`]
|
||||||
|
The name to display for the message
|
||||||
|
avatar: Optional[:class:`str`]
|
||||||
|
The avatar's url to display for the message
|
||||||
|
colour: Optional[:class:`str`]
|
||||||
|
The colour of the name, similar to role colours
|
||||||
|
"""
|
||||||
|
__slots__ = ("name", "avatar", "colour")
|
||||||
|
|
||||||
|
def __init__(self, name: Optional[str] = None, avatar: Optional[str] = None, colour: Optional[str] = None):
|
||||||
|
self.name: str | None = name
|
||||||
|
self.avatar: str | None = avatar
|
||||||
|
self.colour: str | None = colour
|
||||||
|
|
||||||
|
def to_dict(self) -> MasqueradePayload:
|
||||||
|
output: MasqueradePayload = {}
|
||||||
|
|
||||||
|
if name := self.name:
|
||||||
|
output["name"] = name
|
||||||
|
|
||||||
|
if avatar := self.avatar:
|
||||||
|
output["avatar"] = avatar
|
||||||
|
|
||||||
|
if colour := self.colour:
|
||||||
|
output["colour"] = colour
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
class MessageInteractions:
|
||||||
|
"""Represents a message's interactions, this is for allowing preset reactions and restricting adding reactions to only those.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
reactions: Optional[list[:class:`str`]]
|
||||||
|
The preset reactions on the message
|
||||||
|
restrict_reactions: bool
|
||||||
|
Whether or not users can only react to the interaction's reactions
|
||||||
|
"""
|
||||||
|
__slots__ = ("reactions", "restrict_reactions")
|
||||||
|
|
||||||
|
def __init__(self, *, reactions: Optional[list[str]] = None, restrict_reactions: bool = False):
|
||||||
|
self.reactions: list[str] | None = reactions
|
||||||
|
self.restrict_reactions: bool = restrict_reactions
|
||||||
|
|
||||||
|
def to_dict(self) -> InteractionsPayload:
|
||||||
|
output: InteractionsPayload = {}
|
||||||
|
|
||||||
|
if reactions := self.reactions:
|
||||||
|
output["reactions"] = reactions
|
||||||
|
|
||||||
|
if restrict_reactions := self.restrict_reactions:
|
||||||
|
output["restrict_reactions"] = restrict_reactions
|
||||||
|
|
||||||
|
return output
|
169
next/messageable.py
Normal file
169
next/messageable.py
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from .enums import SortType
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .embed import SendableEmbed
|
||||||
|
from .file import File
|
||||||
|
from .message import Masquerade, Message, MessageInteractions, MessageReply
|
||||||
|
from .state import State
|
||||||
|
from .types.http import MessageWithUserData
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ("Messageable",)
|
||||||
|
|
||||||
|
class Messageable:
|
||||||
|
"""Base class for all channels that you can send messages in
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
id: :class:`str`
|
||||||
|
The id of the channel
|
||||||
|
"""
|
||||||
|
state: State
|
||||||
|
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
async def _get_channel_id(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def send(self, content: Optional[str] = None, *, embeds: Optional[list[SendableEmbed]] = None, embed: Optional[SendableEmbed] = None, attachments: Optional[list[File]] = None, replies: Optional[list[MessageReply]] = None, reply: Optional[MessageReply] = None, masquerade: Optional[Masquerade] = None, interactions: Optional[MessageInteractions] = 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
|
||||||
|
embed: Optional[:class:`SendableEmbed`]
|
||||||
|
The embed to send with the message
|
||||||
|
embeds: Optional[list[:class:`SendableEmbed`]]
|
||||||
|
The embeds to send with the message
|
||||||
|
replies: Optional[list[:class:`MessageReply`]]
|
||||||
|
The list of messages to reply to.
|
||||||
|
masquerade: Optional[:class:`Masquerade`]
|
||||||
|
The masquerade for the message, this can overwrite the username and avatar shown
|
||||||
|
interactions: Optional[:class:`MessageInteractions`]
|
||||||
|
The interactions for the message
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Message`
|
||||||
|
The message that was just sent
|
||||||
|
"""
|
||||||
|
if embed:
|
||||||
|
embeds = [embed]
|
||||||
|
|
||||||
|
if reply:
|
||||||
|
replies = [reply]
|
||||||
|
|
||||||
|
embed_payload = [embed.to_dict() for embed in embeds] if embeds else None
|
||||||
|
reply_payload = [reply.to_dict() for reply in replies] if replies else None
|
||||||
|
masquerade_payload = masquerade.to_dict() if masquerade else None
|
||||||
|
interactions_payload = interactions.to_dict() if interactions else None
|
||||||
|
|
||||||
|
message = await self.state.http.send_message(await self._get_channel_id(), content, embed_payload, attachments, reply_payload, masquerade_payload, interactions_payload)
|
||||||
|
return self.state.add_message(message)
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_message(self, message_id: str) -> Message:
|
||||||
|
"""Fetches a message from the channel
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
message_id: :class:`str`
|
||||||
|
The id of the message you want to fetch
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Message`
|
||||||
|
The message with the matching id
|
||||||
|
"""
|
||||||
|
from .message import Message
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
sort: :class:`SortType`
|
||||||
|
The order to sort the messages in
|
||||||
|
limit: :class:`int`
|
||||||
|
How many messages to fetch
|
||||||
|
before: Optional[:class:`str`]
|
||||||
|
The id of the message which should come *before* all the messages to be fetched
|
||||||
|
after: Optional[:class:`str`]
|
||||||
|
The id of the message which should come *after* all the messages to be fetched
|
||||||
|
nearby: Optional[:class:`str`]
|
||||||
|
The id of the message which should be nearby all the messages to be fetched
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
list[:class:`Message`]
|
||||||
|
The messages found in order of the sort parameter
|
||||||
|
"""
|
||||||
|
from .message import Message
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
query: :class:`str`
|
||||||
|
The query to search for in the channel
|
||||||
|
sort: :class:`SortType`
|
||||||
|
The order to sort the messages in
|
||||||
|
limit: :class:`int`
|
||||||
|
How many messages to fetch
|
||||||
|
before: Optional[:class:`str`]
|
||||||
|
The id of the message which should come *before* all the messages to be fetched
|
||||||
|
after: Optional[:class:`str`]
|
||||||
|
The id of the message which should come *after* all the messages to be fetched
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
list[:class:`Message`]
|
||||||
|
The messages found in order of the sort parameter
|
||||||
|
"""
|
||||||
|
from .message import Message
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
.. note:: The messages must have been sent in the last 7 days.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
messages: list[:class:`Message`]
|
||||||
|
The messages for deletion, this can be up to 100 messages
|
||||||
|
"""
|
||||||
|
|
||||||
|
await self.state.http.delete_messages(await self._get_channel_id(), [message.id for message in messages])
|
233
next/permissions.py
Normal file
233
next/permissions.py
Normal file
|
@ -0,0 +1,233 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any, Optional
|
||||||
|
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
from .flags import Flag, Flags
|
||||||
|
from .types.permissions import Overwrite
|
||||||
|
|
||||||
|
__all__ = ("Permissions", "PermissionsOverwrite", "UserPermissions")
|
||||||
|
|
||||||
|
class UserPermissions(Flags):
|
||||||
|
"""Permissions for users"""
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def access() -> int:
|
||||||
|
return 1 << 0
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def view_profile() -> int:
|
||||||
|
return 1 << 1
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def send_message() -> int:
|
||||||
|
return 1 << 2
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def invite() -> int:
|
||||||
|
return 1 << 3
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def all(cls) -> Self:
|
||||||
|
return cls(access=True, view_profile=True, send_message=True, invite=True)
|
||||||
|
|
||||||
|
class Permissions(Flags):
|
||||||
|
"""Server permissions for members and roles"""
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def manage_channel() -> int:
|
||||||
|
return 1 << 0
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def manage_server() -> int:
|
||||||
|
return 1 << 1
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def manage_permissions() -> int:
|
||||||
|
return 1 << 2
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def manage_role() -> int:
|
||||||
|
return 1 << 3
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def kick_members() -> int:
|
||||||
|
return 1 << 6
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def ban_members() -> int:
|
||||||
|
return 1 << 7
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def timeout_members() -> int:
|
||||||
|
return 1 << 8
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def asign_roles() -> int:
|
||||||
|
return 1 << 9
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def change_nickname() -> int:
|
||||||
|
return 1 << 10
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def manage_nicknames() -> int:
|
||||||
|
return 1 << 11
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def change_avatars() -> int:
|
||||||
|
return 1 << 12
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def remove_avatars() -> int:
|
||||||
|
return 1 << 13
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def view_channel() -> int:
|
||||||
|
return 1 << 20
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def read_message_history() -> int:
|
||||||
|
return 1 << 21
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def send_messages() -> int:
|
||||||
|
return 1 << 22
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def manage_messages() -> int:
|
||||||
|
return 1 << 23
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def manage_webhooks() -> int:
|
||||||
|
return 1 << 24
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def invite_others() -> int:
|
||||||
|
return 1 << 25
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def send_embeds() -> int:
|
||||||
|
return 1 << 26
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def upload_files() -> int:
|
||||||
|
return 1 << 27
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def masquerade() -> int:
|
||||||
|
return 1 << 28
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def connect() -> int:
|
||||||
|
return 1 << 30
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def speak() -> int:
|
||||||
|
return 1 << 31
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def video() -> int:
|
||||||
|
return 1 << 32
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def mute_members() -> int:
|
||||||
|
return 1 << 33
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def deafen_members() -> int:
|
||||||
|
return 1 << 34
|
||||||
|
|
||||||
|
@Flag
|
||||||
|
def move_members() -> int:
|
||||||
|
return 1 << 35
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def all(cls) -> Self:
|
||||||
|
return cls(0x000F_FFFF_FFFF_FFFF)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def default_view_only(cls) -> Self:
|
||||||
|
return cls(view_channel=True, read_message_history=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def default(cls) -> Self:
|
||||||
|
return cls.default_view_only() | cls(send_messages=True, invite_others=True, send_embeds=True, upload_files=True, connect=True, speak=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def default_direct_message(cls) -> Self:
|
||||||
|
return cls.default_view_only() | cls(react=True, manage_channel=True)
|
||||||
|
|
||||||
|
class PermissionsOverwrite:
|
||||||
|
"""A permissions overwrite in a channel"""
|
||||||
|
|
||||||
|
def __init__(self, allow: Permissions, deny: Permissions):
|
||||||
|
self._allow = allow
|
||||||
|
self._deny = deny
|
||||||
|
|
||||||
|
for perm in Permissions.FLAG_NAMES:
|
||||||
|
if getattr(allow, perm):
|
||||||
|
value = True
|
||||||
|
elif getattr(deny, perm):
|
||||||
|
value = False
|
||||||
|
else:
|
||||||
|
value = None
|
||||||
|
|
||||||
|
super().__setattr__(perm, value)
|
||||||
|
|
||||||
|
def __setattr__(self, key: str, value: Any) -> None:
|
||||||
|
if key in Permissions.FLAG_NAMES:
|
||||||
|
if key is True:
|
||||||
|
setattr(self._allow, key, True)
|
||||||
|
super().__setattr__(key, True)
|
||||||
|
|
||||||
|
elif key is False:
|
||||||
|
setattr(self._deny, key, True)
|
||||||
|
super().__setattr__(key, False)
|
||||||
|
|
||||||
|
else:
|
||||||
|
setattr(self._allow, key, False)
|
||||||
|
setattr(self._deny, key, False)
|
||||||
|
super().__setattr__(key, None)
|
||||||
|
else:
|
||||||
|
super().__setattr__(key, value)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
manage_channel: Optional[bool]
|
||||||
|
manage_server: Optional[bool]
|
||||||
|
manage_permissions: Optional[bool]
|
||||||
|
manage_role: Optional[bool]
|
||||||
|
kick_members: Optional[bool]
|
||||||
|
ban_members: Optional[bool]
|
||||||
|
timeout_members: Optional[bool]
|
||||||
|
asign_roles: Optional[bool]
|
||||||
|
change_nickname: Optional[bool]
|
||||||
|
manage_nicknames: Optional[bool]
|
||||||
|
change_avatars: Optional[bool]
|
||||||
|
remove_avatars: Optional[bool]
|
||||||
|
view_channel: Optional[bool]
|
||||||
|
read_message_history: Optional[bool]
|
||||||
|
send_messages: Optional[bool]
|
||||||
|
manage_messages: Optional[bool]
|
||||||
|
manage_webhooks: Optional[bool]
|
||||||
|
invite_others: Optional[bool]
|
||||||
|
send_embeds: Optional[bool]
|
||||||
|
upload_files: Optional[bool]
|
||||||
|
masquerade: Optional[bool]
|
||||||
|
connect: Optional[bool]
|
||||||
|
speak: Optional[bool]
|
||||||
|
video: Optional[bool]
|
||||||
|
mute_members: Optional[bool]
|
||||||
|
deafen_members: Optional[bool]
|
||||||
|
move_members: Optional[bool]
|
||||||
|
|
||||||
|
def to_pair(self) -> tuple[Permissions, Permissions]:
|
||||||
|
return self._allow, self._deny
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_overwrite(cls, overwrite: Overwrite) -> Self:
|
||||||
|
allow = Permissions(overwrite["a"])
|
||||||
|
deny = Permissions(overwrite["d"])
|
||||||
|
|
||||||
|
return cls(allow, deny)
|
82
next/permissions_calculator.py
Normal file
82
next/permissions_calculator.py
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import TYPE_CHECKING, cast
|
||||||
|
|
||||||
|
from next.enums import ChannelType
|
||||||
|
|
||||||
|
from .permissions import Permissions
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
permissions = target.default_permissions
|
||||||
|
|
||||||
|
for role in member.roles:
|
||||||
|
permissions = (permissions | role.permissions._allow) & (~role.permissions._deny)
|
||||||
|
|
||||||
|
if member.current_timeout and member.current_timeout > datetime.now():
|
||||||
|
permissions = permissions & Permissions.default_view_only()
|
||||||
|
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
else:
|
||||||
|
channel_type = target.channel_type
|
||||||
|
|
||||||
|
if channel_type is ChannelType.saved_messages:
|
||||||
|
return Permissions.all()
|
||||||
|
|
||||||
|
elif channel_type is ChannelType.direct_message:
|
||||||
|
target = cast("DMChannel", target)
|
||||||
|
|
||||||
|
user_permissions = target.recipient.get_permissions()
|
||||||
|
|
||||||
|
if user_permissions.send_message:
|
||||||
|
return Permissions.default_direct_message()
|
||||||
|
|
||||||
|
else:
|
||||||
|
return Permissions.default_view_only()
|
||||||
|
|
||||||
|
elif channel_type is ChannelType.group:
|
||||||
|
target = cast("GroupDMChannel", target)
|
||||||
|
|
||||||
|
if target.owner.id != member.id:
|
||||||
|
return Permissions.default_direct_message()
|
||||||
|
else:
|
||||||
|
if target.permissions.value == 0:
|
||||||
|
return Permissions.default_direct_message()
|
||||||
|
else:
|
||||||
|
return target.permissions
|
||||||
|
|
||||||
|
else:
|
||||||
|
target = cast("ServerChannel", target)
|
||||||
|
server = target.server
|
||||||
|
|
||||||
|
if server.owner_id == member.id:
|
||||||
|
return Permissions.all()
|
||||||
|
|
||||||
|
else:
|
||||||
|
perms = calculate_permissions(member, server)
|
||||||
|
perms = (perms | target.default_permissions._allow) & (~target.default_permissions._deny)
|
||||||
|
|
||||||
|
for role in server.roles[::-1]:
|
||||||
|
if overwrite :=target.permissions.get(role.id):
|
||||||
|
perms = (perms | overwrite._allow) & (~overwrite._deny)
|
||||||
|
|
||||||
|
if member.current_timeout and member.current_timeout > datetime.now():
|
||||||
|
perms = perms & Permissions(view_channel=True, read_message_history=True)
|
||||||
|
|
||||||
|
return perms
|
0
next/py.typed
Normal file
0
next/py.typed
Normal file
105
next/role.py
Normal file
105
next/role.py
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any, Optional
|
||||||
|
|
||||||
|
from .permissions import Overwrite, PermissionsOverwrite
|
||||||
|
from .utils import Missing, Ulid
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .server import Server
|
||||||
|
from .state import State
|
||||||
|
from .types import Role as RolePayload
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ("Role",)
|
||||||
|
|
||||||
|
class Role(Ulid):
|
||||||
|
"""Represents a role
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
id: :class:`str`
|
||||||
|
The id of the role
|
||||||
|
name: :class:`str`
|
||||||
|
The name of the role
|
||||||
|
colour: Optional[: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
|
||||||
|
server: :class:`Server`
|
||||||
|
The server the role belongs to
|
||||||
|
server_permissions: :class:`ServerPermissions`
|
||||||
|
The server permissions for the role
|
||||||
|
channel_permissions: :class:`ChannelPermissions`
|
||||||
|
The channel permissions for the role
|
||||||
|
"""
|
||||||
|
__slots__: tuple[str, ...] = ("id", "name", "colour", "hoist", "rank", "state", "server", "permissions")
|
||||||
|
|
||||||
|
def __init__(self, data: RolePayload, role_id: str, server: Server, state: State):
|
||||||
|
self.state: State = state
|
||||||
|
self.id: str = role_id
|
||||||
|
self.name: str = data["name"]
|
||||||
|
self.colour: str | None = data.get("colour", None)
|
||||||
|
self.hoist: bool = data.get("hoist", False)
|
||||||
|
self.rank: int = data["rank"]
|
||||||
|
self.server: Server = server
|
||||||
|
self.permissions: PermissionsOverwrite = PermissionsOverwrite._from_overwrite(data.get("permissions", {"a": 0, "d": 0}))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color(self) -> str | None:
|
||||||
|
return self.colour
|
||||||
|
|
||||||
|
async def set_permissions_overwrite(self, *, permissions: PermissionsOverwrite) -> None:
|
||||||
|
"""Sets the permissions for a role in a server.
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
server_permissions: Optional[:class:`ServerPermissions`]
|
||||||
|
The new server permissions for the role
|
||||||
|
channel_permissions: Optional[:class:`ChannelPermissions`]
|
||||||
|
The new channel permissions for the role
|
||||||
|
"""
|
||||||
|
allow, deny = permissions.to_pair()
|
||||||
|
await self.state.http.set_server_role_permissions(self.server.id, self.id, allow.value, deny.value)
|
||||||
|
|
||||||
|
def _update(self, *, name: Optional[str] = None, colour: Optional[str] = None, hoist: Optional[bool] = None, rank: Optional[int] = None, permissions: Optional[Overwrite] = None) -> None:
|
||||||
|
if name is not None:
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
if colour is not None:
|
||||||
|
self.colour = colour
|
||||||
|
|
||||||
|
if hoist is not None:
|
||||||
|
self.hoist = hoist
|
||||||
|
|
||||||
|
if rank is not None:
|
||||||
|
self.rank = rank
|
||||||
|
|
||||||
|
if permissions is not None:
|
||||||
|
self.permissions = PermissionsOverwrite._from_overwrite(permissions)
|
||||||
|
|
||||||
|
async def delete(self) -> None:
|
||||||
|
"""Deletes the role"""
|
||||||
|
await self.state.http.delete_role(self.server.id, self.id)
|
||||||
|
|
||||||
|
async def edit(self, **kwargs: Any) -> None:
|
||||||
|
"""Edits the role
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
name: str
|
||||||
|
The name of the role
|
||||||
|
colour: str
|
||||||
|
The colour of the role
|
||||||
|
hoist: bool
|
||||||
|
Whether the role should make the member display seperately in the member list
|
||||||
|
rank: int
|
||||||
|
The position of the role
|
||||||
|
"""
|
||||||
|
if kwargs.get("colour", Missing) is None:
|
||||||
|
remove = ["Colour"]
|
||||||
|
else:
|
||||||
|
remove = None
|
||||||
|
|
||||||
|
await self.state.http.edit_role(self.server.id, self.id, remove, kwargs)
|
472
next/server.py
Normal file
472
next/server.py
Normal file
|
@ -0,0 +1,472 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Optional, cast
|
||||||
|
|
||||||
|
from .asset import Asset
|
||||||
|
from .category import Category
|
||||||
|
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 .emoji import Emoji
|
||||||
|
from .file import File
|
||||||
|
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")
|
||||||
|
self.user_left_id: str | None = data.get("user_left")
|
||||||
|
self.user_kicked_id: str | None = data.get("user_kicked")
|
||||||
|
self.user_banned_id: str | None = data.get("user_banned")
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
channel = self.state.get_channel(self.user_joined_id)
|
||||||
|
assert isinstance(channel, TextChannel)
|
||||||
|
return channel
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
channel = self.state.get_channel(self.user_left_id)
|
||||||
|
assert isinstance(channel, TextChannel)
|
||||||
|
return channel
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
channel = self.state.get_channel(self.user_kicked_id)
|
||||||
|
assert isinstance(channel, TextChannel)
|
||||||
|
return channel
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
channel = self.state.get_channel(self.user_banned_id)
|
||||||
|
assert isinstance(channel, TextChannel)
|
||||||
|
return channel
|
||||||
|
|
||||||
|
class Server(Ulid):
|
||||||
|
"""Represents a server
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
id: :class:`str`
|
||||||
|
The id of the server
|
||||||
|
name: :class:`str`
|
||||||
|
The name of the server
|
||||||
|
owner_id: :class:`str`
|
||||||
|
The owner's id of the server
|
||||||
|
description: Optional[:class:`str`]
|
||||||
|
The servers description
|
||||||
|
nsfw: :class:`bool`
|
||||||
|
Whether the server is nsfw or not
|
||||||
|
system_messages: :class:`SystemMessages`
|
||||||
|
The system message config for the server
|
||||||
|
icon: Optional[:class:`Asset`]
|
||||||
|
The servers icon
|
||||||
|
banner: Optional[:class:`Asset`]
|
||||||
|
The servers banner
|
||||||
|
default_permissions: :class:`Permissions`
|
||||||
|
The permissions for the default role
|
||||||
|
"""
|
||||||
|
__slots__ = ("state", "id", "name", "owner_id", "default_permissions", "_members", "_roles", "_channels", "description", "icon", "banner", "nsfw", "system_messages", "_categories", "_emojis")
|
||||||
|
|
||||||
|
def __init__(self, data: ServerPayload, state: State):
|
||||||
|
self.state: State = state
|
||||||
|
self.id: str = data["_id"]
|
||||||
|
self.name: str = data["name"]
|
||||||
|
self.owner_id: str = data["owner"]
|
||||||
|
self.description: str | None = data.get("description") or None
|
||||||
|
self.nsfw: bool = data.get("nsfw", False)
|
||||||
|
self.system_messages: SystemMessages = SystemMessages(data.get("system_messages", cast("SystemMessagesConfig", {})), state)
|
||||||
|
self._categories: dict[str, Category] = {data["id"]: Category(data, state) for data in data.get("categories", [])}
|
||||||
|
self.default_permissions: Permissions = Permissions(data["default_permissions"])
|
||||||
|
|
||||||
|
self.icon: Asset | None
|
||||||
|
|
||||||
|
if icon := data.get("icon"):
|
||||||
|
self.icon = Asset(icon, state)
|
||||||
|
else:
|
||||||
|
self.icon = None
|
||||||
|
|
||||||
|
self.banner: Asset | None
|
||||||
|
|
||||||
|
if banner := data.get("banner"):
|
||||||
|
self.banner = Asset(banner, state)
|
||||||
|
else:
|
||||||
|
self.banner = None
|
||||||
|
|
||||||
|
self._members: dict[str, Member] = {}
|
||||||
|
self._roles: dict[str, Role] = {role_id: Role(role, role_id, self, state) for role_id, role in data.get("roles", {}).items()}
|
||||||
|
|
||||||
|
self._channels: dict[str, Channel] = {}
|
||||||
|
|
||||||
|
# The api doesnt send us all the channels but sends us all the ids, this is because channels we dont have permissions to see are not sent
|
||||||
|
# this causes get_channel to error so we have to first check ourself if its in the cache.
|
||||||
|
|
||||||
|
for channel_id in data["channels"]:
|
||||||
|
if channel := state.channels.get(channel_id):
|
||||||
|
self._channels[channel_id] = channel
|
||||||
|
|
||||||
|
self._emojis: dict[str, Emoji] = {}
|
||||||
|
|
||||||
|
def _update(self, *, owner: Optional[str] = None, name: Optional[str] = None, description: Optional[str] = None, icon: Optional[FilePayload] = None, banner: Optional[FilePayload] = None, default_permissions: Optional[int] = None, nsfw: Optional[bool] = None, system_messages: Optional[SystemMessagesConfig] = None, categories: Optional[list[CategoryPayload]] = None, channels: Optional[list[str]] = None):
|
||||||
|
if owner is not None:
|
||||||
|
self.owner_id = owner
|
||||||
|
if name is not None:
|
||||||
|
self.name = name
|
||||||
|
if description is not None:
|
||||||
|
self.description = description or None
|
||||||
|
if icon is not None:
|
||||||
|
self.icon = Asset(icon, self.state)
|
||||||
|
if banner is not None:
|
||||||
|
self.banner = Asset(banner, self.state)
|
||||||
|
if default_permissions is not None:
|
||||||
|
self.default_permissions = Permissions(default_permissions)
|
||||||
|
if nsfw is not None:
|
||||||
|
self.nsfw = nsfw
|
||||||
|
if system_messages is not None:
|
||||||
|
self.system_messages = SystemMessages(system_messages, self.state)
|
||||||
|
if categories is not None:
|
||||||
|
self._categories = {data["id"]: Category(data, self.state) for data in categories}
|
||||||
|
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"""
|
||||||
|
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())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def categories(self) -> list[Category]:
|
||||||
|
"""list[:class:`Category`] Gets all categories in the server"""
|
||||||
|
return list(self._categories.values())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def emojis(self) -> list[Emoji]:
|
||||||
|
"""list[:class:`Emoji`] Gets all emojis in the server"""
|
||||||
|
return list(self._emojis.values())
|
||||||
|
|
||||||
|
def get_role(self, role_id: str) -> Role:
|
||||||
|
"""Gets a role from the cache
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
id: :class:`str`
|
||||||
|
The id of the role
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Role`
|
||||||
|
The role
|
||||||
|
"""
|
||||||
|
return self._roles[role_id]
|
||||||
|
|
||||||
|
def get_member(self, member_id: str) -> Member:
|
||||||
|
"""Gets a member from the cache
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
id: :class:`str`
|
||||||
|
The id of the member
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Member`
|
||||||
|
The member
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self._members[member_id]
|
||||||
|
except KeyError:
|
||||||
|
raise LookupError from None
|
||||||
|
|
||||||
|
def get_channel(self, channel_id: str) -> Channel:
|
||||||
|
"""Gets a channel from the cache
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
id: :class:`str`
|
||||||
|
The id of the channel
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Channel`
|
||||||
|
The channel
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self._channels[channel_id]
|
||||||
|
except KeyError:
|
||||||
|
raise LookupError from None
|
||||||
|
|
||||||
|
def get_category(self, category_id: str) -> Category:
|
||||||
|
"""Gets a category from the cache
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
id: :class:`str`
|
||||||
|
The id of the category
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Category`
|
||||||
|
The category
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self._categories[category_id]
|
||||||
|
except KeyError:
|
||||||
|
raise LookupError from None
|
||||||
|
|
||||||
|
def get_emoji(self, emoji_id: str) -> Emoji:
|
||||||
|
"""Gets a emoji from the cache
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
id: :class:`str`
|
||||||
|
The id of the emoji
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Emoji`
|
||||||
|
The emoji
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self._emojis[emoji_id]
|
||||||
|
except KeyError as e:
|
||||||
|
raise LookupError from e
|
||||||
|
|
||||||
|
@property
|
||||||
|
def owner(self) -> Member:
|
||||||
|
""":class:`Member` The owner of the server"""
|
||||||
|
return self.get_member(self.owner_id)
|
||||||
|
|
||||||
|
async def set_default_permissions(self, permissions: Permissions) -> None:
|
||||||
|
"""Sets the default server permissions.
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
server_permissions: Optional[:class:`ServerPermissions`]
|
||||||
|
The new default server permissions
|
||||||
|
channel_permissions: Optional[:class:`ChannelPermissions`]
|
||||||
|
the new default channel permissions
|
||||||
|
"""
|
||||||
|
|
||||||
|
await self.state.http.set_server_default_permissions(self.id, permissions.value)
|
||||||
|
|
||||||
|
async def leave_server(self) -> None:
|
||||||
|
"""Leaves or deletes the server"""
|
||||||
|
await self.state.http.delete_leave_server(self.id)
|
||||||
|
|
||||||
|
async def delete_server(self) -> None:
|
||||||
|
"""Leaves or deletes a server, alias to :meth`Server.leave_server`"""
|
||||||
|
await self.leave_server()
|
||||||
|
|
||||||
|
async def create_text_channel(self, *, name: str, description: Optional[str] = None) -> TextChannel:
|
||||||
|
"""Creates a text channel in the server
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
name: :class:`str`
|
||||||
|
The name of the channel
|
||||||
|
description: Optional[:class:`str`]
|
||||||
|
The channel's description
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`TextChannel`
|
||||||
|
The text channel that was just created
|
||||||
|
"""
|
||||||
|
payload = await self.state.http.create_channel(self.id, "Text", name, description)
|
||||||
|
|
||||||
|
channel = TextChannel(payload, self.state)
|
||||||
|
self._channels[channel.id] = channel
|
||||||
|
|
||||||
|
return channel
|
||||||
|
|
||||||
|
async def create_voice_channel(self, *, name: str, description: Optional[str] = None) -> VoiceChannel:
|
||||||
|
"""Creates a voice channel in the server
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
name: :class:`str`
|
||||||
|
The name of the channel
|
||||||
|
description: Optional[:class:`str`]
|
||||||
|
The channel's description
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`VoiceChannel`
|
||||||
|
The voice channel that was just created
|
||||||
|
"""
|
||||||
|
payload = await self.state.http.create_channel(self.id, "Voice", name, description)
|
||||||
|
|
||||||
|
channel = self.state.add_channel(payload)
|
||||||
|
self._channels[channel.id] = channel
|
||||||
|
|
||||||
|
return cast(VoiceChannel, channel)
|
||||||
|
|
||||||
|
async def fetch_invites(self) -> list[Invite]:
|
||||||
|
"""Fetches all invites in the server
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
list[:class:`Invite`]
|
||||||
|
"""
|
||||||
|
invite_payloads = await self.state.http.fetch_server_invites(self.id)
|
||||||
|
|
||||||
|
return [Invite._from_partial(payload["_id"], payload["server"], payload["creator"], payload["channel"], self.state) for payload in invite_payloads]
|
||||||
|
|
||||||
|
async def fetch_member(self, member_id: str) -> Member:
|
||||||
|
"""Fetches a member from this server
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
member_id: :class:`str`
|
||||||
|
The id of the member you are fetching
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Member`
|
||||||
|
The member with the matching id
|
||||||
|
"""
|
||||||
|
payload = await self.state.http.fetch_member(self.id, member_id)
|
||||||
|
|
||||||
|
return Member(payload, self, self.state)
|
||||||
|
|
||||||
|
async def fetch_bans(self) -> list[ServerBan]:
|
||||||
|
"""Fetches all bans in the server
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
list[:class:`ServerBan`]
|
||||||
|
"""
|
||||||
|
payload = await self.state.http.fetch_bans(self.id)
|
||||||
|
|
||||||
|
return [ServerBan(ban, self.state) for ban in payload["bans"]]
|
||||||
|
|
||||||
|
async def create_role(self, name: str) -> Role:
|
||||||
|
"""Creates a role in the server
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
name: :class:`str`
|
||||||
|
The name of the role
|
||||||
|
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Role`
|
||||||
|
The role that was just created
|
||||||
|
"""
|
||||||
|
payload = await self.state.http.create_role(self.id, name)
|
||||||
|
|
||||||
|
return Role(payload["role"], payload["id"], self, self.state)
|
||||||
|
|
||||||
|
async def create_emoji(self, name: str, file: File, *, nsfw: bool = False) -> Emoji:
|
||||||
|
"""Creates an emoji
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
name: :class:`str`
|
||||||
|
The name for the emoji
|
||||||
|
file: :class:`File`
|
||||||
|
The image for the emoji
|
||||||
|
nsfw: :class:`bool`
|
||||||
|
Whether or not the emoji is nsfw
|
||||||
|
"""
|
||||||
|
payload = await self.state.http.create_emoji(name, file, nsfw, {"type": "Server", "id": self.id})
|
||||||
|
|
||||||
|
return self.state.add_emoji(payload)
|
||||||
|
|
||||||
|
|
||||||
|
class ServerBan:
|
||||||
|
"""Represents a server ban
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
reason: Optional[:class:`str`]
|
||||||
|
The reason the user was banned
|
||||||
|
server: :class:`Server`
|
||||||
|
The server the user was banned in
|
||||||
|
user_id: :class:`str`
|
||||||
|
The id of the user who was banned
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("reason", "server", "user_id", "state")
|
||||||
|
|
||||||
|
def __init__(self, ban: Ban, state: State):
|
||||||
|
self.reason: str | None = ban.get("reason")
|
||||||
|
self.server: Server = state.get_server(ban["_id"]["server"])
|
||||||
|
self.user_id: str = ban["_id"]["user"]
|
||||||
|
self.state: State = state
|
||||||
|
|
||||||
|
async def unban(self) -> None:
|
||||||
|
"""Unbans the user"""
|
||||||
|
await self.state.http.unban_member(self.server.id, self.user_id)
|
126
next/state.py
Normal file
126
next/state.py
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import deque
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from .channel import Channel, channel_factory
|
||||||
|
from .emoji import Emoji
|
||||||
|
from .member import Member
|
||||||
|
from .message import Message
|
||||||
|
from .server import Server
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .http import HttpClient
|
||||||
|
from .types import ApiInfo
|
||||||
|
from .types import Channel as ChannelPayload
|
||||||
|
from .types import Emoji as EmojiPayload
|
||||||
|
from .types import Member as MemberPayload
|
||||||
|
from .types import Message as MessagePayload
|
||||||
|
from .types import Server as ServerPayload
|
||||||
|
from .types import User as UserPayload
|
||||||
|
|
||||||
|
__all__ = ("State",)
|
||||||
|
|
||||||
|
class State:
|
||||||
|
__slots__ = ("http", "api_info", "max_messages", "users", "channels", "servers", "messages", "global_emojis", "user_id", "me")
|
||||||
|
|
||||||
|
def __init__(self, http: HttpClient, api_info: ApiInfo, max_messages: int):
|
||||||
|
self.http: HttpClient = http
|
||||||
|
self.api_info: ApiInfo = api_info
|
||||||
|
self.max_messages: int = max_messages
|
||||||
|
|
||||||
|
self.me: User
|
||||||
|
|
||||||
|
self.users: dict[str, User] = {}
|
||||||
|
self.channels: dict[str, Channel] = {}
|
||||||
|
self.servers: dict[str, Server] = {}
|
||||||
|
self.messages: deque[Message] = deque()
|
||||||
|
self.global_emojis: list[Emoji] = []
|
||||||
|
|
||||||
|
def get_user(self, id: str) -> User:
|
||||||
|
try:
|
||||||
|
return self.users[id]
|
||||||
|
except KeyError:
|
||||||
|
raise LookupError from None
|
||||||
|
|
||||||
|
def get_member(self, server_id: str, member_id: str) -> Member:
|
||||||
|
server = self.servers[server_id]
|
||||||
|
return server.get_member(member_id)
|
||||||
|
|
||||||
|
def get_channel(self, id: str) -> Channel:
|
||||||
|
try:
|
||||||
|
return self.channels[id]
|
||||||
|
except KeyError:
|
||||||
|
raise LookupError from None
|
||||||
|
|
||||||
|
def get_server(self, id: str) -> Server:
|
||||||
|
try:
|
||||||
|
return self.servers[id]
|
||||||
|
except KeyError:
|
||||||
|
raise LookupError from None
|
||||||
|
|
||||||
|
def add_user(self, payload: UserPayload) -> User:
|
||||||
|
|
||||||
|
|
||||||
|
user = User(payload, self)
|
||||||
|
|
||||||
|
if payload.get("relationship") == "User":
|
||||||
|
self.me = user
|
||||||
|
|
||||||
|
self.users[user.id] = user
|
||||||
|
return user
|
||||||
|
|
||||||
|
def add_member(self, server_id: str, payload: MemberPayload) -> Member:
|
||||||
|
server = self.get_server(server_id)
|
||||||
|
|
||||||
|
return server._add_member(payload)
|
||||||
|
|
||||||
|
def add_channel(self, payload: ChannelPayload) -> Channel:
|
||||||
|
channel = channel_factory(payload, self)
|
||||||
|
self.channels[channel.id] = channel
|
||||||
|
return channel
|
||||||
|
|
||||||
|
def add_server(self, payload: ServerPayload) -> Server:
|
||||||
|
server = Server(payload, self)
|
||||||
|
self.servers[server.id] = server
|
||||||
|
return server
|
||||||
|
|
||||||
|
def add_message(self, payload: MessagePayload) -> Message:
|
||||||
|
message = Message(payload, self)
|
||||||
|
if len(self.messages) >= self.max_messages:
|
||||||
|
self.messages.pop()
|
||||||
|
|
||||||
|
self.messages.appendleft(message)
|
||||||
|
return message
|
||||||
|
|
||||||
|
def add_emoji(self, payload: EmojiPayload) -> Emoji:
|
||||||
|
emoji = Emoji(payload, self)
|
||||||
|
|
||||||
|
if server_id := emoji.server_id:
|
||||||
|
server = self.get_server(server_id)
|
||||||
|
server._emojis[emoji.id] = emoji
|
||||||
|
else:
|
||||||
|
self.global_emojis.append(emoji)
|
||||||
|
|
||||||
|
return emoji
|
||||||
|
|
||||||
|
def get_message(self, message_id: str) -> Message:
|
||||||
|
for msg in self.messages:
|
||||||
|
if msg.id == message_id:
|
||||||
|
return msg
|
||||||
|
|
||||||
|
raise LookupError
|
||||||
|
|
||||||
|
async def fetch_server_members(self, server_id: str) -> None:
|
||||||
|
data = await self.http.fetch_members(server_id)
|
||||||
|
|
||||||
|
for user in data["users"]:
|
||||||
|
self.add_user(user)
|
||||||
|
|
||||||
|
for member in data["members"]:
|
||||||
|
self.add_member(server_id, member)
|
||||||
|
|
||||||
|
async def fetch_all_server_members(self) -> None:
|
||||||
|
for server_id in self.servers:
|
||||||
|
await self.fetch_server_members(server_id)
|
14
next/types/__init__.py
Normal file
14
next/types/__init__.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
from .category import *
|
||||||
|
from .channel import *
|
||||||
|
from .embed import *
|
||||||
|
from .emoji import *
|
||||||
|
from .file import *
|
||||||
|
from .gateway import *
|
||||||
|
from .http import *
|
||||||
|
from .invite import *
|
||||||
|
from .member import *
|
||||||
|
from .message import *
|
||||||
|
from .permissions import *
|
||||||
|
from .role import *
|
||||||
|
from .server import *
|
||||||
|
from .user import *
|
8
next/types/category.py
Normal file
8
next/types/category.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
__all__ = ("Category",)
|
||||||
|
|
||||||
|
class Category(TypedDict):
|
||||||
|
id: str
|
||||||
|
title: str
|
||||||
|
channels: list[str]
|
68
next/types/channel.py
Normal file
68
next/types/channel.py
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Literal, TypedDict, Union
|
||||||
|
|
||||||
|
from typing_extensions import NotRequired
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .file import File
|
||||||
|
from .permissions import Overwrite
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"SavedMessages",
|
||||||
|
"DMChannel",
|
||||||
|
"GroupDMChannel",
|
||||||
|
"TextChannel",
|
||||||
|
"VoiceChannel",
|
||||||
|
"ServerChannel",
|
||||||
|
"Channel",
|
||||||
|
)
|
||||||
|
|
||||||
|
class BaseChannel(TypedDict):
|
||||||
|
_id: str
|
||||||
|
nonce: str
|
||||||
|
|
||||||
|
class SavedMessages(BaseChannel):
|
||||||
|
user: str
|
||||||
|
channel_type: Literal["SavedMessages"]
|
||||||
|
|
||||||
|
class DMChannel(BaseChannel):
|
||||||
|
active: bool
|
||||||
|
recipients: list[str]
|
||||||
|
last_message_id: NotRequired[str]
|
||||||
|
channel_type: Literal["DirectMessage"]
|
||||||
|
|
||||||
|
class GroupDMChannel(BaseChannel):
|
||||||
|
recipients: list[str]
|
||||||
|
name: str
|
||||||
|
owner: str
|
||||||
|
channel_type: Literal["Group"]
|
||||||
|
icon: NotRequired[File]
|
||||||
|
permissions: NotRequired[int]
|
||||||
|
description: NotRequired[str]
|
||||||
|
nsfw: NotRequired[bool]
|
||||||
|
last_message_id: NotRequired[str]
|
||||||
|
|
||||||
|
class TextChannel(BaseChannel):
|
||||||
|
server: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
channel_type: Literal["TextChannel"]
|
||||||
|
icon: NotRequired[File]
|
||||||
|
default_permissions: NotRequired[Overwrite]
|
||||||
|
role_permissions: NotRequired[dict[str, Overwrite]]
|
||||||
|
nsfw: NotRequired[bool]
|
||||||
|
last_message_id: NotRequired[str]
|
||||||
|
|
||||||
|
class VoiceChannel(BaseChannel):
|
||||||
|
server: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
channel_type: Literal["VoiceChannel"]
|
||||||
|
icon: NotRequired[File]
|
||||||
|
default_permissions: NotRequired[Overwrite]
|
||||||
|
role_permissions: NotRequired[dict[str, Overwrite]]
|
||||||
|
nsfw: NotRequired[bool]
|
||||||
|
|
||||||
|
ServerChannel = Union[TextChannel, VoiceChannel]
|
||||||
|
Channel = Union[SavedMessages, DMChannel, GroupDMChannel, TextChannel, VoiceChannel]
|
85
next/types/embed.py
Normal file
85
next/types/embed.py
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Literal, TypedDict, Union
|
||||||
|
|
||||||
|
from typing_extensions import NotRequired
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .file import File
|
||||||
|
|
||||||
|
__all__ = ("Embed", "SendableEmbed", "WebsiteEmbed", "ImageEmbed", "TextEmbed", "NoneEmbed", "YoutubeSpecial", "TwitchSpecial", "SpotifySpecial", "SoundcloudSpecial", "BandcampSpecial", "WebsiteSpecial", "JanuaryImage", "JanuaryVideo")
|
||||||
|
|
||||||
|
class YoutubeSpecial(TypedDict):
|
||||||
|
type: Literal["Youtube"]
|
||||||
|
id: str
|
||||||
|
timestamp: NotRequired[str]
|
||||||
|
|
||||||
|
class TwitchSpecial(TypedDict):
|
||||||
|
type: Literal["Twitch"]
|
||||||
|
content_type: Literal["Channel", "Video", "Clip"]
|
||||||
|
id: str
|
||||||
|
|
||||||
|
class SpotifySpecial(TypedDict):
|
||||||
|
type: Literal["Spotify"]
|
||||||
|
content_type: str
|
||||||
|
id: str
|
||||||
|
|
||||||
|
class SoundcloudSpecial(TypedDict):
|
||||||
|
type: Literal["Soundcloud"]
|
||||||
|
|
||||||
|
class BandcampSpecial(TypedDict):
|
||||||
|
type: Literal["Bandcamp"]
|
||||||
|
content_type: Literal["Album", "Track"]
|
||||||
|
id: str
|
||||||
|
|
||||||
|
WebsiteSpecial = Union[YoutubeSpecial, TwitchSpecial, SpotifySpecial, SoundcloudSpecial, BandcampSpecial]
|
||||||
|
|
||||||
|
class JanuaryImage(TypedDict):
|
||||||
|
url: str
|
||||||
|
width: int
|
||||||
|
height: int
|
||||||
|
size: Literal["Large", "Preview"]
|
||||||
|
|
||||||
|
class JanuaryVideo(TypedDict):
|
||||||
|
url: str
|
||||||
|
width: int
|
||||||
|
height: int
|
||||||
|
|
||||||
|
class WebsiteEmbed(TypedDict):
|
||||||
|
type: Literal["Website"]
|
||||||
|
url: NotRequired[str]
|
||||||
|
special: NotRequired[WebsiteSpecial]
|
||||||
|
title: NotRequired[str]
|
||||||
|
description: NotRequired[str]
|
||||||
|
image: NotRequired[JanuaryImage]
|
||||||
|
video: NotRequired[JanuaryVideo]
|
||||||
|
site_name: NotRequired[str]
|
||||||
|
icon_url: NotRequired[str]
|
||||||
|
colour: NotRequired[str]
|
||||||
|
|
||||||
|
class ImageEmbed(JanuaryImage):
|
||||||
|
type: Literal["Image"]
|
||||||
|
|
||||||
|
class TextEmbed(TypedDict):
|
||||||
|
type: Literal["Text"]
|
||||||
|
icon_url: NotRequired[str]
|
||||||
|
url: NotRequired[str]
|
||||||
|
title: NotRequired[str]
|
||||||
|
description: NotRequired[str]
|
||||||
|
media: NotRequired[File]
|
||||||
|
colour: NotRequired[str]
|
||||||
|
|
||||||
|
class NoneEmbed(TypedDict):
|
||||||
|
type: Literal["None"]
|
||||||
|
|
||||||
|
Embed = Union[WebsiteEmbed, ImageEmbed, TextEmbed, NoneEmbed]
|
||||||
|
|
||||||
|
class SendableEmbed(TypedDict):
|
||||||
|
type: Literal["Text"]
|
||||||
|
icon_url: NotRequired[str]
|
||||||
|
url: NotRequired[str]
|
||||||
|
title: NotRequired[str]
|
||||||
|
description: NotRequired[str]
|
||||||
|
media: NotRequired[str]
|
||||||
|
colour: NotRequired[str]
|
||||||
|
|
21
next/types/emoji.py
Normal file
21
next/types/emoji.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
from typing import Literal, TypedDict, Union
|
||||||
|
|
||||||
|
from typing_extensions import NotRequired
|
||||||
|
|
||||||
|
|
||||||
|
class EmojiParentServer(TypedDict):
|
||||||
|
type: Literal["Server"]
|
||||||
|
id: str
|
||||||
|
|
||||||
|
class EmojiParentDetached(TypedDict):
|
||||||
|
type: Literal["Detached"]
|
||||||
|
|
||||||
|
EmojiParent = Union[EmojiParentServer, EmojiParentDetached]
|
||||||
|
|
||||||
|
class Emoji(TypedDict):
|
||||||
|
_id: str
|
||||||
|
parent: EmojiParent
|
||||||
|
creator_id: str
|
||||||
|
name: str
|
||||||
|
animated: NotRequired[bool]
|
||||||
|
nsfw: NotRequired[bool]
|
23
next/types/file.py
Normal file
23
next/types/file.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Literal, TypedDict, Union
|
||||||
|
|
||||||
|
__all__ = ("File",)
|
||||||
|
|
||||||
|
class SizedMetadata(TypedDict):
|
||||||
|
type: Literal["Image", "Video"]
|
||||||
|
height: int
|
||||||
|
width: int
|
||||||
|
|
||||||
|
class SimpleMetadata(TypedDict):
|
||||||
|
type: Literal["File", "Text", "Audio"]
|
||||||
|
|
||||||
|
FileMetadata = Union[SizedMetadata, SimpleMetadata]
|
||||||
|
|
||||||
|
class File(TypedDict):
|
||||||
|
_id: str
|
||||||
|
tag: str
|
||||||
|
size: int
|
||||||
|
filename: str
|
||||||
|
metadata: FileMetadata
|
||||||
|
content_type: str
|
215
next/types/gateway.py
Normal file
215
next/types/gateway.py
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Literal, TypedDict, Union
|
||||||
|
|
||||||
|
from typing_extensions import NotRequired
|
||||||
|
|
||||||
|
from .channel import Channel, DMChannel, GroupDMChannel, SavedMessages, TextChannel, VoiceChannel
|
||||||
|
from .message import Message
|
||||||
|
from .permissions import Overwrite
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .category import Category
|
||||||
|
from .embed import Embed
|
||||||
|
from .emoji import Emoji
|
||||||
|
from .file import File
|
||||||
|
from .member import Member, MemberID
|
||||||
|
from .server import Server, SystemMessagesConfig
|
||||||
|
from .user import Status, User, UserProfile, UserRelation
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"BasePayload",
|
||||||
|
"AuthenticatePayload",
|
||||||
|
"ReadyEventPayload",
|
||||||
|
"MessageEventPayload",
|
||||||
|
"MessageUpdateData",
|
||||||
|
"MessageUpdateEventPayload",
|
||||||
|
"MessageDeleteEventPayload",
|
||||||
|
"ChannelCreateEventPayload",
|
||||||
|
"ChannelUpdateEventPayload",
|
||||||
|
"ChannelDeleteEventPayload",
|
||||||
|
"ChannelStartTypingEventPayload",
|
||||||
|
"ChannelDeleteTypingEventPayload",
|
||||||
|
"ServerUpdateEventPayload",
|
||||||
|
"ServerDeleteEventPayload",
|
||||||
|
"ServerMemberUpdateEventPayload",
|
||||||
|
"ServerMemberJoinEventPayload",
|
||||||
|
"ServerMemberLeaveEventPayload",
|
||||||
|
"ServerRoleUpdateEventPayload",
|
||||||
|
"ServerRoleDeleteEventPayload",
|
||||||
|
"UserUpdateEventPayload",
|
||||||
|
"UserRelationshipEventPayload",
|
||||||
|
"ServerCreateEventPayload",
|
||||||
|
"MessageReactEventPayload",
|
||||||
|
"MessageUnreactEventPayload",
|
||||||
|
"MessageRemoveReactionEventPayload",
|
||||||
|
"BulkMessageDeleteEventPayload"
|
||||||
|
)
|
||||||
|
|
||||||
|
class BasePayload(TypedDict):
|
||||||
|
type: str
|
||||||
|
|
||||||
|
class AuthenticatePayload(BasePayload):
|
||||||
|
token: str
|
||||||
|
|
||||||
|
class ReadyEventPayload(BasePayload):
|
||||||
|
users: list[User]
|
||||||
|
servers: list[Server]
|
||||||
|
channels: list[Channel]
|
||||||
|
members: list[Member]
|
||||||
|
emojis: list[Emoji]
|
||||||
|
|
||||||
|
class MessageEventPayload(BasePayload, Message):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class MessageUpdateData(TypedDict):
|
||||||
|
content: str
|
||||||
|
embeds: list[Embed]
|
||||||
|
edited: Union[str, int]
|
||||||
|
|
||||||
|
class MessageUpdateEventPayload(BasePayload):
|
||||||
|
channel: str
|
||||||
|
data: MessageUpdateData
|
||||||
|
id: str
|
||||||
|
|
||||||
|
class MessageDeleteEventPayload(BasePayload):
|
||||||
|
channel: str
|
||||||
|
id: str
|
||||||
|
|
||||||
|
class ChannelCreateEventPayload_SavedMessages(BasePayload, SavedMessages):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ChannelCreateEventPayload_Group(BasePayload, GroupDMChannel):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ChannelCreateEventPayload_TextChannel(BasePayload, TextChannel):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ChannelCreateEventPayload_VoiceChannel(BasePayload, VoiceChannel):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ChannelCreateEventPayload_DMChannel(BasePayload, DMChannel):
|
||||||
|
pass
|
||||||
|
|
||||||
|
ChannelCreateEventPayload = Union[ChannelCreateEventPayload_Group, ChannelCreateEventPayload_Group, ChannelCreateEventPayload_TextChannel, ChannelCreateEventPayload_VoiceChannel, ChannelCreateEventPayload_DMChannel]
|
||||||
|
|
||||||
|
class ChannelUpdateEventPayloadData(TypedDict, total=False):
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
icon: File
|
||||||
|
nsfw: bool
|
||||||
|
active: bool
|
||||||
|
role_permissions: dict[str, Overwrite]
|
||||||
|
default_permissions: Overwrite
|
||||||
|
|
||||||
|
class ChannelUpdateEventPayload(BasePayload):
|
||||||
|
id: str
|
||||||
|
data: ChannelUpdateEventPayloadData
|
||||||
|
clear: Literal["Icon", "Description"]
|
||||||
|
|
||||||
|
class ChannelDeleteEventPayload(BasePayload):
|
||||||
|
id: str
|
||||||
|
|
||||||
|
class ChannelStartTypingEventPayload(BasePayload):
|
||||||
|
id: str
|
||||||
|
user: str
|
||||||
|
|
||||||
|
ChannelDeleteTypingEventPayload = ChannelStartTypingEventPayload
|
||||||
|
|
||||||
|
class ServerUpdateEventPayloadData(TypedDict, total=False):
|
||||||
|
owner: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
icon: File
|
||||||
|
banner: File
|
||||||
|
default_permissions: int
|
||||||
|
nsfw: bool
|
||||||
|
system_messages: SystemMessagesConfig
|
||||||
|
categories: list[Category]
|
||||||
|
|
||||||
|
class ServerUpdateEventPayload(BasePayload):
|
||||||
|
id: str
|
||||||
|
data: ServerUpdateEventPayloadData
|
||||||
|
clear: Literal["Icon", "Banner", "Description"]
|
||||||
|
|
||||||
|
class ServerDeleteEventPayload(BasePayload):
|
||||||
|
id: str
|
||||||
|
|
||||||
|
class ServerCreateEventPayload(BasePayload):
|
||||||
|
id: str
|
||||||
|
server: Server
|
||||||
|
channels: list[Channel]
|
||||||
|
|
||||||
|
class ServerMemberUpdateEventPayloadData(TypedDict, total=False):
|
||||||
|
nickname: str
|
||||||
|
avatar: File
|
||||||
|
roles: list[str]
|
||||||
|
timeout: str | int
|
||||||
|
|
||||||
|
class ServerMemberUpdateEventPayload(BasePayload):
|
||||||
|
id: MemberID
|
||||||
|
data: ServerMemberUpdateEventPayloadData
|
||||||
|
clear: Literal["Nickname", "Avatar"]
|
||||||
|
|
||||||
|
class ServerMemberJoinEventPayload(BasePayload):
|
||||||
|
id: str
|
||||||
|
user: str
|
||||||
|
|
||||||
|
ServerMemberLeaveEventPayload = ServerMemberJoinEventPayload
|
||||||
|
|
||||||
|
class ServerRoleUpdateEventPayloadData(TypedDict, total=False):
|
||||||
|
name: str
|
||||||
|
colour: str
|
||||||
|
hoist: bool
|
||||||
|
rank: int
|
||||||
|
|
||||||
|
class ServerRoleUpdateEventPayload(BasePayload):
|
||||||
|
id: str
|
||||||
|
role_id: str
|
||||||
|
data: ServerRoleUpdateEventPayloadData
|
||||||
|
clear: Literal["Colour"]
|
||||||
|
|
||||||
|
class ServerRoleDeleteEventPayload(BasePayload):
|
||||||
|
id: str
|
||||||
|
role_id: str
|
||||||
|
|
||||||
|
class UserUpdateEventPayloadData(TypedDict):
|
||||||
|
status: NotRequired[Status]
|
||||||
|
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
|
||||||
|
data: UserUpdateEventPayloadData
|
||||||
|
clear: Literal["ProfileContent", "ProfileBackground", "StatusText", "Avatar"]
|
||||||
|
|
||||||
|
class UserRelationshipEventPayload(BasePayload):
|
||||||
|
id: str
|
||||||
|
user: str
|
||||||
|
status: Status
|
||||||
|
|
||||||
|
class MessageReactEventPayload(BasePayload):
|
||||||
|
id: str
|
||||||
|
channel_id: str
|
||||||
|
user_id: str
|
||||||
|
emoji_id: str
|
||||||
|
|
||||||
|
MessageUnreactEventPayload = MessageReactEventPayload
|
||||||
|
|
||||||
|
class MessageRemoveReactionEventPayload(BasePayload):
|
||||||
|
id: str
|
||||||
|
channel_id: str
|
||||||
|
emoji_id: str
|
||||||
|
|
||||||
|
class BulkMessageDeleteEventPayload(BasePayload):
|
||||||
|
channel: str
|
||||||
|
ids: list[str]
|
59
next/types/http.py
Normal file
59
next/types/http.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
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__ = (
|
||||||
|
"VosoFeature",
|
||||||
|
"ApiInfo",
|
||||||
|
"Autumn",
|
||||||
|
"GetServerMembers",
|
||||||
|
"MessageWithUserData",
|
||||||
|
"CreateRole",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ApiFeature(TypedDict):
|
||||||
|
enabled: bool
|
||||||
|
url: str
|
||||||
|
|
||||||
|
class VosoFeature(ApiFeature):
|
||||||
|
ws: str
|
||||||
|
|
||||||
|
class Features(TypedDict):
|
||||||
|
email: bool
|
||||||
|
invite_only: bool
|
||||||
|
captcha: ApiFeature
|
||||||
|
autumn: ApiFeature
|
||||||
|
january: ApiFeature
|
||||||
|
voso: VosoFeature
|
||||||
|
|
||||||
|
class ApiInfo(TypedDict):
|
||||||
|
revolt: str
|
||||||
|
features: Features
|
||||||
|
ws: str
|
||||||
|
app: str
|
||||||
|
vapid: str
|
||||||
|
|
||||||
|
class Autumn(TypedDict):
|
||||||
|
id: str
|
||||||
|
|
||||||
|
class GetServerMembers(TypedDict):
|
||||||
|
members: list[Member]
|
||||||
|
users: list[User]
|
||||||
|
|
||||||
|
class MessageWithUserData(TypedDict):
|
||||||
|
messages: list[Message]
|
||||||
|
members: NotRequired[list[Member]]
|
||||||
|
users: list[User]
|
||||||
|
|
||||||
|
class CreateRole(TypedDict):
|
||||||
|
id: str
|
||||||
|
role: Role
|
30
next/types/invite.py
Normal file
30
next/types/invite.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Literal, TypedDict
|
||||||
|
|
||||||
|
from typing_extensions import NotRequired
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .file import File
|
||||||
|
|
||||||
|
__all__ = ("Invite", "PartialInvite")
|
||||||
|
|
||||||
|
|
||||||
|
class Invite(TypedDict):
|
||||||
|
type: Literal["Server"]
|
||||||
|
server_id: str
|
||||||
|
server_name: str
|
||||||
|
server_icon: NotRequired[str]
|
||||||
|
server_banner: NotRequired[str]
|
||||||
|
channel_id: str
|
||||||
|
channel_name: str
|
||||||
|
channel_description: NotRequired[str]
|
||||||
|
user_name: str
|
||||||
|
user_avatar: NotRequired[File]
|
||||||
|
member_count: int
|
||||||
|
|
||||||
|
class PartialInvite(TypedDict):
|
||||||
|
_id: str
|
||||||
|
server: str
|
||||||
|
channel: str
|
||||||
|
creator: str
|
23
next/types/member.py
Normal file
23
next/types/member.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, TypedDict
|
||||||
|
|
||||||
|
from typing_extensions import NotRequired
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .file import File
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ("Member", "MemberID")
|
||||||
|
|
||||||
|
class MemberID(TypedDict):
|
||||||
|
server: str
|
||||||
|
user: str
|
||||||
|
|
||||||
|
class Member(TypedDict):
|
||||||
|
_id: MemberID
|
||||||
|
nickname: NotRequired[str]
|
||||||
|
avatar: NotRequired[File]
|
||||||
|
roles: NotRequired[list[str]]
|
||||||
|
joined_at: int | str
|
||||||
|
timeout: NotRequired[str | int]
|
88
next/types/message.py
Normal file
88
next/types/message.py
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, TypedDict, Union
|
||||||
|
|
||||||
|
from typing_extensions import NotRequired
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .embed import Embed
|
||||||
|
from .file import File
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"UserAddContent",
|
||||||
|
"UserRemoveContent",
|
||||||
|
"UserJoinedContent",
|
||||||
|
"UserLeftContent",
|
||||||
|
"UserKickedContent",
|
||||||
|
"UserBannedContent",
|
||||||
|
"ChannelRenameContent",
|
||||||
|
"ChannelDescriptionChangeContent",
|
||||||
|
"ChannelIconChangeContent",
|
||||||
|
"Masquerade",
|
||||||
|
"Interactions",
|
||||||
|
"Message",
|
||||||
|
"MessageReplyPayload",
|
||||||
|
"SystemMessageContent",
|
||||||
|
)
|
||||||
|
|
||||||
|
class UserAddContent(TypedDict):
|
||||||
|
id: str
|
||||||
|
by: str
|
||||||
|
|
||||||
|
class UserRemoveContent(TypedDict):
|
||||||
|
id: str
|
||||||
|
by: str
|
||||||
|
|
||||||
|
class UserJoinedContent(TypedDict):
|
||||||
|
id: str
|
||||||
|
by: str
|
||||||
|
|
||||||
|
class UserLeftContent(TypedDict):
|
||||||
|
id: str
|
||||||
|
|
||||||
|
class UserKickedContent(TypedDict):
|
||||||
|
id: str
|
||||||
|
|
||||||
|
class UserBannedContent(TypedDict):
|
||||||
|
id: str
|
||||||
|
|
||||||
|
class ChannelRenameContent(TypedDict):
|
||||||
|
name: str
|
||||||
|
by: str
|
||||||
|
|
||||||
|
class ChannelDescriptionChangeContent(TypedDict):
|
||||||
|
by: str
|
||||||
|
|
||||||
|
class ChannelIconChangeContent(TypedDict):
|
||||||
|
by: str
|
||||||
|
|
||||||
|
class Masquerade(TypedDict, total=False):
|
||||||
|
name: str
|
||||||
|
avatar: str
|
||||||
|
colour: str
|
||||||
|
|
||||||
|
class Interactions(TypedDict):
|
||||||
|
reactions: NotRequired[list[str]]
|
||||||
|
restrict_reactions: NotRequired[bool]
|
||||||
|
|
||||||
|
SystemMessageContent = Union[UserAddContent, UserRemoveContent, UserJoinedContent, UserLeftContent, UserKickedContent, UserBannedContent, ChannelRenameContent, ChannelDescriptionChangeContent, ChannelIconChangeContent]
|
||||||
|
|
||||||
|
class Message(TypedDict):
|
||||||
|
_id: str
|
||||||
|
channel: str
|
||||||
|
author: str
|
||||||
|
content: str
|
||||||
|
system: NotRequired[SystemMessageContent]
|
||||||
|
attachments: NotRequired[list[File]]
|
||||||
|
embeds: NotRequired[list[Embed]]
|
||||||
|
mentions: NotRequired[list[str]]
|
||||||
|
replies: NotRequired[list[str]]
|
||||||
|
edited: NotRequired[str | int]
|
||||||
|
masquerade: NotRequired[Masquerade]
|
||||||
|
interactions: NotRequired[Interactions]
|
||||||
|
reactions: dict[str, list[str]]
|
||||||
|
|
||||||
|
class MessageReplyPayload(TypedDict):
|
||||||
|
id: str
|
||||||
|
mention: bool
|
8
next/types/permissions.py
Normal file
8
next/types/permissions.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
|
||||||
|
class Overwrite(TypedDict):
|
||||||
|
a: int
|
||||||
|
d: int
|
19
next/types/role.py
Normal file
19
next/types/role.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, TypedDict
|
||||||
|
|
||||||
|
from typing_extensions import NotRequired
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .permissions import Overwrite
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"Role",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Role(TypedDict):
|
||||||
|
name: str
|
||||||
|
permissions: Overwrite
|
||||||
|
colour: NotRequired[str]
|
||||||
|
hoist: NotRequired[bool]
|
||||||
|
rank: int
|
57
next/types/server.py
Normal file
57
next/types/server.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, TypedDict
|
||||||
|
|
||||||
|
from typing_extensions import NotRequired
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .category import Category
|
||||||
|
from .file import File
|
||||||
|
from .role import Role
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"Server",
|
||||||
|
"BannedUser",
|
||||||
|
"Ban",
|
||||||
|
"ServerBans",
|
||||||
|
"SystemMessagesConfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
class SystemMessagesConfig(TypedDict, total=False):
|
||||||
|
user_joined: str
|
||||||
|
user_left: str
|
||||||
|
user_kicked: str
|
||||||
|
user_banned: str
|
||||||
|
|
||||||
|
|
||||||
|
class Server(TypedDict):
|
||||||
|
_id: str
|
||||||
|
owner: str
|
||||||
|
name: str
|
||||||
|
channels: list[str]
|
||||||
|
default_permissions: int
|
||||||
|
nonce: NotRequired[str]
|
||||||
|
description: NotRequired[str]
|
||||||
|
categories: NotRequired[list[Category]]
|
||||||
|
system_messages: NotRequired[SystemMessagesConfig]
|
||||||
|
roles: NotRequired[dict[str, Role]]
|
||||||
|
icon: NotRequired[File]
|
||||||
|
banner: NotRequired[File]
|
||||||
|
nsfw: NotRequired[bool]
|
||||||
|
|
||||||
|
class BannedUser(TypedDict):
|
||||||
|
_id: str
|
||||||
|
username: str
|
||||||
|
avatar: NotRequired[File]
|
||||||
|
|
||||||
|
class BanId(TypedDict):
|
||||||
|
server: str
|
||||||
|
user: str
|
||||||
|
|
||||||
|
class Ban(TypedDict):
|
||||||
|
_id: BanId
|
||||||
|
reason: NotRequired[str]
|
||||||
|
|
||||||
|
class ServerBans(TypedDict):
|
||||||
|
users: list[BannedUser]
|
||||||
|
bans: list[Ban]
|
49
next/types/user.py
Normal file
49
next/types/user.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Literal, TypedDict
|
||||||
|
|
||||||
|
from typing_extensions import NotRequired
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .file import File
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"UserRelation",
|
||||||
|
"Relation",
|
||||||
|
"UserBot",
|
||||||
|
"Status",
|
||||||
|
"User",
|
||||||
|
"UserProfile",
|
||||||
|
)
|
||||||
|
|
||||||
|
Relation = Literal["Blocked", "BlockedOther", "Friend", "Incoming", "None", "Outgoing", "User"]
|
||||||
|
|
||||||
|
class UserBot(TypedDict):
|
||||||
|
owner: str
|
||||||
|
|
||||||
|
class Status(TypedDict, total=False):
|
||||||
|
text: str
|
||||||
|
presence: Literal["Busy", "Idle", "Invisible", "Online"]
|
||||||
|
|
||||||
|
class UserRelation(TypedDict):
|
||||||
|
status: Relation
|
||||||
|
_id: str
|
||||||
|
|
||||||
|
class User(TypedDict):
|
||||||
|
_id: str
|
||||||
|
username: str
|
||||||
|
discriminator: str
|
||||||
|
display_name: NotRequired[str]
|
||||||
|
avatar: NotRequired[File]
|
||||||
|
relations: NotRequired[list[UserRelation]]
|
||||||
|
badges: NotRequired[int]
|
||||||
|
status: NotRequired[Status]
|
||||||
|
relationship: NotRequired[Relation]
|
||||||
|
online: NotRequired[bool]
|
||||||
|
flags: NotRequired[int]
|
||||||
|
bot: NotRequired[UserBot]
|
||||||
|
privileged: NotRequired[bool]
|
||||||
|
|
||||||
|
class UserProfile(TypedDict, total=False):
|
||||||
|
content: str
|
||||||
|
background: File
|
367
next/user.py
Normal file
367
next/user.py
Normal file
|
@ -0,0 +1,367 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, NamedTuple, Optional, Union
|
||||||
|
from weakref import WeakValueDictionary
|
||||||
|
|
||||||
|
from next.types.user import UserRelation
|
||||||
|
|
||||||
|
from .asset import Asset, PartialAsset
|
||||||
|
from .channel import DMChannel, GroupDMChannel, SavedMessageChannel
|
||||||
|
from .enums import PresenceType, RelationshipType
|
||||||
|
from .flags import UserBadges
|
||||||
|
from .messageable import Messageable
|
||||||
|
from .permissions import UserPermissions
|
||||||
|
from .utils import Ulid
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .member import Member
|
||||||
|
from .state import State
|
||||||
|
from .types import File
|
||||||
|
from .types import Status as StatusPayload
|
||||||
|
from .types import User as UserPayload
|
||||||
|
from .types import UserProfile as UserProfileData
|
||||||
|
from .server import Server
|
||||||
|
|
||||||
|
__all__ = ("User", "Status", "Relation", "UserProfile")
|
||||||
|
|
||||||
|
class Relation(NamedTuple):
|
||||||
|
"""A namedtuple representing a relation between the bot and a user"""
|
||||||
|
type: RelationshipType
|
||||||
|
user: User
|
||||||
|
|
||||||
|
class Status(NamedTuple):
|
||||||
|
"""A namedtuple representing a users status"""
|
||||||
|
text: Optional[str]
|
||||||
|
presence: Optional[PresenceType]
|
||||||
|
|
||||||
|
class UserProfile(NamedTuple):
|
||||||
|
"""A namedtuple representing a users profile"""
|
||||||
|
content: Optional[str]
|
||||||
|
background: Optional[Asset]
|
||||||
|
|
||||||
|
class User(Messageable, Ulid):
|
||||||
|
"""Represents a user
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
id: :class:`str`
|
||||||
|
The user's id
|
||||||
|
discriminator: :class:`str`
|
||||||
|
The user's discriminator
|
||||||
|
display_name: Optional[:class:`str`]
|
||||||
|
The user's display name if they have one
|
||||||
|
bot: :class:`bool`
|
||||||
|
Whether or not the user is a bot
|
||||||
|
owner_id: Optional[:class:`str`]
|
||||||
|
The bot's owner id if the user is a bot
|
||||||
|
badges: :class:`UserBadges`
|
||||||
|
The users badges
|
||||||
|
online: :class:`bool`
|
||||||
|
Whether or not the user is online
|
||||||
|
flags: :class:`int`
|
||||||
|
The user flags
|
||||||
|
relations: list[:class:`Relation`]
|
||||||
|
A list of the users relations
|
||||||
|
relationship: Optional[:class:`RelationshipType`]
|
||||||
|
The relationship between the user and the bot
|
||||||
|
status: Optional[:class:`Status`]
|
||||||
|
The users status
|
||||||
|
dm_channel: Optional[:class:`DMChannel`]
|
||||||
|
The dm channel between the client and the user, this will only be set if the client has dm'ed the user or :meth:`User.open_dm` was run
|
||||||
|
privileged: :class:`bool`
|
||||||
|
Whether the user is privileged
|
||||||
|
"""
|
||||||
|
__flattern_attributes__: tuple[str, ...] = ("id", "discriminator", "display_name", "bot", "owner_id", "badges", "online", "flags", "relations", "relationship", "status", "masquerade_avatar", "masquerade_name", "original_name", "original_avatar", "profile", "dm_channel", "privileged")
|
||||||
|
__slots__: tuple[str, ...] = (*__flattern_attributes__, "state", "_members")
|
||||||
|
|
||||||
|
def __init__(self, data: UserPayload, state: State):
|
||||||
|
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: str = data["discriminator"]
|
||||||
|
self.display_name: str | None = data.get("display_name")
|
||||||
|
self.original_name: str = data["username"]
|
||||||
|
self.dm_channel: DMChannel | SavedMessageChannel | None = None
|
||||||
|
|
||||||
|
bot = data.get("bot")
|
||||||
|
|
||||||
|
self.bot: bool
|
||||||
|
self.owner_id: str | None
|
||||||
|
|
||||||
|
if bot:
|
||||||
|
self.bot = True
|
||||||
|
self.owner_id = bot["owner"]
|
||||||
|
else:
|
||||||
|
self.bot = False
|
||||||
|
self.owner_id = None
|
||||||
|
|
||||||
|
self.badges: UserBadges = UserBadges._from_value(data.get("badges", 0))
|
||||||
|
self.online: bool = data.get("online", False)
|
||||||
|
self.flags: int = data.get("flags", 0)
|
||||||
|
self.privileged: bool = data.get("privileged", False)
|
||||||
|
|
||||||
|
avatar = data.get("avatar")
|
||||||
|
self.original_avatar: Asset | None = Asset(avatar, state) if avatar else None
|
||||||
|
|
||||||
|
relations: list[Relation] = []
|
||||||
|
|
||||||
|
for relation in data.get("relations", []):
|
||||||
|
user = state.get_user(relation["_id"])
|
||||||
|
if user:
|
||||||
|
relations.append(Relation(RelationshipType(relation["status"]), user))
|
||||||
|
|
||||||
|
self.relations: list[Relation] = relations
|
||||||
|
|
||||||
|
relationship = data.get("relationship")
|
||||||
|
self.relationship: RelationshipType | None = RelationshipType(relationship) if relationship else None
|
||||||
|
|
||||||
|
status = data.get("status")
|
||||||
|
self.status: Status | None
|
||||||
|
|
||||||
|
if status:
|
||||||
|
presence = status.get("presence")
|
||||||
|
self.status = Status(status.get("text"), PresenceType(presence) if presence else None) if status else None
|
||||||
|
else:
|
||||||
|
self.status = None
|
||||||
|
|
||||||
|
self.profile: Optional[UserProfile] = None
|
||||||
|
|
||||||
|
self.masquerade_avatar: Optional[PartialAsset] = None
|
||||||
|
self.masquerade_name: Optional[str] = None
|
||||||
|
|
||||||
|
def get_permissions(self) -> UserPermissions:
|
||||||
|
"""Gets the permissions for the user
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`UserPermissions`
|
||||||
|
The users permissions
|
||||||
|
"""
|
||||||
|
permissions = UserPermissions()
|
||||||
|
|
||||||
|
if self.relationship in [RelationshipType.friend, RelationshipType.user]:
|
||||||
|
return UserPermissions.all()
|
||||||
|
|
||||||
|
elif self.relationship in [RelationshipType.blocked, RelationshipType.blocked_other]:
|
||||||
|
return UserPermissions(access=True)
|
||||||
|
|
||||||
|
elif self.relationship in [RelationshipType.incoming_friend_request, RelationshipType.outgoing_friend_request]:
|
||||||
|
permissions.access = True
|
||||||
|
|
||||||
|
for channel in self.state.channels.values():
|
||||||
|
if (isinstance(channel, (GroupDMChannel, DMChannel)) and self.id in channel.recipient_ids) or any(self.id in (m.id for m in server.members) for server in self.state.servers.values()):
|
||||||
|
if self.state.me.bot or self.bot:
|
||||||
|
permissions.send_message = True
|
||||||
|
|
||||||
|
permissions.access = True
|
||||||
|
permissions.view_profile = True
|
||||||
|
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
def has_permissions(self, **permissions: bool) -> bool:
|
||||||
|
"""Computes if the user has the specified permissions
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
permissions: :class:`bool`
|
||||||
|
The permissions to check, this also accepted `False` if you need to check if the user does not have the permission
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`bool`
|
||||||
|
Whether or not they have the permissions
|
||||||
|
"""
|
||||||
|
perms = self.get_permissions()
|
||||||
|
|
||||||
|
return all([getattr(perms, key, False) == value for key, value in permissions.items()])
|
||||||
|
|
||||||
|
async def _get_channel_id(self):
|
||||||
|
if not self.dm_channel:
|
||||||
|
payload = await self.state.http.open_dm(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:
|
||||||
|
""":class:`User` the owner of the bot account"""
|
||||||
|
|
||||||
|
if not self.owner_id:
|
||||||
|
raise LookupError
|
||||||
|
|
||||||
|
return self.state.get_user(self.owner_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
""":class:`str` The name the user is displaying, this includes (in order) their masqueraded name, display name and orginal name"""
|
||||||
|
return self.display_name or self.masquerade_name or self.original_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def avatar(self) -> Union[Asset, PartialAsset, None]:
|
||||||
|
"""Optional[:class:`Asset`] The avatar the member is displaying, this includes there orginal avatar and masqueraded avatar"""
|
||||||
|
return self.masquerade_avatar or self.original_avatar
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mention(self) -> str:
|
||||||
|
""":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,
|
||||||
|
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)
|
||||||
|
|
||||||
|
if profile is not None:
|
||||||
|
if background_file := profile.get("background"):
|
||||||
|
background = Asset(background_file, self.state)
|
||||||
|
else:
|
||||||
|
background = None
|
||||||
|
|
||||||
|
self.profile = UserProfile(profile.get("content"), background)
|
||||||
|
|
||||||
|
if avatar is not None:
|
||||||
|
self.original_avatar = Asset(avatar, self.state)
|
||||||
|
|
||||||
|
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,
|
||||||
|
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
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`bytes`
|
||||||
|
The bytes of the image
|
||||||
|
"""
|
||||||
|
return await self.state.http.fetch_default_avatar(self.id)
|
||||||
|
|
||||||
|
async def fetch_profile(self) -> UserProfile:
|
||||||
|
"""Fetches the user's profile
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`UserProfile`
|
||||||
|
The user's profile
|
||||||
|
"""
|
||||||
|
if profile := self.profile:
|
||||||
|
return profile
|
||||||
|
|
||||||
|
payload = await self.state.http.fetch_profile(self.id)
|
||||||
|
|
||||||
|
if file := payload.get("background"):
|
||||||
|
background = Asset(file, self.state)
|
||||||
|
else:
|
||||||
|
background = None
|
||||||
|
|
||||||
|
self.profile = UserProfile(payload.get("content"), background)
|
||||||
|
return self.profile
|
||||||
|
|
||||||
|
def to_member(self, server: Server) -> Member:
|
||||||
|
"""Gets the member instance for this user for a specific server.
|
||||||
|
|
||||||
|
Roughly equivelent to:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
member = server.get_member(user.id)
|
||||||
|
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
server: :class:`Server`
|
||||||
|
The server to get the member for
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Member`
|
||||||
|
The member
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
:class:`LookupError`
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
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
|
130
next/utils.py
Normal file
130
next/utils.py
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import inspect
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from operator import attrgetter
|
||||||
|
from typing import Any, Callable, Coroutine, Iterable, Literal, TypeVar, Union
|
||||||
|
|
||||||
|
import ulid
|
||||||
|
from aiohttp import ClientSession
|
||||||
|
from typing_extensions import ParamSpec
|
||||||
|
|
||||||
|
__all__ = ("_Missing", "Missing", "copy_doc", "maybe_coroutine", "get", "client_session", "parse_timestamp")
|
||||||
|
|
||||||
|
class _Missing:
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return "<Missing>"
|
||||||
|
|
||||||
|
def __bool__(self) -> Literal[False]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
Missing: _Missing = _Missing()
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
def copy_doc(from_t: T) -> Callable[[T], T]:
|
||||||
|
def inner(to_t: T) -> T:
|
||||||
|
to_t.__doc__ = from_t.__doc__
|
||||||
|
return to_t
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
R_T = TypeVar("R_T")
|
||||||
|
P = ParamSpec("P")
|
||||||
|
|
||||||
|
# it is impossible to type this function correctly as typeguard does not narrow for the negative case,
|
||||||
|
# so `value` would stay being a union even after the if statement (PEP 647 - "The type is not narrowed in the negative case")
|
||||||
|
# see typing#926, typing#930, typing#996
|
||||||
|
|
||||||
|
async def maybe_coroutine(func: Callable[P, Union[R_T, Coroutine[Any, Any, R_T]]], *args: P.args, **kwargs: P.kwargs) -> R_T:
|
||||||
|
value = func(*args, **kwargs)
|
||||||
|
|
||||||
|
if inspect.isawaitable(value):
|
||||||
|
value = await value
|
||||||
|
|
||||||
|
return value # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class Ulid:
|
||||||
|
id: str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def created_at(self) -> datetime.datetime:
|
||||||
|
return ulid.from_str(self.id).timestamp().datetime
|
||||||
|
|
||||||
|
class Object(Ulid):
|
||||||
|
"""Class to mock objects with an id"""
|
||||||
|
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
|
||||||
|
|
||||||
|
Examples
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:emphasize-lines: 3
|
||||||
|
|
||||||
|
from next import utils
|
||||||
|
|
||||||
|
channel = utils.get(server.channels, name="General")
|
||||||
|
await channel.send("Hello general chat.")
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
iterable: Iterable
|
||||||
|
The values to search though
|
||||||
|
**attrs: Any
|
||||||
|
The attributes to check
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
Any
|
||||||
|
The value from the iterable with the met attributes
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
LookupError
|
||||||
|
Raises when none of the values in the iterable matches the attributes
|
||||||
|
|
||||||
|
"""
|
||||||
|
converted = [(attrgetter(attr.replace('__', '.')), value) for attr, value in attrs.items()]
|
||||||
|
|
||||||
|
for elem in iterable:
|
||||||
|
if all(pred(elem) == value for pred, value in converted):
|
||||||
|
return elem
|
||||||
|
|
||||||
|
raise LookupError
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def client_session():
|
||||||
|
"""A context manager that creates a new aiohttp.ClientSession() and closes it when exiting the context.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:emphasize-lines: 3
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with client_session() as session:
|
||||||
|
client = next.Client(session, "TOKEN")
|
||||||
|
await client.start()
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
"""
|
||||||
|
session = ClientSession()
|
||||||
|
|
||||||
|
try:
|
||||||
|
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")
|
496
next/websocket.py
Normal file
496
next/websocket.py
Normal file
|
@ -0,0 +1,496 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from copy import copy
|
||||||
|
from typing import TYPE_CHECKING, Callable, NamedTuple, cast
|
||||||
|
|
||||||
|
from .errors import NextError
|
||||||
|
from . import utils
|
||||||
|
from .channel import GroupDMChannel, TextChannel, VoiceChannel
|
||||||
|
from .enums import RelationshipType
|
||||||
|
from .role import Role
|
||||||
|
from .types import (BulkMessageDeleteEventPayload, ChannelCreateEventPayload,
|
||||||
|
ChannelDeleteEventPayload, ChannelDeleteTypingEventPayload,
|
||||||
|
ChannelStartTypingEventPayload, ChannelUpdateEventPayload)
|
||||||
|
from .types import Member as MemberPayload
|
||||||
|
from .types import MemberID as MemberIDPayload
|
||||||
|
from .types import Message as MessagePayload
|
||||||
|
from .types import (MessageDeleteEventPayload, MessageReactEventPayload,
|
||||||
|
MessageRemoveReactionEventPayload,
|
||||||
|
MessageUnreactEventPayload, MessageUpdateEventPayload)
|
||||||
|
from .types import Role as RolePayload
|
||||||
|
from .types import (ServerCreateEventPayload, ServerDeleteEventPayload,
|
||||||
|
ServerMemberJoinEventPayload,
|
||||||
|
ServerMemberLeaveEventPayload,
|
||||||
|
ServerMemberUpdateEventPayload,
|
||||||
|
ServerRoleDeleteEventPayload, ServerRoleUpdateEventPayload,
|
||||||
|
ServerUpdateEventPayload, UserRelationshipEventPayload,
|
||||||
|
UserUpdateEventPayload)
|
||||||
|
from .user import Status, User, UserProfile
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ujson as json
|
||||||
|
except ImportError:
|
||||||
|
import json
|
||||||
|
|
||||||
|
use_msgpack: bool
|
||||||
|
|
||||||
|
try:
|
||||||
|
import msgpack
|
||||||
|
use_msgpack = True
|
||||||
|
except ImportError:
|
||||||
|
use_msgpack = False
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from .state import State
|
||||||
|
from .types import (AuthenticatePayload, BasePayload, MessageEventPayload,
|
||||||
|
ReadyEventPayload)
|
||||||
|
from .message import Message
|
||||||
|
|
||||||
|
class WSMessage(NamedTuple):
|
||||||
|
type: aiohttp.WSMsgType
|
||||||
|
data: str | bytes | aiohttp.WSCloseCode
|
||||||
|
|
||||||
|
__all__: tuple[str, ...] = ("WebsocketHandler",)
|
||||||
|
|
||||||
|
logger: logging.Logger = logging.getLogger("next")
|
||||||
|
|
||||||
|
class WebsocketHandler:
|
||||||
|
__slots__ = ("session", "token", "ws_url", "dispatch", "state", "websocket", "loop", "user", "ready", "server_events")
|
||||||
|
|
||||||
|
def __init__(self, session: aiohttp.ClientSession, token: str, ws_url: str, dispatch: Callable[..., None], state: State):
|
||||||
|
self.session: aiohttp.ClientSession = session
|
||||||
|
self.token: str = token
|
||||||
|
self.ws_url: str = ws_url
|
||||||
|
self.dispatch: Callable[..., None] = dispatch
|
||||||
|
self.state: State = state
|
||||||
|
self.websocket: aiohttp.ClientWebSocketResponse
|
||||||
|
self.loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
|
||||||
|
self.user: User | None = None
|
||||||
|
self.ready: asyncio.Event = asyncio.Event()
|
||||||
|
self.server_events: dict[str, asyncio.Event] = {}
|
||||||
|
|
||||||
|
async def _wait_for_server_ready(self, server_id: str) -> None:
|
||||||
|
if event := self.server_events.get(server_id):
|
||||||
|
await event.wait()
|
||||||
|
|
||||||
|
async def send_payload(self, payload: BasePayload) -> None:
|
||||||
|
if use_msgpack:
|
||||||
|
await self.websocket.send_bytes(msgpack.packb(payload)) # type: ignore
|
||||||
|
else:
|
||||||
|
await self.websocket.send_str(json.dumps(payload))
|
||||||
|
|
||||||
|
async def heartbeat(self) -> None:
|
||||||
|
while not self.websocket.closed:
|
||||||
|
logger.info("Sending hearbeat")
|
||||||
|
await self.websocket.ping()
|
||||||
|
await asyncio.sleep(15)
|
||||||
|
|
||||||
|
async def send_authenticate(self) -> None:
|
||||||
|
payload: AuthenticatePayload = {
|
||||||
|
"type": "Authenticate",
|
||||||
|
"token": self.token
|
||||||
|
}
|
||||||
|
|
||||||
|
await self.send_payload(payload)
|
||||||
|
|
||||||
|
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 not in ["ready", "notfound"]:
|
||||||
|
await self.ready.wait()
|
||||||
|
|
||||||
|
func = getattr(self, f"handle_{event_type}")
|
||||||
|
except AttributeError:
|
||||||
|
return logger.debug("Unknown event '%s'", event_type)
|
||||||
|
|
||||||
|
await func(payload)
|
||||||
|
|
||||||
|
async def handle_authenticated(self, _: BasePayload) -> None:
|
||||||
|
logger.info("Successfully authenticated")
|
||||||
|
|
||||||
|
async def handle_notfound(self, _: BasePayload) -> None:
|
||||||
|
raise NextError("Invalid token")
|
||||||
|
|
||||||
|
async def handle_ready(self, payload: ReadyEventPayload) -> None:
|
||||||
|
# Сначала добавляем пользователей
|
||||||
|
for user_payload in payload["users"]:
|
||||||
|
user = self.state.add_user(user_payload)
|
||||||
|
|
||||||
|
if user.relationship == RelationshipType.user:
|
||||||
|
self.user = user
|
||||||
|
|
||||||
|
for server in payload["servers"]:
|
||||||
|
self.state.add_server(server)
|
||||||
|
|
||||||
|
for channel in payload["channels"]:
|
||||||
|
self.state.add_channel(channel)
|
||||||
|
|
||||||
|
for member in payload["members"]:
|
||||||
|
self.state.add_member(member["_id"]["server"], member)
|
||||||
|
|
||||||
|
for emoji in payload["emojis"]:
|
||||||
|
emoji = self.state.add_emoji(emoji)
|
||||||
|
|
||||||
|
await self.state.fetch_all_server_members()
|
||||||
|
|
||||||
|
self.ready.set()
|
||||||
|
self.dispatch("ready")
|
||||||
|
|
||||||
|
async def handle_message(self, payload: MessageEventPayload) -> None:
|
||||||
|
if server := self.state.get_channel(payload["channel"]).server_id:
|
||||||
|
await self._wait_for_server_ready(server)
|
||||||
|
|
||||||
|
message = self.state.add_message(cast(MessagePayload, payload))
|
||||||
|
|
||||||
|
|
||||||
|
self.dispatch("message", message)
|
||||||
|
|
||||||
|
async def handle_messageupdate(self, payload: MessageUpdateEventPayload) -> None:
|
||||||
|
self.dispatch("raw_message_update", payload)
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = self.state.get_message(payload["id"])
|
||||||
|
except LookupError:
|
||||||
|
return
|
||||||
|
|
||||||
|
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", before, message)
|
||||||
|
|
||||||
|
async def handle_messagedelete(self, payload: MessageDeleteEventPayload) -> None:
|
||||||
|
self.dispatch("raw_message_delete", payload)
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = self.state.get_message(payload["id"])
|
||||||
|
except LookupError:
|
||||||
|
return
|
||||||
|
|
||||||
|
if server_id := message.channel.server_id:
|
||||||
|
await self._wait_for_server_ready(server_id)
|
||||||
|
|
||||||
|
self.state.messages.remove(message)
|
||||||
|
|
||||||
|
|
||||||
|
self.dispatch("message_delete", message)
|
||||||
|
|
||||||
|
async def handle_channelcreate(self, payload: ChannelCreateEventPayload) -> None:
|
||||||
|
channel = self.state.add_channel(payload)
|
||||||
|
|
||||||
|
if server_id := channel.server_id:
|
||||||
|
await self._wait_for_server_ready(server_id)
|
||||||
|
|
||||||
|
self.dispatch("channel_create", channel)
|
||||||
|
|
||||||
|
async def handle_channelupdate(self, payload: ChannelUpdateEventPayload) -> None:
|
||||||
|
# Next sends channel updates for channels we dont have permissions to see, a bug, but still can cause issues as its not in the cache
|
||||||
|
|
||||||
|
if not (channel := self.state.channels.get(payload["id"], None)):
|
||||||
|
return
|
||||||
|
|
||||||
|
if server_id := channel.server_id:
|
||||||
|
await self._wait_for_server_ready(server_id)
|
||||||
|
|
||||||
|
old_channel = copy(channel)
|
||||||
|
|
||||||
|
channel._update(**payload["data"])
|
||||||
|
|
||||||
|
if clear := payload.get("clear"):
|
||||||
|
if clear == "Icon":
|
||||||
|
if isinstance(channel, (TextChannel, VoiceChannel, GroupDMChannel)):
|
||||||
|
channel.icon = None
|
||||||
|
|
||||||
|
elif clear == "Description":
|
||||||
|
if isinstance(channel, (TextChannel, VoiceChannel, GroupDMChannel)):
|
||||||
|
channel.description = None
|
||||||
|
|
||||||
|
|
||||||
|
self.dispatch("channel_update", old_channel, channel)
|
||||||
|
|
||||||
|
async def handle_channeldelete(self, payload: ChannelDeleteEventPayload) -> None:
|
||||||
|
channel = self.state.channels.pop(payload["id"])
|
||||||
|
|
||||||
|
if server_id := channel.server_id:
|
||||||
|
await self._wait_for_server_ready(server_id)
|
||||||
|
|
||||||
|
self.dispatch("channel_delete", channel)
|
||||||
|
|
||||||
|
async def handle_channelstarttyping(self, payload: ChannelStartTypingEventPayload) -> None:
|
||||||
|
channel = self.state.get_channel(payload["id"])
|
||||||
|
|
||||||
|
if server_id := channel.server_id:
|
||||||
|
await self._wait_for_server_ready(server_id)
|
||||||
|
|
||||||
|
user = self.state.get_user(payload["user"])
|
||||||
|
|
||||||
|
self.dispatch("typing_start", channel, user)
|
||||||
|
|
||||||
|
async def handle_channelstoptyping(self, payload: ChannelDeleteTypingEventPayload) -> None:
|
||||||
|
channel = self.state.get_channel(payload["id"])
|
||||||
|
|
||||||
|
if server_id := channel.server_id:
|
||||||
|
await self._wait_for_server_ready(server_id)
|
||||||
|
|
||||||
|
user = self.state.get_user(payload["user"])
|
||||||
|
|
||||||
|
self.dispatch("typing_stop", channel, user)
|
||||||
|
|
||||||
|
async def handle_serverupdate(self, payload: ServerUpdateEventPayload) -> None:
|
||||||
|
await self._wait_for_server_ready(payload["id"])
|
||||||
|
|
||||||
|
server = self.state.get_server(payload["id"])
|
||||||
|
|
||||||
|
old_server = copy(server)
|
||||||
|
|
||||||
|
server._update(**payload["data"])
|
||||||
|
|
||||||
|
if clear := payload.get("clear"):
|
||||||
|
if clear == "Icon":
|
||||||
|
server.icon = None
|
||||||
|
|
||||||
|
elif clear == "Banner":
|
||||||
|
server.banner = None
|
||||||
|
|
||||||
|
elif clear == "Description":
|
||||||
|
server.description = None
|
||||||
|
|
||||||
|
|
||||||
|
self.dispatch("server_update", old_server, server)
|
||||||
|
|
||||||
|
async def handle_serverdelete(self, payload: ServerDeleteEventPayload) -> None:
|
||||||
|
server = self.state.servers.pop(payload["id"])
|
||||||
|
|
||||||
|
for channel in server.channels:
|
||||||
|
del self.state.channels[channel.id]
|
||||||
|
|
||||||
|
await self._wait_for_server_ready(server.id)
|
||||||
|
|
||||||
|
self.dispatch("server_delete", server)
|
||||||
|
|
||||||
|
async def handle_servercreate(self, payload: ServerCreateEventPayload) -> None:
|
||||||
|
for channel in payload["channels"]:
|
||||||
|
self.state.add_channel(channel)
|
||||||
|
|
||||||
|
server = self.state.add_server(payload["server"])
|
||||||
|
|
||||||
|
# lock all server events until we fetch all the members, otherwise the cache will be incomplete
|
||||||
|
self.server_events[server.id] = asyncio.Event()
|
||||||
|
await self.state.fetch_server_members(server.id)
|
||||||
|
self.server_events.pop(server.id).set()
|
||||||
|
|
||||||
|
self.dispatch("server_join", server)
|
||||||
|
|
||||||
|
async def handle_servermemberupdate(self, payload: ServerMemberUpdateEventPayload) -> None:
|
||||||
|
await self._wait_for_server_ready(payload["id"]["server"])
|
||||||
|
|
||||||
|
member = self.state.get_member(payload["id"]["server"], payload["id"]["user"])
|
||||||
|
old_member = copy(member)
|
||||||
|
|
||||||
|
if clear := payload.get("clear"):
|
||||||
|
if clear == "Nickname":
|
||||||
|
member.nickname = None
|
||||||
|
elif clear == "Avatar":
|
||||||
|
member.guild_avatar = None
|
||||||
|
|
||||||
|
member._update(**payload["data"])
|
||||||
|
|
||||||
|
self.dispatch("member_update", old_member, member)
|
||||||
|
|
||||||
|
async def handle_servermemberjoin(self, payload: ServerMemberJoinEventPayload) -> None:
|
||||||
|
# avoid an api request if possible
|
||||||
|
if payload["user"] not in self.state.users:
|
||||||
|
user = await self.state.http.fetch_user(payload["user"])
|
||||||
|
self.state.add_user(user)
|
||||||
|
|
||||||
|
member = self.state.add_member(payload["id"], MemberPayload(_id=MemberIDPayload(server=payload["id"], user=payload["user"]), joined_at=int(time.time()))) # next doesnt give us the joined at time
|
||||||
|
|
||||||
|
self.dispatch("member_join", member)
|
||||||
|
|
||||||
|
async def handle_memberleave(self, payload: ServerMemberLeaveEventPayload) -> None:
|
||||||
|
await self._wait_for_server_ready(payload["id"])
|
||||||
|
|
||||||
|
server = self.state.get_server(payload["id"])
|
||||||
|
member = server._members.pop(payload["user"])
|
||||||
|
|
||||||
|
# remove the member from the user
|
||||||
|
|
||||||
|
user = self.state.get_user(payload["user"])
|
||||||
|
user._members.pop(server.id)
|
||||||
|
|
||||||
|
self.dispatch("member_leave", member)
|
||||||
|
|
||||||
|
async def handle_serverroleupdate(self, payload: ServerRoleUpdateEventPayload) -> None:
|
||||||
|
server = self.state.get_server(payload["id"])
|
||||||
|
await self._wait_for_server_ready(server.id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
role = server.get_role(payload["role_id"])
|
||||||
|
except LookupError:
|
||||||
|
# the role wasnt found meaning it was just created
|
||||||
|
|
||||||
|
role = Role(cast(RolePayload, payload["data"]), payload["role_id"], server, self.state)
|
||||||
|
server._roles[role.id] = role
|
||||||
|
self.dispatch("role_create", role)
|
||||||
|
else:
|
||||||
|
old_role = copy(role)
|
||||||
|
|
||||||
|
if clear := payload.get("clear"):
|
||||||
|
if clear == "Colour":
|
||||||
|
role.colour = None
|
||||||
|
|
||||||
|
role._update(**payload["data"])
|
||||||
|
|
||||||
|
self.dispatch("role_update", old_role, role)
|
||||||
|
|
||||||
|
async def handle_serverroledelete(self, payload: ServerRoleDeleteEventPayload) -> None:
|
||||||
|
server = self.state.get_server(payload["id"])
|
||||||
|
role = server._roles.pop(payload["role_id"])
|
||||||
|
|
||||||
|
await self._wait_for_server_ready(server.id)
|
||||||
|
|
||||||
|
self.dispatch("role_delete", role)
|
||||||
|
|
||||||
|
async def handle_userupdate(self, payload: UserUpdateEventPayload) -> None:
|
||||||
|
user = self.state.get_user(payload["id"])
|
||||||
|
old_user = copy(user)
|
||||||
|
|
||||||
|
if clear := payload.get("clear"):
|
||||||
|
if clear == "ProfileContent":
|
||||||
|
if profile := user.profile:
|
||||||
|
user.profile = UserProfile(None, profile.background)
|
||||||
|
|
||||||
|
elif clear == "ProfileBackground":
|
||||||
|
if profile := user.profile:
|
||||||
|
user.profile = UserProfile(profile.content, None)
|
||||||
|
|
||||||
|
elif clear == "StatusText":
|
||||||
|
user.status = Status(None, user.status.presence if user.status else None)
|
||||||
|
|
||||||
|
elif clear == "Avatar":
|
||||||
|
user.original_avatar = None
|
||||||
|
|
||||||
|
user._update(**payload["data"])
|
||||||
|
|
||||||
|
self.dispatch("user_update", old_user, user)
|
||||||
|
|
||||||
|
async def handle_userrelationship(self, payload: UserRelationshipEventPayload) -> None:
|
||||||
|
user = self.state.get_user(payload["user"])
|
||||||
|
old_relationship = user.relationship
|
||||||
|
user.relationship = RelationshipType(payload["status"])
|
||||||
|
|
||||||
|
self.dispatch("user_relationship_update", user, old_relationship, user.relationship)
|
||||||
|
|
||||||
|
async def handle_messagereact(self, payload: MessageReactEventPayload) -> None:
|
||||||
|
if server := self.state.get_channel(payload["channel_id"]).server_id:
|
||||||
|
await self._wait_for_server_ready(server)
|
||||||
|
|
||||||
|
self.dispatch("raw_reaction_add", payload)
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = utils.get(self.state.messages, id=payload["id"])
|
||||||
|
except LookupError:
|
||||||
|
return
|
||||||
|
|
||||||
|
user = self.state.get_user(payload["user_id"])
|
||||||
|
message.reactions.setdefault(payload["emoji_id"], []).append(user)
|
||||||
|
emoji_id = payload["emoji_id"]
|
||||||
|
|
||||||
|
self.dispatch("reaction_add", message, user, emoji_id)
|
||||||
|
|
||||||
|
async def handle_messageunreact(self, payload: MessageUnreactEventPayload) -> None:
|
||||||
|
if server := self.state.get_channel(payload["channel_id"]).server_id:
|
||||||
|
await self._wait_for_server_ready(server)
|
||||||
|
|
||||||
|
self.dispatch("raw_reaction_remove", payload)
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = utils.get(self.state.messages, id=payload["id"])
|
||||||
|
except LookupError:
|
||||||
|
return
|
||||||
|
|
||||||
|
user = self.state.get_user(payload["user_id"])
|
||||||
|
message.reactions[payload["emoji_id"]].remove(user)
|
||||||
|
|
||||||
|
self.dispatch("reaction_remove", message, user, payload["emoji_id"])
|
||||||
|
|
||||||
|
async def handle_messageremovereaction(self, payload: MessageRemoveReactionEventPayload) -> None:
|
||||||
|
if server := self.state.get_channel(payload["channel_id"]).server_id:
|
||||||
|
await self._wait_for_server_ready(server)
|
||||||
|
|
||||||
|
self.dispatch("raw_reaction_clear", payload)
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = utils.get(self.state.messages, id=payload["id"])
|
||||||
|
except LookupError:
|
||||||
|
return
|
||||||
|
|
||||||
|
users = message.reactions.pop(payload["emoji_id"])
|
||||||
|
|
||||||
|
self.dispatch("reaction_clear", message, users, payload["emoji_id"])
|
||||||
|
|
||||||
|
async def handle_bulkmessagedelete(self, payload: BulkMessageDeleteEventPayload) -> None:
|
||||||
|
channel = self.state.get_channel(payload["channel"])
|
||||||
|
|
||||||
|
self.dispatch("raw_bulk_message_delete", payload)
|
||||||
|
|
||||||
|
messages: list[Message] = []
|
||||||
|
|
||||||
|
for message_id in payload["ids"]:
|
||||||
|
if server_id := channel.server_id:
|
||||||
|
await self._wait_for_server_ready(server_id)
|
||||||
|
|
||||||
|
self.dispatch("raw_message_delete", MessageDeleteEventPayload(type="messagedelete", channel=payload["channel"], id=message_id))
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = self.state.get_message(message_id)
|
||||||
|
except LookupError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.state.messages.remove(message)
|
||||||
|
self.dispatch("message_delete", message)
|
||||||
|
|
||||||
|
messages.append(message)
|
||||||
|
|
||||||
|
self.dispatch("bulk_message_delete", messages)
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if use_msgpack:
|
||||||
|
data = cast(bytes, msg.data)
|
||||||
|
|
||||||
|
payload = msgpack.unpackb(data) # type: ignore
|
||||||
|
else:
|
||||||
|
data = cast(str, msg.data)
|
||||||
|
|
||||||
|
payload = json.loads(data)
|
||||||
|
|
||||||
|
self.loop.create_task(self.handle_event(payload))
|
||||||
|
|
||||||
|
hb.cancel()
|
||||||
|
|
||||||
|
if not reconnect:
|
||||||
|
return
|
69
pyproject.toml
Normal file
69
pyproject.toml
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
[project]
|
||||||
|
name = "next-api-py"
|
||||||
|
dynamic = ["version"]
|
||||||
|
description = "Python wrapper for the next.avanpost20.ru API"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
license = "MIT"
|
||||||
|
readme = "README.md"
|
||||||
|
keywords = ["wrapper", "async", "api", "websockets", "http"]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"aiohttp==3.9.*",
|
||||||
|
"ulid-py==1.1.*",
|
||||||
|
"aenum==3.1.*",
|
||||||
|
"typing_extensions>=4.4.0"
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
speedups = [
|
||||||
|
"ujson==5.1.*",
|
||||||
|
"msgpack==1.0.*"
|
||||||
|
]
|
||||||
|
docs = [
|
||||||
|
"Sphinx==5.2.*",
|
||||||
|
"sphinx-nameko-theme==0.0.*",
|
||||||
|
"sphinx-toolbox==3.2.*",
|
||||||
|
"setuptools==65.4.*"
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://git.avanpost20.ru/next/next.py"
|
||||||
|
Documentation = "https://nextpy.avanpost20.ru/"
|
||||||
|
"Source Code" = "https://git.avanpost20.ru/next/next.py"
|
||||||
|
"Bug Tracker" = "https://git.avanpost20.ru/next/next.py/issues"
|
||||||
|
|
||||||
|
[[project.authors]]
|
||||||
|
name = "Avanpost"
|
||||||
|
email = "me@avanpost20.ru"
|
||||||
|
|
||||||
|
[tool.hatch.version]
|
||||||
|
path = "next/__init__.py"
|
||||||
|
|
||||||
|
[tool.hatch.build]
|
||||||
|
only-packages = true
|
||||||
|
include = ["next/**/*"]
|
||||||
|
|
||||||
|
|
||||||
|
[tool.pyright]
|
||||||
|
reportPrivateUsage = false
|
||||||
|
reportImportCycles = false
|
||||||
|
reportIncompatibleMethodOverride = false
|
||||||
|
typeCheckingMode = "strict"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.sdist]
|
||||||
|
strict-naming = false
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
strict-naming = false
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
40
typings/msgpack/__init__.pyi
Normal file
40
typings/msgpack/__init__.pyi
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
from typing_extensions import Protocol
|
||||||
|
|
||||||
|
class _FileLike(Protocol):
|
||||||
|
def read(self, n: int) -> bytes: ...
|
||||||
|
|
||||||
|
def unpackb(
|
||||||
|
packed: bytes,
|
||||||
|
file_like: Optional[_FileLike] = ...,
|
||||||
|
read_size: int = ...,
|
||||||
|
use_list: bool = ...,
|
||||||
|
raw: bool = ...,
|
||||||
|
timestamp: int = ...,
|
||||||
|
strict_map_key: bool = ...,
|
||||||
|
object_hook: Optional[Callable[[Dict[Any, Any]], Any]] = ...,
|
||||||
|
object_pairs_hook: Optional[Callable[[List[Tuple[Any, Any]]], Any]] = ...,
|
||||||
|
list_hook: Optional[Callable[[List[Any]], Any]] = ...,
|
||||||
|
unicode_errors: Optional[str] = ...,
|
||||||
|
max_buffer_size: int = ...,
|
||||||
|
ext_hook: Callable[[int, bytes], Any] = ...,
|
||||||
|
max_str_len: int = ...,
|
||||||
|
max_bin_len: int = ...,
|
||||||
|
max_array_len: int = ...,
|
||||||
|
max_map_len: int = ...,
|
||||||
|
max_ext_len: int = ...,
|
||||||
|
) -> Any: ...
|
||||||
|
|
||||||
|
def packb(
|
||||||
|
o: Any,
|
||||||
|
default: Optional[Callable[[Any], Any]] = ...,
|
||||||
|
use_single_float: bool = ...,
|
||||||
|
autoreset: bool = ...,
|
||||||
|
use_bin_type: bool = ...,
|
||||||
|
strict_types: bool = ...,
|
||||||
|
datetime: bool = ...,
|
||||||
|
unicode_errors: Optional[str] = ...,
|
||||||
|
) -> bytes: ...
|
1
typings/sphinx_nameko_theme/__init__.pyi
Normal file
1
typings/sphinx_nameko_theme/__init__.pyi
Normal file
|
@ -0,0 +1 @@
|
||||||
|
def get_html_theme_path() -> str: ...
|
Loading…
Reference in a new issue