├── requirements.txt
├── .github
├── linters
│ └── .python-black
├── CODEOWNERS
└── workflows
│ ├── lint.yml
│ ├── analyze.yml
│ ├── build.yml
│ ├── release.yml
│ └── stale.yml
├── discord
└── ext
│ └── alternatives
│ ├── material_colors.py
│ ├── __init__.py
│ ├── message_eq.py
│ ├── _common.py
│ ├── silent_delete.py
│ ├── int_map.py
│ ├── bot_send_help.py
│ ├── command_piping.py
│ ├── menus_remove_reaction.py
│ ├── material_colours.py
│ ├── messageable_wait_for.py
│ ├── guild_converter.py
│ ├── literal_converter.py
│ ├── jump_url.py
│ ├── role.py
│ ├── subcommand_error.py
│ ├── specific_error_handler.py
│ ├── inline_bot_commands.py
│ ├── asset_converter.py
│ ├── class_commands.py
│ ├── converter_dict.py
│ ├── category_channel.py
│ ├── object_contains.py
│ ├── fix_4098.py
│ ├── webhook_channel.py
│ ├── binary_checks.py
│ ├── dict_converter.py
│ └── command_suffix.py
├── .gitignore
├── README.rst
├── setup.py
└── LICENSE
/requirements.txt:
--------------------------------------------------------------------------------
1 | discord.py>=1.0.0,<2.0.0
2 |
--------------------------------------------------------------------------------
/.github/linters/.python-black:
--------------------------------------------------------------------------------
1 | [tool.black]
2 | line-length = 100
3 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/material_colors.py:
--------------------------------------------------------------------------------
1 | from discord.ext.alternatives.material_colours import *
2 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @Ext-Creators/ext-alternatives
2 | /.github/workflows/ @ShineyDev
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # python
2 | __pycache__/
3 |
4 | # python distribution
5 | build/
6 | dist/
7 | *.egg-info/
8 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/__init__.py:
--------------------------------------------------------------------------------
1 | import collections
2 |
3 |
4 | _VersionInfo = collections.namedtuple("_VersionInfo", "year month day release serial")
5 |
6 | version = "2021.4.13"
7 | version_info = _VersionInfo(2021, 4, 13, "final", 0)
8 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/message_eq.py:
--------------------------------------------------------------------------------
1 | """An experiment that enables ``Message.__eq__``.
2 |
3 | It compares the IDs of each ``Message``.
4 |
5 | Example:
6 |
7 | ```py
8 | >>> m1 == m1
9 | True # Same IDs
10 | >>> m1 == m2
11 | False # Different IDs
12 | ```
13 | """
14 |
15 | import discord
16 |
17 |
18 | discord.Message.__eq__ = lambda s, o: isinstance(o, discord.Message) and s.id == o.id
19 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/_common.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 |
4 | _ALL = {
5 | # This will be populated by loaded alternative converters at runtime
6 | }
7 |
8 |
9 | def py_allow(major: int, minor: int, micro: int) -> None:
10 | if sys.version_info < (major, minor, micro):
11 | raise RuntimeError(
12 | "This extension requires Python>={0}.{1}.{2}".format(major, minor, micro)
13 | )
14 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on: [pull_request, push]
4 |
5 | jobs:
6 | job:
7 | name: Lint
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v2
13 |
14 | - name: Lint
15 | uses: github/super-linter@v3
16 | env:
17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
18 | VALIDATE_PYTHON_BLACK: true
19 | VALIDATE_YAML: true
20 |
--------------------------------------------------------------------------------
/.github/workflows/analyze.yml:
--------------------------------------------------------------------------------
1 | name: Analyze
2 |
3 | on:
4 | pull_request:
5 | push:
6 | schedule:
7 | - cron: "0 0 * * 0"
8 |
9 | jobs:
10 | job:
11 | name: Analyze
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v2
17 |
18 | - name: Init CodeQL
19 | uses: github/codeql-action/init@v1
20 |
21 | - name: Analyze
22 | uses: github/codeql-action/analyze@v1
23 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/silent_delete.py:
--------------------------------------------------------------------------------
1 | """An experiment to allow for ``Message.delete`` to be silenced
2 | of any exception.
3 |
4 | It uses a keyword argument called `silent`, and is by default
5 | ``False``.
6 | """
7 |
8 | import discord
9 |
10 |
11 | _old_delete = discord.Message.delete
12 |
13 |
14 | async def delete(self, *, silent=False, **kwargs):
15 | try:
16 | await _old_delete(self, **kwargs)
17 | except Exception as e:
18 | if not silent:
19 | raise e
20 |
21 |
22 | discord.Message.delete = delete
23 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/int_map.py:
--------------------------------------------------------------------------------
1 | """An experiment that enables ``__int__`` on certain objects
2 | to return the ``id`` attribute.
3 | """
4 |
5 | import discord
6 |
7 |
8 | _int = lambda self: self.id
9 |
10 | discord.AppInfo.__int__ = _int
11 | discord.Attachment.__int__ = _int
12 | discord.AuditLogEntry.__int__ = _int
13 | discord.emoji._EmojiTag.__int__ = _int
14 | discord.mixins.Hashable.__int__ = _int
15 | discord.Member.__int__ = _int
16 | discord.Message.__int__ = _int
17 | discord.Reaction.__int__ = _int
18 | discord.Team.__int__ = _int
19 | discord.Webhook.__int__ = _int
20 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/bot_send_help.py:
--------------------------------------------------------------------------------
1 | """An experiment that allows you to send help without requiring ``Context``,
2 | or inside of ``on_message``.
3 |
4 | Example:
5 | ```py
6 | @bot.event
7 | async def on_message(message):
8 | if message.content == message.guild.me.mention:
9 | await bot.send_help(message) # sends the entire help command.
10 | ```
11 | """
12 |
13 | from discord.ext import commands
14 |
15 |
16 | def send_help(self, message, *args, **kwargs):
17 | ctx = kwargs.get("cls", commands.Context)(prefix=self.user.mention, bot=self, message=message)
18 | return ctx.send_help(*args)
19 |
20 |
21 | commands.bot.BotBase.send_help = send_help
22 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/command_piping.py:
--------------------------------------------------------------------------------
1 | """An experiment that allows the use of returns in the command callback to reply to the user
2 |
3 | Example:
4 | ```py
5 | @bot.command()
6 | async def foo(ctx):
7 | await ctx.send('hello')
8 | return 'world!'
9 |
10 | '?foo' -->
11 |
12 | 'hello'
13 | 'world!'
14 | """
15 |
16 | from discord.ext import commands
17 |
18 |
19 | async def invoke(self, ctx):
20 | await self.prepare(ctx)
21 |
22 | ctx.invoked_subcommand = None
23 | injected = commands.core.hooked_wrapped_callback(self, ctx, self.callback)
24 |
25 | ret = await injected(*ctx.args, **ctx.kwargs)
26 | if ret is not None:
27 | await ctx.send(ret)
28 |
29 |
30 | commands.Command.invoke = invoke
31 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on: [pull_request, push]
4 |
5 | jobs:
6 | job:
7 | strategy:
8 | fail-fast: false
9 | matrix:
10 | python-version: [3.6, 3.7, 3.8, 3.9]
11 |
12 | name: Build
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v2
18 |
19 | - name: Set up Python ${{ matrix.python-version }}
20 | uses: actions/setup-python@v2
21 | with:
22 | python-version: ${{ matrix.python-version }}
23 |
24 | - name: Install dependencies
25 | run: |-
26 | python -m pip install --upgrade pip
27 | python -m pip install --upgrade setuptools wheel
28 |
29 | - name: Build
30 | run: python setup.py sdist bdist_wheel
31 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/menus_remove_reaction.py:
--------------------------------------------------------------------------------
1 | """An experiment that enables auto removing reactions added to menus.
2 |
3 | """
4 |
5 | import discord
6 | from discord.ext import menus
7 |
8 |
9 | _old_update = menus.Menu.update
10 |
11 |
12 | async def update(self: menus.Menu, payload: discord.RawReactionActionEvent):
13 | await _old_update(self, payload)
14 |
15 | if payload.event_type != "REACTION_ADD":
16 | return
17 |
18 | permissions = self.ctx.channel.permissions_for(self.ctx.me)
19 | if not (permissions.manage_messages or permissions.administrator):
20 | return
21 |
22 | await self.message.remove_reaction(payload.emoji, discord.Object(id=payload.user_id))
23 |
24 |
25 | update.__doc__ = _old_update.__doc__
26 | menus.Menu.update = update
27 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | release:
5 | types: [created]
6 |
7 | jobs:
8 | job:
9 | name: Upload
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v2
15 |
16 | - name: Set up Python 3.9
17 | uses: actions/setup-python@v2
18 | with:
19 | python-version: 3.9
20 |
21 | - name: Install dependencies
22 | run: |-
23 | python -m pip install --upgrade pip
24 | python -m pip install --upgrade setuptools twine wheel
25 |
26 | - name: Build
27 | run: python setup.py sdist bdist_wheel
28 |
29 | - name: Upload to PyPI
30 | env:
31 | TWINE_USERNAME: __token__
32 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
33 | run: twine upload dist/*
34 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/material_colours.py:
--------------------------------------------------------------------------------
1 | from discord.colour import Colour
2 |
3 |
4 | MDIO_COLOURS = {
5 | "400": {
6 | "red": 0xEF534E,
7 | "pink": 0xEC407E,
8 | "purple": 0xAB47BC,
9 | "deep_purple": 0x7E56C1,
10 | "indigo": 0x5C6BC0,
11 | "blue": 0x42A5F5,
12 | "light_blue": 0x29B6F6,
13 | "cyan": 0x26C6DA,
14 | "teal": 0x26A69A,
15 | "green": 0x66BB6A,
16 | "light_green": 0x9CCC65,
17 | "lime": 0xD4E157,
18 | "yellow": 0xFFEE58,
19 | "amber": 0xFFCA28,
20 | "orange": 0xFFA726,
21 | "deep_orange": 0xFF7043,
22 | }
23 | }
24 |
25 | for shade, colours in MDIO_COLOURS.items():
26 | for name, value in colours.items():
27 | delegate = lambda cls, value=value: cls(value)
28 | setattr(Colour, "material_{0}_{1}".format(shade, name), classmethod(delegate))
29 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/messageable_wait_for.py:
--------------------------------------------------------------------------------
1 | from discord.abc import Messageable
2 | from discord.message import Message
3 | import discord
4 | from discord.ext import commands
5 |
6 |
7 | def wait_for(self, event, *, check=None, timeout=None):
8 | actual_wait_for = self._state.dispatch.__self__.wait_for
9 |
10 | if check is None:
11 |
12 | def check(*args):
13 | return True
14 |
15 | def actual_check(*args):
16 | for arg in args:
17 | if isinstance(arg, (discord.Message, commands.Context)):
18 | if arg.channel.id == self.id:
19 | return check(*args)
20 | elif isinstance(arg, discord.abc.Messageable):
21 | if arg.id == self.id:
22 | return check(*args)
23 |
24 | return actual_wait_for(event, check=actual_check, timeout=timeout)
25 |
26 |
27 | discord.abc.Messageable.wait_for = wait_for
28 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/guild_converter.py:
--------------------------------------------------------------------------------
1 | """An experiment that allows for conversion of ``Guild``
2 | arguments for commands.
3 |
4 | Example:
5 | ```py
6 | @commands.command()
7 | async def test(ctx, server: Guild):
8 | await ctx.send(f"You selected **{server.name}**")
9 | ```
10 | """
11 |
12 | from discord.ext.commands import BadArgument, converter, Context
13 | from discord import Guild, utils
14 |
15 | from ._common import _ALL
16 |
17 | # Basic Guild Converter
18 |
19 |
20 | class _GuildConverter(converter.IDConverter):
21 | async def convert(self, ctx: Context, argument: str):
22 | bot = ctx.bot
23 |
24 | match = self._get_id_match(argument)
25 | result = None
26 |
27 | if match is None:
28 | result = utils.get(bot.guilds, name=argument)
29 | else:
30 | guild_id = int(match.group(1))
31 | result = ctx.bot.get_guild(guild_id)
32 |
33 | if result is None:
34 | raise BadArgument('Guild "{}" not found'.format(argument))
35 | return result
36 |
37 |
38 | converter.GuildConverter = _GuildConverter
39 | _ALL[Guild] = _GuildConverter
40 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/literal_converter.py:
--------------------------------------------------------------------------------
1 | from typing import Literal, get_args, get_origin
2 | from discord.ext.commands import Command
3 | from discord.ext.commands.errors import ConversionError, BadArgument
4 |
5 | _old_actual_conversion = Command._actual_conversion
6 |
7 |
8 | async def _actual_conversion(self, ctx, converter, argument, param):
9 | origin = get_origin(converter)
10 |
11 | if origin is Literal:
12 | items = get_args(converter)
13 |
14 | if all(i for i in items if isinstance(i, str)):
15 | if argument in items:
16 | return argument
17 |
18 | raise BadArgument(f"Expected literal: one of {list(map(repr, items))}")
19 | elif all(i for i in items if not isinstance(i, str)):
20 | ret = await _old_actual_conversion(self, ctx, type(items[0]), argument, param)
21 | return ret in items
22 | else:
23 | raise ConversionError("Literal contains multiple conflicting types.")
24 |
25 | return await _old_actual_conversion(self, ctx, converter, argument, param)
26 |
27 |
28 | Command._actual_conversion = _actual_conversion
29 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/jump_url.py:
--------------------------------------------------------------------------------
1 | """An experiment that enables ``Guild.jump_url`` and ``abc.Messagable.jump_url``.
2 |
3 | Example:
4 |
5 | ```py
6 | >>> message.channel.jump_url
7 | https://discord.com/channels/364412422540361729/708518465820033105
8 | ```
9 | """
10 |
11 | import discord
12 |
13 |
14 | @property
15 | def guild_jump_url(self):
16 | """:class:`str`: Returns a URL that allows the client to jump to this guild."""
17 |
18 | return "https://discord.com/channels/{0.id}".format(self)
19 |
20 |
21 | discord.Guild.jump_url = guild_jump_url
22 |
23 |
24 | @property
25 | def messageable_jump_url(self):
26 | """:class:`str`: Returns a URL that allows the client to jump to this channel."""
27 |
28 | if isinstance(self, discord.abc.User):
29 | if self.dm_channel is None:
30 | raise AttributeError("Could not find DM channel for user '{0}'".format(self))
31 |
32 | channel_id = self.dm_channel.id
33 | else:
34 | channel_id = self.channel.id if hasattr(self, "channel") else self.id
35 |
36 | guild_id = self.guild.id if isinstance(self, discord.abc.GuildChannel) else "@me"
37 |
38 | return "https://discord.com/channels/{0}/{1}".format(guild_id, channel_id)
39 |
40 |
41 | discord.abc.Messageable.jump_url = messageable_jump_url
42 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | .. raw:: html
2 |
3 |
4 |
5 |
7 |
8 |
9 |
10 |
12 |
13 |
14 |
15 |
17 |
18 |
19 |
20 | ----------
21 |
22 | .. raw:: html
23 |
24 | discord-ext-alternatives
25 | A discord.py extension with additional and alternative features.
26 | Copyright 2020-present Ext-Creators
27 |
28 |
29 | Installation
30 | ------------
31 |
32 | .. code-block:: sh
33 |
34 | pip install --upgrade discord-ext-alternatives
35 |
36 |
37 | Usage
38 | -----
39 |
40 | .. code-block:: py
41 |
42 | from discord.ext.alternatives import asset_converter, message_eq
43 | # Patches the related features into discord.py
44 | # OR
45 | from discord.ext.alternatives.class_commands import ClassGroup, Config
46 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/role.py:
--------------------------------------------------------------------------------
1 | """An experiment that allows you to shave a role off of all members. Optionally,
2 | you can pass Member objects (or IDs) to spare those members from having the role shaved.
3 | """
4 |
5 | from discord import Role
6 | from discord.abc import Snowflake
7 | from typing import Union, Optional
8 |
9 |
10 | async def _shave(
11 | self,
12 | *except_members: Union[int, Snowflake],
13 | reason: Optional[str] = None,
14 | atomic: bool = True,
15 | ):
16 | r"""|coro|
17 |
18 | Shaves :class:`Role`\s from all members with the role, unless explicitly
19 | added to except_members.
20 |
21 | You must have the :attr:`~Permissions.manage_roles` permission to
22 | use this.
23 |
24 | Parameters
25 | -----------
26 | \*except_members: :class:`abc.Snowflake`
27 | An argument list of :class:`abc.Snowflake` representing a :class:`Member`
28 | to not shave the role from.
29 | reason: Optional[:class:`str`]
30 | The reason for removing these roles. Shows up on the audit log.
31 | atomic: :class:`bool`
32 | Whether to atomically remove roles. This will ensure that multiple
33 | operations will always be applied regardless of the current
34 | state of the cache.
35 |
36 | Raises
37 | -------
38 | Forbidden
39 | You do not have permissions to remove these roles.
40 | HTTPException
41 | Removing the roles failed.
42 | """
43 |
44 | except_members_ids = {int(m) for m in except_members}
45 |
46 | for member in self.members:
47 | if member.id in except_members_ids:
48 | continue
49 |
50 | await member.remove_roles(self, reason=reason, atomic=atomic)
51 |
52 |
53 | Role.shave = _shave
54 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/subcommand_error.py:
--------------------------------------------------------------------------------
1 | """An experiment that allows you to handle all exceptions of
2 | group commands in the error handler of the root parent.
3 |
4 | Example:
5 | ```py
6 | @bot.group()
7 | async def profile(ctx):
8 | pass
9 |
10 | @profile.error
11 | async def profile_error(ctx, error):
12 | if isinstance(error.original, Exception):
13 | await ctx.send("You have not created your profile yet. Use ;profile create command now to create your profile.")
14 |
15 | @profile.command()
16 | async def name(ctx):
17 | raise Exception
18 |
19 | @profile.command()
20 | async def color(ctx):
21 | raise Exception
22 | ```
23 | """
24 |
25 | from discord.ext import commands
26 |
27 |
28 | async def dispatch_error(self, ctx, error):
29 | ctx.command_failed = True
30 | cog = self.cog
31 |
32 | try:
33 | coro = self.on_error
34 | except AttributeError:
35 | pass
36 | else:
37 | injected = commands.core.wrap_callback(coro)
38 |
39 | if cog is not None:
40 | await injected(cog, ctx, error)
41 | else:
42 | await injected(ctx, error)
43 |
44 | try:
45 | coro = self.root_parent.on_error
46 | except AttributeError:
47 | pass
48 | else:
49 | injected = commands.core.wrap_callback(coro)
50 |
51 | if cog is not None:
52 | await injected(cog, ctx, error)
53 | else:
54 | await injected(ctx, error)
55 |
56 | try:
57 | if cog is not None:
58 | local = commands.Cog._get_overridden_method(cog.cog_command_error)
59 | if local is not None:
60 | wrapped = commands.core.wrap_callback(local)
61 | await wrapped(ctx, error)
62 | finally:
63 | ctx.bot.dispatch("command_error", ctx, error)
64 |
65 |
66 | commands.Command.dispatch_error = dispatch_error
67 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: Stale
2 |
3 | on:
4 | schedule:
5 | - cron: "0 0 * * *"
6 | issue_comment:
7 | types: [created]
8 |
9 | jobs:
10 | stale:
11 | if: github.event_name == 'schedule'
12 |
13 | name: Mark as stale
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Stale
18 | uses: actions/stale@v3
19 | with:
20 | repo-token: ${{ secrets.GITHUB_TOKEN }}
21 | days-before-stale: 30
22 | days-before-close: 14
23 | exempt-issue-labels: A:backlog
24 | stale-issue-label: A:stale
25 | stale-issue-message: |-
26 | This issue has been marked as stale due to its inactivity and will be closed in 14 days.
27 |
28 | If you believe the issue should remain open, remove the `A:stale` label or add a comment.
29 | If you believe the issue will continue to be inactive but should remain open, add the `A:backlog` label.
30 |
31 | exempt-pr-labels: A:backlog
32 | stale-pr-label: A:stale
33 | stale-pr-message: |-
34 | This pull request has been marked as stale due to its inactivity and will be closed in 14 days.
35 |
36 | If you believe the pull request should remain open, remove the `A:stale` label or add a comment.
37 | If you believe the pull request will continue to be inactive but should remain open, add the `A:backlog` label.
38 |
39 | unstale:
40 | if: github.event_name == 'issue_comment' && contains(github.event.issue.labels.*.name, 'A:stale') && github.event.issue.user.type != 'Bot'
41 |
42 | name: Unmark as stale
43 | runs-on: ubuntu-latest
44 |
45 | steps:
46 | - name: Unstale
47 | uses: actions/github-script@v3
48 | with:
49 | github-token: ${{ secrets.GITHUB_TOKEN }}
50 | script: |-
51 | github.issues.removeLabel({
52 | issue_number: context.issue.number,
53 | owner: context.repo.owner,
54 | repo: context.repo.repo,
55 | name: 'A:stale'
56 | })
57 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/specific_error_handler.py:
--------------------------------------------------------------------------------
1 | """An experiment that allows handlers to handle specific errors.
2 |
3 | This overrides the default behaviour of ``Command.error``.
4 | (Change ``@Command.error`` to ``@Command.error()``).
5 |
6 | Example:
7 | ```py
8 | @bot.command()
9 | async def throw_error(ctx):
10 | raise Exception
11 |
12 | @command.error(CommandInvokeError)
13 | async def handle(ctx, error):
14 | if isinstance(error.original, Exception):
15 | await ctx.send('oh no')
16 | ```
17 | """
18 |
19 | import asyncio
20 |
21 | from discord.ext import commands
22 |
23 |
24 | async def on_error(*args): # cog?, ctx, error
25 | error, ctx = args[-1], args[-2]
26 | cls = error.__class__
27 |
28 | for exc, callback in ctx.command._handled_errors.items():
29 | if exc in cls.__mro__: # walk mro to check if the handler can handle it
30 | await callback(*args)
31 |
32 |
33 | def error(self, *exceptions):
34 | def decorator(func):
35 | if not asyncio.iscoroutinefunction(func):
36 | raise TypeError("The error must be a coroutine.")
37 |
38 | if not exceptions:
39 | self.on_error = func
40 | return
41 |
42 | try:
43 | self._handled_errors
44 | except AttributeError:
45 | self._handled_errors = {}
46 | finally:
47 | for exc in exceptions:
48 | self._handled_errors[exc] = func
49 |
50 | try:
51 | self.on_error
52 | except AttributeError as e:
53 | self.on_error = on_error
54 |
55 | return func
56 |
57 | return decorator
58 |
59 |
60 | commands.Command.error = error
61 |
62 | _old_ensure_assignment_on_copy = commands.Command._ensure_assignment_on_copy
63 |
64 |
65 | def _ensure_assignment_on_copy(self, other):
66 | other = _old_ensure_assignment_on_copy(self, other)
67 |
68 | try:
69 | other._handled_errors = self._handled_errors
70 | except AttributeError:
71 | pass
72 |
73 | return other
74 |
75 |
76 | commands.Command._ensure_assignment_on_copy = _ensure_assignment_on_copy
77 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/inline_bot_commands.py:
--------------------------------------------------------------------------------
1 | """An experiment that allows you to define commands in the bot subclass
2 |
3 | Example:
4 | ```py
5 | from discord.ext.alternatives import inline_bot_commands
6 | from discord.ext import commands
7 |
8 | class MyBot(commands.Bot):
9 | @commands.command()
10 | async def test(self, ctx):
11 | await ctx.send(f'{len(self.all_commands)} commands registered on {self.__class__.__name__}!')
12 |
13 | @commands.command()
14 | async def echo(self, ctx, *, words):
15 | await ctx.send(words)
16 |
17 |
18 | bot = MyBot(command_prefix='?')
19 | bot.run(token)
20 | ```
21 | """
22 |
23 | from discord.ext import commands
24 |
25 |
26 | class InlineMeta(type):
27 | def __new__(cls, *args, **kwargs):
28 | new_cls = super().__new__(cls, *args)
29 | cmds = {}
30 | for base in reversed(new_cls.__mro__):
31 | for elem, value in base.__dict__.items():
32 | if elem in cmds:
33 | del cmds[elem]
34 | if isinstance(value, commands.Command):
35 | cmds[elem] = value
36 |
37 | new_cls.__inline_commands__ = list(cmds.values())
38 | return new_cls
39 |
40 | @property
41 | def qualified_name(cls):
42 | # for the default help command, since the bot is acting as a cog
43 | return "No Category"
44 |
45 |
46 | class BotBase(commands.bot.BotBase, metaclass=InlineMeta):
47 | def __new__(cls, *args, **kwargs):
48 | self = super().__new__(cls)
49 |
50 | self.__inline_commands__ = tuple(c.copy() for c in cls.__inline_commands__)
51 |
52 | lookup = {cmd.qualified_name: cmd for cmd in self.__inline_commands__}
53 |
54 | # Update the Command instances dynamically as well
55 | for command in self.__inline_commands__:
56 | setattr(self, command.callback.__name__, command)
57 | command.cog = self
58 | parent = command.parent
59 | if parent is not None:
60 | # Get the latest parent reference
61 | parent = lookup[parent.qualified_name]
62 |
63 | # Update our parent's reference to our self
64 | parent.remove_command(command.name)
65 | parent.add_command(command)
66 |
67 | return self
68 |
69 | def __init__(self, *args, **kwargs):
70 | super().__init__(*args, **kwargs)
71 | for command in self.__inline_commands__:
72 | self.add_command(command)
73 |
74 |
75 | commands.bot.BotBase = BotBase
76 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import re
2 | import setuptools
3 |
4 |
5 | classifiers = [
6 | "Development Status :: 5 - Production/Stable",
7 | "Intended Audience :: Developers",
8 | "License :: OSI Approved :: Apache Software License",
9 | "Natural Language :: English",
10 | "Operating System :: OS Independent",
11 | "Programming Language :: Python :: 3",
12 | "Programming Language :: Python :: 3 :: Only",
13 | "Programming Language :: Python :: 3.5",
14 | "Programming Language :: Python :: 3.6",
15 | "Programming Language :: Python :: 3.7",
16 | "Programming Language :: Python :: 3.8",
17 | "Programming Language :: Python :: 3.9",
18 | "Programming Language :: Python :: Implementation :: CPython",
19 | "Topic :: Software Development",
20 | "Topic :: Software Development :: Libraries",
21 | "Topic :: Software Development :: Libraries :: Python Modules",
22 | ]
23 |
24 | with open("requirements.txt") as stream:
25 | install_requires = stream.read().splitlines()
26 |
27 | packages = [
28 | "discord.ext.alternatives",
29 | ]
30 |
31 | project_urls = {
32 | "Issue Tracker": "https://github.com/Ext-Creators/discord-ext-alternatives/issues",
33 | "Source": "https://github.com/Ext-Creators/discord-ext-alternatives",
34 | }
35 |
36 | _version_regex = r"^version = ('|\")((?:[0-9]+\.)*[0-9]+(?:\.?([a-z]+)(?:\.?[0-9])?)?)\1$"
37 |
38 | with open("discord/ext/alternatives/__init__.py") as stream:
39 | match = re.search(_version_regex, stream.read(), re.MULTILINE)
40 |
41 | version = match.group(2)
42 |
43 | if match.group(3) is not None:
44 | try:
45 | import subprocess
46 |
47 | process = subprocess.Popen(["git", "rev-list", "--count", "HEAD"], stdout=subprocess.PIPE)
48 | out, _ = process.communicate()
49 | if out:
50 | version += out.decode("utf-8").strip()
51 |
52 | process = subprocess.Popen(["git", "rev-parse", "--short", "HEAD"], stdout=subprocess.PIPE)
53 | out, _ = process.communicate()
54 | if out:
55 | version += "+g" + out.decode("utf-8").strip()
56 | except (Exception) as e:
57 | pass
58 |
59 | setuptools.setup(
60 | author="Ext-Creators",
61 | classifiers=classifiers,
62 | description="A discord.py extension with additional and alternative features.",
63 | install_requires=install_requires,
64 | license="Apache Software License",
65 | name="discord-ext-alternatives",
66 | packages=packages,
67 | project_urls=project_urls,
68 | url="https://github.com/Ext-Creators/discord-ext-alternatives",
69 | version=version,
70 | )
71 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/asset_converter.py:
--------------------------------------------------------------------------------
1 | """An experiment that allows for conversion of ``Asset``
2 | arguments for commands.
3 |
4 | It first detects if there's an attachment available or a URL in the
5 | message content, it will return it as an ``Asset``. If not,
6 | it will default onto a "null" ``Asset`` (one with no URL).
7 |
8 | Example:
9 | ```py
10 | @commands.command()
11 | async def test(ctx, image: Asset):
12 | asset_bytes = io.BytesIO()
13 | await image.save(asset_bytes)
14 | do_some_cool_image_manipulation(asset_bytes)
15 |
16 | await ctx.send(file=discord.File(asset_bytes, 'cool_image.png'))
17 | ```
18 | """
19 |
20 | from discord.ext.commands import converter, Context, errors, Command
21 | from inspect import Parameter
22 | from discord import Asset, DiscordException
23 | import typing
24 |
25 | from ._common import _ALL
26 |
27 | # Basic Asset Converter
28 |
29 |
30 | class _AssetConverter(converter.Converter):
31 | async def convert(self, ctx: Context, argument: str):
32 | if argument.startswith("http"):
33 | return Asset(ctx.bot._connection, argument)
34 |
35 | raise errors.BadArgument("No image found!")
36 |
37 |
38 | converter.AssetConverter = _AssetConverter
39 |
40 | _ALL[Asset] = _AssetConverter
41 |
42 | Asset.__str__ = (
43 | lambda s: "" if s._url is None else (s._url if s._url.startswith("http") else s.BASE + s._url)
44 | )
45 |
46 |
47 | async def _read(self):
48 | if not self._url:
49 | raise DiscordException("Invalid asset (no URL provided)")
50 |
51 | if self._state is None:
52 | raise DiscordException("Invalid state (no ConnectionState provided)")
53 |
54 | return await self._state.http.get_from_cdn(str(self))
55 |
56 |
57 | Asset.read = _read
58 |
59 | # "Hijack" transform to set default for Asset to preprocess possibility of attachment
60 |
61 | _old_transform = Command.transform
62 |
63 |
64 | def _transform(self, ctx, param):
65 | if param.annotation is Asset and param.default is param.empty:
66 | if ctx.message.attachments:
67 | default = Asset(ctx.bot._connection, ctx.message.attachments[0].url)
68 | param = Parameter(
69 | param.name,
70 | param.kind,
71 | default=default,
72 | annotation=typing.Optional[param.annotation],
73 | )
74 | else:
75 | default = Asset(ctx.bot._connection, "")
76 | param = Parameter(param.name, param.kind, default=default, annotation=param.annotation)
77 |
78 | return _old_transform(self, ctx, param)
79 |
80 |
81 | Command.transform = _transform
82 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/class_commands.py:
--------------------------------------------------------------------------------
1 | """**STANDALONE**: An experiment that allows the use of classes and
2 | functions as a way to represent a group of commands.
3 |
4 | Example:
5 | ```py
6 | @bot.group(cls=ClassGroup)
7 | class A(Config, invoke_without_command=True): # A group command
8 | _CONFIG = Config(invoke_without_command=True, description='no')
9 |
10 | async def __call__(ctx):
11 | await ctx.send('test')
12 |
13 | async def fmt(ctx): # A command
14 | await ctx.send('toot')
15 |
16 | class B: # A group command
17 | #_CONFIG = Config(description='yes')
18 |
19 | async def __call__(ctx):
20 | await ctx.send('no')
21 |
22 | async def oops(ctx): # A command
23 | await ctx.send('yert')
24 | ```
25 |
26 | ```
27 | !A -> 'test'
28 | !A fmt -> 'toot'
29 | !A B -> 'no'
30 | !A B oops -> 'no', 'yert'
31 | ```
32 | """
33 |
34 | import inspect
35 |
36 | from discord.ext import commands
37 |
38 |
39 | class ClassGroup(commands.Group):
40 | def __init__(self, cls, *, name=None, parent=None):
41 | kwargs = {"name": name or cls.__name__, "parent": parent}
42 | func = cls.__call__
43 |
44 | try:
45 | cls._CONFIG
46 | except AttributeError:
47 | pass
48 | else:
49 | kwargs.update(cls._CONFIG.to_dict())
50 |
51 | super().__init__(func, **kwargs)
52 |
53 | for f in dir(cls):
54 | if f.startswith("_"):
55 | continue
56 |
57 | attr = getattr(cls, f)
58 |
59 | if inspect.isclass(attr):
60 | self.add_command(ClassGroup(attr, parent=self))
61 | elif inspect.iscoroutinefunction(attr):
62 | self.add_command(commands.Command(attr))
63 |
64 |
65 | class Config:
66 | def __init__(
67 | self,
68 | *,
69 | invoke_without_command: bool = False,
70 | case_insensitive: bool = False,
71 | help: str = "",
72 | brief: str = "",
73 | usage: str = "",
74 | aliases: list = [],
75 | checks: list = [],
76 | description: str = "",
77 | hidden: bool = False,
78 | ):
79 | self.invoke_without_command = invoke_without_command
80 | self.case_insensitive = case_insensitive
81 | self.help = help
82 | self.brief = brief
83 | self.usage = usage
84 | self.aliases = aliases
85 | self.checks = checks
86 | self.description = description
87 | self.hidden = hidden
88 |
89 | def to_dict(self):
90 | d = {}
91 | for attr in dir(self):
92 | if not attr.startswith("_"):
93 | d[attr] = getattr(self, attr)
94 |
95 | return d
96 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/converter_dict.py:
--------------------------------------------------------------------------------
1 | """An experiment that allows you to register converters for classes,
2 | as a helpful aid for readability when using typehints for custom converters.
3 |
4 | You could also override base converters if you wanted to.
5 |
6 | Example:
7 | ```py
8 | class YertConverter(commands.Converter):
9 | async def convert(self, ctx, arg):
10 | return 'yert'
11 |
12 | class Yert(object):
13 | ...
14 |
15 | bot.converters[Yert] = YertConverter
16 |
17 | @bot.command()
18 | async def yert(ctx, yert: Yert):
19 | '''This will always send `yert`!'''
20 | await ctx.send(yert)
21 | ```
22 | """
23 | from types import FunctionType
24 |
25 | import discord
26 | from discord.ext import commands
27 | from discord.ext.commands import converter, Command
28 |
29 | from ._common import _ALL
30 |
31 | _BUILTINS = (
32 | bool,
33 | str,
34 | int,
35 | float,
36 | )
37 |
38 | _CONVERTERS = {
39 | # fmt: off
40 | discord.CategoryChannel: converter.CategoryChannelConverter,
41 | discord.Colour: converter.ColourConverter,
42 | discord.Emoji: converter.EmojiConverter,
43 | discord.Game: converter.GameConverter,
44 | discord.Invite: converter.InviteConverter,
45 | discord.Member: converter.MemberConverter,
46 | discord.Message: converter.MessageConverter,
47 | discord.PartialEmoji: converter.PartialEmojiConverter,
48 | discord.Role: converter.RoleConverter,
49 | discord.TextChannel: converter.TextChannelConverter,
50 | discord.User: converter.UserConverter,
51 | discord.VoiceChannel: converter.VoiceChannelConverter,
52 | # fmt: on
53 | }
54 |
55 | _CONVERTERS.update({b: b for b in _BUILTINS})
56 |
57 |
58 | class _ConverterDict(dict):
59 | """An easy way to register converters for classes.
60 |
61 | Can help for both linting and readability.
62 | """
63 |
64 | def __init__(self):
65 | super().__init__(_CONVERTERS)
66 | super().update(_ALL)
67 |
68 | def __setitem__(self, k, v):
69 | if not (isinstance(v, FunctionType) or issubclass(v, (*_BUILTINS, converter.Converter))):
70 | raise TypeError(
71 | "Excepted value of type 'Converter' or built-in, received %r" % v.__name__
72 | )
73 | super().__setitem__(k, v)
74 |
75 | def set(self, k, v):
76 | """Same as doing ``ConverterDict[Obj] = ObjConverter`` but fluid."""
77 | self.__setitem__(k, v)
78 | return self
79 |
80 |
81 | _GLOBAL_CONVERTER_DICT = _ConverterDict()
82 |
83 | commands.bot.BotBase.converters = _GLOBAL_CONVERTER_DICT
84 |
85 | _old_actual_conversion = Command._actual_conversion
86 |
87 |
88 | async def _actual_conversion(self, ctx, converter, argument, param):
89 | converter = _GLOBAL_CONVERTER_DICT.get(converter, converter)
90 | return await _old_actual_conversion(self, ctx, converter, argument, param)
91 |
92 |
93 | Command._actual_conversion = _actual_conversion
94 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/category_channel.py:
--------------------------------------------------------------------------------
1 | """An experiment that allows you to change the positions of channels
2 | in a CategoryChannel in a way that isn't quickly rate-limited.
3 |
4 | Works with TextChannels and VoiceChannels alike.
5 |
6 | Example:
7 | ```py
8 | @is_owner()
9 | @bot.command()
10 | async def by_length(ctx):
11 | await ctx.channel.category.sort(key=lambda c: len(c.name))
12 |
13 | @is_owner()
14 | @bot.command()
15 | async def alphabetize(ctx):
16 | await ctx.channel.category.alphabetize()
17 |
18 | @is_owner()
19 | @bot.command()
20 | async def shuffle(ctx):
21 | await ctx.channel.category.shuffle()
22 | ```
23 | """
24 |
25 | import random
26 |
27 | from discord import CategoryChannel
28 |
29 |
30 | async def _sort(self, *, key=None, reverse=False):
31 | """|coro|
32 |
33 | Sorts the channels within the CategoryChannel, similar to Python's list.sort().
34 |
35 | You must have the :attr:`~discord.Permissions.manage_channels` permission to
36 | do this.
37 |
38 | Parameters
39 | -----------
40 | key: Callable
41 | A callable function to customize the sort order.
42 | The supplied argument is of type ``GuildChannel``.
43 | reverse: :class:`bool`
44 | Whether or not to sort in descending order. False by default.
45 |
46 | Raises
47 | -------
48 | Forbidden
49 | You do not have permissions to sort the channels.
50 | HTTPException
51 | Sorting the channels failed.
52 | """
53 | payload = [
54 | {"id": channel.id, "position": index}
55 | for index, channel in enumerate(sorted(self.channels, key=key, reverse=reverse))
56 | ]
57 |
58 | await self._state.http.bulk_channel_update(self.guild.id, payload)
59 |
60 |
61 | async def _alphabetize(self, *, reverse=False):
62 | """|coro|
63 |
64 | Alphabetizes the channels within the CategoryChannel.
65 |
66 | You must have the :attr:`~discord.Permissions.manage_channels` permission to
67 | do this.
68 |
69 | Parameters
70 | -----------
71 | reverse: :class:`bool`
72 | Whether or not to alphabetize in descending order. False by default.
73 |
74 | Raises
75 | -------
76 | Forbidden
77 | You do not have permissions to alphabetize the channels.
78 | HTTPException
79 | Alphabetizing the channels failed.
80 | """
81 |
82 | await self.sort(key=lambda c: c.name, reverse=reverse)
83 |
84 |
85 | async def _shuffle(self):
86 | """|coro|
87 |
88 | Shuffles the channels within the CategoryChannel.
89 |
90 | You must have the :attr:`~discord.Permissions.manage_channels` permission to
91 | do this.
92 |
93 | Raises
94 | -------
95 | Forbidden
96 | You do not have permissions to shuffle the channels.
97 | HTTPException
98 | Shuffling the channels failed.
99 | """
100 |
101 | await self.sort(key=lambda _: random.random())
102 |
103 |
104 | CategoryChannel.sort = _sort
105 | CategoryChannel.alphabetise = _alphabetize
106 | CategoryChannel.alphabetize = _alphabetize
107 | CategoryChannel.shuffle = _shuffle
108 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/object_contains.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright 2021 Ext-Creators
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | """
16 |
17 | """An experiment that allows `x in y` syntax for various discord objects.
18 |
19 | Example:
20 | ```py
21 | channel in guild
22 |
23 | member in channel
24 |
25 | member in role
26 | ```
27 | """
28 |
29 | import discord
30 | from discord.channel import CategoryChannel, DMChannel, TextChannel
31 |
32 | if discord.version_info < (1, 7, 0):
33 | from discord.channel import VoiceChannel as VocalGuildChannel
34 | else:
35 | from discord.channel import VocalGuildChannel
36 |
37 | from discord.guild import Guild
38 | from discord.member import Member
39 | from discord.message import Message
40 | from discord.role import Role
41 | from discord.user import User, BaseUser
42 |
43 |
44 | def _Guild__contains__(self, item):
45 | if hasattr(item, "guild"):
46 | return item.guild == self
47 |
48 | if isinstance(item, BaseUser):
49 | return item.id in self._members
50 |
51 | return False
52 |
53 |
54 | Guild.__contains__ = _Guild__contains__
55 |
56 |
57 | def _Role__contains__(self, item):
58 | if isinstance(item, User):
59 | item = self.guild._members.get(item.id)
60 |
61 | if isinstance(item, Member):
62 | return item._roles.has(self.id)
63 |
64 | return False
65 |
66 |
67 | Role.__contains__ = _Role__contains__
68 |
69 |
70 | def _TextChannel__contains__(self, item):
71 | if hasattr(item, "channel"):
72 | return item.channel == self
73 |
74 | if isinstance(item, User):
75 | item = self.guild._members.get(item.id)
76 |
77 | if isinstance(item, Member):
78 | return self.permissions_for(item).read_messages
79 |
80 | return False
81 |
82 |
83 | TextChannel.__contains__ = _TextChannel__contains__
84 |
85 |
86 | def _VocalGuildChannel__contains__(self, item):
87 | if isinstance(item, BaseUser) and item.id in self.voice_states:
88 | return True
89 |
90 | return False
91 |
92 |
93 | VocalGuildChannel.__contains__ = _VocalGuildChannel__contains__
94 |
95 |
96 | def _CategoryChannel__contains__(self, item):
97 | if hasattr(item, "category"):
98 | return item.category == self
99 |
100 | return False
101 |
102 |
103 | CategoryChannel.__contains__ = _CategoryChannel__contains__
104 |
105 |
106 | def _DMChannel__contains__(self, item):
107 | if hasattr(item, "channel"):
108 | return item.channel == self
109 |
110 | if isinstance(item, BaseUser):
111 | return item in (self.me, self.recipient)
112 |
113 | return False
114 |
115 |
116 | DMChannel.__contains__ = _DMChannel__contains__
117 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/fix_4098.py:
--------------------------------------------------------------------------------
1 | """
2 | See https://github.com/Rapptz/discord.py/issues/4098.
3 | """
4 | import asyncio
5 | from discord import Role, ChannelType, InvalidArgument, PermissionOverwrite
6 | import discord.abc
7 |
8 |
9 | async def _edit(self, options, reason):
10 | try:
11 | parent = options.pop("category")
12 | except KeyError:
13 | parent_id = discord.abc._undefined
14 | else:
15 | parent_id = parent and parent.id
16 | try:
17 | options["rate_limit_per_user"] = options.pop("slowmode_delay")
18 | except KeyError:
19 | pass
20 | lock_permissions = options.pop("sync_permissions", False)
21 | try:
22 | position = options.pop("position")
23 | except KeyError:
24 | if parent_id is not discord.abc._undefined:
25 | if lock_permissions:
26 | category = self.guild.get_channel(parent_id)
27 | options["permission_overwrites"] = [c._asdict() for c in category._overwrites]
28 | options["parent_id"] = parent_id
29 | elif lock_permissions and self.category_id is not None:
30 | # if we're syncing permissions on a pre-existing channel category without changing it
31 | # we need to update the permissions to point to the pre-existing category
32 | category = self.guild.get_channel(self.category_id)
33 | options["permission_overwrites"] = [c._asdict() for c in category._overwrites]
34 | else:
35 | await self._move(
36 | position, parent_id=parent_id, lock_permissions=lock_permissions, reason=reason
37 | )
38 | overwrites = options.get("overwrites", None)
39 | if overwrites is not None:
40 | perms = []
41 | for target, perm in overwrites.items():
42 | if not isinstance(perm, PermissionOverwrite):
43 | raise InvalidArgument(
44 | "Expected PermissionOverwrite received {0.__name__}".format(type(perm))
45 | )
46 | allow, deny = perm.pair()
47 | payload = {
48 | "allow": allow.value,
49 | "deny": deny.value,
50 | "id": target.id,
51 | }
52 | if isinstance(target, Role):
53 | payload["type"] = "role"
54 | else:
55 | payload["type"] = "member"
56 | perms.append(payload)
57 | options["permission_overwrites"] = perms
58 | try:
59 | ch_type = options["type"]
60 | except KeyError:
61 | pass
62 | else:
63 | if not isinstance(ch_type, ChannelType):
64 | raise InvalidArgument("type field must be of type ChannelType")
65 | options["type"] = ch_type.value
66 |
67 | if options:
68 | data = await self._state.http.edit_channel(self.id, reason=reason, **options)
69 | # see issue Rapptz/discord.py#4098
70 | if "parent_id" in options:
71 | client = self._state._get_client()
72 | try:
73 | await client.wait_for(
74 | "guild_channel_update",
75 | check=lambda b, a: b.id == a.id and b.id == self.id,
76 | timeout=2,
77 | )
78 | return
79 | except asyncio.TimeoutError:
80 | # fallback, we didn't receive the event within 2s
81 | pass
82 | self._update(self.guild, data)
83 |
84 |
85 | discord.abc.GuildChannel._edit = _edit
86 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/webhook_channel.py:
--------------------------------------------------------------------------------
1 | """An experiment to allow for webhooks to change channels.
2 |
3 | Example:
4 | ```py
5 | webhook = await ctx.bot.fetch_webhook(id)
6 | channel = ctx.guild.text_channels[1]
7 |
8 | await webhook.move_to(channel=channel)
9 | ```
10 | """
11 | from discord import utils, errors, webhook, TextChannel
12 | import asyncio
13 | import json
14 | import aiohttp
15 |
16 | # register endpoint
17 |
18 | _old_prepare = webhook.WebhookAdapter._prepare
19 |
20 |
21 | def _prepare(self, webhook):
22 | _old_prepare(self, webhook)
23 | self._move_url = "{0.BASE}/webhooks/{1}".format(self, webhook.id)
24 |
25 |
26 | webhook.WebhookAdapter._prepare = _prepare
27 |
28 |
29 | def _move_webhook(self, token, **payload):
30 | return self.request("PATCH", self._move_url, payload=payload, token=token)
31 |
32 |
33 | webhook.WebhookAdapter.move_webhook = _move_webhook
34 |
35 | # fix up AsyncWebhookAdapter.request to provide token -- was hoping to not need to replace it completely
36 |
37 |
38 | async def _request(self, verb, url, payload=None, multipart=None, *, files=None, token=None):
39 | headers = {}
40 | if token:
41 | headers["Authorization"] = token
42 |
43 | data = None
44 | files = files or []
45 | if payload:
46 | headers["Content-Type"] = "application/json"
47 | data = utils.to_json(payload)
48 |
49 | if multipart:
50 | data = aiohttp.FormData()
51 | for key, value in multipart.items():
52 | if key.startswith("file"):
53 | data.add_field(key, value[1], filename=value[0], content_type=value[2])
54 | else:
55 | data.add_field(key, value)
56 |
57 | for tries in range(5):
58 | for file in files:
59 | file.reset(seek=tries)
60 |
61 | async with self.session.request(verb, url, headers=headers, data=data) as r:
62 | # Coerce empty strings to return None for hygiene purposes
63 | response = (await r.text(encoding="utf-8")) or None
64 | if r.headers["Content-Type"] == "application/json":
65 | response = json.loads(response)
66 |
67 | # check if we have rate limit header information
68 | remaining = r.headers.get("X-Ratelimit-Remaining")
69 | if remaining == "0" and r.status != 429:
70 | delta = utils._parse_ratelimit_header(r)
71 | await asyncio.sleep(delta)
72 |
73 | if 300 > r.status >= 200:
74 | return response
75 |
76 | # we are being rate limited
77 | if r.status == 429:
78 | retry_after = response["retry_after"] / 1000.0
79 | await asyncio.sleep(retry_after)
80 | continue
81 |
82 | if r.status in (500, 502):
83 | await asyncio.sleep(1 + tries * 2)
84 | continue
85 |
86 | if r.status == 403:
87 | raise errors.Forbidden(r, response)
88 | elif r.status == 404:
89 | raise errors.NotFound(r, response)
90 | else:
91 | raise errors.HTTPException(r, response)
92 | # no more retries
93 | raise errors.HTTPException(r, response)
94 |
95 |
96 | webhook.AsyncWebhookAdapter.request = _request
97 |
98 | # add the method + _async_move
99 |
100 |
101 | async def _async_move(self, channel_id, token, payload):
102 | await self._adapter.move_webhook(token, **payload)
103 |
104 | self.channel_id = channel_id
105 |
106 |
107 | def _move_to(self, channel):
108 | if not isinstance(channel, TextChannel):
109 | raise TypeError(
110 | "Expected TextChannel parameter, received %s instead." % channel.__class__.__name__
111 | )
112 |
113 | payload = {"channel_id": channel.id}
114 |
115 | token = channel._state.http.token
116 |
117 | if channel._state.is_bot:
118 | token = "Bot " + token
119 |
120 | if isinstance(self._adapter, webhook.AsyncWebhookAdapter):
121 | return self._async_move(channel.id, token, payload)
122 |
123 |
124 | webhook.Webhook._async_move = _async_move
125 | webhook.Webhook.move_to = _move_to
126 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/binary_checks.py:
--------------------------------------------------------------------------------
1 | """An experiment that allows you to use the `&` and `|` operators
2 | on checks, allowing for easier control over check conditions.
3 |
4 | This should not break pre-existing checks.
5 |
6 | Examples:
7 | ```py
8 | @guild_only() | dm_only()
9 | @b.command()
10 | async def there(ctx):
11 | await ctx.send('is literally no reason to use both the guild_only and dm_only checks on the same command.')
12 | ```
13 |
14 | ```py
15 | @dm_only() | is_owner()
16 | @bot.command()
17 | async def hello(ctx):
18 | await ctx.send('world!')
19 | ```
20 |
21 | ```py
22 | @guild_only() & ((has_role('Capitalist') & bot_has_role('Leader of the Communist Revolution') ) | !is_owner())
23 | @bot.command()
24 | async def no(ctx):
25 | await ctx.send('stop this')
26 | ```
27 | """
28 |
29 | import inspect
30 |
31 | from discord.ext import commands
32 | from discord.ext.alternatives._common import py_allow
33 |
34 |
35 | py_allow(3, 9, 0)
36 |
37 |
38 | class CheckDecorator:
39 | def __init__(self, predicate):
40 | self.predicate = predicate
41 | self.check = Only(Check(predicate))
42 |
43 | def __call__(self, func):
44 | if isinstance(func, commands.Command):
45 | func.checks.append(self.check)
46 | else:
47 | if not hasattr(func, "__commands_checks__"):
48 | func.__commands_checks__ = []
49 |
50 | func.__commands_checks__.append(self.check)
51 |
52 | return func
53 |
54 | def __repr__(self):
55 | return f"CheckDecorator"
56 |
57 | def __invert__(self):
58 | ~self.check.first
59 | return self.check
60 |
61 | def __or__(self, other):
62 | self.check.first = Either(
63 | self.check.first, other.check.first if isinstance(other, CheckDecorator) else other
64 | )
65 | return self.check
66 |
67 | def __and__(self, other):
68 | self.check.first = Both(
69 | self.check.first, other.check.first if isinstance(other, CheckDecorator) else other
70 | )
71 | return self.check
72 |
73 |
74 | class Check:
75 | def __init__(self, predicate):
76 | self.predicate = predicate
77 | self.inverted = False
78 |
79 | def __repr__(self):
80 | return f"Check(predicate={self.predicate!r}, inverted={self.inverted})"
81 |
82 | async def __call__(self, *args, **kwargs):
83 | r = self.predicate(*args, **kwargs)
84 | if isinstance(r, bool):
85 | r = r
86 | else:
87 | r = await r
88 |
89 | if self.inverted:
90 | r = not r
91 |
92 | return r
93 |
94 | def __invert__(self):
95 | self.inverted = not self.inverted
96 | return self
97 |
98 | def __or__(self, other):
99 | return Either(self.predicate, other if isinstance(other, CheckOp) else other.predicate)
100 |
101 | def __and__(self, other):
102 | return Both(self.predicate, other if isinstance(other, CheckOp) else other.predicate)
103 |
104 |
105 | commands.core.check = CheckDecorator
106 | commands.check = CheckDecorator
107 |
108 |
109 | class Only:
110 | def __init__(self, first: Check):
111 | self.first = first
112 | self.inverted = False
113 |
114 | def _call(self, *args, **kwargs):
115 | return self.first(*args, **kwargs)
116 |
117 | def __call__(self, *args, **kwargs):
118 | if isinstance(args[0], commands.Command):
119 | args[0].checks.append(self)
120 | return args[0]
121 | else:
122 | return self._call(*args, **kwargs)
123 |
124 | def __repr__(self):
125 | return f"Only(first={self.first!r})"
126 |
127 | def __invert__(self):
128 | self.inverted = not self.inverted
129 | return self
130 |
131 | def __or__(self, other):
132 | return Either(self, other)
133 |
134 | def __and__(self, other):
135 | return Both(self, other)
136 |
137 |
138 | class CheckOp:
139 | def __init__(self, first: Check, second: Check):
140 | self.first = first
141 | self.second = second
142 | self.inverted = False
143 | self.check = self
144 |
145 | def __repr__(self):
146 | return f"{self.__class__.__name__}(first={self.first!r}, second={self.second!r}, inverted={self.inverted})"
147 |
148 | async def _try_single(self, callback, *args, **kwargs):
149 | r = await callback(*args, **kwargs)
150 |
151 | return not r if self.inverted else r
152 |
153 | async def _try_call(self, *args, **kwargs):
154 | return await self._try_single(self.first, *args, **kwargs), await self._try_single(
155 | self.second, *args, **kwargs
156 | )
157 |
158 | def __invert__(self):
159 | self.inverted = not self.inverted
160 | return self
161 |
162 | def __or__(self, other):
163 | return Either(self, other)
164 |
165 | def __and__(self, other):
166 | return Both(self, other)
167 |
168 | async def _call(self, *args, **kwargs):
169 | ...
170 |
171 | def __call__(self, *args, **kwargs):
172 | if isinstance(args[0], commands.Command):
173 | args[0].checks.append(self)
174 | return args[0]
175 | else:
176 | return self._call(*args, **kwargs)
177 |
178 |
179 | class Both(CheckOp):
180 | async def _call(self, *args, **kwargs):
181 | fs, ss = await self._try_call(*args, **kwargs)
182 | return fs and ss
183 |
184 |
185 | class Either(CheckOp):
186 | async def _call(self, *args, **kwargs):
187 | try:
188 | if await self._try_single(self.first, *args, **kwargs):
189 | return True
190 | except:
191 | pass
192 |
193 | try:
194 | if await self._try_single(self.second, *args, **kwargs):
195 | return True
196 | except:
197 | pass
198 |
199 | return False
200 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/dict_converter.py:
--------------------------------------------------------------------------------
1 | """An experiment that allows you to use a dict converter using **kwarg
2 | notation.
3 |
4 | .. note::
5 | This uses key, value pairs for construction
6 |
7 | Specificy key and value types can be set using typing.Dict[key_type, value_type],
8 | this defaults to Dict[str, str] the builtin dict type can also be used (that will default
9 | to [str, str] aswell).
10 |
11 | Example:
12 | ```py
13 | import typing
14 |
15 | @bot.command()
16 | async def ban(ctx, **users_reasons_mapping: typing.Dict[discord.Member, str]):
17 | for (member, reason) in users_reasons_mapping['users_reasons_mapping'].items(): # this is necessary unfortunately
18 | await member.ban(reason=reason)
19 | await ctx.send(f'Banned {len(users_reasons_mapping["users_reasons_mapping"])} members')
20 | ```
21 | """
22 |
23 | import inspect
24 | from typing import Dict
25 |
26 | import discord
27 | from discord.ext.commands import Command, TooManyArguments, view as _view
28 |
29 |
30 | class DictStringView(_view.StringView):
31 | def get_quoted_word(self):
32 | current = self.current
33 | if current is None:
34 | return None
35 |
36 | close_quote = _view._quotes.get(current)
37 | is_quoted = bool(close_quote)
38 | if is_quoted:
39 | result = []
40 | _escaped_quotes = (current, close_quote)
41 | else:
42 | result = [current]
43 | _escaped_quotes = _view._all_quotes
44 |
45 | while not self.eof:
46 | current = self.get()
47 | if not current:
48 | if is_quoted:
49 | # unexpected EOF
50 | raise _view.ExpectedClosingQuoteError(close_quote)
51 | return "".join(result)
52 |
53 | # currently we accept strings in the format of "hello world"
54 | # to embed a quote inside the string you must escape it: "a \"world\""
55 | if current == "\\":
56 | next_char = self.get()
57 | if not next_char:
58 | # string ends with \ and no character after it
59 | if is_quoted:
60 | # if we're quoted then we're expecting a closing quote
61 | raise _view.ExpectedClosingQuoteError(close_quote)
62 | # if we aren't then we just let it through
63 | return "".join(result)
64 |
65 | if next_char in _escaped_quotes:
66 | # escaped quote
67 | result.append(next_char)
68 | else:
69 | # different escape character, ignore it
70 | self.undo()
71 | result.append(current)
72 | continue
73 |
74 | if not is_quoted and current in _view._all_quotes:
75 | # we aren't quoted
76 | try:
77 | return self.get_quoted_word()
78 | except _view.UnexpectedQuoteError:
79 | raise
80 |
81 | # closing quote
82 | if is_quoted and current == close_quote:
83 | next_char = self.get()
84 | # all this for that
85 | valid_eof = not next_char or next_char.isspace() or next_char == "="
86 | if not valid_eof:
87 | raise _view.InvalidEndOfQuotedStringError(next_char)
88 |
89 | # we're quoted so it's okay
90 | return "".join(result)
91 |
92 | if current.isspace() and not is_quoted:
93 | # end of word found
94 | return "".join(result)
95 |
96 | result.append(current)
97 |
98 |
99 | async def _parse_arguments(self, ctx):
100 | ctx.args = [ctx] if self.cog is None else [self.cog, ctx]
101 | ctx.kwargs = {}
102 | args = ctx.args
103 | kwargs = ctx.kwargs
104 |
105 | view = ctx.view
106 | iterator = iter(self.params.items())
107 |
108 | if self.cog is not None:
109 | # we have 'self' as the first parameter so just advance
110 | # the iterator and resume parsing
111 | try:
112 | next(iterator)
113 | except StopIteration:
114 | fmt = 'Callback for {0.name} command is missing "self" parameter.'
115 | raise discord.ClientException(fmt.format(self))
116 |
117 | # next we have the 'ctx' as the next parameter
118 | try:
119 | next(iterator)
120 | except StopIteration:
121 | fmt = 'Callback for {0.name} command is missing "ctx" parameter.'
122 | raise discord.ClientException(fmt.format(self))
123 |
124 | for name, param in iterator:
125 | if param.kind == param.POSITIONAL_OR_KEYWORD:
126 | transformed = await self.transform(ctx, param)
127 | args.append(transformed)
128 | elif param.kind == param.KEYWORD_ONLY:
129 | # kwarg only param denotes "consume rest" semantics
130 | if self.rest_is_raw:
131 | converter = self._get_converter(param)
132 | argument = view.read_rest()
133 | kwargs[name] = await self.do_conversion(ctx, converter, argument, param)
134 | else:
135 | kwargs[name] = await self.transform(ctx, param)
136 | break
137 | elif param.kind == param.VAR_POSITIONAL:
138 | while not view.eof:
139 | try:
140 | transformed = await self.transform(ctx, param)
141 | args.append(transformed)
142 | except RuntimeError:
143 | break
144 | elif param.kind == param.VAR_KEYWORD:
145 | # we have received **kwargs
146 | annotation = param.annotation
147 | if annotation == param.empty or annotation is dict:
148 | annotation = Dict[str, str] # default to {str: str}
149 |
150 | key_converter = annotation.__args__[0]
151 | value_converter = annotation.__args__[1]
152 | argument = view.read_rest()
153 | view = DictStringView(argument)
154 | kv_list = []
155 |
156 | while 1:
157 | kv = view.get_quoted_word()
158 | if kv is None:
159 | break
160 | else:
161 | kv_list.append(kv.strip())
162 | kv_pairs = []
163 | for current in kv_list:
164 | if current[0] == "=":
165 | kv_pairs.remove([previous])
166 | kv_pairs.append([previous, current[1:]])
167 | else:
168 | kv_pairs.append(current.split("="))
169 | previous = current
170 | kwargs[name] = {
171 | await self.do_conversion(ctx, key_converter, key, param): await self.do_conversion(
172 | ctx, value_converter, value, param
173 | )
174 | for (key, value) in kv_pairs
175 | }
176 | break
177 |
178 | if not self.ignore_extra:
179 | if not view.eof:
180 | raise TooManyArguments("Too many arguments passed to " + self.qualified_name)
181 |
182 |
183 | Command._parse_arguments = _parse_arguments
184 |
--------------------------------------------------------------------------------
/discord/ext/alternatives/command_suffix.py:
--------------------------------------------------------------------------------
1 | """An experiment that allows you to set a command suffix on a Bot
2 |
3 | Example:
4 | ```py
5 | bot = commands.Bot(command_suffix='!', command_prefix='!') # both the prefix and the suffix will work when invoking commands
6 |
7 |
8 | @bot.command()
9 | async def say(ctx, *, words):
10 | ret = 'You said \"{}\" with the {} `{}`!'
11 |
12 | if ctx.prefix is not None:
13 | ret = ret.format(words, 'prefix', ctx.prefix)
14 | else:
15 | ret = ret.format(words, 'suffix', ctx.prefix)
16 |
17 | await ctx.send(ret)
18 |
19 |
20 | @bot.command()
21 | async def ping(ctx):
22 | await ctx.send('Pong! {}ms'.format(ctx.bot.latency * 1000))
23 |
24 |
25 | @bot.command()
26 | async def hello(ctx):
27 | await ctx.send('Hello! I\'m a bot with command suffixes!')
28 | ```
29 |
30 | 'hello!' --> 'Hello! I'm a bot with command suffixes!'
31 | 'say! this is neat' --> 'You said "this is neat" with the suffix `!`'
32 | '!say this is neat' --> 'You said "this is neat" with the prefix `!`'
33 | '!say! this is neat' --> No reply
34 | """
35 |
36 | import collections
37 |
38 | import discord
39 | from discord.ext import commands
40 |
41 |
42 | def _suffix_used(suffix, content):
43 | space_index = content.find(" ")
44 | suffix_index = content.find(suffix)
45 | return suffix_index > 0 and (space_index == -1 or suffix_index < space_index)
46 |
47 |
48 | class Context(commands.Context):
49 | def __init__(self, **attrs):
50 | super().__init__(**attrs)
51 | self.suffix = attrs.pop("suffix")
52 |
53 | @property
54 | def valid(self):
55 | return (self.suffix is not None or self.prefix is not None) and self.command is not None
56 |
57 | async def reinvoke(self, *, call_hooks=False, restart=True):
58 | if self.suffix is not None:
59 | # since the command was invoked with a suffix,
60 | # we need to make sure the view doesn't try to skip a nonexistent prefix
61 | original_prefix = self.prefix
62 | self.prefix = ""
63 |
64 | await super().reinvoke(call_hooks=call_hooks, restart=restart)
65 |
66 | try:
67 | self.prefix = original_prefix
68 | except NameError:
69 | pass
70 |
71 |
72 | class BotBase(commands.bot.BotBase):
73 | def __init__(self, command_prefix=None, command_suffix=None, **options):
74 | if command_prefix is None and command_suffix is None:
75 | raise ValueError("Bot must have a prefix or suffix")
76 |
77 | super().__init__(command_prefix=command_prefix, **options)
78 | self.command_suffix = command_suffix
79 |
80 | async def get_prefix(self, message):
81 | if self.command_prefix is None:
82 | return None
83 |
84 | return await super().get_prefix(message)
85 |
86 | async def get_suffix(self, message):
87 | """|coro|
88 | Retrieves the prefix the bot is listening to
89 | with the message as a context.
90 | Parameters
91 | -----------
92 | message: :class:`discord.Message`
93 | The message context to get the prefix of.
94 | Returns
95 | --------
96 | Optional[Union[List[:class:`str`], :class:`str`]]
97 | A list of prefixes or a single prefix that the bot is
98 | listening for.
99 | """
100 | if self.command_suffix is None:
101 | return None
102 |
103 | suffix = ret = self.command_suffix
104 | if callable(suffix):
105 | ret = await discord.utils.maybe_coroutine(suffix, self, message)
106 |
107 | if not isinstance(ret, str):
108 | try:
109 | ret = list(ret)
110 | except TypeError:
111 | # It's possible that a generator raised this exception. Don't
112 | # replace it with our own error if that's the case.
113 | if isinstance(ret, collections.abc.Iterable):
114 | raise
115 |
116 | raise TypeError(
117 | "command_suffix must be plain string, iterable of strings, or callable "
118 | "returning either of these, not {}".format(ret.__class__.__name__)
119 | )
120 |
121 | if not ret:
122 | raise ValueError("Iterable command_prefix must contain at least one suffix")
123 |
124 | return ret
125 |
126 | async def get_context(self, message, *, cls=Context):
127 | """Defaults to check for prefix first."""
128 | view = commands.view.StringView(message.content)
129 | ctx = cls(prefix=None, suffix=None, view=view, bot=self, message=message)
130 |
131 | if self._skip_check(message.author.id, self.user.id):
132 | return ctx
133 |
134 | prefix = await self.get_prefix(message)
135 | suffix = await self.get_suffix(message)
136 |
137 | if prefix is not None:
138 | if isinstance(prefix, str):
139 | if view.skip_string(prefix):
140 | invoked_prefix = prefix
141 | else:
142 | try:
143 | if message.content.startswith(tuple(prefix)):
144 | invoked_prefix = discord.utils.find(view.skip_string, prefix)
145 | except TypeError:
146 | if not isinstance(prefix, list):
147 | raise TypeError(
148 | "get_prefix must return either a string or a list of string, "
149 | "not {}".format(prefix.__class__.__name__)
150 | )
151 |
152 | for value in prefix:
153 | if not isinstance(value, str):
154 | raise TypeError(
155 | "Iterable command_prefix or list returned from get_prefix must "
156 | "contain only strings, not {}".format(value.__class__.__name__)
157 | )
158 |
159 | raise
160 | else:
161 | if isinstance(suffix, str):
162 | if _suffix_used(suffix, message.content):
163 | invoked_suffix = suffix
164 | else:
165 | return ctx
166 | else:
167 | try:
168 | invoked_suffixes = [s for s in suffix if _suffix_used(s, message.content)]
169 | if not invoked_suffixes:
170 | return ctx
171 |
172 | for suf in invoked_suffixes:
173 | invoker = view.get_word()[: -len(suf)]
174 | command = self.all_commands.get(invoker)
175 | if command is not None:
176 | view.undo()
177 | invoked_suffix = suf
178 | break
179 | else:
180 | return ctx
181 |
182 | except TypeError:
183 | if not isinstance(suffix, list):
184 | raise TypeError(
185 | "get_suffix must return either a string or a list of string, "
186 | "not {}".format(suffix.__class__.__name__)
187 | )
188 |
189 | for value in suffix:
190 | if not isinstance(value, str):
191 | raise TypeError(
192 | "Iterable command_suffix or list returned from get_suffix must "
193 | "contain only strings, not {}".format(value.__class__.__name__)
194 | )
195 |
196 | raise
197 |
198 | invoker = view.get_word()
199 |
200 | try:
201 | ctx.suffix = invoked_suffix
202 | except NameError:
203 | try:
204 | ctx.prefix = invoked_prefix
205 | except NameError:
206 | pass
207 | else:
208 | invoker = invoker[: -len(invoked_suffix)]
209 |
210 | ctx.invoked_with = invoker
211 | ctx.command = self.all_commands.get(invoker)
212 | return ctx
213 |
214 |
215 | commands.bot.BotBase = BotBase
216 | commands.Context = Context
217 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2020-present Ext-Creators
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
203 | Portions of the this software may also be licensed under:
204 |
205 | MIT License
206 |
207 | Copyright © 2015-2020 Rapptz
208 |
209 | Permission is hereby granted, free of charge, to any person obtaining a copy
210 | of this software and associated documentation files (the "Software"), to deal
211 | in the Software without restriction, including without limitation the rights
212 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
213 | copies of the Software, and to permit persons to whom the Software is
214 | furnished to do so, subject to the following conditions:
215 |
216 | The above copyright notice and this permission notice shall be included in all
217 | copies or substantial portions of the Software.
218 |
219 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
220 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
221 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
222 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
223 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
224 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
225 | SOFTWARE.
226 |
--------------------------------------------------------------------------------