From 986f5a89cb1f1ce22c0b09c0d22fc028775faff1 Mon Sep 17 00:00:00 2001 From: Kento <75509362+nkstonks@users.noreply.github.com> Date: Mon, 17 May 2021 12:03:17 +1000 Subject: [PATCH 01/13] Update README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2640437ed..73a0cf552 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Documentation ⦿ Discord Server

- + ## About Discord Slash Commands are a new implementation for the Bot API that utilize the forward-slash "/" symbol. Released on 15 December 2020, many bot developers are still learning to learn how to implement this into @@ -79,8 +79,8 @@ def setup(bot): ``` -------- -This library is based on gateway event. If you are looking for webserver based, have a look at this: -[dispike](https://github.com/ms7m/dispike) -[discord-interactions-python](https://github.com/discord/discord-interactions-python) -Or for other languages: -[discord-api-docs Community Resources: Interactions](https://discord.com/developers/docs/topics/community-resources#interactions) \ No newline at end of file +- This library is based on gateway event. If you are looking for webserver based, have a look at this: + - [dispike](https://github.com/ms7m/dispike) + - [discord-interactions-python](https://github.com/discord/discord-interactions-python) +- Or for other languages: + - [discord-api-docs Community Resources: Interactions](https://discord.com/developers/docs/topics/community-resources#interactions) \ No newline at end of file From 4c0c0a864ca3ac563712149fbd6f190907878c28 Mon Sep 17 00:00:00 2001 From: LilSpazJoekp <15524072+LilSpazJoekp@users.noreply.github.com> Date: Fri, 30 Apr 2021 17:17:25 -0500 Subject: [PATCH 02/13] Move __version__ and BASE to const.py --- discord_slash/__init__.py | 3 +-- discord_slash/const.py | 5 +++++ discord_slash/http.py | 5 +++-- 3 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 discord_slash/const.py diff --git a/discord_slash/__init__.py b/discord_slash/__init__.py index 662495e71..ca1590cf1 100644 --- a/discord_slash/__init__.py +++ b/discord_slash/__init__.py @@ -15,5 +15,4 @@ from .dpy_overrides import ComponentMessage from .utils import manage_commands from .utils import manage_components - -__version__ = "1.2.2" +from .const import __version__ diff --git a/discord_slash/const.py b/discord_slash/const.py new file mode 100644 index 000000000..5b64473ba --- /dev/null +++ b/discord_slash/const.py @@ -0,0 +1,5 @@ +"""Discord Slash Constants""" + +__version__ = "1.2.2" + +BASE_API = "https://discord.com/api/v8" diff --git a/discord_slash/http.py b/discord_slash/http.py index 6becfc078..eb5c30148 100644 --- a/discord_slash/http.py +++ b/discord_slash/http.py @@ -4,11 +4,12 @@ import discord from discord.http import Route from . import error +from .const import BASE_API class CustomRoute(Route): """discord.py's Route but changed ``BASE`` to use at slash command.""" - BASE = "https://discord.com/api/v8" + BASE = BASE_API class SlashCommandRequest: @@ -120,7 +121,7 @@ def post_followup(self, _resp, token, files: typing.List[discord.File] = None): def post_initial_response(self, _resp, interaction_id, token): """ Sends an initial "POST" response to the Discord API. - + :param _resp: Command response. :type _resp: dict :param interaction_id: Interaction ID. From ed96c374299f5fa16afcca7b2667d887cf1bce47 Mon Sep 17 00:00:00 2001 From: LilSpazJoekp <15524072+LilSpazJoekp@users.noreply.github.com> Date: Fri, 30 Apr 2021 17:19:59 -0500 Subject: [PATCH 03/13] Parse package version from const.py file --- setup.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1cdbde936..fa79958c6 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,19 @@ +import re import setuptools +from codecs import open +from os import path + +PACKAGE_NAME = "discord_slash" +HERE = path.abspath(path.dirname(__file__)) with open("README.md", "r", encoding="UTF-8") as f: long_description = f.read() +with open(path.join(HERE, PACKAGE_NAME, "const.py"), encoding="utf-8") as fp: + VERSION = re.search('__version__ = "([^"]+)"', fp.read()).group(1) setuptools.setup( name="discord-py-slash-command", - version="1.2.2", + version=VERSION, author="eunwoo1104", author_email="sions04@naver.com", description="A simple discord slash command handler for discord.py.", From b32efa5dc6647f43ec1af01fb1aea891587cba64 Mon Sep 17 00:00:00 2001 From: LilSpazJoekp <15524072+LilSpazJoekp@users.noreply.github.com> Date: Fri, 30 Apr 2021 17:22:37 -0500 Subject: [PATCH 04/13] Move requirements into setup.py --- readthedocs.yml | 5 ++++- requirements.txt | 4 ---- setup.py | 9 +++++++++ 3 files changed, 13 insertions(+), 5 deletions(-) delete mode 100644 requirements.txt diff --git a/readthedocs.yml b/readthedocs.yml index 30c52062b..2acdf8945 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -21,4 +21,7 @@ formats: python: version: 3.7 install: - - requirements: requirements.txt \ No newline at end of file + - method: pip + extra_requirements: + - readthedocs + path: . \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 58e12d83b..000000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -discord.py -aiohttp -sphinx -sphinx-rtd-theme \ No newline at end of file diff --git a/setup.py b/setup.py index fa79958c6..c1ef7bc55 100644 --- a/setup.py +++ b/setup.py @@ -11,12 +11,21 @@ with open(path.join(HERE, PACKAGE_NAME, "const.py"), encoding="utf-8") as fp: VERSION = re.search('__version__ = "([^"]+)"', fp.read()).group(1) +extras = { + "readthedocs": [ + "sphinx", + "sphinx-rtd-theme" + ], +} + setuptools.setup( name="discord-py-slash-command", version=VERSION, author="eunwoo1104", author_email="sions04@naver.com", description="A simple discord slash command handler for discord.py.", + extras_require=extras, + install_requires=["discord.py", "aiohttp"], long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/eunwoo1104/discord-py-slash-command", From 5fb533f2b0342eb8f456f5f29b4467c3ba8b285d Mon Sep 17 00:00:00 2001 From: LilSpazJoekp <15524072+LilSpazJoekp@users.noreply.github.com> Date: Fri, 30 Apr 2021 17:33:40 -0500 Subject: [PATCH 05/13] Cleanup setup.py: added more classifiers, changed how README and const.py are opened, added license parameter --- setup.py | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/setup.py b/setup.py index c1ef7bc55..de708d2e9 100644 --- a/setup.py +++ b/setup.py @@ -1,24 +1,22 @@ import re -import setuptools from codecs import open from os import path +from setuptools import find_packages, setup + PACKAGE_NAME = "discord_slash" HERE = path.abspath(path.dirname(__file__)) with open("README.md", "r", encoding="UTF-8") as f: - long_description = f.read() + README = f.read() with open(path.join(HERE, PACKAGE_NAME, "const.py"), encoding="utf-8") as fp: VERSION = re.search('__version__ = "([^"]+)"', fp.read()).group(1) extras = { - "readthedocs": [ - "sphinx", - "sphinx-rtd-theme" - ], + "readthedocs": ["sphinx", "sphinx-rtd-theme"], } -setuptools.setup( +setup( name="discord-py-slash-command", version=VERSION, author="eunwoo1104", @@ -26,12 +24,26 @@ description="A simple discord slash command handler for discord.py.", extras_require=extras, install_requires=["discord.py", "aiohttp"], - long_description=long_description, + license="MIT License", + long_description=README, long_description_content_type="text/markdown", url="https://github.com/eunwoo1104/discord-py-slash-command", - packages=setuptools.find_packages(), - python_requires='>=3.6', + packages=find_packages(), + python_requires=">=3.6", classifiers=[ - "Programming Language :: Python :: 3" - ] + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Topic :: Internet", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Libraries", + "Topic :: Utilities", + ], ) From aa829e4855db5a38434a756b80e74f60df81e648 Mon Sep 17 00:00:00 2001 From: LilSpazJoekp <15524072+LilSpazJoekp@users.noreply.github.com> Date: Fri, 30 Apr 2021 17:37:39 -0500 Subject: [PATCH 06/13] Remove non-project specific patterns from .gitignore. Many of these should be placed in a global gitignore: https://help.github.com/en/github/using-git/ignoring-files#create-a-global-gitignore --- .gitignore | 36 +++++++----------------------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index 839b32575..9fa8b2b65 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,8 @@ -.idea -__pycache__ -test.py -test2.py -test3.py -docs/_build -slash.log -test -__*.py -soontm.png - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg *.egg -MANIFEST +*.egg-info/ +*.eggs/ +*.pyc +.cache/ +_build/ +build/ +dist/ \ No newline at end of file From 79decae515bf1142152007ebee32ac87d898a4e5 Mon Sep 17 00:00:00 2001 From: LilSpazJoekp <15524072+LilSpazJoekp@users.noreply.github.com> Date: Fri, 30 Apr 2021 17:47:02 -0500 Subject: [PATCH 07/13] Remove unused imports --- discord_slash/context.py | 2 -- docs/conf.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/discord_slash/context.py b/discord_slash/context.py index fa56a10cb..c231168d1 100644 --- a/discord_slash/context.py +++ b/discord_slash/context.py @@ -1,10 +1,8 @@ import datetime import typing -import asyncio from warnings import warn import discord -from contextlib import suppress from discord.ext import commands from discord.utils import snowflake_time diff --git a/docs/conf.py b/docs/conf.py index 8cb9de81e..6be85ec7d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,7 +12,7 @@ # import os import sys -import sphinx_rtd_theme + sys.path.insert(0, os.path.abspath('..')) From 4888b59d3842bb3a25574f64a44f90a19b9477f3 Mon Sep 17 00:00:00 2001 From: LilSpazJoekp <15524072+LilSpazJoekp@users.noreply.github.com> Date: Fri, 30 Apr 2021 17:47:52 -0500 Subject: [PATCH 08/13] Add version and release info to docs --- docs/conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 6be85ec7d..92bc1f68e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,13 +15,15 @@ sys.path.insert(0, os.path.abspath('..')) +from discord_slash import __version__ # -- Project information ----------------------------------------------------- project = 'discord-py-slash-command' copyright = '2020-2021, eunwoo1104' author = 'eunwoo1104' - +release = __version__ +version = ".".join(__version__.split(".", 2)[:2]) # -- General configuration --------------------------------------------------- From 9b951e4165881afaab43e1f171e3af90581e8b92 Mon Sep 17 00:00:00 2001 From: LilSpazJoekp <15524072+LilSpazJoekp@users.noreply.github.com> Date: Fri, 30 Apr 2021 17:57:52 -0500 Subject: [PATCH 09/13] Remove non-project specific patterns from exclude_patterns in docs/conf.py --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 92bc1f68e..6667ead0c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -49,7 +49,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', "test.py", "test2.py", "test3.py", ".idea", "setup.py"] +exclude_patterns = ['_build'] # This should fix wrong sort autodoc_member_order = 'bysource' From 56f0fc1282f3de923f90fffb87521fe61c71d499 Mon Sep 17 00:00:00 2001 From: LilSpazJoekp <15524072+LilSpazJoekp@users.noreply.github.com> Date: Fri, 30 Apr 2021 17:58:41 -0500 Subject: [PATCH 10/13] Move scope.jpg to _static folder --- docs/{images => _static}/scope.jpg | Bin docs/quickstart.rst | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename docs/{images => _static}/scope.jpg (100%) diff --git a/docs/images/scope.jpg b/docs/_static/scope.jpg similarity index 100% rename from docs/images/scope.jpg rename to docs/_static/scope.jpg diff --git a/docs/quickstart.rst b/docs/quickstart.rst index b5542d9f9..0e2cf44ce 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -15,7 +15,7 @@ After reading that, there is one more step before inviting your bot. The second step will now be setting your scope correctly for the bot to properly recognize slash commands, as shown here: -.. image:: images/scope.jpg +.. image:: _static/scope.jpg Then, invite your bot to your guild. @@ -47,7 +47,7 @@ slash commands just yet. We can do that by adding this code shown here: Make sure this code is added before the client.run() call! It also needs to be under on_ready, otherwise, this will not work. """ - + guild_ids = [789032594456576001] # Put your server ID in this array. @slash.slash(name="ping", guild_ids=guild_ids) From 68167a889b787dccef813cef9c7a35e1bf0574a2 Mon Sep 17 00:00:00 2001 From: LilSpazJoekp <15524072+LilSpazJoekp@users.noreply.github.com> Date: Fri, 30 Apr 2021 18:23:12 -0500 Subject: [PATCH 11/13] Add black, isort, and flake8 linters --- .flake8 | 2 + .github/PULL_REQUEST_TEMPLATE.md | 1 + .github/workflows/ci.yml | 33 +++++++++++++++ pre_push.py | 70 ++++++++++++++++++++++++++++++++ pyproject.toml | 7 ++++ setup.py | 3 ++ 6 files changed, 116 insertions(+) create mode 100644 .flake8 create mode 100644 .github/workflows/ci.yml create mode 100755 pre_push.py create mode 100644 pyproject.toml diff --git a/.flake8 b/.flake8 new file mode 100644 index 000000000..e61de53c0 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +ignore = E203 E501 W503 W504 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9a882c3d7..bb0fcae48 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,6 +8,7 @@ What changes were made? ## Checklist +- [ ] I've run the `pre_push.py` script to format and lint code. - [ ] I've checked this pull request runs on `Python 3.6.X`. - [ ] This fixes something in [Issues](https://github.com/eunwoo1104/discord-py-slash-command/issues). - Issue: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..a5c70a06d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +jobs: + lint-multi-os: + name: Lint ${{ matrix.os }} + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: 3.x + - uses: actions/cache@v1 + with: + key: v0-${{ runner.os }}-pip-lint-${{ hashFiles('setup.py') }} + path: ~/.cache/pip + restore-keys: | + v0-${{ runner.os }}-pip-lint- + v0-${{ runner.os }}-pip- + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install .[lint] + - name: Run black + run: black --check --verbose . + - name: Run flake8 + run: flake8 --exclude docs --statistics + - name: Run isort + run: isort -cv . + - name: Run sphinx + run: sphinx-build -W --keep-going docs/ /tmp/foo + strategy: + matrix: + os: [macOS-latest, ubuntu-latest, windows-latest] +name: CI +on: [pull_request, push] diff --git a/pre_push.py b/pre_push.py new file mode 100755 index 000000000..42f63078e --- /dev/null +++ b/pre_push.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Run static analysis on the project.""" + +import sys +from os import path +from shutil import rmtree +from subprocess import CalledProcessError, check_call +from tempfile import mkdtemp + +current_directory = path.abspath(path.join(__file__, "..")) + + +def do_process(args, shell=False): + """Run program provided by args. + + Return True on success. + + Output failed message on non-zero exit and return False. + + Exit if command is not found. + + """ + print(f"Running: {' '.join(args)}") + try: + check_call(args, shell=shell) + except CalledProcessError: + print(f"\nFailed: {' '.join(args)}") + return False + except Exception as exc: + sys.stderr.write(f"{str(exc)}\n") + sys.exit(1) + return True + + +def run_static(): + """Runs static tests. + + Returns a statuscode of 0 if everything ran correctly. Otherwise, it will return + statuscode 1 + + """ + success = True + # Formatters + success &= do_process(["black", "."]) + success &= do_process(["isort", "."]) + # Linters + success &= do_process(["flake8", "--exclude=.eggs,build,docs,.venv*"]) + + tmp_dir = mkdtemp() + try: + success &= do_process(["sphinx-build", "-W", "--keep-going", "docs", tmp_dir]) + finally: + rmtree(tmp_dir) + + return success + + +def main(): + success = True + try: + success &= run_static() + except KeyboardInterrupt: + return 1 + return int(not success) + + +if __name__ == "__main__": + exit_code = main() + print("\npre_push.py: Success!" if not exit_code else "\npre_push.py: Fail") + sys.exit(exit_code) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..23201eee1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[tool.black] +exclude = '/(\.eggs|\.git|\.mypy_cache|\.venv.*|_build|build|dist)/' +line-length = 100 + +[tool.isort] +profile = "black" +line_length = 100 \ No newline at end of file diff --git a/setup.py b/setup.py index de708d2e9..8a215962d 100644 --- a/setup.py +++ b/setup.py @@ -13,8 +13,11 @@ VERSION = re.search('__version__ = "([^"]+)"', fp.read()).group(1) extras = { + "lint": ["black", "flake8", "isort"], "readthedocs": ["sphinx", "sphinx-rtd-theme"], } +extras["lint"] += extras["readthedocs"] +extras["dev"] = extras["lint"] + extras["readthedocs"] setup( name="discord-py-slash-command", From cb967cd7897c5c05f2bfba16eb0e85b7f55076cd Mon Sep 17 00:00:00 2001 From: LilSpazJoekp <15524072+LilSpazJoekp@users.noreply.github.com> Date: Fri, 30 Apr 2021 18:35:03 -0500 Subject: [PATCH 12/13] fix linting errors --- discord_slash/__init__.py | 16 ++++++++-------- discord_slash/client.py | 13 ++++++------- discord_slash/context.py | 11 +++++------ discord_slash/http.py | 2 +- discord_slash/model.py | 4 ++-- discord_slash/utils/manage_commands.py | 4 ++-- 6 files changed, 24 insertions(+), 26 deletions(-) diff --git a/discord_slash/__init__.py b/discord_slash/__init__.py index ca1590cf1..37db84bb2 100644 --- a/discord_slash/__init__.py +++ b/discord_slash/__init__.py @@ -8,11 +8,11 @@ :license: MIT """ -from .client import SlashCommand -from .model import SlashCommandOptionType -from .context import SlashContext -from .context import ComponentContext -from .dpy_overrides import ComponentMessage -from .utils import manage_commands -from .utils import manage_components -from .const import __version__ +from .client import SlashCommand # noqa: F401 +from .model import SlashCommandOptionType # noqa: F401 +from .context import SlashContext # noqa: F401 +from .context import ComponentContext # noqa: F401 +from .dpy_overrides import ComponentMessage # noqa: F401 +from .utils import manage_commands # noqa: F401 +from .utils import manage_components # noqa: F401 +from .const import __version__ # noqa: F401 diff --git a/discord_slash/client.py b/discord_slash/client.py index 82bc521f8..9b624b237 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -9,7 +9,6 @@ from . import model from . import error from . import context -from . import dpy_overrides from .utils import manage_commands @@ -336,7 +335,7 @@ async def sync_all_commands( for cmd in existing_cmds: existing_by_name[cmd["name"]] = model.CommandData(**cmd) - if len(new_cmds) != len(existing_cmds): + if len(new_cmds) != len(existing_cmds): changed = True for command in new_cmds: @@ -356,7 +355,7 @@ async def sync_all_commands( changed=True to_send.append(command) - + if changed: self.logger.debug(f"Detected changes on {scope if scope is not None else 'global'}, updating them") existing_cmds = await self.req.put_slash_commands(slash_commands=to_send, guild_id=scope) @@ -401,7 +400,7 @@ async def sync_all_commands( if existing_perms_model[new_perm["id"]] != model.GuildPermissionsData(**new_perm): changed = True break - + if changed: self.logger.debug(f"Detected permissions changes on {scope}, updating them") await self.req.update_guild_commands_permissions(scope, new_perms) @@ -414,7 +413,7 @@ async def sync_all_commands( other_guilds = [guild.id for guild in self._discord.guilds if guild.id not in cmds["guild"]] # This is an extremly bad way to do this, because slash cmds can be in guilds the bot isn't in # But it's the only way until discord makes an endpoint to request all the guild with cmds registered. - + for guild in other_guilds: with suppress(discord.Forbidden): existing = await self.req.get_all_commands(guild_id = guild) @@ -772,11 +771,11 @@ def wrapper(cmd): def permission(self, guild_id: int, permissions: list): """ Decorator that add permissions. This will set the permissions for a single guild, you can use it more than once for each command. - :param guild_id: ID of the guild for the permissions. + :param guild_id: ID of the guild for the permissions. :type guild_id: int :param permissions: Permission requirements of the slash command. Default ``None``. :type permissions: dict - + """ def wrapper(cmd): if not getattr(cmd, "__permissions__", None): diff --git a/discord_slash/context.py b/discord_slash/context.py index c231168d1..58635ab50 100644 --- a/discord_slash/context.py +++ b/discord_slash/context.py @@ -338,12 +338,11 @@ async def edit_origin(self, **fields): if self.deferred: _json = await self._http.edit(_resp, self._token, files=files) self.deferred = False - else: - json_data = { - "type": 7, - "data": _resp - } - _json = await self._http.post_initial_response(json_data, self.interaction_id, self._token) + else: # noqa: F841 + json_data = {"type": 7, "data": _resp} + _json = await self._http.post_initial_response( # noqa: F841 + json_data, self.interaction_id, self._token + ) self.responded = True else: raise error.IncorrectFormat("Already responded") diff --git a/discord_slash/http.py b/discord_slash/http.py index eb5c30148..3d63ee8f0 100644 --- a/discord_slash/http.py +++ b/discord_slash/http.py @@ -131,7 +131,7 @@ def post_initial_response(self, _resp, interaction_id, token): return self.command_response(token, False, "POST", interaction_id, json=_resp) def command_response(self, token, use_webhook, method, interaction_id= None, url_ending = "", **kwargs): - """ + r""" Sends a command response to discord (POST, PATCH, DELETE) :param token: Interaction token diff --git a/discord_slash/model.py b/discord_slash/model.py index df9f7021b..7dd2d1f6b 100644 --- a/discord_slash/model.py +++ b/discord_slash/model.py @@ -177,7 +177,7 @@ async def _concurrency_checks(self, ctx): try: # cooldown checks self._prepare_cooldowns(ctx) - except: + except Exception: if self._max_concurrency is not None: await self._max_concurrency.release(ctx) raise @@ -498,7 +498,7 @@ class GuildPermissionsData: Slash permissions data for a command in a guild. :ivar id: Command id, provided by discord. - :ivar guild_id: Guild id that the permissions are in. + :ivar guild_id: Guild id that the permissions are in. :ivar permissions: List of permissions dict. """ def __init__(self, id, guild_id, permissions, **kwargs): diff --git a/discord_slash/utils/manage_commands.py b/discord_slash/utils/manage_commands.py index 37d4dc849..e6ed4399d 100644 --- a/discord_slash/utils/manage_commands.py +++ b/discord_slash/utils/manage_commands.py @@ -269,7 +269,7 @@ def create_option(name: str, .. note:: ``choices`` must either be a list of `option type dicts `_ - or a list of single string values. + or a list of single string values. """ if not isinstance(option_type, int) or isinstance(option_type, bool): #Bool values are a subclass of int original_type = option_type @@ -320,7 +320,7 @@ def generate_options(function: Callable, description: str = "No description.", c args = getattr(param.annotation, "__args__", None) if args: param = param.replace(annotation=args[0]) - required = not args[-1] is type(None) + required = not isinstance(args[-1], type(None)) option_type = SlashCommandOptionType.from_type(param.annotation) or SlashCommandOptionType.STRING name = param.name if not connector else connector[param.name] From edfdeac9efe9167b6c5c3c0cab8912873eab456f Mon Sep 17 00:00:00 2001 From: LilSpazJoekp <15524072+LilSpazJoekp@users.noreply.github.com> Date: Sun, 6 Jun 2021 11:51:38 -0500 Subject: [PATCH 13/13] Run black and isort on codebase --- discord_slash/__init__.py | 6 +- discord_slash/client.py | 342 +++++++++++++++-------- discord_slash/cog_ext.py | 60 ++-- discord_slash/context.py | 132 +++++---- discord_slash/dpy_overrides.py | 175 ++++++++---- discord_slash/error.py | 4 +- discord_slash/http.py | 53 ++-- discord_slash/model.py | 79 ++++-- discord_slash/utils/manage_commands.py | 194 ++++++------- discord_slash/utils/manage_components.py | 58 ++-- docs/conf.py | 30 +- 11 files changed, 687 insertions(+), 446 deletions(-) diff --git a/discord_slash/__init__.py b/discord_slash/__init__.py index 37db84bb2..7fa481236 100644 --- a/discord_slash/__init__.py +++ b/discord_slash/__init__.py @@ -9,10 +9,10 @@ """ from .client import SlashCommand # noqa: F401 -from .model import SlashCommandOptionType # noqa: F401 -from .context import SlashContext # noqa: F401 +from .const import __version__ # noqa: F401 from .context import ComponentContext # noqa: F401 +from .context import SlashContext # noqa: F401 from .dpy_overrides import ComponentMessage # noqa: F401 +from .model import SlashCommandOptionType # noqa: F401 from .utils import manage_commands # noqa: F401 from .utils import manage_components # noqa: F401 -from .const import __version__ # noqa: F401 diff --git a/discord_slash/client.py b/discord_slash/client.py index 9b624b237..840565647 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -1,14 +1,13 @@ import copy import logging import typing -import discord -from inspect import iscoroutinefunction, getdoc from contextlib import suppress +from inspect import getdoc, iscoroutinefunction + +import discord from discord.ext import commands -from . import http -from . import model -from . import error -from . import context + +from . import context, error, http, model from .utils import manage_commands @@ -42,13 +41,15 @@ class SlashCommand: :ivar has_listener: Whether discord client has listener add function. """ - def __init__(self, - client: typing.Union[discord.Client, commands.Bot], - sync_commands: bool = False, - delete_from_unused_guilds: bool = False, - sync_on_cog_reload: bool = False, - override_type: bool = False, - application_id: typing.Optional[int] = None): + def __init__( + self, + client: typing.Union[discord.Client, commands.Bot], + sync_commands: bool = False, + delete_from_unused_guilds: bool = False, + sync_on_cog_reload: bool = False, + override_type: bool = False, + application_id: typing.Optional[int] = None, + ): self._discord = client self.commands = {} self.subcommands = {} @@ -60,12 +61,18 @@ def __init__(self, if self.sync_commands: self._discord.loop.create_task(self.sync_all_commands(delete_from_unused_guilds)) - if not isinstance(client, commands.Bot) and not isinstance(client, commands.AutoShardedBot) and not override_type: - self.logger.warning("Detected discord.Client! It is highly recommended to use `commands.Bot`. Do not add any `on_socket_response` event.") + if ( + not isinstance(client, commands.Bot) + and not isinstance(client, commands.AutoShardedBot) + and not override_type + ): + self.logger.warning( + "Detected discord.Client! It is highly recommended to use `commands.Bot`. Do not add any `on_socket_response` event." + ) self._discord.on_socket_response = self.on_socket_response self.has_listener = False else: - if not hasattr(self._discord, 'slash'): + if not hasattr(self._discord, "slash"): self._discord.slash = self else: raise error.DuplicateSlashClient("You can't have duplicate SlashCommand instances!") @@ -95,7 +102,9 @@ def override_remove_cog(name: str): def override_reload_extension(*args): orig_reload(*args) - self._discord.loop.create_task(self.sync_all_commands(delete_from_unused_guilds)) + self._discord.loop.create_task( + self.sync_all_commands(delete_from_unused_guilds) + ) self._discord.reload_extension = override_reload_extension @@ -109,12 +118,18 @@ def get_cog_commands(self, cog: commands.Cog): :param cog: Cog that has slash commands. :type cog: discord.ext.commands.Cog """ - if hasattr(cog, '_slash_registered'): # Temporary warning - return self.logger.warning("Calling get_cog_commands is no longer required " - "to add cog slash commands. Make sure to remove all calls to this function.") + if hasattr(cog, "_slash_registered"): # Temporary warning + return self.logger.warning( + "Calling get_cog_commands is no longer required " + "to add cog slash commands. Make sure to remove all calls to this function." + ) cog._slash_registered = True # Assuming all went well func_list = [getattr(cog, x) for x in dir(cog)] - res = [x for x in func_list if isinstance(x, (model.CogBaseCommandObject, model.CogSubcommandObject))] + res = [ + x + for x in func_list + if isinstance(x, (model.CogBaseCommandObject, model.CogSubcommandObject)) + ] for x in res: x.cog = cog if isinstance(x, model.CogBaseCommandObject): @@ -133,7 +148,9 @@ def get_cog_commands(self, cog: commands.Cog): for applicable_guild in base_permissions: if applicable_guild not in base_command.permissions: base_command.permissions[applicable_guild] = [] - base_command.permissions[applicable_guild].extend(base_permissions[applicable_guild]) + base_command.permissions[applicable_guild].extend( + base_permissions[applicable_guild] + ) self.commands[x.base].has_subcommands = True @@ -162,11 +179,14 @@ def remove_cog_commands(self, cog): :param cog: Cog that has slash commands. :type cog: discord.ext.commands.Cog """ - if hasattr(cog, '_slash_registered'): + if hasattr(cog, "_slash_registered"): del cog._slash_registered func_list = [getattr(cog, x) for x in dir(cog)] - res = [x for x in func_list if - isinstance(x, (model.CogBaseCommandObject, model.CogSubcommandObject))] + res = [ + x + for x in func_list + if isinstance(x, (model.CogBaseCommandObject, model.CogSubcommandObject)) + ] for x in res: if isinstance(x, model.CogBaseCommandObject): if x.name not in self.commands: @@ -214,10 +234,7 @@ async def to_dict(self): for i in self.commands[x].allowed_guild_ids: if i not in all_guild_ids: all_guild_ids.append(i) - cmds = { - "global": [], - "guild": {x: [] for x in all_guild_ids} - } + cmds = {"global": [], "guild": {x: [] for x in all_guild_ids}} wait = {} # Before merging to return dict, let's first put commands to temporary dict. for x in self.commands: selected = self.commands[x] @@ -230,7 +247,7 @@ async def to_dict(self): "description": selected.description or "No Description.", "options": selected.options or [], "default_permission": selected.default_permission, - "permissions": {} + "permissions": {}, } if y in selected.permissions: command_dict["permissions"][y] = selected.permissions[y] @@ -243,7 +260,7 @@ async def to_dict(self): "description": selected.description or "No Description.", "options": selected.options or [], "default_permission": selected.default_permission, - "permissions": selected.permissions or {} + "permissions": selected.permissions or {}, } wait["global"][x] = copy.deepcopy(command_dict) @@ -262,7 +279,7 @@ async def to_dict(self): "name": sub.name, "description": sub.description or "No Description.", "type": model.SlashCommandOptionType.SUB_COMMAND, - "options": sub.options or [] + "options": sub.options or [], } if sub.allowed_guild_ids: for z in sub.allowed_guild_ids: @@ -275,7 +292,7 @@ async def to_dict(self): "name": y, "description": "No Description.", "type": model.SlashCommandOptionType.SUB_COMMAND_GROUP, - "options": [] + "options": [], } for z in sub: sub_sub = sub[z] @@ -283,7 +300,7 @@ async def to_dict(self): "name": sub_sub.name, "description": sub_sub.description or "No Description.", "type": model.SlashCommandOptionType.SUB_COMMAND, - "options": sub_sub.options or [] + "options": sub_sub.options or [], } if sub_sub.allowed_guild_ids: for i in sub_sub.allowed_guild_ids: @@ -321,16 +338,16 @@ async def sync_all_commands( permissions_map = {} cmds = await self.to_dict() self.logger.info("Syncing commands...") - cmds_formatted = {None: cmds['global']} - for guild in cmds['guild']: - cmds_formatted[guild] = cmds['guild'][guild] + cmds_formatted = {None: cmds["global"]} + for guild in cmds["guild"]: + cmds_formatted[guild] = cmds["guild"][guild] for scope in cmds_formatted: permissions = {} new_cmds = cmds_formatted[scope] - existing_cmds = await self.req.get_all_commands(guild_id = scope) + existing_cmds = await self.req.get_all_commands(guild_id=scope) existing_by_name = {} - to_send=[] + to_send = [] changed = False for cmd in existing_cmds: existing_by_name[cmd["name"]] = model.CommandData(**cmd) @@ -345,22 +362,27 @@ async def sync_all_commands( cmd_data = model.CommandData(**command) existing_cmd = existing_by_name[cmd_name] if cmd_data != existing_cmd: - changed=True + changed = True to_send.append(command) else: command_with_id = command command_with_id["id"] = existing_cmd.id to_send.append(command_with_id) else: - changed=True + changed = True to_send.append(command) - if changed: - self.logger.debug(f"Detected changes on {scope if scope is not None else 'global'}, updating them") - existing_cmds = await self.req.put_slash_commands(slash_commands=to_send, guild_id=scope) + self.logger.debug( + f"Detected changes on {scope if scope is not None else 'global'}, updating them" + ) + existing_cmds = await self.req.put_slash_commands( + slash_commands=to_send, guild_id=scope + ) else: - self.logger.debug(f"Detected no changes on {scope if scope is not None else 'global'}, skipping") + self.logger.debug( + f"Detected no changes on {scope if scope is not None else 'global'}, skipping" + ) id_name_map = {} for cmd in existing_cmds: @@ -375,11 +397,10 @@ async def sync_all_commands( permission = { "id": cmd_id, "guild_id": applicable_guild, - "permissions": cmd_permissions[applicable_guild] + "permissions": cmd_permissions[applicable_guild], } permissions_map[applicable_guild].append(permission) - self.logger.info("Syncing permissions...") self.logger.debug(f"Commands permission data are {permissions_map}") for scope in permissions_map: @@ -392,12 +413,16 @@ async def sync_all_commands( else: existing_perms_model = {} for existing_perm in existing_perms: - existing_perms_model[existing_perm["id"]] = model.GuildPermissionsData(**existing_perm) + existing_perms_model[existing_perm["id"]] = model.GuildPermissionsData( + **existing_perm + ) for new_perm in new_perms: if new_perm["id"] not in existing_perms_model: changed = True break - if existing_perms_model[new_perm["id"]] != model.GuildPermissionsData(**new_perm): + if existing_perms_model[new_perm["id"]] != model.GuildPermissionsData( + **new_perm + ): changed = True break @@ -407,24 +432,26 @@ async def sync_all_commands( else: self.logger.debug(f"Detected no permissions changes on {scope}, skipping") - if delete_from_unused_guilds: self.logger.info("Deleting unused guild commands...") - other_guilds = [guild.id for guild in self._discord.guilds if guild.id not in cmds["guild"]] + other_guilds = [ + guild.id for guild in self._discord.guilds if guild.id not in cmds["guild"] + ] # This is an extremly bad way to do this, because slash cmds can be in guilds the bot isn't in # But it's the only way until discord makes an endpoint to request all the guild with cmds registered. for guild in other_guilds: with suppress(discord.Forbidden): - existing = await self.req.get_all_commands(guild_id = guild) + existing = await self.req.get_all_commands(guild_id=guild) if len(existing) != 0: self.logger.debug(f"Deleting commands from {guild}") await self.req.put_slash_commands(slash_commands=[], guild_id=guild) - if delete_perms_from_unused_guilds: self.logger.info("Deleting unused guild permissions...") - other_guilds = [guild.id for guild in self._discord.guilds if guild.id not in permissions_map.keys()] + other_guilds = [ + guild.id for guild in self._discord.guilds if guild.id not in permissions_map.keys() + ] for guild in other_guilds: with suppress(discord.Forbidden): self.logger.debug(f"Deleting permissions from {guild}") @@ -434,16 +461,18 @@ async def sync_all_commands( self.logger.info("Completed syncing all commands!") - def add_slash_command(self, - cmd, - name: str = None, - description: str = None, - guild_ids: typing.List[int] = None, - options: list = None, - default_permission: bool = True, - permissions: typing.Dict[int, list] = None, - connector: dict = None, - has_subcommands: bool = False): + def add_slash_command( + self, + cmd, + name: str = None, + description: str = None, + guild_ids: typing.List[int] = None, + options: list = None, + default_permission: bool = True, + permissions: typing.Dict[int, list] = None, + connector: dict = None, + has_subcommands: bool = False, + ): """ Registers slash command to SlashCommand. @@ -495,26 +524,28 @@ def add_slash_command(self, "default_permission": default_permission, "api_permissions": permissions, "connector": connector or {}, - "has_subcommands": has_subcommands + "has_subcommands": has_subcommands, } obj = model.BaseCommandObject(name, _cmd) self.commands[name] = obj self.logger.debug(f"Added command `{name}`") return obj - def add_subcommand(self, - cmd, - base, - subcommand_group=None, - name=None, - description: str = None, - base_description: str = None, - base_default_permission: bool = True, - base_permissions: typing.Dict[int, list] = None, - subcommand_group_description: str = None, - guild_ids: typing.List[int] = None, - options: list = None, - connector: dict = None): + def add_subcommand( + self, + cmd, + base, + subcommand_group=None, + name=None, + description: str = None, + base_description: str = None, + base_default_permission: bool = True, + base_permissions: typing.Dict[int, list] = None, + subcommand_group_description: str = None, + guild_ids: typing.List[int] = None, + options: list = None, + connector: dict = None, + ): """ Registers subcommand to SlashCommand. @@ -566,7 +597,7 @@ def add_subcommand(self, "default_permission": base_default_permission, "api_permissions": base_permissions, "connector": {}, - "has_subcommands": True + "has_subcommands": True, } _sub = { "func": cmd, @@ -576,7 +607,7 @@ def add_subcommand(self, "sub_group_desc": subcommand_group_description, "guild_ids": guild_ids, "api_options": options, - "connector": connector or {} + "connector": connector or {}, } if base not in self.commands: self.commands[base] = model.BaseCommandObject(base, _cmd) @@ -587,7 +618,9 @@ def add_subcommand(self, for applicable_guild in base_permissions: if applicable_guild not in base_command.permissions: base_command.permissions[applicable_guild] = [] - base_command.permissions[applicable_guild].extend(base_permissions[applicable_guild]) + base_command.permissions[applicable_guild].extend( + base_permissions[applicable_guild] + ) if base_command.description: _cmd["description"] = base_command.description if base not in self.subcommands: @@ -604,18 +637,22 @@ def add_subcommand(self, raise error.DuplicateCommand(f"{base} {name}") obj = model.SubcommandObject(_sub, base, name) self.subcommands[base][name] = obj - self.logger.debug(f"Added subcommand `{base} {subcommand_group or ''} {name or cmd.__name__}`") + self.logger.debug( + f"Added subcommand `{base} {subcommand_group or ''} {name or cmd.__name__}`" + ) return obj - def slash(self, - *, - name: str = None, - description: str = None, - guild_ids: typing.List[int] = None, - options: typing.List[dict] = None, - default_permission: bool = True, - permissions: dict = None, - connector: dict = None): + def slash( + self, + *, + name: str = None, + description: str = None, + guild_ids: typing.List[int] = None, + options: typing.List[dict] = None, + default_permission: bool = True, + permissions: dict = None, + connector: dict = None, + ): """ Decorator that registers coroutine as a slash command.\n All decorator args must be passed as keyword-only args.\n @@ -680,26 +717,37 @@ def wrapper(cmd): if decorator_permissions: permissions.update(decorator_permissions) - obj = self.add_slash_command(cmd, name, description, guild_ids, options, default_permission, permissions, connector) + obj = self.add_slash_command( + cmd, + name, + description, + guild_ids, + options, + default_permission, + permissions, + connector, + ) return obj return wrapper - def subcommand(self, - *, - base, - subcommand_group=None, - name=None, - description: str = None, - base_description: str = None, - base_desc: str = None, - base_default_permission: bool = True, - base_permissions: dict = None, - subcommand_group_description: str = None, - sub_group_desc: str = None, - guild_ids: typing.List[int] = None, - options: typing.List[dict] = None, - connector: dict = None): + def subcommand( + self, + *, + base, + subcommand_group=None, + name=None, + description: str = None, + base_description: str = None, + base_desc: str = None, + base_default_permission: bool = True, + base_permissions: dict = None, + subcommand_group_description: str = None, + sub_group_desc: str = None, + guild_ids: typing.List[int] = None, + options: typing.List[dict] = None, + connector: dict = None, + ): """ Decorator that registers subcommand.\n Unlike discord.py, you don't need base command.\n @@ -763,7 +811,20 @@ def wrapper(cmd): if decorator_permissions: base_permissions.update(decorator_permissions) - obj = self.add_subcommand(cmd, base, subcommand_group, name, description, base_description, base_default_permission, base_permissions, subcommand_group_description, guild_ids, options, connector) + obj = self.add_subcommand( + cmd, + base, + subcommand_group, + name, + description, + base_description, + base_default_permission, + base_permissions, + subcommand_group_description, + guild_ids, + options, + connector, + ) return obj return wrapper @@ -777,6 +838,7 @@ def permission(self, guild_id: int, permissions: list): :type permissions: dict """ + def wrapper(cmd): if not getattr(cmd, "__permissions__", None): cmd.__permissions__ = {} @@ -785,8 +847,13 @@ def wrapper(cmd): return wrapper - async def process_options(self, guild: discord.Guild, options: list, connector: dict, - temporary_auto_convert: dict = None) -> dict: + async def process_options( + self, + guild: discord.Guild, + options: list, + connector: dict, + temporary_auto_convert: dict = None, + ) -> dict: """ Processes Role, User, and Channel option types to discord.py's models. @@ -808,7 +875,7 @@ async def process_options(self, guild: discord.Guild, options: list, connector: # and 2nd as a actual fetching method. [guild.get_member, guild.fetch_member], guild.get_channel, - guild.get_role + guild.get_role, ] types = { @@ -826,7 +893,7 @@ async def process_options(self, guild: discord.Guild, options: list, connector: "ROLE": 2, model.SlashCommandOptionType.ROLE: 2, 8: 2, - "8": 2 + "8": 2, } to_return = {} @@ -851,10 +918,16 @@ async def process_options(self, guild: discord.Guild, options: list, connector: loaded_converter = loaded_converter[1] if not processed: try: - processed = await loaded_converter(int(x["value"])) \ - if iscoroutinefunction(loaded_converter) else \ - loaded_converter(int(x["value"])) - except (discord.Forbidden, discord.HTTPException, discord.NotFound): # Just in case. + processed = ( + await loaded_converter(int(x["value"])) + if iscoroutinefunction(loaded_converter) + else loaded_converter(int(x["value"])) + ) + except ( + discord.Forbidden, + discord.HTTPException, + discord.NotFound, + ): # Just in case. self.logger.warning("Failed fetching discord object! Passing ID instead.") processed = int(x["value"]) to_return[connector.get(x["name"]) or x["name"]] = processed @@ -922,7 +995,10 @@ async def _on_slash(self, to_use): selected_cmd = self.commands[to_use["data"]["name"]] - if selected_cmd.allowed_guild_ids and ctx.guild_id not in selected_cmd.allowed_guild_ids: + if ( + selected_cmd.allowed_guild_ids + and ctx.guild_id not in selected_cmd.allowed_guild_ids + ): return if selected_cmd.has_subcommands and not selected_cmd.func: @@ -939,8 +1015,16 @@ async def _on_slash(self, to_use): for x in selected_cmd.options: temporary_auto_convert[x["name"].lower()] = x["type"] - args = await self.process_options(ctx.guild, to_use["data"]["options"], selected_cmd.connector, temporary_auto_convert) \ - if "options" in to_use["data"] else {} + args = ( + await self.process_options( + ctx.guild, + to_use["data"]["options"], + selected_cmd.connector, + temporary_auto_convert, + ) + if "options" in to_use["data"] + else {} + ) self._discord.dispatch("slash_command", ctx) @@ -979,8 +1063,13 @@ async def handle_subcommand(self, ctx: context.SlashContext, data: dict): for n in selected.options: temporary_auto_convert[n["name"].lower()] = n["type"] - args = await self.process_options(ctx.guild, x["options"], selected.connector, temporary_auto_convert) \ - if "options" in x else {} + args = ( + await self.process_options( + ctx.guild, x["options"], selected.connector, temporary_auto_convert + ) + if "options" in x + else {} + ) self._discord.dispatch("slash_command", ctx) await self.invoke_command(selected, ctx, args) return @@ -992,8 +1081,13 @@ async def handle_subcommand(self, ctx: context.SlashContext, data: dict): for n in selected.options: temporary_auto_convert[n["name"].lower()] = n["type"] - args = await self.process_options(ctx.guild, sub_opts, selected.connector, temporary_auto_convert) \ - if "options" in sub else {} + args = ( + await self.process_options( + ctx.guild, sub_opts, selected.connector, temporary_auto_convert + ) + if "options" in sub + else {} + ) self._discord.dispatch("slash_command", ctx) await self.invoke_command(selected, ctx, args) @@ -1024,7 +1118,7 @@ async def on_slash_command_error(ctx, ex): :return: """ if self.has_listener: - if self._discord.extra_events.get('on_slash_command_error'): + if self._discord.extra_events.get("on_slash_command_error"): self._discord.dispatch("slash_command_error", ctx, ex) return if hasattr(self._discord, "on_slash_command_error"): diff --git a/discord_slash/cog_ext.py b/discord_slash/cog_ext.py index 1d5589d5c..54e699f1f 100644 --- a/discord_slash/cog_ext.py +++ b/discord_slash/cog_ext.py @@ -1,17 +1,20 @@ -import typing import inspect +import typing + from .model import CogBaseCommandObject, CogSubcommandObject from .utils import manage_commands -def cog_slash(*, - name: str = None, - description: str = None, - guild_ids: typing.List[int] = None, - options: typing.List[dict] = None, - default_permission: bool = True, - permissions: typing.Dict[int, list] = None, - connector: dict = None): +def cog_slash( + *, + name: str = None, + description: str = None, + guild_ids: typing.List[int] = None, + options: typing.List[dict] = None, + default_permission: bool = True, + permissions: typing.Dict[int, list] = None, + connector: dict = None +): """ Decorator for Cog to add slash command.\n Almost same as :func:`.client.SlashCommand.slash`. @@ -43,6 +46,7 @@ async def ping(self, ctx: SlashContext): :param connector: Kwargs connector for the command. Default ``None``. :type connector: dict """ + def wrapper(cmd): desc = description or inspect.getdoc(cmd) if options is None: @@ -58,26 +62,29 @@ def wrapper(cmd): "default_permission": default_permission, "api_permissions": permissions, "connector": connector, - "has_subcommands": False + "has_subcommands": False, } return CogBaseCommandObject(name or cmd.__name__, _cmd) + return wrapper -def cog_subcommand(*, - base, - subcommand_group=None, - name=None, - description: str = None, - base_description: str = None, - base_desc: str = None, - base_default_permission: bool = True, - base_permissions: typing.Dict[int, list] = None, - subcommand_group_description: str = None, - sub_group_desc: str = None, - guild_ids: typing.List[int] = None, - options: typing.List[dict] = None, - connector: dict = None): +def cog_subcommand( + *, + base, + subcommand_group=None, + name=None, + description: str = None, + base_description: str = None, + base_desc: str = None, + base_default_permission: bool = True, + base_permissions: typing.Dict[int, list] = None, + subcommand_group_description: str = None, + sub_group_desc: str = None, + guild_ids: typing.List[int] = None, + options: typing.List[dict] = None, + connector: dict = None +): """ Decorator for Cog to add subcommand.\n Almost same as :func:`.client.SlashCommand.subcommand`. @@ -138,7 +145,7 @@ def wrapper(cmd): "default_permission": base_default_permission, "api_permissions": base_permissions, "connector": {}, - "has_subcommands": True + "has_subcommands": True, } _sub = { @@ -149,7 +156,8 @@ def wrapper(cmd): "sub_group_desc": subcommand_group_description, "guild_ids": guild_ids, "api_options": opts, - "connector": connector + "connector": connector, } return CogSubcommandObject(base, _cmd, subcommand_group, name or cmd.__name__, _sub) + return wrapper diff --git a/discord_slash/context.py b/discord_slash/context.py index 58635ab50..5972ccad5 100644 --- a/discord_slash/context.py +++ b/discord_slash/context.py @@ -6,10 +6,8 @@ from discord.ext import commands from discord.utils import snowflake_time -from . import http -from . import error -from . import model -from . dpy_overrides import ComponentMessage +from . import error, http, model +from .dpy_overrides import ComponentMessage class InteractionContext: @@ -34,11 +32,13 @@ class InteractionContext: :ivar author: User or Member instance of the command invoke. """ - def __init__(self, - _http: http.SlashCommandRequest, - _json: dict, - _discord: typing.Union[discord.Client, commands.Bot], - logger): + def __init__( + self, + _http: http.SlashCommandRequest, + _json: dict, + _discord: typing.Union[discord.Client, commands.Bot], + logger, + ): self._token = _json["token"] self.message = None # Should be set later. self.interaction_id = _json["id"] @@ -49,10 +49,14 @@ def __init__(self, self.responded = False self._deferred_hidden = False # To check if the patch to the deferred response matches self.guild_id = int(_json["guild_id"]) if "guild_id" in _json.keys() else None - self.author_id = int(_json["member"]["user"]["id"] if "member" in _json.keys() else _json["user"]["id"]) + self.author_id = int( + _json["member"]["user"]["id"] if "member" in _json.keys() else _json["user"]["id"] + ) self.channel_id = int(_json["channel_id"]) if self.guild: - self.author = discord.Member(data=_json["member"], state=self.bot._connection, guild=self.guild) + self.author = discord.Member( + data=_json["member"], state=self.bot._connection, guild=self.guild + ) elif self.guild_id: self.author = discord.User(data=_json["member"]["user"], state=self.bot._connection) else: @@ -61,12 +65,20 @@ def __init__(self, @property def _deffered_hidden(self): - warn("`_deffered_hidden` as been renamed to `_deferred_hidden`.", DeprecationWarning, stacklevel=2) + warn( + "`_deffered_hidden` as been renamed to `_deferred_hidden`.", + DeprecationWarning, + stacklevel=2, + ) return self._deferred_hidden @_deffered_hidden.setter def _deffered_hidden(self, value): - warn("`_deffered_hidden` as been renamed to `_deferred_hidden`.", DeprecationWarning, stacklevel=2) + warn( + "`_deffered_hidden` as been renamed to `_deferred_hidden`.", + DeprecationWarning, + stacklevel=2, + ) self._deferred_hidden = value @property @@ -112,18 +124,20 @@ async def defer(self, hidden: bool = False): await self._http.post_initial_response(base, self.interaction_id, self._token) self.deferred = True - async def send(self, - content: str = "", *, - embed: discord.Embed = None, - embeds: typing.List[discord.Embed] = None, - tts: bool = False, - file: discord.File = None, - files: typing.List[discord.File] = None, - allowed_mentions: discord.AllowedMentions = None, - hidden: bool = False, - delete_after: float = None, - components: typing.List[dict] = None, - ) -> model.SlashMessage: + async def send( + self, + content: str = "", + *, + embed: discord.Embed = None, + embeds: typing.List[discord.Embed] = None, + tts: bool = False, + file: discord.File = None, + files: typing.List[discord.File] = None, + allowed_mentions: discord.AllowedMentions = None, + hidden: bool = False, + delete_after: float = None, + components: typing.List[dict] = None, + ) -> model.SlashMessage: """ Sends response of the slash command. @@ -170,14 +184,19 @@ async def send(self, if delete_after and hidden: raise error.IncorrectFormat("You can't delete a hidden message!") if components and not all(comp.get("type") == 1 for comp in components): - raise error.IncorrectFormat("The top level of the components list must be made of ActionRows!") + raise error.IncorrectFormat( + "The top level of the components list must be made of ActionRows!" + ) base = { "content": content, "tts": tts, "embeds": [x.to_dict() for x in embeds] if embeds else [], - "allowed_mentions": allowed_mentions.to_dict() if allowed_mentions - else self.bot.allowed_mentions.to_dict() if self.bot.allowed_mentions else {}, + "allowed_mentions": allowed_mentions.to_dict() + if allowed_mentions + else self.bot.allowed_mentions.to_dict() + if self.bot.allowed_mentions + else {}, "components": components or [], } if hidden: @@ -197,10 +216,7 @@ async def send(self, resp = await self._http.edit(base, self._token, files=files) self.deferred = False else: - json_data = { - "type": 4, - "data": base - } + json_data = {"type": 4, "data": base} await self._http.post_initial_response(json_data, self.interaction_id, self._token) if not hidden: resp = await self._http.edit({}, self._token) @@ -213,11 +229,13 @@ async def send(self, for file in files: file.close() if not hidden: - smsg = model.SlashMessage(state=self.bot._connection, - data=resp, - channel=self.channel or discord.Object(id=self.channel_id), - _http=self._http, - interaction_token=self._token) + smsg = model.SlashMessage( + state=self.bot._connection, + data=resp, + channel=self.channel or discord.Object(id=self.channel_id), + _http=self._http, + interaction_token=self._token, + ) if delete_after: self.bot.loop.create_task(smsg.delete(delay=delete_after)) if initial_message: @@ -238,11 +256,14 @@ class SlashContext(InteractionContext): :ivar subcommand_group: Subcommand group of the command. :ivar command_id: ID of the command. """ - def __init__(self, - _http: http.SlashCommandRequest, - _json: dict, - _discord: typing.Union[discord.Client, commands.Bot], - logger): + + def __init__( + self, + _http: http.SlashCommandRequest, + _json: dict, + _discord: typing.Union[discord.Client, commands.Bot], + logger, + ): self.name = self.command = self.invoked_with = _json["data"]["name"] self.args = [] self.kwargs = {} @@ -262,11 +283,14 @@ class ComponentContext(InteractionContext): :ivar origin_message: The origin message of the component. Not available if the origin message was ephemeral. :ivar origin_message_id: The ID of the origin message. """ - def __init__(self, - _http: http.SlashCommandRequest, - _json: dict, - _discord: typing.Union[discord.Client, commands.Bot], - logger): + + def __init__( + self, + _http: http.SlashCommandRequest, + _json: dict, + _discord: typing.Union[discord.Client, commands.Bot], + logger, + ): self.custom_id = self.component_id = _json["data"]["custom_id"] self.component_type = _json["data"]["component_type"] super().__init__(_http=_http, _json=_json, _discord=_discord, logger=logger) @@ -274,8 +298,9 @@ def __init__(self, self.origin_message_id = int(_json["message"]["id"]) if "message" in _json.keys() else None if self.origin_message_id and (_json["message"]["flags"] & 64) != 64: - self.origin_message = ComponentMessage(state=self.bot._connection, channel=self.channel, - data=_json["message"]) + self.origin_message = ComponentMessage( + state=self.bot._connection, channel=self.channel, data=_json["message"] + ) async def defer(self, hidden: bool = False, edit_origin: bool = False): """ @@ -329,8 +354,13 @@ async def edit_origin(self, **fields): _resp["embeds"] = [x.to_dict() for x in embeds] allowed_mentions = fields.get("allowed_mentions") - _resp["allowed_mentions"] = allowed_mentions.to_dict() if allowed_mentions else \ - self.bot.allowed_mentions.to_dict() if self.bot.allowed_mentions else {} + _resp["allowed_mentions"] = ( + allowed_mentions.to_dict() + if allowed_mentions + else self.bot.allowed_mentions.to_dict() + if self.bot.allowed_mentions + else {} + ) if not self.responded: if files and not self.deferred: diff --git a/discord_slash/dpy_overrides.py b/discord_slash/dpy_overrides.py index 58d63c732..f943f29b4 100644 --- a/discord_slash/dpy_overrides.py +++ b/discord_slash/dpy_overrides.py @@ -1,10 +1,7 @@ import discord +from discord import AllowedMentions, File, InvalidArgument, abc, http, utils from discord.ext import commands -from discord import AllowedMentions, InvalidArgument, File from discord.http import Route -from discord import http -from discord import abc -from discord import utils class ComponentMessage(discord.Message): @@ -12,7 +9,7 @@ class ComponentMessage(discord.Message): def __init__(self, *, state, channel, data): super().__init__(state=state, channel=channel, data=data) - self.components = data['components'] + self.components = data["components"] def new_override(cls, *args, **kwargs): @@ -25,71 +22,96 @@ def new_override(cls, *args, **kwargs): discord.message.Message.__new__ = new_override -def send_files(self, channel_id, *, files, content=None, tts=False, embed=None, components=None, - nonce=None, allowed_mentions=None, message_reference=None): - r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id) +def send_files( + self, + channel_id, + *, + files, + content=None, + tts=False, + embed=None, + components=None, + nonce=None, + allowed_mentions=None, + message_reference=None +): + r = Route("POST", "/channels/{channel_id}/messages", channel_id=channel_id) form = [] - payload = {'tts': tts} + payload = {"tts": tts} if content: - payload['content'] = content + payload["content"] = content if embed: - payload['embed'] = embed + payload["embed"] = embed if components: - payload['components'] = components + payload["components"] = components if nonce: - payload['nonce'] = nonce + payload["nonce"] = nonce if allowed_mentions: - payload['allowed_mentions'] = allowed_mentions + payload["allowed_mentions"] = allowed_mentions if message_reference: - payload['message_reference'] = message_reference + payload["message_reference"] = message_reference - form.append({'name': 'payload_json', 'value': utils.to_json(payload)}) + form.append({"name": "payload_json", "value": utils.to_json(payload)}) if len(files) == 1: file = files[0] - form.append({ - 'name': 'file', - 'value': file.fp, - 'filename': file.filename, - 'content_type': 'application/octet-stream' - }) + form.append( + { + "name": "file", + "value": file.fp, + "filename": file.filename, + "content_type": "application/octet-stream", + } + ) else: for index, file in enumerate(files): - form.append({ - 'name': 'file%s' % index, - 'value': file.fp, - 'filename': file.filename, - 'content_type': 'application/octet-stream' - }) + form.append( + { + "name": "file%s" % index, + "value": file.fp, + "filename": file.filename, + "content_type": "application/octet-stream", + } + ) return self.request(r, form=form, files=files) -def send_message(self, channel_id, content, *, tts=False, embed=None, components=None, - nonce=None, allowed_mentions=None, message_reference=None): - r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id) +def send_message( + self, + channel_id, + content, + *, + tts=False, + embed=None, + components=None, + nonce=None, + allowed_mentions=None, + message_reference=None +): + r = Route("POST", "/channels/{channel_id}/messages", channel_id=channel_id) payload = {} if content: - payload['content'] = content + payload["content"] = content if tts: - payload['tts'] = True + payload["tts"] = True if embed: - payload['embed'] = embed + payload["embed"] = embed if components: - payload['components'] = components + payload["components"] = components if nonce: - payload['nonce'] = nonce + payload["nonce"] = nonce if allowed_mentions: - payload['allowed_mentions'] = allowed_mentions + payload["allowed_mentions"] = allowed_mentions if message_reference: - payload['message_reference'] = message_reference + payload["message_reference"] = message_reference return self.request(r, json=payload) @@ -98,10 +120,21 @@ def send_message(self, channel_id, content, *, tts=False, embed=None, components http.HTTPClient.send_message = send_message -async def send(self, content=None, *, tts=False, embed=None, file=None, components=None, - files=None, delete_after=None, nonce=None, - allowed_mentions=None, reference=None, - mention_author=None): +async def send( + self, + content=None, + *, + tts=False, + embed=None, + file=None, + components=None, + files=None, + delete_after=None, + nonce=None, + allowed_mentions=None, + reference=None, + mention_author=None +): """|coro| Sends a message to the destination with the content given. @@ -195,47 +228,70 @@ async def send(self, content=None, *, tts=False, embed=None, file=None, componen if mention_author is not None: allowed_mentions = allowed_mentions or AllowedMentions().to_dict() - allowed_mentions['replied_user'] = bool(mention_author) + allowed_mentions["replied_user"] = bool(mention_author) if reference is not None: try: reference = reference.to_message_reference_dict() except AttributeError: - raise InvalidArgument('reference parameter must be Message or MessageReference') from None + raise InvalidArgument( + "reference parameter must be Message or MessageReference" + ) from None if file is not None and files is not None: - raise InvalidArgument('cannot pass both file and files parameter to send()') + raise InvalidArgument("cannot pass both file and files parameter to send()") if file is not None: if not isinstance(file, File): - raise InvalidArgument('file parameter must be File') + raise InvalidArgument("file parameter must be File") try: - data = await state.http.send_files(channel.id, files=[file], allowed_mentions=allowed_mentions, - content=content, tts=tts, embed=embed, nonce=nonce, - components=components, - message_reference=reference) + data = await state.http.send_files( + channel.id, + files=[file], + allowed_mentions=allowed_mentions, + content=content, + tts=tts, + embed=embed, + nonce=nonce, + components=components, + message_reference=reference, + ) finally: file.close() elif files is not None: if len(files) > 10: - raise InvalidArgument('files parameter must be a list of up to 10 elements') + raise InvalidArgument("files parameter must be a list of up to 10 elements") elif not all(isinstance(file, File) for file in files): - raise InvalidArgument('files parameter must be a list of File') + raise InvalidArgument("files parameter must be a list of File") try: - data = await state.http.send_files(channel.id, files=files, content=content, tts=tts, - embed=embed, nonce=nonce, allowed_mentions=allowed_mentions, - components=components, - message_reference=reference) + data = await state.http.send_files( + channel.id, + files=files, + content=content, + tts=tts, + embed=embed, + nonce=nonce, + allowed_mentions=allowed_mentions, + components=components, + message_reference=reference, + ) finally: for f in files: f.close() else: - data = await state.http.send_message(channel.id, content, tts=tts, embed=embed, components=components, - nonce=nonce, allowed_mentions=allowed_mentions, - message_reference=reference) + data = await state.http.send_message( + channel.id, + content, + tts=tts, + embed=embed, + components=components, + nonce=nonce, + allowed_mentions=allowed_mentions, + message_reference=reference, + ) ret = state.create_message(channel=channel, data=data) if delete_after is not None: @@ -251,4 +307,5 @@ async def send_override(context_or_channel, *args, **kwargs): return await send(channel, *args, **kwargs) + abc.Messageable.send = send_override diff --git a/discord_slash/error.py b/discord_slash/error.py index 8df1b92de..897576281 100644 --- a/discord_slash/error.py +++ b/discord_slash/error.py @@ -18,6 +18,7 @@ class RequestFailure(SlashCommandError): :ivar status: Status code of failed response. :ivar msg: Message of failed response. """ + def __init__(self, status: int, msg: str): self.status = status self.msg = msg @@ -34,6 +35,7 @@ class DuplicateCommand(SlashCommandError): """ There is a duplicate command name. """ + def __init__(self, name: str): super().__init__(f"Duplicate command name detected: {name}") @@ -60,7 +62,7 @@ class IncorrectCommandData(SlashCommandError): """ Incorrect data was passed to a slash command data object """ - + class AlreadyResponded(SlashCommandError): """ diff --git a/discord_slash/http.py b/discord_slash/http.py index 3d63ee8f0..ee5ebb056 100644 --- a/discord_slash/http.py +++ b/discord_slash/http.py @@ -1,14 +1,17 @@ import json import typing + import aiohttp import discord from discord.http import Route + from . import error from .const import BASE_API class CustomRoute(Route): """discord.py's Route but changed ``BASE`` to use at slash command.""" + BASE = BASE_API @@ -30,9 +33,7 @@ def put_slash_commands(self, slash_commands: list, guild_id): :param slash_commands: List of all the slash commands to make a put request to discord with. :param guild_id: ID of the guild to set the commands on. Pass `None` for the global scope. """ - return self.command_request( - method="PUT", guild_id = guild_id, json = slash_commands - ) + return self.command_request(method="PUT", guild_id=guild_id, json=slash_commands) def remove_slash_command(self, guild_id, cmd_id): """ @@ -42,9 +43,7 @@ def remove_slash_command(self, guild_id, cmd_id): :param cmd_id: ID of the command. :return: Response code of the request. """ - return self.command_request( - method="DELETE", guild_id=guild_id, url_ending=f"/{cmd_id}" - ) + return self.command_request(method="DELETE", guild_id=guild_id, url_ending=f"/{cmd_id}") def get_all_commands(self, guild_id=None): """ @@ -71,11 +70,11 @@ def update_guild_commands_permissions(self, guild_id, perms_dict): :param guild_id: ID of the target guild to register command permissions. :return: JSON Response of the request. """ - return self.command_request(method="PUT", guild_id=guild_id, json=perms_dict, url_ending="/permissions") + return self.command_request( + method="PUT", guild_id=guild_id, json=perms_dict, url_ending="/permissions" + ) - def add_slash_command( - self, guild_id, cmd_name: str, description: str, options: list = None - ): + def add_slash_command(self, guild_id, cmd_name: str, description: str, options: list = None): """ Sends a slash command add request to Discord API. @@ -86,7 +85,7 @@ def add_slash_command( :return: JSON Response of the request. """ base = {"name": cmd_name, "description": description, "options": options or []} - return self.command_request(json=base, method="POST", guild_id = guild_id) + return self.command_request(json=base, method="POST", guild_id=guild_id) def command_request(self, method, guild_id, url_ending="", **kwargs): r""" @@ -130,7 +129,9 @@ def post_initial_response(self, _resp, interaction_id, token): """ return self.command_response(token, False, "POST", interaction_id, json=_resp) - def command_response(self, token, use_webhook, method, interaction_id= None, url_ending = "", **kwargs): + def command_response( + self, token, use_webhook, method, interaction_id=None, url_ending="", **kwargs + ): r""" Sends a command response to discord (POST, PATCH, DELETE) @@ -143,21 +144,33 @@ def command_response(self, token, use_webhook, method, interaction_id= None, url :return: Coroutine """ if not use_webhook and not interaction_id: - raise error.IncorrectFormat("Internal Error! interaction_id must be set if use_webhook is False") - req_url = f"/webhooks/{self.application_id}/{token}" if use_webhook else f"/interactions/{interaction_id}/{token}/callback" + raise error.IncorrectFormat( + "Internal Error! interaction_id must be set if use_webhook is False" + ) + req_url = ( + f"/webhooks/{self.application_id}/{token}" + if use_webhook + else f"/interactions/{interaction_id}/{token}/callback" + ) req_url += url_ending route = CustomRoute(method, req_url) return self._discord.http.request(route, **kwargs) - def request_with_files(self, _resp, files: typing.List[discord.File], token, method, url_ending = ""): + def request_with_files( + self, _resp, files: typing.List[discord.File], token, method, url_ending="" + ): form = aiohttp.FormData() form.add_field("payload_json", json.dumps(_resp)) for x in range(len(files)): name = f"file{x if len(files) > 1 else ''}" sel = files[x] - form.add_field(name, sel.fp, filename=sel.filename, content_type="application/octet-stream") - return self.command_response(token, True, method, data=form, files=files, url_ending=url_ending) + form.add_field( + name, sel.fp, filename=sel.filename, content_type="application/octet-stream" + ) + return self.command_response( + token, True, method, data=form, files=files, url_ending=url_ending + ) def edit(self, _resp, token, message_id="@original", files: typing.List[discord.File] = None): """ @@ -173,8 +186,8 @@ def edit(self, _resp, token, message_id="@original", files: typing.List[discord. """ req_url = f"/messages/{message_id}" if files: - return self.request_with_files(_resp, files, token, "PATCH", url_ending = req_url) - return self.command_response(token, True, "PATCH", url_ending = req_url, json=_resp) + return self.request_with_files(_resp, files, token, "PATCH", url_ending=req_url) + return self.command_response(token, True, "PATCH", url_ending=req_url, json=_resp) def delete(self, token, message_id="@original"): """ @@ -185,4 +198,4 @@ def delete(self, token, message_id="@original"): :return: Coroutine """ req_url = f"/messages/{message_id}" - return self.command_response(token, True, "DELETE", url_ending = req_url) + return self.command_response(token, True, "DELETE", url_ending=req_url) diff --git a/discord_slash/model.py b/discord_slash/model.py index 7dd2d1f6b..c8b4f2a34 100644 --- a/discord_slash/model.py +++ b/discord_slash/model.py @@ -1,16 +1,14 @@ import asyncio import datetime - -import discord -from enum import IntEnum from contextlib import suppress +from enum import IntEnum from inspect import iscoroutinefunction -from discord.ext.commands import CooldownMapping, CommandOnCooldown +import discord +from discord.ext.commands import CommandOnCooldown, CooldownMapping -from . import http -from . import error -from . dpy_overrides import ComponentMessage +from . import error, http +from .dpy_overrides import ComponentMessage class ChoiceData: @@ -40,9 +38,7 @@ class OptionData: :ivar options: List of :class:`OptionData`, this will be present if it's a subcommand group """ - def __init__( - self, name, description, required=False, choices=None, options=None, **kwargs - ): + def __init__(self, name, description, required=False, choices=None, options=None, **kwargs): self.name = name self.description = description self.type = kwargs.get("type") @@ -83,7 +79,15 @@ class CommandData: """ def __init__( - self, name, description, options=None, default_permission=True, id=None, application_id=None, version=None, **kwargs + self, + name, + description, + options=None, + default_permission=True, + id=None, + application_id=None, + version=None, + **kwargs ): self.name = name self.description = description @@ -101,10 +105,10 @@ def __init__( def __eq__(self, other): if isinstance(other, CommandData): return ( - self.name == other.name - and self.description == other.description - and self.options == other.options - and self.default_permission == other.default_permission + self.name == other.name + and self.description == other.description + and self.options == other.options + and self.default_permission == other.default_permission ) else: return False @@ -137,7 +141,7 @@ def __init__(self, name, cmd): # Let's reuse old command formatting. # Since this isn't inherited from `discord.ext.commands.Command`, discord.py's check decorator will # add checks at this var. self.__commands_checks__ = [] - if hasattr(self.func, '__commands_checks__'): + if hasattr(self.func, "__commands_checks__"): self.__commands_checks__ = self.func.__commands_checks__ cooldown = None @@ -282,7 +286,10 @@ async def can_run(self, ctx) -> bool: :type ctx: .context.SlashContext :return: bool """ - res = [bool(x(ctx)) if not iscoroutinefunction(x) else bool(await x(ctx)) for x in self.__commands_checks__] + res = [ + bool(x(ctx)) if not iscoroutinefunction(x) else bool(await x(ctx)) + for x in self.__commands_checks__ + ] return False not in res @@ -307,6 +314,7 @@ def __init__(self, name, cmd): # Let's reuse old command formatting. self.default_permission = cmd["default_permission"] self.permissions = cmd["api_permissions"] or {} + class SubcommandObject(CommandObject): """ Subcommand object of this extension. @@ -362,6 +370,7 @@ class SlashCommandOptionType(IntEnum): """ Equivalent of `ApplicationCommandOptionType `_ in the Discord API. """ + SUB_COMMAND = 1 SUB_COMMAND_GROUP = 2 STRING = 3 @@ -379,13 +388,19 @@ def from_type(cls, t: type): :param t: The type or object to get a SlashCommandOptionType for. :return: :class:`.model.SlashCommandOptionType` or ``None`` """ - if issubclass(t, str): return cls.STRING - if issubclass(t, bool): return cls.BOOLEAN + if issubclass(t, str): + return cls.STRING + if issubclass(t, bool): + return cls.BOOLEAN # The check for bool MUST be above the check for integers as booleans subclass integers - if issubclass(t, int): return cls.INTEGER - if issubclass(t, discord.abc.User): return cls.USER - if issubclass(t, discord.abc.GuildChannel): return cls.CHANNEL - if issubclass(t, discord.abc.Role): return cls.ROLE + if issubclass(t, int): + return cls.INTEGER + if issubclass(t, discord.abc.User): + return cls.USER + if issubclass(t, discord.abc.GuildChannel): + return cls.CHANNEL + if issubclass(t, discord.abc.Role): + return cls.ROLE class SlashMessage(ComponentMessage): @@ -432,8 +447,13 @@ async def _slash_edit(self, **fields): _resp["embeds"] = [x.to_dict() for x in embeds] allowed_mentions = fields.get("allowed_mentions") - _resp["allowed_mentions"] = allowed_mentions.to_dict() if allowed_mentions else \ - self._state.allowed_mentions.to_dict() if self._state.allowed_mentions else {} + _resp["allowed_mentions"] = ( + allowed_mentions.to_dict() + if allowed_mentions + else self._state.allowed_mentions.to_dict() + if self._state.allowed_mentions + else {} + ) await self._http.edit(_resp, self.__interaction_token, self.id, files=files) @@ -477,6 +497,7 @@ class PermissionData: :ivar type: The ``SlashCommandPermissionsType`` type of this permission. :ivar permission: State of permission. ``True`` to allow, ``False`` to disallow. """ + def __init__(self, id, type, permission, **kwargs): self.id = id self.type = type @@ -501,6 +522,7 @@ class GuildPermissionsData: :ivar guild_id: Guild id that the permissions are in. :ivar permissions: List of permissions dict. """ + def __init__(self, id, guild_id, permissions, **kwargs): self.id = id self.guild_id = guild_id @@ -524,10 +546,13 @@ class SlashCommandPermissionType(IntEnum): """ Equivalent of `ApplicationCommandPermissionType `_ in the Discord API. """ + ROLE = 1 USER = 2 @classmethod def from_type(cls, t: type): - if issubclass(t, discord.abc.Role): return cls.ROLE - if issubclass(t, discord.abc.User): return cls.USER + if issubclass(t, discord.abc.Role): + return cls.ROLE + if issubclass(t, discord.abc.User): + return cls.USER diff --git a/discord_slash/utils/manage_commands.py b/discord_slash/utils/manage_commands.py index e6ed4399d..8544b9ba0 100644 --- a/discord_slash/utils/manage_commands.py +++ b/discord_slash/utils/manage_commands.py @@ -1,19 +1,18 @@ -import typing -import inspect import asyncio -import aiohttp -from ..error import RequestFailure, IncorrectType -from ..model import SlashCommandOptionType, SlashCommandPermissionType +import inspect +import typing from collections.abc import Callable from typing import Union +import aiohttp -async def add_slash_command(bot_id, - bot_token: str, - guild_id, - cmd_name: str, - description: str, - options: list = None): +from ..error import IncorrectType, RequestFailure +from ..model import SlashCommandOptionType, SlashCommandPermissionType + + +async def add_slash_command( + bot_id, bot_token: str, guild_id, cmd_name: str, description: str, options: list = None +): """ A coroutine that sends a slash command add request to Discord API. @@ -28,27 +27,24 @@ async def add_slash_command(bot_id, """ url = f"https://discord.com/api/v8/applications/{bot_id}" url += "/commands" if not guild_id else f"/guilds/{guild_id}/commands" - base = { - "name": cmd_name, - "description": description, - "options": options or [] - } + base = {"name": cmd_name, "description": description, "options": options or []} async with aiohttp.ClientSession() as session: - async with session.post(url, headers={"Authorization": f"Bot {bot_token}"}, json=base) as resp: + async with session.post( + url, headers={"Authorization": f"Bot {bot_token}"}, json=base + ) as resp: if resp.status == 429: _json = await resp.json() await asyncio.sleep(_json["retry_after"]) - return await add_slash_command(bot_id, bot_token, guild_id, cmd_name, description, options) + return await add_slash_command( + bot_id, bot_token, guild_id, cmd_name, description, options + ) if not 200 <= resp.status < 300: raise RequestFailure(resp.status, await resp.text()) return await resp.json() -async def remove_slash_command(bot_id, - bot_token, - guild_id, - cmd_id): +async def remove_slash_command(bot_id, bot_token, guild_id, cmd_id): """ A coroutine that sends a slash command remove request to Discord API. @@ -73,9 +69,7 @@ async def remove_slash_command(bot_id, return resp.status -async def get_all_commands(bot_id, - bot_token, - guild_id=None): +async def get_all_commands(bot_id, bot_token, guild_id=None): """ A coroutine that sends a slash command get request to Discord API. @@ -98,9 +92,7 @@ async def get_all_commands(bot_id, return await resp.json() -async def remove_all_commands(bot_id, - bot_token, - guild_ids: typing.List[int] = None): +async def remove_all_commands(bot_id, bot_token, guild_ids: typing.List[int] = None): """ Remove all slash commands. @@ -118,9 +110,7 @@ async def remove_all_commands(bot_id, pass -async def remove_all_commands_in(bot_id, - bot_token, - guild_id=None): +async def remove_all_commands_in(bot_id, bot_token, guild_id=None): """ Remove all slash commands in area. @@ -128,24 +118,13 @@ async def remove_all_commands_in(bot_id, :param bot_token: Token of the bot. :param guild_id: ID of the guild to remove commands. Pass `None` to remove all global commands. """ - commands = await get_all_commands( - bot_id, - bot_token, - guild_id - ) + commands = await get_all_commands(bot_id, bot_token, guild_id) for x in commands: - await remove_slash_command( - bot_id, - bot_token, - guild_id, - x['id'] - ) + await remove_slash_command(bot_id, bot_token, guild_id, x["id"]) -async def get_all_guild_commands_permissions(bot_id, - bot_token, - guild_id): +async def get_all_guild_commands_permissions(bot_id, bot_token, guild_id): """ A coroutine that sends a gets all the commands permissions for that guild. @@ -167,10 +146,7 @@ async def get_all_guild_commands_permissions(bot_id, return await resp.json() -async def get_guild_command_permissions(bot_id, - bot_token, - guild_id, - command_id): +async def get_guild_command_permissions(bot_id, bot_token, guild_id, command_id): """ A coroutine that sends a request to get a single command's permissions in guild @@ -192,13 +168,8 @@ async def get_guild_command_permissions(bot_id, raise RequestFailure(resp.status, await resp.text()) return await resp.json() - -async def update_single_command_permissions(bot_id, - bot_token, - guild_id, - command_id, - permissions): +async def update_single_command_permissions(bot_id, bot_token, guild_id, command_id, permissions): """ A coroutine that sends a request to update a single command's permissions in guild @@ -212,22 +183,23 @@ async def update_single_command_permissions(bot_id, """ url = f"https://discord.com/api/v8/applications/{bot_id}/guilds/{guild_id}/commands/{command_id}/permissions" async with aiohttp.ClientSession() as session: - async with session.put(url, headers={"Authorization": f"Bot {bot_token}"}, json=permissions) as resp: + async with session.put( + url, headers={"Authorization": f"Bot {bot_token}"}, json=permissions + ) as resp: if resp.status == 429: _json = await resp.json() await asyncio.sleep(_json["retry_after"]) - return await update_single_command_permissions(bot_id, bot_token, guild_id, command_id, permissions) + return await update_single_command_permissions( + bot_id, bot_token, guild_id, command_id, permissions + ) if not 200 <= resp.status < 300: raise RequestFailure(resp.status, await resp.text()) return await resp.json() -async def update_guild_commands_permissions(bot_id, - bot_token, - guild_id, - cmd_permissions): +async def update_guild_commands_permissions(bot_id, bot_token, guild_id, cmd_permissions): """ - A coroutine that updates permissions for all commands in a guild. + A coroutine that updates permissions for all commands in a guild. :param bot_id: User ID of the bot. :param bot_token: Token of the bot. @@ -238,21 +210,27 @@ async def update_guild_commands_permissions(bot_id, """ url = f"https://discord.com/api/v8/applications/{bot_id}/guilds/{guild_id}/commands/permissions" async with aiohttp.ClientSession() as session: - async with session.put(url, headers={"Authorization": f"Bot {bot_token}"}, json=cmd_permissions) as resp: + async with session.put( + url, headers={"Authorization": f"Bot {bot_token}"}, json=cmd_permissions + ) as resp: if resp.status == 429: _json = await resp.json() await asyncio.sleep(_json["retry_after"]) - return await update_guild_commands_permissions(bot_id, bot_token, guild_id, cmd_permissions) + return await update_guild_commands_permissions( + bot_id, bot_token, guild_id, cmd_permissions + ) if not 200 <= resp.status < 300: raise RequestFailure(resp.status, await resp.text()) return await resp.json() -def create_option(name: str, - description: str, - option_type: typing.Union[int, type], - required: bool, - choices: list = None) -> dict: +def create_option( + name: str, + description: str, + option_type: typing.Union[int, type], + required: bool, + choices: list = None, +) -> dict: """ Creates option used for creating slash command. @@ -271,23 +249,32 @@ def create_option(name: str, ``choices`` must either be a list of `option type dicts `_ or a list of single string values. """ - if not isinstance(option_type, int) or isinstance(option_type, bool): #Bool values are a subclass of int + if not isinstance(option_type, int) or isinstance( + option_type, bool + ): # Bool values are a subclass of int original_type = option_type option_type = SlashCommandOptionType.from_type(original_type) if option_type is None: - raise IncorrectType(f"The type {original_type} is not recognized as a type that Discord accepts for slash commands.") + raise IncorrectType( + f"The type {original_type} is not recognized as a type that Discord accepts for slash commands." + ) choices = choices or [] - choices = [choice if isinstance(choice, dict) else {"name": choice, "value": choice} for choice in choices] + choices = [ + choice if isinstance(choice, dict) else {"name": choice, "value": choice} + for choice in choices + ] return { "name": name, "description": description, "type": option_type, "required": required, - "choices": choices + "choices": choices, } -def generate_options(function: Callable, description: str = "No description.", connector: dict = None) -> list: +def generate_options( + function: Callable, description: str = "No description.", connector: dict = None +) -> list: """ Generates a list of options from the type hints of a command. You currently can type hint: str, int, bool, discord.User, discord.Channel, discord.Role @@ -301,7 +288,7 @@ def generate_options(function: Callable, description: str = "No description.", c """ options = [] if connector: - connector = {y: x for x, y in connector.items()} # Flip connector. + connector = {y: x for x, y in connector.items()} # Flip connector. params = iter(inspect.signature(function).parameters.values()) if next(params).name in ("self", "cls"): # Skip 1. (+ 2.) parameter, self/cls and ctx @@ -322,7 +309,9 @@ def generate_options(function: Callable, description: str = "No description.", c param = param.replace(annotation=args[0]) required = not isinstance(args[-1], type(None)) - option_type = SlashCommandOptionType.from_type(param.annotation) or SlashCommandOptionType.STRING + option_type = ( + SlashCommandOptionType.from_type(param.annotation) or SlashCommandOptionType.STRING + ) name = param.name if not connector else connector[param.name] options.append(create_option(name, description or "No Description.", option_type, required)) @@ -337,13 +326,12 @@ def create_choice(value: Union[str, int], name: str): :param name: Name of the choice. :return: dict """ - return { - "value": value, - "name": name - } + return {"value": value, "name": name} -def create_permission(id:int, id_type: typing.Union[int, SlashCommandPermissionType], permission: bool): +def create_permission( + id: int, id_type: typing.Union[int, SlashCommandPermissionType], permission: bool +): """ Create a single command permission. @@ -355,32 +343,36 @@ def create_permission(id:int, id_type: typing.Union[int, SlashCommandPermissionT .. note:: For @everyone permission, set id_type as role and id as guild id. """ - if not (isinstance(id_type, int) or isinstance(id_type, bool)): #Bool values are a subclass of int + if not ( + isinstance(id_type, int) or isinstance(id_type, bool) + ): # Bool values are a subclass of int original_type = id_type id_type = SlashCommandPermissionType.from_type(original_type) if id_type is None: - raise IncorrectType(f"The type {original_type} is not recognized as a type that Discord accepts for slash command permissions.") - return { - "id": id, - "type": id_type, - "permission": permission - } + raise IncorrectType( + f"The type {original_type} is not recognized as a type that Discord accepts for slash command permissions." + ) + return {"id": id, "type": id_type, "permission": permission} -def create_multi_ids_permission(ids: typing.List[int], id_type: typing.Union[int, SlashCommandPermissionType], permission: bool): +def create_multi_ids_permission( + ids: typing.List[int], id_type: typing.Union[int, SlashCommandPermissionType], permission: bool +): """ Creates a list of permissions from list of ids with common id_type and permission state. :param ids: List of target ids to apply the permission on. - :param id_type: Type of the id. + :param id_type: Type of the id. :param permission: State of the permission. ``True`` to allow access, ``False`` to disallow access. """ return [create_permission(id, id_type, permission) for id in set(ids)] def generate_permissions( - allowed_roles: typing.List[int] = None, allowed_users: typing.List[int] = None, - disallowed_roles: typing.List[int] = None, disallowed_users: typing.List[int] = None + allowed_roles: typing.List[int] = None, + allowed_users: typing.List[int] = None, + disallowed_roles: typing.List[int] = None, + disallowed_users: typing.List[int] = None, ): """ Creates a list of permissions. @@ -392,14 +384,22 @@ def generate_permissions( :return: list """ permissions = [] - + if allowed_roles: - permissions.extend(create_multi_ids_permission(allowed_roles, SlashCommandPermissionType.ROLE, True)) + permissions.extend( + create_multi_ids_permission(allowed_roles, SlashCommandPermissionType.ROLE, True) + ) if allowed_users: - permissions.extend(create_multi_ids_permission(allowed_users, SlashCommandPermissionType.USER, True)) + permissions.extend( + create_multi_ids_permission(allowed_users, SlashCommandPermissionType.USER, True) + ) if disallowed_roles: - permissions.extend(create_multi_ids_permission(disallowed_roles, SlashCommandPermissionType.ROLE, False)) + permissions.extend( + create_multi_ids_permission(disallowed_roles, SlashCommandPermissionType.ROLE, False) + ) if disallowed_users: - permissions.extend(create_multi_ids_permission(disallowed_users, SlashCommandPermissionType.USER, False)) + permissions.extend( + create_multi_ids_permission(disallowed_users, SlashCommandPermissionType.USER, False) + ) return permissions diff --git a/discord_slash/utils/manage_components.py b/discord_slash/utils/manage_components.py index add1a104e..6d64cc9a9 100644 --- a/discord_slash/utils/manage_components.py +++ b/discord_slash/utils/manage_components.py @@ -1,7 +1,9 @@ -import uuid import enum import typing +import uuid + import discord + from ..context import ComponentContext from ..error import IncorrectFormat @@ -21,13 +23,13 @@ def create_actionrow(*components: dict) -> dict: """ if not components or len(components) > 5: raise IncorrectFormat("Number of components in one row should be between 1 and 5.") - if ComponentsType.select in [component["type"] for component in components] and len(components) > 1: + if ( + ComponentsType.select in [component["type"] for component in components] + and len(components) > 1 + ): raise IncorrectFormat("Action row must have only one select component and nothing else") - return { - "type": ComponentsType.actionrow, - "components": components - } + return {"type": ComponentsType.actionrow, "components": components} class ButtonStyle(enum.IntEnum): @@ -59,12 +61,14 @@ def emoji_to_dict(emoji: typing.Union[discord.Emoji, discord.PartialEmoji, str]) return emoji if emoji else {} -def create_button(style: typing.Union[ButtonStyle, int], - label: str = None, - emoji: typing.Union[discord.Emoji, discord.PartialEmoji, str] = None, - custom_id: str = None, - url: str = None, - disabled: bool = False) -> dict: +def create_button( + style: typing.Union[ButtonStyle, int], + label: str = None, + emoji: typing.Union[discord.Emoji, discord.PartialEmoji, str] = None, + custom_id: str = None, + url: str = None, + disabled: bool = False, +) -> dict: """ Creates a button component for use with the ``components`` field. Must be inside an ActionRow to be used (see :meth:`create_actionrow`). @@ -118,7 +122,9 @@ def create_button(style: typing.Union[ButtonStyle, int], return data -def create_select_option(label: str, value: str, emoji=None, description: str = None, default: bool = False): +def create_select_option( + label: str, value: str, emoji=None, description: str = None, default: bool = False +): """ Creates an option for select components. @@ -135,11 +141,13 @@ def create_select_option(label: str, value: str, emoji=None, description: str = "value": value, "description": description, "default": default, - "emoji": emoji + "emoji": emoji, } -def create_select(options: typing.List[dict], custom_id=None, placeholder=None, min_values=None, max_values=None): +def create_select( + options: typing.List[dict], custom_id=None, placeholder=None, min_values=None, max_values=None +): """ Creates a select (dropdown) component for use with the ``components`` field. Must be inside an ActionRow to be used (see :meth:`create_actionrow`). @@ -159,8 +167,9 @@ def create_select(options: typing.List[dict], custom_id=None, placeholder=None, } -async def wait_for_component(client: discord.Client, component: typing.Union[dict, str], check=None, timeout=None) \ - -> ComponentContext: +async def wait_for_component( + client: discord.Client, component: typing.Union[dict, str], check=None, timeout=None +) -> ComponentContext: """ Waits for a component interaction. Only accepts interactions based on the custom ID of the component, and optionally a check function. @@ -172,16 +181,20 @@ async def wait_for_component(client: discord.Client, component: typing.Union[dic :param timeout: The number of seconds to wait before timing out and raising :exc:`asyncio.TimeoutError`. :raises: :exc:`asyncio.TimeoutError` """ + def _check(ctx): if check and not check(ctx): return False - return (component["custom_id"] if isinstance(component, dict) else component) == ctx.custom_id + return ( + component["custom_id"] if isinstance(component, dict) else component + ) == ctx.custom_id return await client.wait_for("component", check=_check, timeout=timeout) -async def wait_for_any_component(client: discord.Client, message: typing.Union[discord.Message, int], - check=None, timeout=None) -> ComponentContext: +async def wait_for_any_component( + client: discord.Client, message: typing.Union[discord.Message, int], check=None, timeout=None +) -> ComponentContext: """ Waits for any component interaction. Only accepts interactions based on the message ID given and optionally a check function. @@ -193,9 +206,12 @@ async def wait_for_any_component(client: discord.Client, message: typing.Union[d :param timeout: The number of seconds to wait before timing out and raising :exc:`asyncio.TimeoutError`. :raises: :exc:`asyncio.TimeoutError` """ + def _check(ctx): if check and not check(ctx): return False - return (message.id if isinstance(message, discord.Message) else message) == ctx.origin_message_id + return ( + message.id if isinstance(message, discord.Message) else message + ) == ctx.origin_message_id return await client.wait_for("component", check=_check, timeout=timeout) diff --git a/docs/conf.py b/docs/conf.py index 6667ead0c..1e77fbb77 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,15 +13,15 @@ import os import sys -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath("..")) from discord_slash import __version__ # -- Project information ----------------------------------------------------- -project = 'discord-py-slash-command' -copyright = '2020-2021, eunwoo1104' -author = 'eunwoo1104' +project = "discord-py-slash-command" +copyright = "2020-2021, eunwoo1104" +author = "eunwoo1104" release = __version__ version = ".".join(__version__.split(".", 2)[:2]) @@ -30,29 +30,25 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.intersphinx", - "sphinx_rtd_theme" -] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx_rtd_theme"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = 'en' +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # This should fix wrong sort -autodoc_member_order = 'bysource' +autodoc_member_order = "bysource" # -- Options for HTML output ------------------------------------------------- @@ -64,10 +60,10 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Intersphinx intersphinx_mapping = { - 'py': ('https://docs.python.org/3', None), - 'discord': ("https://discordpy.readthedocs.io/en/latest/", None) -} \ No newline at end of file + "py": ("https://docs.python.org/3", None), + "discord": ("https://discordpy.readthedocs.io/en/latest/", None), +}