├── wyvern ├── internals │ ├── __init__.py │ ├── enum_flags.py │ ├── gateway.py │ └── rest.py ├── builders │ ├── __init__.py │ ├── components.py │ └── embeds.py ├── models │ ├── __init__.py │ ├── abc.py │ ├── snowflake.py │ ├── messages.py │ ├── members.py │ ├── components.py │ └── users.py ├── api │ ├── __init__.py │ ├── gateway.py │ ├── event_decos.py │ ├── rest_client.py │ ├── event_handler.py │ ├── bot.py │ └── intents.py ├── utils │ ├── __init__.py │ ├── hooks.py │ └── consts.py ├── events │ ├── __init__.py │ ├── base.py │ └── lib_events.py ├── __init__.py ├── types.py ├── __main__.py ├── logger.py └── colors.py ├── docs ├── requirements.txt ├── api_reference │ ├── api │ │ ├── event_decos.rst │ │ ├── rest_client.rst │ │ ├── bot.rst │ │ ├── event_handler.rst │ │ └── intents.rst │ ├── colors.rst │ ├── utils.rst │ ├── api.rst │ ├── events │ │ └── lib_events.rst │ ├── models │ │ ├── users.rst │ │ ├── members.rst │ │ ├── messages.rst │ │ └── components.rst │ ├── events.rst │ └── models.rst ├── api_reference.rst ├── index.rst ├── Makefile ├── getting_started.md ├── make.bat ├── conf.py └── _static │ └── style.css ├── .github ├── dependabot.yml └── workflows │ └── format-and-lint.yml ├── .readthedocs.yaml ├── examples └── basic.py ├── LICENSE ├── .pre-commit-config.yaml ├── pyproject.toml ├── README.md └── .gitignore /wyvern/internals/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wyvern/builders/__init__.py: -------------------------------------------------------------------------------- 1 | from .components import * 2 | from .embeds import * 3 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | furo 3 | myst-parser 4 | aiohttp 5 | attrs 6 | colorama 7 | -------------------------------------------------------------------------------- /docs/api_reference/api/event_decos.rst: -------------------------------------------------------------------------------- 1 | event_decos 2 | =========== 3 | 4 | .. automodule:: wyvern.api.event_decos 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api_reference/colors.rst: -------------------------------------------------------------------------------- 1 | colors 2 | ====== 3 | 4 | .. currentmodule:: wyvern 5 | 6 | .. automodule:: wyvern.colors 7 | :members: -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /wyvern/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .abc import * 2 | from .components import * 3 | from .messages import * 4 | from .snowflake import * 5 | from .users import * 6 | -------------------------------------------------------------------------------- /docs/api_reference/utils.rst: -------------------------------------------------------------------------------- 1 | utils 2 | ===== 3 | 4 | .. currentmodule:: wyvern 5 | 6 | Constants 7 | --------- 8 | 9 | .. automodule:: wyvern.utils.consts 10 | :members: -------------------------------------------------------------------------------- /docs/api_reference/api.rst: -------------------------------------------------------------------------------- 1 | api 2 | === 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | api/bot 8 | api/event_decos 9 | api/event_handler 10 | api/intents 11 | api/rest_client -------------------------------------------------------------------------------- /docs/api_reference/events/lib_events.rst: -------------------------------------------------------------------------------- 1 | lib_events 2 | ========== 3 | 4 | .. currentmodule:: wyvern 5 | 6 | .. automodule:: wyvern.events.lib_events 7 | :members: 8 | :inherited-members: 9 | :show-inheritance: -------------------------------------------------------------------------------- /docs/api_reference/models/users.rst: -------------------------------------------------------------------------------- 1 | users 2 | ===== 3 | 4 | .. currentmodule:: wyvern 5 | 6 | 7 | .. automodule:: wyvern.models.users 8 | :members: 9 | :inherited-members: 10 | :show-inheritance: -------------------------------------------------------------------------------- /docs/api_reference/models/members.rst: -------------------------------------------------------------------------------- 1 | members 2 | ======= 3 | 4 | .. currentmodule:: wyvern 5 | 6 | 7 | .. automodule:: wyvern.models.members 8 | :members: 9 | :inherited-members: 10 | :show-inheritance: -------------------------------------------------------------------------------- /docs/api_reference/models/messages.rst: -------------------------------------------------------------------------------- 1 | messages 2 | ======== 3 | 4 | .. currentmodule:: wyvern 5 | 6 | 7 | .. automodule:: wyvern.models.messages 8 | :members: 9 | :inherited-members: 10 | :show-inheritance: -------------------------------------------------------------------------------- /docs/api_reference/api/rest_client.rst: -------------------------------------------------------------------------------- 1 | rest_client 2 | =========== 3 | 4 | .. currentmodule:: wyvern 5 | 6 | 7 | .. automodule:: wyvern.api.rest_client 8 | :members: 9 | :inherited-members: 10 | :show-inheritance: -------------------------------------------------------------------------------- /docs/api_reference/api/bot.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: wyvern 2 | 3 | bot 4 | === 5 | 6 | The class to connect your application with the Discord API. 7 | 8 | 9 | .. autoclass:: GatewayBot 10 | :members: 11 | :show-inheritance: -------------------------------------------------------------------------------- /docs/api_reference/models/components.rst: -------------------------------------------------------------------------------- 1 | components 2 | ========== 3 | 4 | .. currentmodule:: wyvern 5 | 6 | 7 | .. automodule:: wyvern.models.components 8 | :members: 9 | :inherited-members: 10 | :show-inheritance: -------------------------------------------------------------------------------- /docs/api_reference/api/event_handler.rst: -------------------------------------------------------------------------------- 1 | event_handler 2 | ============= 3 | 4 | .. currentmodule:: wyvern 5 | 6 | 7 | .. automodule:: wyvern.api.event_handler 8 | :members: 9 | :inherited-members: 10 | :show-inheritance: -------------------------------------------------------------------------------- /docs/api_reference.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | api_reference/api 8 | api_reference/events 9 | api_reference/models 10 | api_reference/colors 11 | api_reference/utils -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-20.04 5 | tools: 6 | python: "3.9" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | formats: 12 | - pdf 13 | 14 | 15 | python: 16 | install: 17 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /docs/api_reference/events.rst: -------------------------------------------------------------------------------- 1 | events 2 | ====== 3 | 4 | Events from discord are recieved in form of raw payloads and are converted into ``Event`` classes internally by the library. 5 | 6 | base 7 | ---- 8 | 9 | .. currentmodule:: wyvern 10 | 11 | .. automodule:: wyvern.events.base 12 | :members: 13 | 14 | .. toctree:: 15 | :maxdepth: 1 16 | 17 | events/lib_events.rst -------------------------------------------------------------------------------- /docs/api_reference/models.rst: -------------------------------------------------------------------------------- 1 | models 2 | ====== 3 | 4 | Base Classes 5 | ------------ 6 | 7 | .. currentmodule:: wyvern 8 | 9 | 10 | .. autoclass:: Snowflake 11 | :members: 12 | 13 | 14 | .. autoclass:: DiscordObject 15 | :members: 16 | 17 | .. toctree:: 18 | :maxdepth: 1 19 | 20 | models/components.rst 21 | models/members.rst 22 | models/messages.rst 23 | models/users -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. wyvern documentation master file, created by 2 | sphinx-quickstart on Thu Mar 9 13:34:02 2023. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to wyvern's documentation! 7 | ================================== 8 | 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | 13 | api_reference 14 | getting_started 15 | -------------------------------------------------------------------------------- /docs/api_reference/api/intents.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: wyvern 2 | 3 | intents 4 | ======= 5 | 6 | Intents are bitwise value that are passed to the Discord API while creating a websocket connection to 7 | let the API know what events the connection is requesting for. 8 | 9 | You can read more about intents in the `Official documentation `_. 10 | 11 | .. autoclass:: Intents 12 | :members: -------------------------------------------------------------------------------- /examples/basic.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | import wyvern 5 | 6 | bot = wyvern.GatewayBot(os.environ["TOKEN"], intents=wyvern.Intents.UNPRIVILEGED) 7 | 8 | 9 | @bot.listener(wyvern.StartingEvent) 10 | async def starting(event: wyvern.StartingEvent) -> None: 11 | bot.logger.info("Starting bot...") 12 | 13 | 14 | # or using the implemented decorators 15 | 16 | 17 | @bot.on_started() 18 | async def started(event: wyvern.StartedEvent) -> None: 19 | bot.logger.info(f"Logged in as {event.user.tag}") 20 | 21 | 22 | async def main(): 23 | async with bot: 24 | await bot.start() 25 | 26 | 27 | asyncio.run(main()) 28 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /.github/workflows/format-and-lint.yml: -------------------------------------------------------------------------------- 1 | name: Code formatter and linter 2 | 3 | on: 4 | push: 5 | branches: ["master", "main", "rewrite"] 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | 12 | build: 13 | 14 | runs-on: windows-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Install Python 19 | uses: actions/setup-python@v3 20 | with: 21 | python-version: '3.8' 22 | 23 | - name: Install Dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install poetry 27 | poetry install 28 | 29 | - name: Format with isort and black 30 | run: | 31 | poetry run isort . 32 | poetry run black . 33 | 34 | - name: Linting with ruff 35 | run: | 36 | poetry run ruff . 37 | -------------------------------------------------------------------------------- /docs/getting_started.md: -------------------------------------------------------------------------------- 1 | Getting started 2 | =============== 3 | 4 | ## First basic bot 5 | 6 | ### Creating a bot instance 7 | 8 | ```python 9 | import asyncio 10 | import os 11 | 12 | import wyvern 13 | 14 | bot = wyvern.GatewayBot(os.environ["TOKEN"], intents=wyvern.Intents.UNPRIVILEGED) 15 | ``` 16 | ### Listening to events 17 | ```python 18 | @bot.listener(wyvern.StartingEvent) 19 | async def starting(event: wyvern.StartingEvent) -> None: 20 | bot.logger.info("Starting bot...") 21 | 22 | 23 | # or using the implemented decorators 24 | 25 | 26 | @bot.on_started() 27 | async def started(event: wyvern.StartedEvent) -> None: 28 | bot.logger.info(f"Logged in as {event.user.tag}") 29 | ``` 30 | 31 | ### Running the bot 32 | ```python 33 | async def main(): 34 | async with bot: 35 | await bot.start() 36 | 37 | 38 | asyncio.run(main()) 39 | ``` -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 by the authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.3.0 5 | hooks: 6 | - id: check-yaml 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | args: 10 | - --markdown-linebreak-ext=md 11 | - id: check-toml 12 | 13 | - repo: https://github.com/pre-commit/pygrep-hooks 14 | rev: v1.9.0 15 | hooks: 16 | - id: python-use-type-annotations 17 | - id: python-check-blanket-noqa 18 | 19 | - repo: local 20 | hooks: 21 | - id: isort 22 | name: isort 23 | pass_filenames: false 24 | entry: poetry run isort . 25 | language: python 26 | stages: 27 | - commit 28 | 29 | - repo: local 30 | hooks: 31 | - id: black 32 | name: black 33 | pass_filenames: false 34 | entry: poetry run black . 35 | language: python 36 | stages: 37 | - commit 38 | 39 | - repo: local 40 | hooks: 41 | - id: mypy 42 | name: mypy 43 | pass_filenames: false 44 | entry: poetry run mypy . 45 | language: python 46 | stages: 47 | - commit 48 | -------------------------------------------------------------------------------- /wyvern/api/__init__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from .bot import * 24 | from .intents import * 25 | -------------------------------------------------------------------------------- /wyvern/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from .consts import * 24 | from .hooks import * 25 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, os.path.abspath(".")) 5 | sys.path.insert(0, os.path.abspath("..")) 6 | sys.path.insert(0, os.path.abspath("../")) 7 | 8 | project = "wyvern" 9 | copyright = "2023, sarthhh" 10 | author = "sarthhh" 11 | release = "0.2.0" 12 | 13 | # sphinx configs 14 | extensions = [ 15 | "sphinx.ext.autodoc", 16 | "sphinx.ext.intersphinx", 17 | "sphinx.ext.napoleon", 18 | "myst_parser", 19 | "sphinx.ext.viewcode", 20 | ] 21 | html_css_files = ["style.css"] 22 | html_static_path = ["_static"] 23 | html_theme = "furo" 24 | templates_path = ["_templates"] 25 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 26 | 27 | # intersphinx configs 28 | intersphinx_mapping = { 29 | "python": ("https://docs.python.org/3", None), 30 | } 31 | 32 | # autodoc configs 33 | autodoc_inherit_docstrings = True 34 | autodoc_default_options = { 35 | "members": True, 36 | } 37 | 38 | # rst vars 39 | rst_prolog = """ 40 | .. |coro| replace:: This function is a |coroutine_link|_. 41 | .. |maybecoro| replace:: This function *could be a* |coroutine_link|_. 42 | .. |coroutine_link| replace:: *coroutine* 43 | .. _coroutine_link: https://docs.python.org/3/library/asyncio-task.html#coroutine 44 | """ 45 | -------------------------------------------------------------------------------- /wyvern/events/__init__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from .base import * 24 | from .lib_events import * 25 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "wyvern" 3 | version = "0.2.0" 4 | description = "A flexible and easy to use Discord API wrapper for python 🚀." 5 | authors = [ "sarthhh", "FallenDeity"] 6 | license = "MIT License" 7 | readme = "README.md" 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.8" 11 | aiohttp = "^3.8.1" 12 | attrs = "^22.1.0" 13 | colorama = "^0.4.6" 14 | 15 | [tool.poetry.dev-dependencies] 16 | pyright = "^1.1.282" 17 | ruff = "^0.0.254" 18 | isort = "^5.10.1" 19 | black = "^23.1.0" 20 | pre-commit = "^2.20.0" 21 | sphinx = "^6.1.3" 22 | typing-extensions = "^4.4.0" 23 | 24 | [build-system] 25 | requires = ["poetry-core>=1.0.0"] 26 | build-backend = "poetry.core.masonry.api" 27 | 28 | [tool.ruff] 29 | line-length = 120 30 | 31 | [tool.pyright] 32 | include = ["wyvern"] 33 | pythonVersion = "3.8" 34 | typeCheckingMode = "strict" 35 | reportPrivateUsage = false 36 | reportImportCycles = false 37 | reportIncompatibleMethodOverride = false 38 | 39 | [tool.black] 40 | line-length = 120 41 | target-version = ['py38'] 42 | 43 | [tool.isort] 44 | line_length = 120 45 | multi_line_output = 3 46 | include_trailing_comma = true 47 | force_grid_wrap = 0 48 | use_parentheses = true 49 | ensure_newline_before_comments = true 50 | src_paths = ["wyvern"] 51 | 52 | -------------------------------------------------------------------------------- /wyvern/__init__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from . import models 24 | from .api import * 25 | from .builders import * 26 | from .events import * 27 | from .models import * 28 | from .utils import * 29 | 30 | __version__: str = "0.2.0" 31 | -------------------------------------------------------------------------------- /wyvern/api/gateway.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import annotations 24 | 25 | from wyvern.internals.gateway import Gateway 26 | 27 | __all__: tuple[str] = ("GatewayImpl",) 28 | 29 | 30 | class GatewayImpl(Gateway): 31 | ... 32 | -------------------------------------------------------------------------------- /wyvern/models/abc.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | from __future__ import annotations 23 | 24 | import abc as pyabc 25 | import typing 26 | 27 | if typing.TYPE_CHECKING: 28 | from wyvern.api.bot import GatewayBot 29 | 30 | from .snowflake import Snowflake 31 | 32 | 33 | class ImplementsMessage(pyabc.ABC): 34 | id: Snowflake 35 | bot: GatewayBot 36 | 37 | @pyabc.abstractmethod 38 | async def create_message(self, content: str) -> ...: 39 | ... 40 | -------------------------------------------------------------------------------- /wyvern/events/base.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import annotations 24 | 25 | import typing 26 | 27 | import attrs 28 | 29 | if typing.TYPE_CHECKING: 30 | from wyvern.api.bot import GatewayBot 31 | 32 | __all__: tuple[str] = ("Event",) 33 | 34 | 35 | @attrs.define(kw_only=True) 36 | class Event: 37 | """The base event class. 38 | All library ( or custom ) events are derived from this class. 39 | """ 40 | 41 | bot: GatewayBot 42 | """The bot bound to the event.""" 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wyvern 2 | 3 | 4 | 5 | A [WIP] flexible and easy to use Discord API wrapper for python 🚀. 6 | > Warning: This library is very unstable and things might not work as expected. Feel free to create an issue. 7 | 8 | ## Important Links 9 | 10 | Support server: https://discord.gg/FyEE54u9GF 11 | 12 | Documentation: https://wyvern.readthedocs.io 13 | 14 | PYPI: https://pypi.org/project/wyvern 15 | 16 | ## Installation 17 | ```sh 18 | $python -m pip install git+https://github.com/sarthhh/wyvern 19 | ``` 20 | ## Example 21 | ```python 22 | import asyncio 23 | import os 24 | 25 | import wyvern 26 | 27 | bot = wyvern.GatewayBot(os.environ["TOKEN"], intents=wyvern.Intents.UNPRIVILEGED) 28 | 29 | 30 | @bot.listener(wyvern.StartingEvent) 31 | async def starting(event: wyvern.StartingEvent) -> None: 32 | bot.logger.info("Starting bot...") 33 | 34 | 35 | # or using the implemented decorators 36 | 37 | 38 | @bot.on_started() 39 | async def started(event: wyvern.StartedEvent) -> None: 40 | bot.logger.info(f"Logged in as {event.user.tag}") 41 | 42 | 43 | async def main(): 44 | async with bot: 45 | await bot.start() 46 | 47 | 48 | asyncio.run(main()) 49 | ``` 50 | -------------------------------------------------------------------------------- /wyvern/types.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | from __future__ import annotations 23 | 24 | import typing 25 | 26 | if typing.TYPE_CHECKING: 27 | from typing_extensions import TypeAlias 28 | 29 | from wyvern import events, models, utils 30 | 31 | T = typing.TypeVar("T") 32 | 33 | EventTypes: TypeAlias = typing.Union[events.Event, events.StartedEvent, events.StartedEvent] 34 | 35 | NullOr = typing.Union[utils.Null, T] 36 | Snowflakish = typing.Union[T, models.Snowflake, int] 37 | AnyCallbackT: TypeAlias = typing.Callable[..., typing.Awaitable[typing.Any]] 38 | EventListenerCallbackT: TypeAlias = typing.Callable[[typing.Any], typing.Awaitable[typing.Any]] 39 | -------------------------------------------------------------------------------- /wyvern/api/event_decos.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | 5 | from wyvern import events, types 6 | from wyvern.api import event_handler as handler 7 | 8 | 9 | class ImplementsEventDecos: 10 | """Interface for event decorators in :class:`.GatewayBot`, 11 | this class is purely for the bot class to inherit from. 12 | 13 | Example 14 | ------- 15 | 16 | .. highlight:: python 17 | .. code-block:: python 18 | 19 | @bot.on_started() 20 | async def started(event: wyvern.StartedEvent) -> None: 21 | bot.logger.info(f"Logged in as {event.user.tag}") 22 | 23 | """ 24 | 25 | event_handler: handler.EventHandler 26 | 27 | def _make_deco( 28 | self, event: type[events.Event], **kwargs: typing.Any 29 | ) -> typing.Callable[[types.EventListenerCallbackT], handler.EventListener]: 30 | def decorator(callable: types.EventListenerCallbackT) -> handler.EventListener: 31 | self.event_handler.add_listener(lsnr := handler.EventListener(type=event, callback=callable, **kwargs)) 32 | return lsnr 33 | 34 | return decorator 35 | 36 | def on_starting( 37 | self, **kwargs: typing.Any 38 | ) -> typing.Callable[[types.EventListenerCallbackT], handler.EventListener]: 39 | """Used to create a :class:`.StartingEvent` listener. 40 | 41 | Returns 42 | ------- 43 | EventListener 44 | The listener that was created.""" 45 | return self._make_deco(events.StartingEvent, **kwargs) 46 | 47 | def on_started( 48 | self, **kwargs: typing.Any 49 | ) -> typing.Callable[[types.EventListenerCallbackT], handler.EventListener]: 50 | """Used to create a :class:`.StartedEvent` listener. 51 | 52 | Returns 53 | ------- 54 | EventListener 55 | The listener that was created.""" 56 | return self._make_deco(events.StartedEvent, **kwargs) 57 | -------------------------------------------------------------------------------- /wyvern/utils/hooks.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import annotations 24 | 25 | import importlib 26 | import typing 27 | 28 | import attrs 29 | 30 | 31 | @attrs.define 32 | class Hook: 33 | name: str 34 | callback: typing.Callable[..., typing.Any] 35 | 36 | def __call__(self, *args: typing.Any, **kwds: typing.Any) -> None: 37 | self.callback(*args, **kwds) 38 | 39 | 40 | def hook(name: str | None = None) -> typing.Callable[[typing.Callable[..., typing.Any]], Hook]: 41 | def decorator(callback: typing.Callable[..., typing.Any]) -> Hook: 42 | return Hook(name or callback.__name__, callback) 43 | 44 | return decorator 45 | 46 | 47 | def parse_hooks(import_path: str) -> dict[str, Hook]: 48 | module = importlib.import_module(import_path) 49 | return {_hook.name: _hook for _hook in module.__dict__.values() if isinstance(_hook, Hook)} 50 | -------------------------------------------------------------------------------- /wyvern/__main__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import argparse 24 | import sys 25 | 26 | import colorama 27 | 28 | from wyvern import __version__ 29 | 30 | parser = argparse.ArgumentParser( 31 | prog="wyvern", description="Commands related to the library and it's usage.", add_help=True 32 | ) 33 | parser.add_argument("-V", "-v", "--version", help="Check the version of library.", action="store_true") 34 | 35 | 36 | def main() -> None: 37 | args = parser.parse_args() 38 | if args.version is True: 39 | print(colorama.Fore.LIGHTCYAN_EX, end="") 40 | print( 41 | "Library version:", 42 | colorama.Style.BRIGHT, 43 | __version__, 44 | colorama.Style.NORMAL, 45 | "\nPython version:", 46 | colorama.Style.BRIGHT, 47 | sys.version, 48 | colorama.Style.RESET_ALL, 49 | ) 50 | 51 | 52 | main() 53 | -------------------------------------------------------------------------------- /wyvern/builders/components.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import abc 4 | import typing 5 | 6 | from wyvern.models import components as base 7 | 8 | 9 | class ConstructedComponent: 10 | @abc.abstractmethod 11 | def to_payload(self) -> dict[str, typing.Any]: 12 | ... 13 | 14 | 15 | class ConstructedButton(base.Button): 16 | def to_payload(self) -> dict[str, typing.Any]: 17 | return { 18 | "type": int(self.type), 19 | "style": int(self.style), 20 | "label": self.label, 21 | "custom_id": self.custom_id or "wyvern.NO_CUSTOM_ID", 22 | "disabled": self.disabled, 23 | "url": self.url, 24 | "emoji": self.emoji, 25 | } 26 | 27 | 28 | class ActionRowBuilder(base.ActionRow): 29 | components: list[ConstructedButton] # type: ignore 30 | 31 | @classmethod 32 | def acquire(cls) -> ActionRowBuilder: 33 | return cls(components=[]) 34 | 35 | def to_payload(self) -> dict[str, typing.Any]: 36 | return {"type": int(self.type), "components": [item.to_payload() for item in self.components]} 37 | 38 | @typing.overload 39 | def add_button(self, *, style: base.ButtonStyle, url: str, disabled: bool) -> ActionRowBuilder: 40 | ... 41 | 42 | @typing.overload 43 | def add_button( 44 | self, 45 | *, 46 | style: base.ButtonStyle, 47 | label: str | None = None, 48 | emoji: typing.Any = None, 49 | custom_id: str | None = None, 50 | disabled: bool = False, 51 | ) -> ActionRowBuilder: 52 | ... 53 | 54 | def add_button( 55 | self, 56 | *, 57 | style: base.ButtonStyle = base.ButtonStyle.PRIMARY, 58 | label: str | None = None, 59 | emoji: typing.Any = None, 60 | custom_id: str | None = None, 61 | disabled: bool = False, 62 | url: str | None = None, 63 | ) -> ActionRowBuilder: 64 | self.components.append( 65 | ConstructedButton(style=style, label=label, emoji=emoji, custom_id=custom_id, disabled=disabled, url=url) 66 | ) 67 | return self 68 | -------------------------------------------------------------------------------- /wyvern/models/snowflake.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import annotations 24 | 25 | import abc 26 | import datetime 27 | 28 | 29 | class Snowflake(int): 30 | """Represents a discord ID, this class inherits from :class:`int`.""" 31 | 32 | ... 33 | 34 | 35 | class DiscordObject(abc.ABC): 36 | """Base class for all discord snowflake models. Models like :class:`.User` inherit from this.""" 37 | 38 | id: Snowflake 39 | """ID of the object.""" 40 | 41 | def __eq__(self, obj: object) -> bool: 42 | return isinstance(obj, DiscordObject) and obj.id == self.id 43 | 44 | @property 45 | def created_at(self) -> datetime.datetime: 46 | """Datetime at which the snowflake was created. 47 | 48 | Returns 49 | ------- 50 | datetime.datetime 51 | Creation time. 52 | """ 53 | timestamp: float = ((self.id >> 22) + 1420070400000) / 1000 54 | return datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc) 55 | -------------------------------------------------------------------------------- /wyvern/events/lib_events.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import annotations 24 | 25 | import typing 26 | 27 | import attrs 28 | 29 | from wyvern.events.base import Event 30 | 31 | if typing.TYPE_CHECKING: 32 | from wyvern.models.users import BotUser 33 | 34 | __all__: tuple[str, ...] = ("StartingEvent", "StartedEvent") 35 | 36 | 37 | @attrs.define(kw_only=True) 38 | class StartingEvent(Event): 39 | """This event is dispatched when the bot is about to connect to the gateway. 40 | 41 | .. note:: 42 | This event gets triggered only once in the runtime. 43 | """ 44 | 45 | 46 | @attrs.define(kw_only=True) 47 | class StartedEvent(Event): 48 | """The StartedEvent is dispatched when the :any:`GatewayBot.start` method is called 49 | and the bot recieves first Websocket response from API after token verification. 50 | 51 | .. note:: 52 | This event gets triggered only once in the runtime. 53 | """ 54 | 55 | user: BotUser 56 | """The bot user that the client is logged in as.""" 57 | -------------------------------------------------------------------------------- /wyvern/utils/consts.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import annotations 24 | 25 | import typing 26 | 27 | 28 | class Undefined: 29 | """An instance of this class is used as a placeholder to represent attributes and variables which 30 | are not defined *yet*.""" 31 | 32 | 33 | UNDEFINED = Undefined() 34 | """Instance of Undefined.""" 35 | 36 | 37 | class Empty: 38 | """This class is used for variables in an embed constructor to represent empty values.""" 39 | 40 | def __xor__(self, other: object) -> object: 41 | return other 42 | 43 | @classmethod 44 | def verify(cls, other: typing.Any) -> typing.Any: 45 | return None if isinstance(other, Empty) else other 46 | 47 | 48 | EMPTY = Empty() 49 | """Instance of Empty.""" 50 | 51 | 52 | class Null: 53 | """Class to simulate difference between an argument provided as None v/s a `null` value to send to the discord API. 54 | This class is used as a placeholder for arguments in REST methods. 55 | """ 56 | 57 | 58 | NULL = Null() 59 | """Instance of Null.""" 60 | -------------------------------------------------------------------------------- /wyvern/logger.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import annotations 24 | 25 | import logging 26 | 27 | 28 | class LoggingFormatter(logging.Formatter): 29 | COLOR_CONFIGS = { 30 | logging.DEBUG: "\x1b[33;49m", 31 | logging.INFO: "\x1b[32;49m", 32 | logging.WARNING: "\x1b[35;49m", 33 | logging.ERROR: "\x1b[31;49m", 34 | logging.CRITICAL: "\x1b[33;41;1m", 35 | } 36 | 37 | def format(self, record: logging.LogRecord) -> str: 38 | log_format = f"[%(asctime)s : {record.levelname.rjust(7)}] | %(message)s " 39 | 40 | formatter = logging.Formatter( 41 | "".join((self.COLOR_CONFIGS.get(record.levelno, "\x1b[32;49m"), log_format, "\x1b[0m")) 42 | ) 43 | return formatter.format(record) 44 | 45 | 46 | def create_logging_setup(logger: logging.Logger) -> None: 47 | stream = logging.StreamHandler() 48 | stream.setFormatter(LoggingFormatter()) 49 | logger.addHandler(stream) 50 | logger.setLevel(logging.INFO) 51 | 52 | 53 | main_logger = logging.getLogger("wyvern") 54 | 55 | create_logging_setup(main_logger) 56 | -------------------------------------------------------------------------------- /wyvern/models/messages.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import annotations 24 | 25 | import typing 26 | 27 | import attrs 28 | 29 | from wyvern.models.snowflake import DiscordObject, Snowflake 30 | 31 | 32 | @attrs.define(kw_only=True, slots=True, frozen=True) 33 | class AllowedMentions: 34 | """Allowed mentions in a message sent from bot. 35 | 36 | Parameters 37 | ---------- 38 | users: bool 39 | Set to `True` to mention users. 40 | roles: bool 41 | Set to `True` to mention roles. 42 | everyone: bool 43 | Set to `True` to allow everyone/here mentions. 44 | replied_user: bool 45 | Weather the replied users should be pinged. 46 | """ 47 | 48 | users: bool = False 49 | roles: bool = False 50 | everyone: bool = False 51 | replied_user: bool = False 52 | 53 | def to_dict(self) -> dict[str, typing.Any]: 54 | return { 55 | "parse": [t for t in ("roles", "users", "everyone") if getattr(self, t, False) is True], 56 | "replied_user": self.replied_user, 57 | } 58 | 59 | 60 | @attrs.define(kw_only=True, slots=True, frozen=True) 61 | class PartialMessage(DiscordObject): 62 | id: Snowflake 63 | """ID of the message.""" 64 | -------------------------------------------------------------------------------- /wyvern/internals/enum_flags.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import annotations 24 | 25 | import typing 26 | 27 | __all__: tuple[str] = ("Flag",) 28 | 29 | 30 | class Flag: 31 | value: int 32 | 33 | def __init__(self, value: int) -> None: 34 | self.value = value 35 | 36 | _ignored_flags: tuple[str, ...] = ("ALL", "PRIVILEGED", "UNPRIVILEGED") 37 | 38 | def __iter__(self) -> typing.Generator[tuple[str, bool], None, None]: 39 | for flag in [attr for attr in dir(self) if attr.isupper() and attr not in self._ignored_flags]: 40 | if self.value & getattr(self, flag) != 0: 41 | yield flag, True 42 | else: 43 | yield flag, False 44 | 45 | def __repr__(self) -> str: 46 | return f"<{self.__class__.__name__}(value={self.value})>" 47 | 48 | def __int__(self) -> int: 49 | return self.value 50 | 51 | def get_enabled(self, *, as_str: bool = False) -> tuple[int | str, ...]: 52 | return tuple((getattr(self, flag[0]) if as_str is False else flag[0]) for flag in list(self) if flag[1] is True) 53 | 54 | def get_disabled(self, *, as_str: bool = False) -> tuple[int | str, ...]: 55 | return tuple( 56 | (getattr(self, flag[0]) if as_str is False else flag[0]) for flag in list(self) if flag[1] is False 57 | ) 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | poetry.lock 2 | test.py 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | # pytype static type analyzer 138 | .pytype/ 139 | 140 | #Pycharm 141 | .idea/ 142 | 143 | # Cython debug symbols 144 | cython_debug/ 145 | .vscode/ 146 | -------------------------------------------------------------------------------- /wyvern/models/members.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import datetime 4 | import typing 5 | 6 | import attrs 7 | 8 | from wyvern.models.snowflake import Snowflake 9 | from wyvern.models.users import PartialUser, User 10 | 11 | if typing.TYPE_CHECKING: 12 | import discord_typings 13 | 14 | from wyvern.api.bot import GatewayBot 15 | 16 | __all__: tuple[str, ...] = ("GuildMember",) 17 | 18 | 19 | @attrs.define(kw_only=True) 20 | class GuildMember(User): 21 | raw: discord_typings.GuildMemberData # type: ignore 22 | """Raw payload.""" 23 | user: User 24 | """User of the member.""" 25 | guild_id: Snowflake 26 | """ID of the guild this member belongs to.""" 27 | nickname: str | None 28 | """Nickname of the member, if any.""" 29 | role_ids: list[Snowflake] 30 | """List of role ids.""" 31 | joined_at: datetime.datetime 32 | """Datetime on which the member joined the guild.""" 33 | premium_since: datetime.datetime | None 34 | """Datetime since member has been boosting the guild.""" 35 | deaf: bool | None 36 | """True if member is deafened.""" 37 | mute: bool | None 38 | """True if member is muted.""" 39 | pending: bool 40 | """True if member is in pending state.""" 41 | communication_disabled_until: datetime.datetime | None 42 | """Timeout for the member.""" 43 | guild_avatar_hash: str | None 44 | """Avatar hash for the guild.""" 45 | 46 | @property 47 | def display_name(self) -> str: 48 | """ 49 | Returns 50 | ------- 51 | str 52 | Nickname of the user if exists, else the username. 53 | """ 54 | return self.nickname or self.username 55 | 56 | @classmethod 57 | def from_payload(cls, bot: GatewayBot, guild_id: int, payload: discord_typings.GuildMemberData) -> GuildMember: 58 | user: User = User.from_partial(bot, PartialUser.from_payload(payload["user"])) # type: ignore 59 | return GuildMember( 60 | user=user, 61 | id=user.id, 62 | username=user.username, 63 | discriminator=user.discriminator, 64 | raw=payload, 65 | avatar_hash=user.avatar_hash, 66 | is_bot=user.is_bot, 67 | is_system=user.is_system, 68 | is_mfa_enabled=user.is_mfa_enabled, 69 | banner_hash=user.banner_hash, 70 | accent_color=user.accent_color, 71 | locale=user.locale, 72 | flags_value=user.flags_value, 73 | premium_type_value=user.premium_type_value, 74 | public_flags_value=user.public_flags_value, 75 | bot=bot, 76 | partial_user=user.partial_user, 77 | guild_id=Snowflake(guild_id), 78 | nickname=payload.get("nick"), 79 | role_ids=[Snowflake(_id) for _id in payload["roles"]], 80 | joined_at=datetime.datetime.fromisoformat(payload["joined_at"]), 81 | premium_since=datetime.datetime.fromisoformat(ps) if (ps := payload.get("premium_since")) else None, 82 | deaf=payload["deaf"], 83 | mute=payload["mute"], 84 | pending=payload.get("pending") or False, 85 | communication_disabled_until=payload.get("communication_disabled_until"), 86 | guild_avatar_hash=av if (av := payload.get("avatar")) else None, 87 | ) 88 | -------------------------------------------------------------------------------- /wyvern/api/rest_client.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import annotations 24 | 25 | import typing 26 | 27 | from wyvern import builders, models, types 28 | from wyvern.internals.rest import Endpoints, RequestRoute, RESTClient 29 | from wyvern.utils.consts import NULL, Null 30 | 31 | if typing.TYPE_CHECKING: 32 | import discord_typings 33 | 34 | __all__: tuple[str, ...] = ("RESTClientImpl",) 35 | 36 | 37 | class RESTClientImpl(RESTClient): 38 | """Class handling operations related to REST/HTTP requests to the discord API. 39 | An instance of this class is binded with the :class:`.GatewayBot` instance.""" 40 | 41 | async def fetch_current_user(self) -> models.BotUser: 42 | """|coro| 43 | 44 | Gets the current bot user. 45 | 46 | Returns 47 | ------- 48 | BotUser 49 | The bot user. 50 | """ 51 | data: discord_typings.UserData = await self.request(RequestRoute(Endpoints.get_current_user())) 52 | return models.BotUser.from_partial(self.bot, models.PartialUser.from_payload(data)) 53 | 54 | async def create_message( 55 | self, 56 | channel_id: types.Snowflakish[models.ImplementsMessage], 57 | content: types.NullOr[str] = NULL, 58 | embed: types.NullOr[builders.Embed] = NULL, 59 | embeds: types.NullOr[typing.Sequence[builders.Embed]] = NULL, 60 | component: types.NullOr[builders.ActionRowBuilder] = NULL, 61 | components: types.NullOr[typing.Sequence[builders.ActionRowBuilder]] = NULL, 62 | allowed_mentions: models.AllowedMentions = models.AllowedMentions(), 63 | ): 64 | data: dict[str, typing.Any] = { 65 | "content": str(content) if content is not NULL else None, 66 | "allowed_mentions": allowed_mentions.to_dict(), 67 | } 68 | if embed is not NULL and embeds is not NULL: 69 | raise ValueError("expected only embed or only embeds argument, got both.") 70 | elif not isinstance(embed, Null): 71 | data["embeds"] = [embed.to_dict()] 72 | elif not isinstance(embeds, Null): 73 | data["embeds"] = [embed.to_dict() for embed in embeds] 74 | if component is not NULL and components is not NULL: 75 | raise ValueError("expected only component or only components, got both.") 76 | elif not isinstance(component, Null): 77 | data["components"] = [component.to_payload()] 78 | elif not isinstance(components, Null): 79 | data["components"] = [component.to_payload() for component in components] 80 | 81 | await self.request( 82 | RequestRoute( 83 | Endpoints.create_message(channel_id if isinstance(channel_id, int) else channel_id.id), 84 | type="POST", 85 | json=data, 86 | ), 87 | ) 88 | -------------------------------------------------------------------------------- /wyvern/api/event_handler.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import annotations 24 | 25 | import asyncio 26 | import typing 27 | 28 | import attrs 29 | 30 | from wyvern import types 31 | from wyvern.events.base import Event 32 | from wyvern.utils.consts import UNDEFINED, Undefined 33 | 34 | if typing.TYPE_CHECKING: 35 | from wyvern.api.bot import GatewayBot 36 | 37 | __all__: tuple[str, ...] = ("EventListener", "EventHandler", "listener") 38 | 39 | 40 | @attrs.define(kw_only=True) 41 | class EventListener: 42 | """Represents an event listener, the callback of this class gets triggered whenever the event 43 | this listener is bound to gets triggered.""" 44 | 45 | type: type[Event] 46 | """Type of event this listener listens to.""" 47 | max_trigger: int | Undefined = UNDEFINED 48 | """Maximum number of times this event can get triggered.""" 49 | callback: types.EventListenerCallbackT 50 | """The callback for listener.""" 51 | bot: GatewayBot | Undefined = UNDEFINED 52 | 53 | def __call__(self, event: Event) -> typing.Any: 54 | return self.callback(event) 55 | 56 | 57 | @attrs.define(kw_only=True) 58 | class EventHandler: 59 | """Class handling dispatches and containing of the event listeners. 60 | An instance of this class is bound to the :class:`.GatewayBot` to handle events. 61 | """ 62 | 63 | bot: GatewayBot 64 | listeners: dict[type[Event], list[EventListener]] = {} 65 | """Mapping of :class:`.Event` types to list of :class:`EventListener` s listening to the event.""" 66 | 67 | def add_listener(self, lsnr: EventListener) -> None: 68 | """Adds a listener to the container. 69 | 70 | Parameters 71 | ---------- 72 | lsnr: EventListener 73 | The listener to add. 74 | """ 75 | if lsnr.bot == UNDEFINED: 76 | lsnr.bot = self.bot 77 | self.listeners.setdefault(lsnr.type, []).append(lsnr) 78 | 79 | def dispatch(self, event: Event) -> None: 80 | asyncio.gather(*list(map(lambda elistener: elistener(event), self.listeners.get(type(event), [])))) 81 | 82 | 83 | def listener( 84 | event: type[Event], *, max_trigger: int | Undefined = UNDEFINED 85 | ) -> typing.Callable[[types.EventListenerCallbackT], EventListener]: 86 | """Used to create an :class:`EventListener`. 87 | 88 | Parameters 89 | ---------- 90 | event: type[Event] 91 | Class of the event this listener is bound to. 92 | max_trigger: int 93 | Maximum trigger limit of the event callback. 94 | 95 | Returns 96 | ------- 97 | EventListener 98 | The listener that was created. 99 | """ 100 | 101 | def decorator(callback: types.EventListenerCallbackT) -> EventListener: 102 | return EventListener( 103 | type=event, 104 | max_trigger=max_trigger, 105 | callback=callback, 106 | ) 107 | 108 | return decorator 109 | -------------------------------------------------------------------------------- /wyvern/internals/gateway.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import annotations 24 | 25 | import asyncio 26 | import enum 27 | import json 28 | import sys 29 | import time 30 | import typing 31 | 32 | import aiohttp 33 | 34 | from wyvern.events import lib_events 35 | 36 | if typing.TYPE_CHECKING: 37 | from wyvern.api.bot import GatewayBot 38 | 39 | import attrs 40 | 41 | 42 | class OPCode(enum.IntEnum): 43 | DISPATCH = 0 44 | HEARTBEAT = 1 45 | IDENTIFY = 2 46 | PRESECNE_UPDATE = 3 47 | VOICE_STATE_UPDATE = 4 48 | RESUME = 6 49 | RECONNECT = 7 50 | REQUEST_GUILD_MEMBERS = 8 51 | INVALID_SESSION = 9 52 | HELLO = 10 53 | HEARTBEAT_ACK = 11 54 | 55 | 56 | @attrs.define 57 | class Gateway: 58 | bot: GatewayBot 59 | socket: aiohttp.ClientWebSocketResponse = attrs.field(init=False) 60 | latency: float = attrs.field(init=False, default=float("NaN")) 61 | heartbeat_interval: float = attrs.field(init=False, default=0) 62 | sequence: int = attrs.field(init=False, default=0) 63 | last_heartbeat: float = attrs.field(init=False, default=0) 64 | 65 | async def connect(self) -> None: 66 | self.bot.event_handler.dispatch(lib_events.StartingEvent(bot=self.bot)) 67 | self.socket = await self.bot.rest.client_session.ws_connect( # type: ignore 68 | f"wss://gateway.discord.gg/?v={self.bot.rest.api_version}&encoding=json" 69 | ) 70 | await self.listen_gateway() 71 | 72 | async def process_gw_event(self, payload: dict[str, typing.Any]) -> None: 73 | op = payload["op"] 74 | if op == OPCode.HELLO: 75 | await self.socket.send_json(self.identify_payload) 76 | self.heartbeat_interval = payload["d"]["heartbeat_interval"] / 1000 77 | asyncio.get_event_loop().create_task(self.keep_alive()) 78 | if op == OPCode.HEARTBEAT_ACK: 79 | self.latency = time.perf_counter() - self.last_heartbeat 80 | 81 | async def keep_alive(self) -> None: 82 | while True: 83 | await self.socket.send_json({"op": OPCode.HEARTBEAT, "d": self.sequence}) 84 | self.last_heartbeat = time.perf_counter() 85 | await asyncio.sleep(self.heartbeat_interval) 86 | 87 | @property 88 | def identify_payload(self) -> dict[str, typing.Any]: 89 | return { 90 | "op": 2, 91 | "d": { 92 | "token": self.bot.rest.token, 93 | "intents": self.bot.intents.value, 94 | "properties": { 95 | "os": sys.platform, 96 | "browser": "wyvern", 97 | "device": "wyvern", 98 | }, 99 | }, 100 | } 101 | 102 | async def listen_gateway(self) -> None: 103 | user = await self.bot.rest.fetch_current_user() 104 | 105 | async for msg in self.socket: 106 | if self.heartbeat_interval == 0: 107 | self.bot.event_handler.dispatch(lib_events.StartedEvent(bot=self.bot, user=user)) 108 | await self.process_gw_event(json.loads(msg.data)) # type: ignore 109 | -------------------------------------------------------------------------------- /wyvern/api/bot.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import annotations 24 | 25 | import traceback 26 | import typing 27 | 28 | import aiohttp 29 | 30 | from wyvern import logger, types 31 | from wyvern.api.event_decos import ImplementsEventDecos 32 | from wyvern.api.event_handler import EventHandler, EventListener 33 | from wyvern.api.gateway import GatewayImpl 34 | from wyvern.api.intents import Intents 35 | from wyvern.api.rest_client import RESTClientImpl 36 | from wyvern.events.base import Event 37 | from wyvern.utils.consts import UNDEFINED, Undefined 38 | 39 | __all__: tuple[str, ...] = ("GatewayBot",) 40 | 41 | 42 | class GatewayBot(ImplementsEventDecos): 43 | """ 44 | The main bot class that interacts with the discord API through both REST and gateway paths. 45 | 46 | Parameters 47 | ---------- 48 | token: str 49 | The bot token to use while execution. 50 | api_version: int 51 | Discord API version in usage, defaults to 10. 52 | intents: typing.SupportsInt | Intents 53 | Library's :class:`Intents` builder or any object that returns the intent value when passed to :class:`int` 54 | 55 | Example 56 | ------- 57 | .. highlight:: python 58 | .. code-block:: python 59 | 60 | import asyncio 61 | 62 | import wyvern 63 | 64 | bot = wyvern.GatewayBot( 65 | "BOT_TOKEN_HERE", 66 | intents=( 67 | wyvern.Intents.GUILD_MEMBERS 68 | | wyvern.Intents.GUILDS 69 | | wyvern.Intents.GUILD_MESSAGES 70 | | wyvern.Intents.DIRECT_MESSAGES 71 | ), 72 | ) 73 | 74 | asyncio.run(bot.start()) 75 | 76 | """ 77 | rest: RESTClientImpl 78 | """The REST handler attached to the instance.""" 79 | intents: Intents 80 | """Intents being used by the bot.""" 81 | event_handler: EventHandler 82 | """The :class:`.EventHandler` attached to the instance.""" 83 | def __init__( 84 | self, token: str, *, api_version: int = 10, intents: typing.SupportsInt | Intents = Intents.UNPRIVILEGED 85 | ) -> None: 86 | self.logger = logger.main_logger 87 | self.intents = Intents(int(intents)) 88 | self.rest = RESTClientImpl(token=token, bot=self, api_version=api_version) 89 | self.gateway = GatewayImpl(self) 90 | self.event_handler = EventHandler(bot=self) 91 | 92 | async def __aenter__(self) -> None: 93 | self.rest.client_session = aiohttp.ClientSession() 94 | 95 | async def __aexit__(self, *args: typing.Any) -> None: 96 | await self.rest.client_session.close() # type: ignore 97 | 98 | def listener( 99 | self, event: type[Event], *, max_trigger: int | Undefined = UNDEFINED 100 | ) -> typing.Callable[[types.EventListenerCallbackT], EventListener]: 101 | """Adds a listener to the bot's event handler 102 | 103 | Parameters 104 | ---------- 105 | event : type[Event] 106 | Type of the event. 107 | max_trigger : int 108 | Maximum number of times this event should be triggered. 109 | 110 | Returns 111 | ------- 112 | listener: EventListener 113 | The event listener that was constructed. 114 | """ 115 | 116 | def decorator(callback: types.EventListenerCallbackT) -> EventListener: 117 | self.event_handler.add_listener( 118 | lsnr := EventListener(type=event, max_trigger=max_trigger, callback=callback, bot=self) 119 | ) 120 | return lsnr 121 | 122 | return decorator 123 | 124 | async def start(self, raise_exception: bool = False) -> None: 125 | """|coro| 126 | 127 | Verifies the bot token and starts listening to the gateway. 128 | 129 | Parameters 130 | ---------- 131 | raise_exception: bool 132 | Set to true if exceptions should be be raised at this entrypoint. 133 | 134 | Raises 135 | ------ 136 | 137 | """ 138 | try: 139 | await self.gateway.connect() 140 | except aiohttp.ClientResponseError as e: 141 | self.logger.error("".join(traceback.format_exception(e))) # type: ignore 142 | self.logger.critical("Invalid token was passed to the GatewayBot constructor.") 143 | if raise_exception is True: 144 | raise e 145 | except AssertionError as e: 146 | self.logger.error("GatewayBot.start() should be used inside an async context manager. Example:") 147 | self.logger.error("async with bot:") 148 | self.logger.error(" await bot.start()") 149 | if raise_exception is True: 150 | raise e 151 | -------------------------------------------------------------------------------- /wyvern/models/components.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import annotations 24 | 25 | import abc 26 | import enum 27 | import typing 28 | 29 | import attrs 30 | 31 | if typing.TYPE_CHECKING: 32 | import discord_typings 33 | 34 | __all__: tuple[str, ...] = ( 35 | "ButtonStyle", 36 | "ComponentType", 37 | "Component", 38 | "Button", 39 | "TextInputStyle", 40 | "TextInput", 41 | "Modal", 42 | "ActionRow", 43 | ) 44 | 45 | 46 | class ButtonStyle(enum.IntEnum): 47 | """Enums for Button style.""" 48 | 49 | PRIMARY = 1 50 | """A primary blurple discord button.""" 51 | SECONDARY = 2 52 | """A secondary gray discord button.""" 53 | SUCCESS = 3 54 | """Green discord button.""" 55 | DANGER = 4 56 | """Red discord button.""" 57 | LINK = 5 58 | """Button pointing to an URL""" 59 | BLURPLE = PRIMARY 60 | """Alias for PRIMARY""" 61 | GRAY = SECONDARY 62 | """Alias for SECONDARY""" 63 | GREY = SECONDARY 64 | """Alias for SECONDARY""" 65 | GREEN = SUCCESS 66 | """Alias for SUCCESS""" 67 | RED = DANGER 68 | """Alias for DANGER""" 69 | URL = LINK 70 | """Alias for LINK""" 71 | 72 | 73 | class ComponentType(enum.IntEnum): 74 | ACTION_ROW = 1 75 | BUTTON = 2 76 | STRING_SELECT = 3 77 | TEXT_INPUT = 4 78 | USER_SELECT = 5 79 | ROLE_SELECT = 6 80 | MENTIONABLE_SELECT = 7 81 | CHANNEL_SELECT = 8 82 | 83 | 84 | class Component(abc.ABC): 85 | """Represents a discord component. 86 | Is the base class for other components.""" 87 | 88 | 89 | @attrs.define(kw_only=True, slots=True, repr=True) 90 | class Button(Component): 91 | """Represents a discord button. 92 | The properties mentioned below can be used to create a button. 93 | """ 94 | 95 | type: ComponentType = attrs.field(init=False, default=ComponentType.BUTTON) 96 | style: ButtonStyle 97 | """Style of the button.""" 98 | label: str | None = None 99 | """Button's label.""" 100 | emoji: typing.Any = None 101 | """Emoji embedded in the button.""" 102 | custom_id: str | None = None 103 | """Custom id for the component.""" 104 | disabled: bool = False 105 | """True if the component is disabled.""" 106 | url: str | None = None 107 | """The URL this button points to, if any.""" 108 | 109 | @classmethod 110 | def from_payload(cls, payload: discord_typings.ButtonComponentData) -> Button: 111 | return Button( 112 | style=ButtonStyle(payload["style"]), 113 | label=payload.get("label"), 114 | emoji=payload.get("emoji"), 115 | custom_id=payload.get("custom_id"), 116 | disabled=payload.get("disabled", False), 117 | url=payload.get("url"), 118 | ) 119 | 120 | 121 | class TextInputStyle(enum.IntEnum): 122 | SHORT = 1 123 | """For single line text-inputs.""" 124 | PARAGRAPH = 2 125 | """For multi line text-inputs.""" 126 | 127 | 128 | @typing.final 129 | @attrs.define(kw_only=True, slots=True, repr=True) 130 | class TextInput(Component): 131 | """Represents a modal text-input.""" 132 | 133 | type: ComponentType = attrs.field(init=False, default=ComponentType.TEXT_INPUT) 134 | custom_id: str 135 | """Custom id of the textinput.""" 136 | label: str 137 | """Label of the textinput.""" 138 | style: TextInputStyle 139 | """Textinput style.""" 140 | min_length: int | None = None 141 | """The maximum allowed length for textinput.""" 142 | max_length: int | None = None 143 | """The minimum allowed length for textinput.""" 144 | required: bool = True 145 | """Weather the field is required.""" 146 | default_value: str | None = None 147 | """Default value for this textinput.""" 148 | placeholder: str | None = None 149 | """The placeholder used, if any.""" 150 | 151 | @classmethod 152 | def from_payload(cls, payload: discord_typings.TextInputComponentData) -> TextInput: 153 | return TextInput( 154 | custom_id=payload["custom_id"], 155 | label=payload["label"], 156 | style=TextInputStyle(payload["style"]), 157 | min_length=payload.get("min_length"), 158 | max_length=payload.get("max_length"), 159 | required=payload.get("required", True), 160 | default_value=payload.get("value"), 161 | placeholder=payload.get("placeholder"), 162 | ) 163 | 164 | 165 | @attrs.define(kw_only=True, slots=True) 166 | class Modal: 167 | """Represents a discord modal form.""" 168 | 169 | title: str 170 | """Title of the modal.""" 171 | custom_id: str 172 | """Custom ID of the modal""" 173 | text_inputs: list[TextInput] 174 | """List of textinputs in the modal.""" 175 | 176 | 177 | @attrs.define(kw_only=True, slots=True) 178 | class ActionRow(Component): 179 | type: ComponentType = attrs.field(init=False, default=ComponentType.ACTION_ROW) 180 | components: list[Component] 181 | -------------------------------------------------------------------------------- /wyvern/api/intents.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import annotations 24 | 25 | import typing 26 | 27 | from wyvern.internals.enum_flags import Flag 28 | 29 | __all__: tuple[str, ...] = ("Intents",) 30 | 31 | 32 | @typing.final 33 | class Intents(Flag): 34 | """ 35 | Intents constructor to provide to the GatewayBot class. 36 | 37 | Attributes 38 | ---------- 39 | value: int 40 | The final value of intents generated by the constructor. 41 | """ 42 | 43 | NONE = 0 44 | """ 45 | No intents. 46 | """ 47 | GUILDS = 1 << 0 48 | """Required for these gateway events: 49 | 50 | * ``GUILD_CREATE`` 51 | * ``GUILD_UPDATE`` 52 | * ``GUILD_DELETE`` 53 | * ``GUILD_ROLE_CREATE`` 54 | * ``GUILD_ROLE_UPDATE`` 55 | * ``GUILD_ROLE_DELETE`` 56 | * ``CHANNEL_CREATE`` 57 | * ``CHANNEL_UPDATE`` 58 | * ``CHANNEL_DELETE`` 59 | * ``CHANNEL_PINS_UPDATE`` 60 | * ``THREAD_CREATE`` 61 | * ``THREAD_UPDATE`` 62 | * ``THREAD_DELETE`` 63 | * ``THREAD_LIST_SYNC`` 64 | * ``THREAD_MEMBER_UPDATE`` 65 | * ``THREAD_MEMBERS_UPDATE`` 66 | * ``STAGE_INSTANCE_CREATE`` 67 | * ``STAGE_INSTANCE_UPDATE`` 68 | * ``STAGE_INSTANCE_DELETE`` 69 | """ 70 | GUILD_MEMBERS = 1 << 1 71 | """Required for these gateway events: 72 | 73 | * ``GUILD_MEMBER_ADD`` 74 | * ``GUILD_MEMBER_UPDATE`` 75 | * ``GUILD_MEMBER_REMOVE`` 76 | * ``THREAD_MEMBERS_UPDATE`` 77 | .. warning:: 78 | This is a privileged intent. 79 | """ 80 | GUILD_BANS = 1 << 2 81 | """Required for these gateway events: 82 | 83 | * ``GUILD_BAN_ADD`` 84 | * ``GUILD_BAN_REMOVE`` 85 | """ 86 | GUILD_EMOJIS = 1 << 3 87 | """Required for these gateway events: 88 | 89 | * ``GUILD_EMOJIS_UPDATE`` 90 | * ``GUILD_STICKERS_UPDATE`` 91 | """ 92 | GUILD_INTEGRATIONS = 1 << 4 93 | """Required for these gateway events: 94 | 95 | * ``GUILD_INTEGRATIONS_UPDATE`` 96 | * ``INTEGRATION_CREATE`` 97 | * ``INTEGRATION_UPDATE`` 98 | * ``INTEGRATION_DELETE`` 99 | """ 100 | GUILD_WEBHOOKS = 1 << 5 101 | """Required for these gateway events: 102 | 103 | * ``WEBHOOKS_UPDATE`` 104 | """ 105 | GUILD_INVITES = 1 << 6 106 | """Required for these gateway events: 107 | 108 | * ``INVITE_CREATE`` 109 | * ``INVITE_DELETE`` 110 | """ 111 | GUILD_VOICE_STATES = 1 << 7 112 | """Required for these gateway events: 113 | 114 | * ``VOICE_STATE_UPDATE`` 115 | """ 116 | GUILD_PRESENCES = 1 << 8 117 | """Required for these gateway events: 118 | 119 | * ``PRESENCE_UPDATE`` 120 | .. warning:: 121 | This is a privileged intent. 122 | """ 123 | GUILD_MESSAGES = 1 << 9 124 | """Required for these gateway events: 125 | 126 | * ``MESSAGE_CREATE`` 127 | * ``MESSAGE_UPDATE`` 128 | * ``MESSAGE_DELETE`` 129 | * ``MESSAGE_DELETE_BULK`` 130 | """ 131 | GUILD_MESSAGE_REACTIONS = 1 << 10 132 | """Required for these gateway events: 133 | 134 | * ``MESSAGE_REACTION_ADD`` 135 | * ``MESSAGE_REACTION_REMOVE`` 136 | * ``MESSAGE_REACTION_REMOVE_ALL`` 137 | * ``MESSAGE_REACTION_REMOVE_EMOJI`` 138 | """ 139 | GUILD_MESSAGE_TYPING = 1 << 11 140 | """Required for these gateway events: 141 | 142 | * ``TYPING_START`` 143 | """ 144 | DIRECT_MESSAGES = 1 << 12 145 | """Required for these gateway events: 146 | 147 | * ``MESSAGE_CREATE`` 148 | * ``MESSAGE_UPDATE`` 149 | * ``MESSAGE_DELETE`` 150 | * ``CHANNEL_PINS_UPDATE`` 151 | """ 152 | DIRECT_MESSAGE_REACTIONS = 1 << 13 153 | """Required for these gateway events: 154 | 155 | * ``MESSAGE_REACTION_ADD`` 156 | * ``MESSAGE_REACTION_REMOVE`` 157 | * ``MESSAGE_REACTION_REMOVE_ALL`` 158 | * ``MESSAGE_REACTION_REMOVE_EMOJI`` 159 | """ 160 | DIRECT_MESSAGE_TYPING = 1 << 14 161 | """Required for these gateway events: 162 | 163 | * ``TYPING_START`` 164 | """ 165 | MESSAGE_CONTENT = 1 << 15 166 | """Required for guild message's content. 167 | 168 | .. warning:: 169 | This is a privileged intent. 170 | """ 171 | GUILD_SCHEDULED_EVENTS = 1 << 16 172 | """Required for these gateway events: 173 | 174 | * ``GUILD_SCHEDULED_EVENT_CREATE`` 175 | * ``GUILD_SCHEDULED_EVENT_UPDATE`` 176 | * ``GUILD_SCHEDULED_EVENT_DELETE`` 177 | * ``GUILD_SCHEDULED_EVENT_USER_ADD`` 178 | * ``GUILD_SCHEDULED_EVENT_USER_REMOVE`` 179 | """ 180 | AUTO_MODERATION_CONFIGURATION = 1 << 20 181 | """Required for these gateway events: 182 | 183 | * ``AUTO_MODERATION_RULE_CREATE`` 184 | * ``AUTO_MODERATION_RULE_UPDATE`` 185 | * ``AUTO_MODERATION_RULE_DELETE`` 186 | """ 187 | 188 | AUTO_MODERATION_EXECUTION = 1 << 21 189 | """Required for these gateway events: 190 | 191 | * ``AUTO_MODERATION_ACTION_EXECUTION`` 192 | """ 193 | 194 | UNPRIVILEGED = ( 195 | GUILDS 196 | | GUILD_EMOJIS 197 | | GUILD_INTEGRATIONS 198 | | GUILD_WEBHOOKS 199 | | GUILD_INVITES 200 | | GUILD_VOICE_STATES 201 | | GUILD_MESSAGE_REACTIONS 202 | | GUILD_MESSAGE_TYPING 203 | | GUILD_MESSAGES 204 | | DIRECT_MESSAGES 205 | | DIRECT_MESSAGE_TYPING 206 | | DIRECT_MESSAGE_REACTIONS 207 | | AUTO_MODERATION_CONFIGURATION 208 | | AUTO_MODERATION_EXECUTION 209 | ) 210 | """All unprivileged intents.""" 211 | PRIVILEGED = MESSAGE_CONTENT | GUILD_MEMBERS | GUILD_PRESENCES 212 | """All privileged intents.""" 213 | ALL = PRIVILEGED | UNPRIVILEGED 214 | """All intents.""" 215 | -------------------------------------------------------------------------------- /wyvern/models/users.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import annotations 24 | 25 | import typing 26 | 27 | import attrs 28 | 29 | from wyvern.models.abc import ImplementsMessage 30 | from wyvern.models.snowflake import DiscordObject, Snowflake 31 | from wyvern.utils.consts import UNDEFINED, Undefined 32 | 33 | if typing.TYPE_CHECKING: 34 | import datetime 35 | 36 | import discord_typings 37 | 38 | from wyvern.api.bot import GatewayBot 39 | 40 | 41 | @attrs.define(kw_only=True) 42 | class UserLike(DiscordObject): 43 | """Object representing a user like entity ( user, member, thread member, etc...).""" 44 | 45 | id: Snowflake 46 | """ID of the particular user.""" 47 | username: str 48 | """Username of the user.""" 49 | discriminator: str 50 | """Discriminator of the user.""" 51 | 52 | def __str__(self) -> str: 53 | return self.tag 54 | 55 | @property 56 | def created_at(self) -> datetime.datetime: 57 | """ 58 | Returns 59 | ------- 60 | datetime.datetime 61 | The datetime when this user was created. 62 | """ 63 | return super().created_at 64 | 65 | @property 66 | def tag(self) -> str: 67 | """Return user tag ( ``username#disciminator`` )""" 68 | return f"{self.username}#{self.discriminator}" 69 | 70 | 71 | @attrs.define(kw_only=True) 72 | class PartialUser(UserLike): 73 | """Object representing a user object.""" 74 | 75 | raw: discord_typings.UserData 76 | """The raw data from discord this object was constructed from.""" 77 | id: Snowflake 78 | """ID of the user.""" 79 | username: str 80 | """Username of the user.""" 81 | discriminator: str 82 | """User's discriminator.""" 83 | avatar_hash: str | None 84 | """Hash of the user's avatar""" 85 | is_bot: bool 86 | """True if the user is a bot.""" 87 | is_system: bool 88 | """True if the user is a system user.""" 89 | is_mfa_enabled: bool 90 | """True if the user has mfa enabled ( can be false even if it's enabled )""" 91 | banner_hash: str | None 92 | """User's banner hash.""" 93 | accent_color: int | None 94 | """User's accent color.""" 95 | locale: str | None 96 | """User's locale.""" 97 | flags_value: int | None 98 | """Integer value for user flags""" 99 | premium_type_value: int | None 100 | """Premium type integer.""" 101 | public_flags_value: int | None 102 | """Integer value for user's public flags""" 103 | 104 | @classmethod 105 | def from_payload(cls, payload: discord_typings.UserData) -> PartialUser: 106 | return PartialUser( 107 | raw=payload, 108 | id=Snowflake(payload["id"]), 109 | username=payload["username"], 110 | discriminator=payload["discriminator"], 111 | avatar_hash=payload.get("avatar"), 112 | is_bot=payload.get("bot", False), 113 | is_system=payload.get("system", False), 114 | is_mfa_enabled=payload.get("mfa_enabled", False), 115 | banner_hash=payload.get("banner"), 116 | accent_color=payload.get("accent_color"), 117 | locale=payload.get("locale"), 118 | flags_value=payload.get("flags"), 119 | premium_type_value=payload.get("premium_type"), 120 | public_flags_value=payload.get("public_flags"), 121 | ) 122 | 123 | 124 | @attrs.define(kw_only=True) 125 | class User(PartialUser, ImplementsMessage): 126 | bot: GatewayBot 127 | """The current bot application.""" 128 | partial_user: Undefined | PartialUser = attrs.field(default=UNDEFINED) 129 | """The partial user this class was constructed from, if any.""" 130 | 131 | async def create_message(self, content: str) -> ...: 132 | return await super().create_message(content) 133 | 134 | @classmethod 135 | def from_partial(cls, bot: GatewayBot, partial_user: PartialUser) -> User: 136 | return User( 137 | raw=partial_user.raw, 138 | id=partial_user.id, 139 | username=partial_user.username, 140 | discriminator=partial_user.discriminator, 141 | avatar_hash=partial_user.avatar_hash, 142 | is_bot=partial_user.is_bot, 143 | is_system=partial_user.is_system, 144 | is_mfa_enabled=partial_user.is_mfa_enabled, 145 | banner_hash=partial_user.banner_hash, 146 | accent_color=partial_user.accent_color, 147 | locale=partial_user.locale, 148 | flags_value=partial_user.flags_value, 149 | premium_type_value=partial_user.premium_type_value, 150 | public_flags_value=partial_user.public_flags_value, 151 | bot=bot, 152 | partial_user=partial_user, 153 | ) 154 | 155 | 156 | class BotUser(User): 157 | @classmethod 158 | def from_partial(cls, bot: GatewayBot, partial_user: PartialUser) -> BotUser: 159 | return BotUser( 160 | raw=partial_user.raw, 161 | id=partial_user.id, 162 | username=partial_user.username, 163 | discriminator=partial_user.discriminator, 164 | avatar_hash=partial_user.avatar_hash, 165 | is_bot=partial_user.is_bot, 166 | is_system=partial_user.is_system, 167 | is_mfa_enabled=partial_user.is_mfa_enabled, 168 | banner_hash=partial_user.banner_hash, 169 | accent_color=partial_user.accent_color, 170 | locale=partial_user.locale, 171 | flags_value=partial_user.flags_value, 172 | premium_type_value=partial_user.premium_type_value, 173 | public_flags_value=partial_user.public_flags_value, 174 | bot=bot, 175 | partial_user=partial_user, 176 | ) 177 | -------------------------------------------------------------------------------- /wyvern/builders/embeds.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import annotations 24 | 25 | import datetime 26 | import enum 27 | import json 28 | import typing 29 | 30 | import attrs 31 | 32 | from wyvern.colors import Color 33 | from wyvern.utils.consts import EMPTY, Empty 34 | 35 | __all__: tuple[str, ...] = ("Embed", "EmbedType") 36 | 37 | 38 | class EmbedType(enum.Enum): 39 | RICH = "rich" 40 | IMAGE = "image" 41 | VIDEO = "video" 42 | GIFV = "gifv" 43 | ARTICLE = "article" 44 | LINK = "link" 45 | 46 | 47 | # from wyvern.logger import main_logger 48 | @attrs.define(kw_only=True, slots=True) 49 | class EmbedAuthor: 50 | name: str | Empty = EMPTY 51 | url: str | Empty = EMPTY 52 | icon_url: str | Empty = EMPTY 53 | proxy_icon_url: str | Empty = EMPTY 54 | 55 | def to_dict(self) -> dict[str, typing.Any]: 56 | return { 57 | "name": Empty.verify(self.name), 58 | "url": Empty.verify(self.url), 59 | "icon_url": Empty.verify(self.icon_url), 60 | } 61 | 62 | 63 | @attrs.define(kw_only=True, slots=True) 64 | class EmbedFooter: 65 | name: str | Empty = EMPTY 66 | icon_url: str | Empty = EMPTY 67 | proxy_icon_url: str | None = None 68 | 69 | def to_dict(self) -> dict[str, typing.Any]: 70 | return { 71 | "name": Empty.verify(self.name), 72 | "icon_url": Empty.verify(self.icon_url), 73 | } 74 | 75 | 76 | @attrs.define(kw_only=True, slots=True) 77 | class EmbedField: 78 | name: str 79 | value: str 80 | inline: bool = False 81 | 82 | def to_dict(self) -> dict[str, typing.Any]: 83 | return { 84 | "name": self.name, 85 | "value": self.value, 86 | "inline": self.inline, 87 | } 88 | 89 | 90 | @attrs.define(kw_only=True, slots=True) 91 | class _Media: 92 | url: str | None 93 | proxy_url: str | Empty = EMPTY 94 | height: int | Empty = EMPTY 95 | width: int | Empty = EMPTY 96 | 97 | def to_dict(self) -> dict[str, typing.Any]: 98 | return {"url": self.url} 99 | 100 | 101 | @attrs.define(kw_only=True, slots=True) 102 | class EmbedThumbnail(_Media): 103 | url: str 104 | 105 | 106 | @attrs.define(kw_only=True, slots=True) 107 | class EmbedImage(_Media): 108 | url: str 109 | 110 | 111 | @attrs.define(kw_only=True, slots=True) 112 | class EmbedVideo(_Media): 113 | url: str | None 114 | 115 | 116 | class Embed: 117 | type: EmbedType 118 | base_dict: dict[str, typing.Any] 119 | 120 | def __init__( 121 | self, 122 | title: typing.Any = EMPTY, 123 | description: typing.Any = EMPTY, 124 | type: EmbedType = EmbedType.RICH, 125 | url: str | Empty = EMPTY, 126 | color: Color | int | Empty = EMPTY, 127 | colour: Color | int | Empty = EMPTY, 128 | timestamp: datetime.datetime | Empty = EMPTY, 129 | ) -> None: 130 | self.type = type 131 | self.base_dict = { 132 | "title": Empty.verify(title), 133 | "description": Empty.verify(description), 134 | "url": Empty.verify(url), 135 | "color": int(c) if (c := Empty.verify(color) or Empty.verify(colour)) is not None else None, 136 | "timestamp": timestamp.timestamp() if not isinstance(timestamp, Empty) else None, 137 | } 138 | 139 | def to_dict(self) -> dict[str, typing.Any]: 140 | return self.base_dict 141 | 142 | @classmethod 143 | def from_dict(cls, data: dict[str, typing.Any] | str, *, loads: bool = False) -> Embed: 144 | instance = cls() 145 | instance.base_dict = data if isinstance(data, dict) else json.loads(data) 146 | return instance 147 | 148 | @property 149 | def title(self) -> str | None: 150 | return self.base_dict.get("title") 151 | 152 | @title.setter 153 | def set_title(self, value: typing.Any) -> None: 154 | self.base_dict["title"] = str(value) 155 | 156 | @property 157 | def description(self) -> str | None: 158 | return self.base_dict.get("description") 159 | 160 | @description.setter 161 | def set_desc(self, other: typing.Any) -> None: 162 | self.base_dict["description"] = str(other) 163 | 164 | @property 165 | def fields(self) -> list[EmbedField]: 166 | return [EmbedField(**field) for field in self.base_dict.get("fields", [])] 167 | 168 | @property 169 | def author(self) -> EmbedAuthor | None: 170 | return EmbedAuthor(**author) if (author := self.base_dict.get("author")) else None 171 | 172 | @property 173 | def footer(self) -> EmbedFooter | None: 174 | return EmbedFooter(**footer) if (footer := self.base_dict.get("footer")) else None 175 | 176 | @property 177 | def image(self) -> EmbedImage | None: 178 | return EmbedImage(**image) if (image := self.base_dict.get("image")) else None 179 | 180 | @property 181 | def video(self) -> EmbedVideo | None: 182 | return EmbedVideo(**video) if (video := self.base_dict.get("video")) else None 183 | 184 | @property 185 | def thumbnail(self) -> EmbedThumbnail | None: 186 | return EmbedThumbnail(**thumbnail) if (thumbnail := self.base_dict.get("thumbnail")) else None 187 | 188 | def set_author(self, *, name: str, icon_url: str | Empty = EMPTY, url: str | Empty = EMPTY) -> Embed: 189 | self.base_dict["author"] = EmbedAuthor(name=name, url=url, icon_url=icon_url).to_dict() 190 | return self 191 | 192 | def add_field(self, name: typing.Any, value: typing.Any, *, inline: bool = False) -> Embed: 193 | self.base_dict.setdefault("fields", []).append(EmbedField(name=name, value=value, inline=inline).to_dict()) 194 | return self 195 | 196 | def set_image(self, url: str) -> Embed: 197 | self.base_dict["image"] = (EmbedImage(url=url)).to_dict() 198 | return self 199 | 200 | def set_thumbnail(self, url: str) -> Embed: 201 | self.base_dict["thumbnail"] = EmbedThumbnail(url=url).to_dict() 202 | return self 203 | -------------------------------------------------------------------------------- /wyvern/colors.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import annotations 24 | 25 | import colorsys 26 | import random 27 | import re 28 | import typing 29 | 30 | __all__: tuple[str, ...] = ("Color", "Colour") 31 | 32 | 33 | @typing.final 34 | class Color: 35 | """ 36 | Class representing a color in the RGB color space. 37 | Alias name Colour exists for convenience. 38 | 39 | Attributes 40 | ---------- 41 | value : int 42 | The value of the color. This is a 24-bit integer, where the first 8 bits are the red value, 43 | the next 8 bits are the green value, and the last 8 bits are the blue value. 44 | """ 45 | 46 | __slots__: tuple[str, ...] = ("value",) 47 | 48 | RGB_REGEX: re.Pattern[str] = re.compile(r"rgb\((\d{1,3}), (\d{1,3}), (\d{1,3})\)") 49 | HSL_REGEX: re.Pattern[str] = re.compile(r"hsl\((\d{1,3}), (\d{1,3})%, (\d{1,3})%\)") 50 | HSV_REGEX: re.Pattern[str] = re.compile(r"hsv\((\d{1,3}), (\d{1,3})%, (\d{1,3})%\)") 51 | HEX_REGEX: re.Pattern[str] = re.compile(r"#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})") 52 | 53 | def __init__(self, value: int) -> None: 54 | self.value = value 55 | 56 | def __repr__(self) -> str: 57 | return f"Color({self.value})" 58 | 59 | def __eq__(self, other: object) -> bool: 60 | return isinstance(other, Color) and self.value == other.value 61 | 62 | def __hash__(self) -> int: 63 | return hash(self.value) 64 | 65 | def __ne__(self, other: object) -> bool: 66 | return self is not other 67 | 68 | @classmethod 69 | def from_hex(cls, hex_value: str) -> Color: 70 | """ 71 | Creates a Color object from a hex value. 72 | 73 | Parameters 74 | ---------- 75 | 76 | hex_value: str 77 | The hex value to use. 78 | 79 | Returns 80 | ------- 81 | 82 | Color 83 | A Color object. 84 | 85 | Examples 86 | -------- 87 | 88 | >>> Color.from_hex('#ff0000') 89 | Color(16776960) 90 | >>> Color.from_hex('#00ff00') 91 | Color(255) 92 | >>> Color.from_hex('#0000ff') 93 | Color(0) 94 | """ 95 | if match := cls.HEX_REGEX.match(hex_value): 96 | hex_value = match.group(1) 97 | if len(hex_value) == 3: 98 | hex_value = "".join(c * 2 for c in hex_value) 99 | return cls(int(hex_value, 16)) 100 | raise ValueError(f"Invalid hex value: {hex_value}") 101 | 102 | @classmethod 103 | def from_rgb(cls, r: int, g: int, b: int) -> Color: 104 | """ 105 | Creates a Color object from RGB values. 106 | 107 | Parameters 108 | ---------- 109 | r: int 110 | The red value. 111 | g: int 112 | The green value. 113 | b: int 114 | The blue value. 115 | 116 | Returns 117 | ------- 118 | Color 119 | A Color object. 120 | 121 | Examples 122 | -------- 123 | 124 | >>> Color.from_rgb(255, 0, 0) 125 | Color(16711680) 126 | >>> Color.from_rgb(0, 255, 0) 127 | Color(65280) 128 | >>> Color.from_rgb(0, 0, 255) 129 | Color(255) 130 | """ 131 | return cls((r << 16) + (g << 8) + b) 132 | 133 | @classmethod 134 | def from_hsv(cls, h: float, s: float, v: float) -> Color: 135 | """ 136 | Creates a Color object from HSV values. 137 | 138 | Parameters 139 | ---------- 140 | h: float 141 | The hue value. 142 | s: float 143 | The saturation value. 144 | v: float 145 | The value in HSV color space. 146 | 147 | Returns 148 | ------- 149 | Color 150 | A Color object. 151 | 152 | Examples 153 | -------- 154 | 155 | >>> Color.from_hsv(0, 1, 1) 156 | Color(16711680) 157 | >>> Color.from_hsv(120, 1, 1) 158 | Color(16711680) 159 | >>> Color.from_hsv(240, 1, 1) 160 | Color(16711680) 161 | 162 | """ 163 | return cls.from_rgb(*[int(round(c * 255)) for c in colorsys.hsv_to_rgb(h, s, v)]) 164 | 165 | @classmethod 166 | def from_hsl(cls, h: float, s: float, l: float) -> Color: # noqa: E741 167 | """ 168 | Creates a Color object from HSL values. 169 | 170 | Parameters 171 | ---------- 172 | h: float 173 | The hue value. 174 | s: float 175 | The saturation value. 176 | l: float 177 | The lightness value. 178 | 179 | Returns 180 | ------- 181 | Color 182 | A Color object. 183 | 184 | Examples 185 | -------- 186 | 187 | >>> Color.from_hsl(0, 1, 0.5) 188 | Color(16711680) 189 | >>> Color.from_hsl(120, 1, 0.5) 190 | Color(16711680) 191 | >>> Color.from_hsl(240, 1, 0.5) 192 | Color(16711680) 193 | """ 194 | return cls.from_rgb(*[int(round(c * 255)) for c in colorsys.hls_to_rgb(h, l, s)]) 195 | 196 | @classmethod 197 | def from_random(cls) -> Color: 198 | """ 199 | Creates a Color object from a random color. Randomly generates a color in the RGB color space. 200 | 201 | Returns 202 | ------- 203 | 204 | Color 205 | A Color object. 206 | """ 207 | return cls.from_rgb(*[random.randint(0, 255) for _ in range(3)]) 208 | 209 | @classmethod 210 | def from_string(cls, string: str) -> Color: 211 | """ 212 | Creates a Color object from a string. 213 | 214 | Parameters 215 | ---------- 216 | 217 | string: str 218 | The string to use. 219 | 220 | Returns 221 | ------- 222 | 223 | Color 224 | A Color object. 225 | 226 | Examples 227 | -------- 228 | 229 | >>> Color.from_string('rgb(255, 0, 0)') 230 | Color(16711680) 231 | >>> Color.from_string('hsl(0, 100%, 50%)') 232 | Color(-80727249750) 233 | >>> Color.from_string('hsv(0, 100%, 100%)') 234 | Color(1022371500) 235 | >>> Color.from_string('#ff0000') 236 | Color(16776960) 237 | 238 | """ 239 | if string.startswith("#"): 240 | return cls.from_hex(string) 241 | elif match := cls.RGB_REGEX.match(string): 242 | return cls.from_rgb(*[int(c) for c in match.groups()]) 243 | elif match := cls.HSL_REGEX.match(string): 244 | return cls.from_hsl(*[float(c) for c in match.groups()]) 245 | elif match := cls.HSV_REGEX.match(string): 246 | return cls.from_hsv(*[float(c) for c in match.groups()]) 247 | raise ValueError(f"Invalid color string: {string}") 248 | 249 | @classmethod 250 | def default(cls) -> Color: 251 | """ 252 | Creates a Color object from the default color. This is ``0x000000``. (Black) 253 | 254 | Examples 255 | -------- 256 | 257 | >>> Color.default() 258 | Color(0) 259 | 260 | """ 261 | return cls(0x000000) 262 | 263 | @property 264 | def hex(self) -> str: 265 | """The hex value of the color.""" 266 | return f"#{self.value:06x}" 267 | 268 | @property 269 | def rgb(self) -> tuple[int, int, int]: 270 | """The RGB values of the color.""" 271 | return (self.value >> 16) & 0xFF, (self.value >> 8) & 0xFF, self.value & 0xFF 272 | 273 | @property 274 | def hsv(self) -> tuple[float, float, float]: 275 | """The HSV values of the color.""" 276 | return colorsys.rgb_to_hsv(*(c / 255 for c in self.rgb)) 277 | 278 | @property 279 | def hsl(self) -> tuple[float, float, float]: 280 | """The HSL values of the color.""" 281 | return colorsys.rgb_to_hls(*(c / 255 for c in self.rgb)) 282 | 283 | @property 284 | def r(self) -> int: 285 | """The red value of the color.""" 286 | return self.rgb[0] 287 | 288 | @property 289 | def g(self) -> int: 290 | """The green value of the color.""" 291 | return self.rgb[1] 292 | 293 | @property 294 | def b(self) -> int: 295 | """The blue value of the color.""" 296 | return self.rgb[2] 297 | 298 | @classmethod 299 | def red(cls) -> Color: 300 | """Creates a Color object from the red color. This is ``0xff0000``. (Red)""" 301 | return cls(0xFF0000) 302 | 303 | @classmethod 304 | def green(cls) -> Color: 305 | """Creates a Color object from the green color. This is ``0x00ff00``. (Green)""" 306 | return cls(0x00FF00) 307 | 308 | @classmethod 309 | def blue(cls) -> Color: 310 | """Creates a Color object from the blue color. This is ``0x0000ff``. (Blue)""" 311 | return cls(0x0000FF) 312 | 313 | @classmethod 314 | def yellow(cls) -> Color: 315 | """Creates a Color object from the yellow color. This is ``0xffff00``. (Yellow)""" 316 | return cls(0xFFFF00) 317 | 318 | @classmethod 319 | def cyan(cls) -> Color: 320 | """Creates a Color object from the cyan color. This is ``0x00ffff``. (Cyan)""" 321 | return cls(0x00FFFF) 322 | 323 | @classmethod 324 | def magenta(cls) -> Color: 325 | """Creates a Color object from the magenta color. This is ``0xff00ff``. (Magenta)""" 326 | return cls(0xFF00FF) 327 | 328 | @classmethod 329 | def black(cls) -> Color: 330 | """Creates a Color object from the black color. This is ``0x000000``. (Black)""" 331 | return cls(0x000000) 332 | 333 | @classmethod 334 | def white(cls) -> Color: 335 | """Creates a Color object from the white color. This is ``0xffffff``. (White)""" 336 | return cls(0xFFFFFF) 337 | 338 | @classmethod 339 | def gray(cls) -> Color: 340 | """Creates a Color object from the gray color. This is ``0x808080``. (Gray)""" 341 | return cls(0x808080) 342 | 343 | @classmethod 344 | def grey(cls) -> Color: 345 | """Creates a Color object from the grey color. This is ``0x808080``. (Grey)""" 346 | return cls(0x808080) 347 | 348 | @classmethod 349 | def orange(cls) -> Color: 350 | """Creates a Color object from the orange color. This is ``0xffa500``. (Orange)""" 351 | return cls(0xFFA500) 352 | 353 | @classmethod 354 | def purple(cls) -> Color: 355 | """Creates a Color object from the purple color. This is ``#800080``. (Purple)""" 356 | return cls(0x800080) 357 | 358 | @classmethod 359 | def brown(cls) -> Color: 360 | """Creates a Color object from the brown color. This is ``#a52a2a``. (Brown)""" 361 | return cls(0xA52A2A) 362 | 363 | @classmethod 364 | def silver(cls) -> Color: 365 | """Creates a Color object from the silver color. This is ``#c0c0c0``. (Silver)""" 366 | return cls(0xC0C0C0) 367 | 368 | @classmethod 369 | def aqua(cls) -> Color: 370 | """Creates a Color object from the aqua color. This is ``#00ffff``. (Aqua)""" 371 | return cls(0x00FFFF) 372 | 373 | 374 | Colour = Color 375 | """An alias for the :class:`Color` class.""" 376 | -------------------------------------------------------------------------------- /docs/_static/style.css: -------------------------------------------------------------------------------- 1 | body{ 2 | font-size: small; 3 | } 4 | 5 | @media not print { 6 | 7 | body[data-theme="dark"] .highlight body[data-theme="dark"] .hll { background-color: #ebdbb2 } 8 | body[data-theme="dark"] .highlight { background: #282828; color: #dddddd } 9 | body[data-theme="dark"] .highlight .c { color: #928374; font-style: italic } /* Comment */ 10 | body[data-theme="dark"] .highlight .err { color: #282828; background-color: #fb4934 } /* Error */ 11 | body[data-theme="dark"] .highlight .esc { color: #dddddd } /* Escape */ 12 | body[data-theme="dark"] .highlight .g { color: #dddddd } /* Generic */ 13 | body[data-theme="dark"] .highlight .k { color: #fb4934 } /* Keyword */ 14 | body[data-theme="dark"] .highlight .l { color: #dddddd } /* Literal */ 15 | body[data-theme="dark"] .highlight .n { color: #dddddd } /* Name */ 16 | body[data-theme="dark"] .highlight .o { color: #dddddd } /* Operator */ 17 | body[data-theme="dark"] .highlight .x { color: #dddddd } /* Other */ 18 | body[data-theme="dark"] .highlight .p { color: #dddddd } /* Punctuation */ 19 | body[data-theme="dark"] .highlight .ch { color: #928374; font-style: italic } /* Commentbody[data-theme="dark"] .hashbang */ 20 | body[data-theme="dark"] .highlight .cm { color: #928374; font-style: italic } /* Comment.Multiline */ 21 | body[data-theme="dark"] .highlight .c-PreProc { color: #8ec07c; font-style: italic } /* Comment.PreProc */ 22 | body[data-theme="dark"] .highlight .cp { color: #928374; font-style: italic } /* Comment.Preproc */ 23 | body[data-theme="dark"] .highlight .cpf { color: #928374; font-style: italic } /* Comment.PreprocFile */ 24 | body[data-theme="dark"] .highlight .c1 { color: #928374; font-style: italic } /* Comment.Single */ 25 | body[data-theme="dark"] .highlight .cs { color: #ebdbb2; font-weight: bold; font-style: italic } /* Comment.Special */ 26 | body[data-theme="dark"] .highlight .gd { color: #282828; background-color: #fb4934 } /* Generic.Deleted */ 27 | body[data-theme="dark"] .highlight .ge { color: #dddddd; font-style: italic } /* Generic.Emph */ 28 | body[data-theme="dark"] .highlight .gr { color: #fb4934 } /* Generic.Error */ 29 | body[data-theme="dark"] .highlight .gh { color: #ebdbb2; font-weight: bold } /* Genericbody[data-theme="dark"] .heading */ 30 | body[data-theme="dark"] .highlight .gi { color: #282828; background-color: #b8bb26 } /* Generic.Inserted */ 31 | body[data-theme="dark"] .highlight .go { color: #f2e5bc } /* Generic.Output */ 32 | body[data-theme="dark"] .highlight .gp { color: #a89984 } /* Generic.Prompt */ 33 | body[data-theme="dark"] .highlight .gs { color: #dddddd; font-weight: bold } /* Generic.Strong */ 34 | body[data-theme="dark"] .highlight .gu { color: #ebdbb2; text-decoration: underline } /* Generic.Subheading */ 35 | body[data-theme="dark"] .highlight .gt { color: #fb4934 } /* Generic.Traceback */ 36 | body[data-theme="dark"] .highlight .kc { color: #fb4934 } /* Keyword.Constant */ 37 | body[data-theme="dark"] .highlight .kd { color: #fb4934 } /* Keyword.Declaration */ 38 | body[data-theme="dark"] .highlight .kn { color: #fb4934 } /* Keyword.Namespace */ 39 | body[data-theme="dark"] .highlight .kp { color: #fb4934 } /* Keyword.Pseudo */ 40 | body[data-theme="dark"] .highlight .kr { color: #fb4934 } /* Keyword.Reserved */ 41 | body[data-theme="dark"] .highlight .kt { color: #fb4934 } /* Keyword.Type */ 42 | body[data-theme="dark"] .highlight .ld { color: #dddddd } /* Literal.Date */ 43 | body[data-theme="dark"] .highlight .m { color: #d3869b } /* Literal.Number */ 44 | body[data-theme="dark"] .highlight .s { color: #b8bb26 } /* Literal.String */ 45 | body[data-theme="dark"] .highlight .na { color: #fabd2f } /* Name.Attribute */ 46 | body[data-theme="dark"] .highlight .nb { color: #fe8019 } /* Name.Builtin */ 47 | body[data-theme="dark"] .highlight .nc { color: #8ec07c } /* Name.Class */ 48 | body[data-theme="dark"] .highlight .no { color: #d3869b } /* Name.Constant */ 49 | body[data-theme="dark"] .highlight .nd { color: #fb4934 } /* Name.Decorator */ 50 | body[data-theme="dark"] .highlight .ni { color: #dddddd } /* Name.Entity */ 51 | body[data-theme="dark"] .highlight .ne { color: #fb4934 } /* Name.Exception */ 52 | body[data-theme="dark"] .highlight .nf { color: #8ec07c } /* Name.Function */ 53 | body[data-theme="dark"] .highlight .nl { color: #dddddd } /* Name.Label */ 54 | body[data-theme="dark"] .highlight .nn { color: #8ec07c } /* Name.Namespace */ 55 | body[data-theme="dark"] .highlight .nx { color: #dddddd } /* Name.Other */ 56 | body[data-theme="dark"] .highlight .py { color: #dddddd } /* Name.Property */ 57 | body[data-theme="dark"] .highlight .nt { color: #8ec07c } /* Name.Tag */ 58 | body[data-theme="dark"] .highlight .nv { color: #83a598 } /* Name.Variable */ 59 | body[data-theme="dark"] .highlight .ow { color: #fb4934 } /* Operator.Word */ 60 | body[data-theme="dark"] .highlight .pm { color: #dddddd } /* Punctuation.Marker */ 61 | body[data-theme="dark"] .highlight .w { color: #dddddd } /* Text.Whitespace */ 62 | body[data-theme="dark"] .highlight .mb { color: #d3869b } /* Literal.Number.Bin */ 63 | body[data-theme="dark"] .highlight .mf { color: #d3869b } /* Literal.Number.Float */ 64 | body[data-theme="dark"] .highlight .mh { color: #d3869b } /* Literal.Numberbody[data-theme="dark"] .hex */ 65 | body[data-theme="dark"] .highlight .mi { color: #d3869b } /* Literal.Number.Integer */ 66 | body[data-theme="dark"] .highlight .mo { color: #d3869b } /* Literal.Number.Oct */ 67 | body[data-theme="dark"] .highlight .sa { color: #b8bb26 } /* Literal.String.Affix */ 68 | body[data-theme="dark"] .highlight .sb { color: #b8bb26 } /* Literal.String.Backtick */ 69 | body[data-theme="dark"] .highlight .sc { color: #b8bb26 } /* Literal.String.Char */ 70 | body[data-theme="dark"] .highlight .dl { color: #b8bb26 } /* Literal.String.Delimiter */ 71 | body[data-theme="dark"] .highlight .sd { color: #b8bb26 } /* Literal.String.Doc */ 72 | body[data-theme="dark"] .highlight .s2 { color: #b8bb26 } /* Literal.String.Double */ 73 | body[data-theme="dark"] .highlight .se { color: #fe8019 } /* Literal.String.Escape */ 74 | body[data-theme="dark"] .highlight .sh { color: #b8bb26 } /* Literal.Stringbody[data-theme="dark"] .heredoc */ 75 | body[data-theme="dark"] .highlight .si { color: #b8bb26 } /* Literal.String.Interpol */ 76 | body[data-theme="dark"] .highlight .sx { color: #b8bb26 } /* Literal.String.Other */ 77 | body[data-theme="dark"] .highlight .sr { color: #b8bb26 } /* Literal.String.Regex */ 78 | body[data-theme="dark"] .highlight .s1 { color: #b8bb26 } /* Literal.String.Single */ 79 | body[data-theme="dark"] .highlight .ss { color: #b8bb26 } /* Literal.String.Symbol */ 80 | body[data-theme="dark"] .highlight .bp { color: #fe8019 } /* Name.Builtin.Pseudo */ 81 | body[data-theme="dark"] .highlight .fm { color: #8ec07c } /* Name.Function.Magic */ 82 | body[data-theme="dark"] .highlight .vc { color: #83a598 } /* Name.Variable.Class */ 83 | body[data-theme="dark"] .highlight .vg { color: #83a598 } /* Name.Variable.Global */ 84 | body[data-theme="dark"] .highlight .vi { color: #83a598 } /* Name.Variable.Instance */ 85 | body[data-theme="dark"] .highlight .vm { color: #83a598 } /* Name.Variable.Magic */ 86 | body[data-theme="dark"] .highlight .il { color: #d3869b } /* Literal.Number.Integer.Long */ 87 | } 88 | @media (prefers-color-scheme: dark) { 89 | 90 | body[data-theme="dark"] .highlight body[data-theme="dark"] .hll { background-color: #ebdbb2 } 91 | body[data-theme="dark"] .highlight { background: #282828; color: #dddddd } 92 | body[data-theme="dark"] .highlight .c { color: #928374; font-style: italic } /* Comment */ 93 | body[data-theme="dark"] .highlight .err { color: #282828; background-color: #fb4934 } /* Error */ 94 | body[data-theme="dark"] .highlight .esc { color: #dddddd } /* Escape */ 95 | body[data-theme="dark"] .highlight .g { color: #dddddd } /* Generic */ 96 | body[data-theme="dark"] .highlight .k { color: #fb4934 } /* Keyword */ 97 | body[data-theme="dark"] .highlight .l { color: #dddddd } /* Literal */ 98 | body[data-theme="dark"] .highlight .n { color: #dddddd } /* Name */ 99 | body[data-theme="dark"] .highlight .o { color: #dddddd } /* Operator */ 100 | body[data-theme="dark"] .highlight .x { color: #dddddd } /* Other */ 101 | body[data-theme="dark"] .highlight .p { color: #dddddd } /* Punctuation */ 102 | body[data-theme="dark"] .highlight .ch { color: #928374; font-style: italic } /* Commentbody[data-theme="dark"] .hashbang */ 103 | body[data-theme="dark"] .highlight .cm { color: #928374; font-style: italic } /* Comment.Multiline */ 104 | body[data-theme="dark"] .highlight .c-PreProc { color: #8ec07c; font-style: italic } /* Comment.PreProc */ 105 | body[data-theme="dark"] .highlight .cp { color: #928374; font-style: italic } /* Comment.Preproc */ 106 | body[data-theme="dark"] .highlight .cpf { color: #928374; font-style: italic } /* Comment.PreprocFile */ 107 | body[data-theme="dark"] .highlight .c1 { color: #928374; font-style: italic } /* Comment.Single */ 108 | body[data-theme="dark"] .highlight .cs { color: #ebdbb2; font-weight: bold; font-style: italic } /* Comment.Special */ 109 | body[data-theme="dark"] .highlight .gd { color: #282828; background-color: #fb4934 } /* Generic.Deleted */ 110 | body[data-theme="dark"] .highlight .ge { color: #dddddd; font-style: italic } /* Generic.Emph */ 111 | body[data-theme="dark"] .highlight .gr { color: #fb4934 } /* Generic.Error */ 112 | body[data-theme="dark"] .highlight .gh { color: #ebdbb2; font-weight: bold } /* Genericbody[data-theme="dark"] .heading */ 113 | body[data-theme="dark"] .highlight .gi { color: #282828; background-color: #b8bb26 } /* Generic.Inserted */ 114 | body[data-theme="dark"] .highlight .go { color: #f2e5bc } /* Generic.Output */ 115 | body[data-theme="dark"] .highlight .gp { color: #a89984 } /* Generic.Prompt */ 116 | body[data-theme="dark"] .highlight .gs { color: #dddddd; font-weight: bold } /* Generic.Strong */ 117 | body[data-theme="dark"] .highlight .gu { color: #ebdbb2; text-decoration: underline } /* Generic.Subheading */ 118 | body[data-theme="dark"] .highlight .gt { color: #fb4934 } /* Generic.Traceback */ 119 | body[data-theme="dark"] .highlight .kc { color: #fb4934 } /* Keyword.Constant */ 120 | body[data-theme="dark"] .highlight .kd { color: #fb4934 } /* Keyword.Declaration */ 121 | body[data-theme="dark"] .highlight .kn { color: #fb4934 } /* Keyword.Namespace */ 122 | body[data-theme="dark"] .highlight .kp { color: #fb4934 } /* Keyword.Pseudo */ 123 | body[data-theme="dark"] .highlight .kr { color: #fb4934 } /* Keyword.Reserved */ 124 | body[data-theme="dark"] .highlight .kt { color: #fb4934 } /* Keyword.Type */ 125 | body[data-theme="dark"] .highlight .ld { color: #dddddd } /* Literal.Date */ 126 | body[data-theme="dark"] .highlight .m { color: #d3869b } /* Literal.Number */ 127 | body[data-theme="dark"] .highlight .s { color: #b8bb26 } /* Literal.String */ 128 | body[data-theme="dark"] .highlight .na { color: #fabd2f } /* Name.Attribute */ 129 | body[data-theme="dark"] .highlight .nb { color: #fe8019 } /* Name.Builtin */ 130 | body[data-theme="dark"] .highlight .nc { color: #8ec07c } /* Name.Class */ 131 | body[data-theme="dark"] .highlight .no { color: #d3869b } /* Name.Constant */ 132 | body[data-theme="dark"] .highlight .nd { color: #fb4934 } /* Name.Decorator */ 133 | body[data-theme="dark"] .highlight .ni { color: #dddddd } /* Name.Entity */ 134 | body[data-theme="dark"] .highlight .ne { color: #fb4934 } /* Name.Exception */ 135 | body[data-theme="dark"] .highlight .nf { color: #8ec07c } /* Name.Function */ 136 | body[data-theme="dark"] .highlight .nl { color: #dddddd } /* Name.Label */ 137 | body[data-theme="dark"] .highlight .nn { color: #8ec07c } /* Name.Namespace */ 138 | body[data-theme="dark"] .highlight .nx { color: #dddddd } /* Name.Other */ 139 | body[data-theme="dark"] .highlight .py { color: #dddddd } /* Name.Property */ 140 | body[data-theme="dark"] .highlight .nt { color: #8ec07c } /* Name.Tag */ 141 | body[data-theme="dark"] .highlight .nv { color: #83a598 } /* Name.Variable */ 142 | body[data-theme="dark"] .highlight .ow { color: #fb4934 } /* Operator.Word */ 143 | body[data-theme="dark"] .highlight .pm { color: #dddddd } /* Punctuation.Marker */ 144 | body[data-theme="dark"] .highlight .w { color: #dddddd } /* Text.Whitespace */ 145 | body[data-theme="dark"] .highlight .mb { color: #d3869b } /* Literal.Number.Bin */ 146 | body[data-theme="dark"] .highlight .mf { color: #d3869b } /* Literal.Number.Float */ 147 | body[data-theme="dark"] .highlight .mh { color: #d3869b } /* Literal.Numberbody[data-theme="dark"] .hex */ 148 | body[data-theme="dark"] .highlight .mi { color: #d3869b } /* Literal.Number.Integer */ 149 | body[data-theme="dark"] .highlight .mo { color: #d3869b } /* Literal.Number.Oct */ 150 | body[data-theme="dark"] .highlight .sa { color: #b8bb26 } /* Literal.String.Affix */ 151 | body[data-theme="dark"] .highlight .sb { color: #b8bb26 } /* Literal.String.Backtick */ 152 | body[data-theme="dark"] .highlight .sc { color: #b8bb26 } /* Literal.String.Char */ 153 | body[data-theme="dark"] .highlight .dl { color: #b8bb26 } /* Literal.String.Delimiter */ 154 | body[data-theme="dark"] .highlight .sd { color: #b8bb26 } /* Literal.String.Doc */ 155 | body[data-theme="dark"] .highlight .s2 { color: #b8bb26 } /* Literal.String.Double */ 156 | body[data-theme="dark"] .highlight .se { color: #fe8019 } /* Literal.String.Escape */ 157 | body[data-theme="dark"] .highlight .sh { color: #b8bb26 } /* Literal.Stringbody[data-theme="dark"] .heredoc */ 158 | body[data-theme="dark"] .highlight .si { color: #b8bb26 } /* Literal.String.Interpol */ 159 | body[data-theme="dark"] .highlight .sx { color: #b8bb26 } /* Literal.String.Other */ 160 | body[data-theme="dark"] .highlight .sr { color: #b8bb26 } /* Literal.String.Regex */ 161 | body[data-theme="dark"] .highlight .s1 { color: #b8bb26 } /* Literal.String.Single */ 162 | body[data-theme="dark"] .highlight .ss { color: #b8bb26 } /* Literal.String.Symbol */ 163 | body[data-theme="dark"] .highlight .bp { color: #fe8019 } /* Name.Builtin.Pseudo */ 164 | body[data-theme="dark"] .highlight .fm { color: #8ec07c } /* Name.Function.Magic */ 165 | body[data-theme="dark"] .highlight .vc { color: #83a598 } /* Name.Variable.Class */ 166 | body[data-theme="dark"] .highlight .vg { color: #83a598 } /* Name.Variable.Global */ 167 | body[data-theme="dark"] .highlight .vi { color: #83a598 } /* Name.Variable.Instance */ 168 | body[data-theme="dark"] .highlight .vm { color: #83a598 } /* Name.Variable.Magic */ 169 | body[data-theme="dark"] .highlight .il { color: #d3869b } /* Literal.Number.Integer.Long */ 170 | } 171 | -------------------------------------------------------------------------------- /wyvern/internals/rest.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Sarthak 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import annotations 24 | 25 | import typing 26 | 27 | import aiohttp 28 | import attrs 29 | import multidict 30 | 31 | from wyvern.logger import main_logger 32 | from wyvern.utils.consts import UNDEFINED, Undefined 33 | 34 | if typing.TYPE_CHECKING: 35 | from wyvern.api.bot import GatewayBot 36 | 37 | 38 | __all__: tuple[str, ...] = ("RESTClient", "Endpoints") 39 | 40 | 41 | @attrs.define(kw_only=True) 42 | class RESTClient: 43 | token: str 44 | bot: GatewayBot 45 | api_version: int 46 | client_session: aiohttp.ClientSession | Undefined = UNDEFINED 47 | 48 | @property 49 | def headers(self) -> dict[str, multidict.istr]: 50 | return {"Authorization": multidict.istr(f"GatewayBot {self.token}")} 51 | 52 | async def request(self, route: RequestRoute) -> typing.Any: 53 | headers = self.headers.copy() 54 | headers["Content-Type"] = multidict.istr("application/json") 55 | main_logger.debug(f"Creating a {route.type} request to {route.end_url} endpoint.") 56 | assert isinstance((session := self.client_session), aiohttp.ClientSession) 57 | res = await session.request(route.type, route.url, headers=headers, json=route.json) 58 | res.raise_for_status() 59 | if res.status in (200, 201): 60 | return await res.json() 61 | if res.status in (204, 304): 62 | return 63 | 64 | 65 | @attrs.define 66 | class RequestRoute: 67 | end_url: str 68 | api_version: int = 10 69 | type: str = "GET" 70 | json: dict[str, typing.Any] | None = None 71 | 72 | @property 73 | def url(self) -> str: 74 | return f"https://discord.com/api/v{self.api_version}/{self.end_url}" 75 | 76 | 77 | class Endpoints: 78 | @classmethod 79 | def guild_audit_logs(cls, guild_id: int) -> str: 80 | return f"guilds/{guild_id}/audit-logs" 81 | 82 | @classmethod 83 | def list_auto_moderation_rules(cls, guild_id: int) -> str: 84 | return f"guilds/{guild_id}/auto-moderation/rules" 85 | 86 | @classmethod 87 | def get_auto_moderation_rule(cls, guild_id: int, rule_id: int) -> str: 88 | return f"guilds/{guild_id}/auto-moderation/rules/{rule_id}" 89 | 90 | @classmethod 91 | def create_auto_moderation_rule(cls, guild_id: int) -> str: 92 | return f"guilds/{guild_id}/auto-moderation/rules" 93 | 94 | @classmethod 95 | def modify_auto_moderation_rule(cls, guild_id: int, rule_id: int) -> str: 96 | return f"guilds/{guild_id}/auto-moderation/rules/{rule_id}" 97 | 98 | @classmethod 99 | def delete_auto_moderation_rule(cls, guild_id: int, rule_id: int) -> str: 100 | return f"guilds/{guild_id}/auto-moderation/rules/{rule_id}" 101 | 102 | @classmethod 103 | def get_channel(cls, channel_id: int) -> str: 104 | return f"channels/{channel_id}" 105 | 106 | @classmethod 107 | def modify_channel(cls, channel_id: int) -> str: 108 | return f"channels/{channel_id}" 109 | 110 | @classmethod 111 | def delete_channel(cls, channel_id: int) -> str: 112 | return f"channels/{channel_id}" 113 | 114 | @classmethod 115 | def get_channel_messages(cls, channel_id: int) -> str: 116 | return f"channels/{channel_id}/messages" 117 | 118 | @classmethod 119 | def get_channel_message(cls, channel_id: int, message_id: int) -> str: 120 | return f"channels/{channel_id}/messages/{message_id}" 121 | 122 | @classmethod 123 | def create_message(cls, channel_id: int) -> str: 124 | return f"channels/{channel_id}/messages" 125 | 126 | @classmethod 127 | def crosspost_message(cls, channel_id: int, message_id: int) -> str: 128 | return f"channels/{channel_id}/messages/{message_id}/crosspost" 129 | 130 | @classmethod 131 | def create_reaction(cls, channel_id: int, message_id: int, emoji: str) -> str: 132 | return f"channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me" 133 | 134 | @classmethod 135 | def delete_own_reaction(cls, channel_id: int, message_id: int, emoji: str) -> str: 136 | return f"channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me" 137 | 138 | @classmethod 139 | def delete_user_reaction(cls, channel_id: int, message_id: int, emoji: str, user_id: int) -> str: 140 | return f"channels/{channel_id}/messages/{message_id}/reactions/{emoji}/{user_id}" 141 | 142 | @classmethod 143 | def get_user_reactions(cls, channel_id: int, message_id: int, emoji: str) -> str: 144 | return f"channels/{channel_id}/messages/{message_id}/reactions/{emoji}" 145 | 146 | @classmethod 147 | def delete_all_reactions(cls, channel_id: int, message_id: int) -> str: 148 | return f"channels/{channel_id}/messages/{message_id}/reactions" 149 | 150 | @classmethod 151 | def delete_all_reactions_for_emoji(cls, channel_id: int, message_id: int, emoji: str) -> str: 152 | return f"channels/{channel_id}/messages/{message_id}/reactions/{emoji}" 153 | 154 | @classmethod 155 | def edit_message(cls, channel_id: int, message_id: int) -> str: 156 | return f"channels/{channel_id}/messages/{message_id}" 157 | 158 | @classmethod 159 | def delete_message(cls, channel_id: int, message_id: int) -> str: 160 | return f"channels/{channel_id}/messages/{message_id}" 161 | 162 | @classmethod 163 | def bulk_delete_messages(cls, channel_id: int) -> str: 164 | return f"channels/{channel_id}/messages/bulk-delete" 165 | 166 | @classmethod 167 | def edit_channel_permissions(cls, channel_id: int, overwrite_id: int) -> str: 168 | return f"channels/{channel_id}/permissions/{overwrite_id}" 169 | 170 | @classmethod 171 | def get_channel_invites(cls, channel_id: int) -> str: 172 | return f"channels/{channel_id}/invites" 173 | 174 | @classmethod 175 | def create_channel_invite(cls, channel_id: int) -> str: 176 | return f"channels/{channel_id}/invites" 177 | 178 | @classmethod 179 | def delete_channel_permission(cls, channel_id: int, overwrite_id: int) -> str: 180 | return f"channels/{channel_id}/permissions/{overwrite_id}" 181 | 182 | @classmethod 183 | def follow_news_channel(cls, channel_id: int) -> str: 184 | return f"channels/{channel_id}/followers" 185 | 186 | @classmethod 187 | def trigger_typing_indicator(cls, channel_id: int) -> str: 188 | return f"channels/{channel_id}/typing" 189 | 190 | @classmethod 191 | def get_pinned_messages(cls, channel_id: int) -> str: 192 | return f"channels/{channel_id}/pins" 193 | 194 | @classmethod 195 | def add_pinned_channel_message(cls, channel_id: int, message_id: int) -> str: 196 | return f"channels/{channel_id}/pins/{message_id}" 197 | 198 | @classmethod 199 | def delete_pinned_channel_message(cls, channel_id: int, message_id: int) -> str: 200 | return f"channels/{channel_id}/pins/{message_id}" 201 | 202 | @classmethod 203 | def group_dm_add_recipient(cls, channel_id: int, user_id: int) -> str: 204 | return f"channels/{channel_id}/recipients/{user_id}" 205 | 206 | @classmethod 207 | def group_dm_remove_recipient(cls, channel_id: int, user_id: int) -> str: 208 | return f"channels/{channel_id}/recipients/{user_id}" 209 | 210 | @classmethod 211 | def start_thread_with_message(cls, channel_id: int, message_id: int) -> str: 212 | return f"channels/{channel_id}/messages/{message_id}/threads" 213 | 214 | @classmethod 215 | def start_thread_without_message(cls, channel_id: int) -> str: 216 | return f"channels/{channel_id}/threads" 217 | 218 | @classmethod 219 | def start_thread_in_forum(cls, channel_id: int) -> str: 220 | return f"channels/{channel_id}/threads" 221 | 222 | @classmethod 223 | def join_thread(cls, channel_id: int, thread_id: int) -> str: 224 | return f"channels/{channel_id}/threads/{thread_id}/members/@me" 225 | 226 | @classmethod 227 | def add_thread_member(cls, channel_id: int, thread_id: int, user_id: int) -> str: 228 | return f"channels/{channel_id}/threads/{thread_id}/members/{user_id}" 229 | 230 | @classmethod 231 | def leave_thread(cls, channel_id: int, thread_id: int) -> str: 232 | return f"channels/{channel_id}/threads/{thread_id}/members/@me" 233 | 234 | @classmethod 235 | def remove_thread_member(cls, channel_id: int, thread_id: int, user_id: int) -> str: 236 | return f"channels/{channel_id}/threads/{thread_id}/members/{user_id}" 237 | 238 | @classmethod 239 | def get_thread_member(cls, channel_id: int, thread_id: int, user_id: int) -> str: 240 | return f"channels/{channel_id}/threads/{thread_id}/members/{user_id}" 241 | 242 | @classmethod 243 | def list_thread_members(cls, channel_id: int) -> str: 244 | return f"channels/{channel_id}/threads-members" 245 | 246 | @classmethod 247 | def list_public_archived_threads(cls, channel_id: int) -> str: 248 | return f"channels/{channel_id}/threads/archived/public" 249 | 250 | @classmethod 251 | def list_private_archived_threads(cls, channel_id: int) -> str: 252 | return f"channels/{channel_id}/threads/archived/private" 253 | 254 | @classmethod 255 | def list_joined_private_archived_threads(cls, channel_id: int) -> str: 256 | return f"channels/{channel_id}/users/@me/threads/archived/private" 257 | 258 | @classmethod 259 | def list_guild_emojis(cls, guild_id: int) -> str: 260 | return f"guilds/{guild_id}/emojis" 261 | 262 | @classmethod 263 | def get_guild_emoji(cls, guild_id: int, emoji_id: int) -> str: 264 | return f"guilds/{guild_id}/emojis/{emoji_id}" 265 | 266 | @classmethod 267 | def create_guild_emoji(cls, guild_id: int) -> str: 268 | return f"guilds/{guild_id}/emojis" 269 | 270 | @classmethod 271 | def modify_guild_emoji(cls, guild_id: int, emoji_id: int) -> str: 272 | return f"guilds/{guild_id}/emojis/{emoji_id}" 273 | 274 | @classmethod 275 | def delete_guild_emoji(cls, guild_id: int, emoji_id: int) -> str: 276 | return f"guilds/{guild_id}/emojis/{emoji_id}" 277 | 278 | @classmethod 279 | def create_guild(cls) -> str: 280 | return "guilds" 281 | 282 | @classmethod 283 | def get_guild(cls, guild_id: int) -> str: 284 | return f"guilds/{guild_id}" 285 | 286 | @classmethod 287 | def get_guild_preview(cls, guild_id: int) -> str: 288 | return f"guilds/{guild_id}/preview" 289 | 290 | @classmethod 291 | def edit_guild(cls, guild_id: int) -> str: 292 | return f"guilds/{guild_id}" 293 | 294 | @classmethod 295 | def delete_guild(cls, guild_id: int) -> str: 296 | return f"guilds/{guild_id}" 297 | 298 | @classmethod 299 | def get_guild_channels(cls, guild_id: int) -> str: 300 | return f"guilds/{guild_id}/channels" 301 | 302 | @classmethod 303 | def create_guild_channel(cls, guild_id: int) -> str: 304 | return f"guilds/{guild_id}/channels" 305 | 306 | @classmethod 307 | def modify_guild_channel_positions(cls, guild_id: int) -> str: 308 | return f"guilds/{guild_id}/channels" 309 | 310 | @classmethod 311 | def active_guild_threads(cls, guild_id: int) -> str: 312 | return f"guilds/{guild_id}/threads/active" 313 | 314 | @classmethod 315 | def get_guild_member(cls, guild_id: int, user_id: int) -> str: 316 | return f"guilds/{guild_id}/members/{user_id}" 317 | 318 | @classmethod 319 | def list_guild_members(cls, guild_id: int) -> str: 320 | return f"guilds/{guild_id}/members" 321 | 322 | @classmethod 323 | def search_guild_members(cls, guild_id: int) -> str: 324 | return f"guilds/{guild_id}/members/search" 325 | 326 | @classmethod 327 | def add_guild_member(cls, guild_id: int, user_id: int) -> str: 328 | return f"guilds/{guild_id}/members/{user_id}" 329 | 330 | @classmethod 331 | def modify_guild_member(cls, guild_id: int, user_id: int) -> str: 332 | return f"guilds/{guild_id}/members/{user_id}" 333 | 334 | @classmethod 335 | def modify_current_member(cls, guild_id: int) -> str: 336 | return f"guilds/{guild_id}/members/@me" 337 | 338 | @classmethod 339 | def guild_member_addrole(cls, guild_id: int, user_id: int, role_id: int) -> str: 340 | return f"guilds/{guild_id}/members/{user_id}/roles/{role_id}" 341 | 342 | @classmethod 343 | def guild_member_removerole(cls, guild_id: int, user_id: int, role_id: int) -> str: 344 | return f"guilds/{guild_id}/members/{user_id}/roles/{role_id}" 345 | 346 | @classmethod 347 | def remove_guild_member(cls, guild_id: int, user_id: int) -> str: 348 | return f"guilds/{guild_id}/members/{user_id}" 349 | 350 | @classmethod 351 | def get_guild_bans(cls, guild_id: int) -> str: 352 | return f"guilds/{guild_id}/bans" 353 | 354 | @classmethod 355 | def get_guild_ban(cls, guild_id: int, user_id: int) -> str: 356 | return f"guilds/{guild_id}/bans/{user_id}" 357 | 358 | @classmethod 359 | def create_guild_ban(cls, guild_id: int, user_id: int) -> str: 360 | return f"guilds/{guild_id}/bans/{user_id}" 361 | 362 | @classmethod 363 | def remove_guild_ban(cls, guild_id: int, user_id: int) -> str: 364 | return f"guilds/{guild_id}/bans/{user_id}" 365 | 366 | @classmethod 367 | def get_guild_roles(cls, guild_id: int) -> str: 368 | return f"guilds/{guild_id}/roles" 369 | 370 | @classmethod 371 | def create_guild_role(cls, guild_id: int) -> str: 372 | return f"guilds/{guild_id}/roles" 373 | 374 | @classmethod 375 | def modify_guild_role_positions(cls, guild_id: int) -> str: 376 | return f"guilds/{guild_id}/roles" 377 | 378 | @classmethod 379 | def modify_guild_role(cls, guild_id: int, role_id: int) -> str: 380 | return f"guilds/{guild_id}/roles/{role_id}" 381 | 382 | @classmethod 383 | def modify_guild_mfa(cls, guild_id: int) -> str: 384 | return f"guilds/{guild_id}/mfa" 385 | 386 | @classmethod 387 | def delete_guild_role(cls, guild_id: int, role_id: int) -> str: 388 | return f"guilds/{guild_id}/roles/{role_id}" 389 | 390 | @classmethod 391 | def get_guild_prune_count(cls, guild_id: int) -> str: 392 | return f"guilds/{guild_id}/prune" 393 | 394 | @classmethod 395 | def begin_guild_prune(cls, guild_id: int) -> str: 396 | return f"guilds/{guild_id}/prune" 397 | 398 | @classmethod 399 | def guild_voice_regions(cls, guild_id: int) -> str: 400 | return f"guilds/{guild_id}/regions" 401 | 402 | @classmethod 403 | def get_guild_invites(cls, guild_id: int) -> str: 404 | return f"guilds/{guild_id}/invites" 405 | 406 | @classmethod 407 | def get_guild_integrations(cls, guild_id: int) -> str: 408 | return f"guilds/{guild_id}/integrations" 409 | 410 | @classmethod 411 | def delete_guild_integration(cls, guild_id: int, integration_id: int) -> str: 412 | return f"guilds/{guild_id}/integrations/{integration_id}" 413 | 414 | @classmethod 415 | def get_guild_widget_settings(cls, guild_id: int) -> str: 416 | return f"guilds/{guild_id}/widget" 417 | 418 | @classmethod 419 | def modify_guild_widget(cls, guild_id: int) -> str: 420 | return f"guilds/{guild_id}/widget" 421 | 422 | @classmethod 423 | def get_guild_widget(cls, guild_id: int) -> str: 424 | return f"guilds/{guild_id}/widget.json" 425 | 426 | @classmethod 427 | def guild_vanity_url(cls, guild_id: int) -> str: 428 | return f"guilds/{guild_id}/vanity-url" 429 | 430 | @classmethod 431 | def get_guild_widget_image(cls, guild_id: int) -> str: 432 | return f"guilds/{guild_id}/widget.png" 433 | 434 | @classmethod 435 | def get_guild_welcome_screen(cls, guild_id: int) -> str: 436 | return f"guilds/{guild_id}/welcome-screen" 437 | 438 | @classmethod 439 | def modify_guild_welcome_screen(cls, guild_id: int) -> str: 440 | return f"guilds/{guild_id}/welcome-screen" 441 | 442 | @classmethod 443 | def modify_current_user_voice_state(cls, guild_id: int) -> str: 444 | return f"guilds/{guild_id}/voice-states/@me" 445 | 446 | @classmethod 447 | def modify_user_voice_state(cls, guild_id: int, user_id: int) -> str: 448 | return f"guilds/{guild_id}/voice-states/{user_id}" 449 | 450 | @classmethod 451 | def list_scheduled_guild_events(cls, guild_id: int) -> str: 452 | return f"guilds/{guild_id}/scheduled-events" 453 | 454 | @classmethod 455 | def create_scheduled_guild_event(cls, guild_id: int) -> str: 456 | return f"guilds/{guild_id}/scheduled-events" 457 | 458 | @classmethod 459 | def get_scheduled_guild_event(cls, guild_id: int, event_id: int) -> str: 460 | return f"guilds/{guild_id}/scheduled-events/{event_id}" 461 | 462 | @classmethod 463 | def modify_scheduled_guild_event(cls, guild_id: int, event_id: int) -> str: 464 | return f"guilds/{guild_id}/scheduled-events/{event_id}" 465 | 466 | @classmethod 467 | def delete_scheduled_guild_event(cls, guild_id: int, event_id: int) -> str: 468 | return f"guilds/{guild_id}/scheduled-events/{event_id}" 469 | 470 | @classmethod 471 | def get_scheduled_guild_event_users(cls, guild_id: int, event_id: int) -> str: 472 | return f"guilds/{guild_id}/scheduled-events/{event_id}/users" 473 | 474 | @classmethod 475 | def get_guild_template(cls, guild_id: int, template_code: str) -> str: 476 | return f"guilds/{guild_id}/templates/{template_code}" 477 | 478 | @classmethod 479 | def create_guild_from_template(cls, guild_id: int, template_code: str) -> str: 480 | return f"guilds/{guild_id}/templates/{template_code}" 481 | 482 | @classmethod 483 | def get_guild_templates(cls, guild_id: int) -> str: 484 | return f"guilds/{guild_id}/templates" 485 | 486 | @classmethod 487 | def create_guild_template(cls, guild_id: int) -> str: 488 | return f"guilds/{guild_id}/templates" 489 | 490 | @classmethod 491 | def sync_guild_template(cls, guild_id: int, template_code: str) -> str: 492 | return f"guilds/{guild_id}/templates/{template_code}" 493 | 494 | @classmethod 495 | def modify_guild_template(cls, guild_id: int, template_code: str) -> str: 496 | return f"guilds/{guild_id}/templates/{template_code}" 497 | 498 | @classmethod 499 | def delete_guild_template(cls, guild_id: int, template_code: str) -> str: 500 | return f"guilds/{guild_id}/templates/{template_code}" 501 | 502 | @classmethod 503 | def get_invite(cls, invite_code: str) -> str: 504 | return f"invites/{invite_code}" 505 | 506 | @classmethod 507 | def delete_invite(cls, invite_code: str) -> str: 508 | return f"invites/{invite_code}" 509 | 510 | @classmethod 511 | def create_stage_instance(cls) -> str: 512 | return "stage-instances" 513 | 514 | @classmethod 515 | def get_stage_instance(cls, channel_id: int) -> str: 516 | return f"stage-instances/{channel_id}" 517 | 518 | @classmethod 519 | def modify_stage_instance(cls, channel_id: int) -> str: 520 | return f"stage-instances/{channel_id}" 521 | 522 | @classmethod 523 | def delete_stage_instance(cls, channel_id: int) -> str: 524 | return f"stage-instances/{channel_id}" 525 | 526 | @classmethod 527 | def get_sticker(cls, sticker_id: int) -> str: 528 | return f"stickers/{sticker_id}" 529 | 530 | @classmethod 531 | def list_nitro_sticker_packs(cls) -> str: 532 | return "sticker-packs" 533 | 534 | @classmethod 535 | def list_guild_stickers(cls, guild_id: int) -> str: 536 | return f"guilds/{guild_id}/stickers" 537 | 538 | @classmethod 539 | def create_guild_sticker(cls, guild_id: int) -> str: 540 | return f"guilds/{guild_id}/stickers" 541 | 542 | @classmethod 543 | def get_guild_sticker(cls, guild_id: int, sticker_id: int) -> str: 544 | return f"guilds/{guild_id}/stickers/{sticker_id}" 545 | 546 | @classmethod 547 | def modify_guild_sticker(cls, guild_id: int, sticker_id: int) -> str: 548 | return f"guilds/{guild_id}/stickers/{sticker_id}" 549 | 550 | @classmethod 551 | def delete_guild_sticker(cls, guild_id: int, sticker_id: int) -> str: 552 | return f"guilds/{guild_id}/stickers/{sticker_id}" 553 | 554 | @classmethod 555 | def get_current_user(cls) -> str: 556 | return "users/@me" 557 | 558 | @classmethod 559 | def get_user(cls, user_id: int) -> str: 560 | return f"users/{user_id}" 561 | 562 | @classmethod 563 | def modify_current_user(cls) -> str: 564 | return "users/@me" 565 | 566 | @classmethod 567 | def get_current_user_guilds(cls) -> str: 568 | return "users/@me/guilds" 569 | 570 | @classmethod 571 | def get_current_user_guilds_membership(cls, guild_id: int) -> str: 572 | return f"users/@me/guilds/{guild_id}/member" 573 | 574 | @classmethod 575 | def leave_guild(cls, guild_id: int) -> str: 576 | return f"users/@me/guilds/{guild_id}" 577 | 578 | @classmethod 579 | def create_dm(cls) -> str: 580 | return "users/@me/channels" 581 | 582 | @classmethod 583 | def create_group_dm(cls) -> str: 584 | return "users/@me/channels" 585 | 586 | @classmethod 587 | def get_user_connections(cls) -> str: 588 | return "users/@me/connections" 589 | 590 | @classmethod 591 | def list_voice_regions(cls) -> str: 592 | return "voice/regions" 593 | 594 | @classmethod 595 | def create_webhook(cls, channel_id: int) -> str: 596 | return f"channels/{channel_id}/webhooks" 597 | 598 | @classmethod 599 | def get_channel_webhooks(cls, channel_id: int) -> str: 600 | return f"channels/{channel_id}/webhooks" 601 | 602 | @classmethod 603 | def get_webhook(cls, webhook_id: int) -> str: 604 | return f"webhooks/{webhook_id}" 605 | 606 | @classmethod 607 | def get_webhook_with_token(cls, webhook_id: int, webhook_token: str) -> str: 608 | return f"webhooks/{webhook_id}/{webhook_token}" 609 | 610 | @classmethod 611 | def modify_webhook(cls, webhook_id: int) -> str: 612 | return f"webhooks/{webhook_id}" 613 | 614 | @classmethod 615 | def modify_webhook_with_token(cls, webhook_id: int, webhook_token: str) -> str: 616 | return f"webhooks/{webhook_id}/{webhook_token}" 617 | 618 | @classmethod 619 | def delete_webhook(cls, webhook_id: int) -> str: 620 | return f"webhooks/{webhook_id}" 621 | 622 | @classmethod 623 | def delete_webhook_with_token(cls, webhook_id: int, webhook_token: str) -> str: 624 | return f"webhooks/{webhook_id}/{webhook_token}" 625 | 626 | @classmethod 627 | def execute_webhook(cls, webhook_id: int, webhook_token: str) -> str: 628 | return f"webhooks/{webhook_id}/{webhook_token}" 629 | 630 | @classmethod 631 | def execute_slack_compatible_webhook(cls, webhook_id: int, webhook_token: str) -> str: 632 | return f"webhooks/{webhook_id}/{webhook_token}/slack" 633 | 634 | @classmethod 635 | def execute_github_compatible_webhook(cls, webhook_id: int, webhook_token: str) -> str: 636 | return f"webhooks/{webhook_id}/{webhook_token}/github" 637 | 638 | @classmethod 639 | def get_webhook_message(cls, webhook_id: int, webhook_token: str, message_id: int) -> str: 640 | return f"webhooks/{webhook_id}/{webhook_token}/messages/{message_id}" 641 | 642 | @classmethod 643 | def edit_webhook_message(cls, webhook_id: int, webhook_token: str, message_id: int) -> str: 644 | return f"webhooks/{webhook_id}/{webhook_token}/messages/{message_id}" 645 | 646 | @classmethod 647 | def delete_webhook_message(cls, webhook_id: int, webhook_token: str, message_id: int) -> str: 648 | return f"webhooks/{webhook_id}/{webhook_token}/messages/{message_id}" 649 | 650 | @classmethod 651 | def get_guild_webhooks(cls, guild_id: int) -> str: 652 | return f"guilds/{guild_id}/webhooks" 653 | 654 | @classmethod 655 | def interaction_command(cls, app_id: int) -> str: 656 | return f"applications/{app_id}/commands" 657 | 658 | @classmethod 659 | def interaction_callback(cls, interaction_id: int, interaction_token: str) -> str: 660 | return f"interactions/{interaction_id}/{interaction_token}/callback" 661 | 662 | @classmethod 663 | def get_original_interaction(cls, interaction_id: int, interaction_token: str) -> str: 664 | return f"/webhooks/{interaction_id}/{interaction_token}/messages/@original" 665 | 666 | @classmethod 667 | def edit_original_interaction(cls, interaction_id: int, interaction_token: str) -> str: 668 | return f"/webhooks/{interaction_id}/{interaction_token}/messages/@original" 669 | 670 | @classmethod 671 | def delete_original_interaction(cls, interaction_id: int, interaction_token: str) -> str: 672 | return f"/webhooks/{interaction_id}/{interaction_token}/messages/@original" 673 | 674 | @classmethod 675 | def create_followup_message(cls, interaction_id: int, interaction_token: str) -> str: 676 | return f"/webhooks/{interaction_id}/{interaction_token}" 677 | 678 | @classmethod 679 | def get_followup_message(cls, interaction_id: int, interaction_token: str, message_id: int) -> str: 680 | return f"/webhooks/{interaction_id}/{interaction_token}/messages/{message_id}" 681 | 682 | @classmethod 683 | def edit_followup_message(cls, interaction_id: int, interaction_token: str, message_id: int) -> str: 684 | return f"/webhooks/{interaction_id}/{interaction_token}/messages/{message_id}" 685 | 686 | @classmethod 687 | def delete_followup_message(cls, interaction_id: int, interaction_token: str, message_id: int) -> str: 688 | return f"/webhooks/{interaction_id}/{interaction_token}/messages/{message_id}" 689 | --------------------------------------------------------------------------------