diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..dd08457b --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: [mccoderpy] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml new file mode 100644 index 00000000..f4cfd57d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml @@ -0,0 +1,51 @@ +name: Feature Request +description: Suggest a feature that should be added to discord4py +title: "[Feature Request]:" +labels: ["feature request"] +assignees: ["mccoderpy"] +body: + - type: input + attributes: + label: Summary + description: > + A short summary of what your feature request is about. + validations: + required: true + - type: dropdown + attributes: + multiple: false + label: What is the feature request for? + options: + - The core library + - discord.ext.commands + - discord.ext.tasks + - The documentation + validations: + required: true + - type: textarea + attributes: + label: The Problem + description: > + What problem is your feature trying to solve? + What becomes easier or possible when this feature is implemented? + validations: + required: true + - type: textarea + attributes: + label: The Ideal Solution + description: > + What is your ideal solution to the problem? + What would you like this feature to do? + validations: + required: true + - type: textarea + attributes: + label: The Current Solution + description: > + What is the current solution to the problem, if any? + validations: + required: false + - type: textarea + attributes: + label: Additional Context + description: If there is anything else to say, please do so here. diff --git a/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE.yml b/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE.yml new file mode 100644 index 00000000..d9412993 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE.yml @@ -0,0 +1,75 @@ +name: Bug Report +description: File a bug report +title: "[Bug]: " +labels: ["bug"] +assignees: ["mccoderpy"] +body: + - type: markdown + attributes: + value: Please fill in the fields below as accurately as possible to help us fix the bug! + - type: textarea + id: description + attributes: + label: Description + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you are trying and what is not working. + validations: + required: true + - type: dropdown + id: branch + attributes: + label: Branch used + description: What branch of the library are you using? + options: + - main (stable) + - developer + validations: + required: true + - type: markdown + attributes: + value: | + For the next 3 fields use the output of the `version` command + ```ps + # Windows + py -m discord --version + + # Linux/macOS + python3 -m discord --version # Replace 'python3' with the prefix of the Python version you are using + ``` + - type: input + id: python-version + attributes: + label: The Python version you are using. + placeholder: Python v3.8.10-final + validations: + required: true + - type: input + id: version-output + attributes: + label: What version (and release) of the library are you using + placeholder: v2.0a294+gfb291a6 + validations: + required: true + - type: input + id: system-info + attributes: + label: System info + placeholder: Linux 5.4.0-126-generic + validations: + required: true + - type: textarea + id: traceback + attributes: + label: Full Traceback (Error) + description: > + Please copy and paste the complete traceback (error). + This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: input + id: contact-info + attributes: + label: Contact info + description: Additional ways to contact you + placeholder: mccuber04#2960 (Discord) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..a40975f6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Discord + url: https://discord.gg/sb69muSqsg + about: Official support server - get in contact with our community and devs diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..4195b6c7 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,74 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ main ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main ] + schedule: + - cron: '35 9 * * 3' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + - name: Setup Python + uses: actions/setup-python@v3.1.2 diff --git a/LICENSE b/LICENSE index 12b7ec03..bf85dba8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,8 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015-present Rapptz - -Implementing of the Discord-Message-components made by mccoderpy (Discord-User mccuber04#2960) +Copyright (c) 2015-2021 Rapptz & 2021-present mccoderpy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), @@ -20,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. \ No newline at end of file +DEALINGS IN THE SOFTWARE. diff --git a/README.rst b/README.rst index f048876b..e164d0d4 100644 --- a/README.rst +++ b/README.rst @@ -1,11 +1,13 @@ -Welcome to discord.py-message-components' documentation! -========================================================= +.. |flag_ua| image:: https://mccoder-py-needs.to-sleep.xyz/r/ua.png + +|flag_ua| Welcome to discord.py-message-components'! |flag_ua| +============================================================== .. figure:: https://cdn.discordapp.com/attachments/852872100073963532/854711446767796286/discord.py-message-components.png :name: discord.py-message-components :align: center :alt: Name of the Project (discord.py-message-components) - + .. .. image:: https://discord.com/api/guilds/852871920411475968/embed.png :target: https://discord.gg/sb69muSqsg @@ -27,7 +29,22 @@ Welcome to discord.py-message-components' documentation! :target: https://discordpy-message-components.readthedocs.io/en/latest/ :alt: Documentation Status - The Original `discord.py `_ Library made by `Rapptz `_ with implementation of the `Discord-Message-Components `_ by `mccoderpy `_ + A "fork" of `discord.py `_ library made by `Rapptz `_ with implemation of the `Discord-Message-Components `_ by `mccoderpy `_ + ++---------------------------------------------------------------------------------------------------------------------------------------------------+ +| **❗ℹIMPORTANTℹ❗** | ++===================================================================================================================================================+ +| This branch represents only the `PyPI `_ version of this library, | +| which is currently **not up to date** due to some (private) issues | +| (as soon as we have documented the new features and the developer branch is stable enough, this will be updated). | +| | +| **To get the latest version with the newest features and bug(-fixes), please take a look at the** `developer <../../tree/developer>`_ **branch.** | ++---------------------------------------------------------------------------------------------------------------------------------------------------+ + +**NOTE:** + This library will be further developed independently of discord.py. + New features are also implemented. It is not an extension! + The name only comes from the fact that the original purpose of the library was to add support for message components and we haven't found a better name yet. .. figure:: https://github.com/mccoderpy/discord.py-message-components/raw/main/images/rtd-logo-wordmark-light.png :name: discord.py-message-components documentation @@ -35,21 +52,21 @@ Welcome to discord.py-message-components' documentation! :align: center :scale: 20% :target: https://discordpy-message-components.readthedocs.io/en/latest/ - + **Read the Documentation** `here `_ -You need help? Or have ideas/feedback? -______________________________________ +You are in need of help or want to leave feedback? +__________________________________________________ -Open a Issue/Pull request on `GitHub `_, join the `support-Server `_ or send me a direct-message on `Discord `_: ``mccuber04#2960`` +Open a `issue <../../issues>`_/`pull request <../../pulls>`_, join the `support server `_ or send me a direct message on `Discord `_: ``mccuber04#2960`` Installing __________ -**Python 3.5.3 or higher is required** +**Python 3.5.3 or higher is required.** -This Library overwrite the original discord.py Library so to be sure all will work fine -first uninstall the original `discord.py `_ Library if it is installed: +This library overwrites the original discord.py library (or any other that would be imported using `import discord`) so to be sure all will work fine +first uninstall the original `discord.py `_ library if it is installed: .. code:: sh @@ -59,7 +76,7 @@ first uninstall the original `discord.py `_ using: +Then install `this library `_ using: .. code:: sh @@ -69,10 +86,22 @@ Then install `this Library `_ of this library which is the **most up to date** and has **fewer bugs** use: + +.. code:: sh + + # Linux/macOS + python3 -m pip install -U git+https://github.com/mccoderpy/discord.py-message-components.git@developer + + # Windows + py -m pip install -U git+https://github.com/mccoderpy/discord.py-message-components.git@developer  + +Of course you nead to have git installed on your device. If you need help with this, take a look `here `_ + Examples -------- -A Command that sends you a Message and edit it when you click a Button: +A command that sends you a message and edit it when you click a Button: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python @@ -129,7 +158,7 @@ A Command that sends you a Message and edit it when you click a Button: client.run('You Bot-Token here') -Another (complex) Example where a small Embed will be send; you can move a small white ⬜ with the Buttons: +Another more complex example where a small embed will be send; you can move a small white ⬜ with the buttons: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python @@ -233,7 +262,7 @@ Another (complex) Example where a small Embed will be send; you can move a small async def down(i: discord.Interaction, button): pointer: Pointer = get_pointer(interaction.guild) pointer.set_y(-1) - await message.edit(embed=discord.Embed(title="Little Game", + await i.edit(embed=discord.Embed(title="Little Game", description=display(x=pointer.possition_x, y=pointer.possition_y)), components=[discord.ActionRow(empty_button, arrow_button().set_label('↑').set_custom_id('up'), empty_button), discord.ActionRow(arrow_button().set_label('←').set_custom_id('left').disable_if(pointer.possition_x <= 0), @@ -245,7 +274,7 @@ Another (complex) Example where a small Embed will be send; you can move a small async def right(i: discord.Interaction, button): pointer: Pointer = get_pointer(interaction.guild) pointer.set_x(1) - await message.edit(embed=discord.Embed(title="Little Game", + await i.edit(embed=discord.Embed(title="Little Game", description=display(x=pointer.possition_x, y=pointer.possition_y)), components=[discord.ActionRow(empty_button, arrow_button().set_label('↑').set_custom_id('up'), empty_button), discord.ActionRow(arrow_button().set_label('←').set_custom_id('left'), @@ -257,7 +286,7 @@ Another (complex) Example where a small Embed will be send; you can move a small async def left(i: discord.Interaction, button): pointer: Pointer = get_pointer(interaction.guild) pointer.set_x(-1) - await message.edit(embed=discord.Embed(title="Little Game", + await i.edit(embed=discord.Embed(title="Little Game", description=display(x=pointer.possition_x, y=pointer.possition_y)), components=[discord.ActionRow(empty_button, arrow_button().set_label('↑').set_custom_id('up'), empty_button), discord.ActionRow(arrow_button().set_label('←').set_custom_id('left').disable_if(pointer.possition_x <= 0), @@ -265,4 +294,4 @@ Another (complex) Example where a small Embed will be send; you can move a small arrow_button().set_label('→').set_custom_id('right'))] ) -Take a look at `the documentation `_ to see more examples. \ No newline at end of file +Please take a look at `the documentation `_ if you want to see more examples. diff --git a/discord/abc.py b/discord/abc.py index a2a7c09e..a202f28c 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1070,17 +1070,16 @@ async def send(self, content=None, *, tts=False, embed=None, embeds=None, compon if file is not None and files is not None: raise InvalidArgument('cannot pass both file and files parameter to send()') - is_interaction_response = kwargs.pop('__is_interaction_response', None) + is_interaction_response = kwargs.pop('__is_interaction_response', False) deferred = kwargs.pop('__deferred', False) use_webhook = kwargs.pop('__use_webhook', False) interaction_id = kwargs.pop('__interaction_id', None) interaction_token = kwargs.pop('__interaction_token', None) application_id = kwargs.pop('__application_id', None) followup = kwargs.pop('followup', False) - if is_interaction_response is False or None: - hidden = None - if hidden is True and file or files: - raise AttributeError('An ephemeral(hidden) Message could not contain file(s)') + if is_interaction_response: + if hidden and file or files: + raise AttributeError('An ephemeral(hidden) Message could not contain file(s)') if file is not None: if not isinstance(file, File): raise InvalidArgument('file parameter must be File') diff --git a/discord/channel.py b/discord/channel.py index bd4a39dc..067470bb 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -26,15 +26,35 @@ import time import asyncio - +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + Mapping, + Optional, + TYPE_CHECKING, + Tuple, + Type, + TypeVar, + Union, + overload, +) import discord.abc from .permissions import Permissions from .enums import ChannelType, try_enum, VoiceRegion from .mixins import Hashable from . import utils +from .object import Object from .asset import Asset from .errors import ClientException, NoMoreItems, InvalidArgument +if TYPE_CHECKING: + from .state import ConnectionState + from .message import Message, PartialMessage + + __all__ = ( 'TextChannel', 'VoiceChannel', @@ -1224,6 +1244,16 @@ def __str__(self): def __repr__(self): return ''.format(self) + @classmethod + def _from_message(cls, state, channel_id: int): + self = cls.__new__(cls) + self._state = state + self.id = channel_id + self.recipient = None + # state.user won't be None here + self.me = state.user # type: ignore + return self + @property def type(self): """:class:`ChannelType`: The channel's Discord type.""" @@ -1545,6 +1575,57 @@ async def leave(self): await self._state.http.leave_group(self.id) + +class PartialMessageable(discord.abc.Messageable, Hashable): + """Represents a partial messageable to aid with working messageable channels when + only a channel ID are present. + The only way to construct this class is through :meth:`Client.get_partial_messageable`. + Note that this class is trimmed down and has no rich attributes. + .. versionadded:: 2.0 + .. container:: operations + .. describe:: x == y + Checks if two partial messageables are equal. + .. describe:: x != y + Checks if two partial messageables are not equal. + .. describe:: hash(x) + Returns the partial messageable's hash. + Attributes + ----------- + id: :class:`int` + The channel ID associated with this partial messageable. + type: Optional[:class:`ChannelType`] + The channel type associated with this partial messageable, if given. + """ + + def __init__(self, state: 'ConnectionState', id: int, type: Optional[ChannelType] = None): + self._state: ConnectionState = state + self._channel: Object = Object(id=id) + self.id: int = id + self.type: Optional[ChannelType] = type + + async def _get_channel(self) -> Object: + return self._channel + + def get_partial_message(self, message_id: int, /): + """Creates a :class:`PartialMessage` from the message ID. + This is useful if you want to work with a message and only have its ID without + doing an unnecessary API call. + Parameters + ------------ + message_id: :class:`int` + The message ID to create a partial message for. + Returns + --------- + :class:`PartialMessage` + The partial message. + """ + + from .message import PartialMessage + + return PartialMessage(channel=self, id=message_id) + + + def _channel_factory(channel_type): value = try_enum(ChannelType, channel_type) if value is ChannelType.text: diff --git a/discord/components.py b/discord/components.py index 40f03020..f20e5a78 100644 --- a/discord/components.py +++ b/discord/components.py @@ -3,9 +3,7 @@ """ The MIT License (MIT) -Copyright (c) 2015-present Rapptz - -Implementing of the Discord-Message-components made by mccoderpy (Discord-User mccuber04#2960) +Copyright (c) 2015-present mccuber04#2960 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), @@ -37,7 +35,6 @@ class Button: - """ Represents an `Discord-Button `_ @@ -72,7 +69,8 @@ def __init__(self, label: str = None, if isinstance(style, int): style = ButtonStyle.from_value(style) if not isinstance(style, ButtonStyle): - raise InvalidArgument("The Style of an discord.Button have to be an Object of discord.ButtonStyle, discord.ButtonColor or usually an Integer between 1 and 5") + raise InvalidArgument( + "The Style of an discord.Button have to be an Object of discord.ButtonStyle, discord.ButtonColor or usually an Integer between 1 and 5") self.style = style if self.style == ButtonStyle.url and not self.url: raise InvalidArgument('You must also pass a URL if the ButtonStyle is a link.') @@ -80,7 +78,8 @@ def __init__(self, label: str = None, self.style = ButtonStyle.Link_Button if custom_id and len(custom_id) > 100: raise InvalidArgument( - 'The maximum length of Button-custom_id\'s are 100; your one is %s long. (%s Characters to long)' % (len(custom_id), len(custom_id) - 100)) + 'The maximum length of Button-custom_id\'s are 100; your one is %s long. (%s Characters to long)' % ( + len(custom_id), len(custom_id) - 100)) if isinstance(custom_id, str) and custom_id.isdigit(): self.custom_id = int(custom_id) else: @@ -88,7 +87,9 @@ def __init__(self, label: str = None, if self.custom_id is not None and self.url: raise URLAndCustomIDNotAlowed(self.custom_id) if label and len(label) > 80: - raise InvalidArgument('The maximum length of Button-Labels\'s are 80; your one is %s long. (%s Characters to long)' % (len(label), len(label) - 80)) + raise InvalidArgument( + 'The maximum length of Button-Labels\'s are 80; your one is %s long. (%s Characters to long)' % ( + len(label), len(label) - 80)) self.label = label if isinstance(emoji, Emoji): self.emoji = PartialEmoji(name=emoji.name, animated=emoji.animated, id=emoji.id) @@ -121,7 +122,9 @@ def set_label(self, label: str): :return: discord.Button """ if len(label) > 80: - raise InvalidArgument('The maximum length of Button-Labels\'s are 80; your one is %s long. (%s Characters to long)' % (len(label), len(label) - 80)) + raise InvalidArgument( + 'The maximum length of Button-Labels\'s are 80; your one is %s long. (%s Characters to long)' % ( + len(label), len(label) - 80)) self.label = label return self @@ -158,7 +161,8 @@ def set_custom_id(self, custom_id: Union[str, int]): raise URLAndCustomIDNotAlowed(custom_id) if len(custom_id) > 100: raise InvalidArgument( - 'The maximum length of Button-custom_id\'s are 100; your one is %s long. (%s Characters to long)' % (len(custom_id), len(custom_id) - 100)) + 'The maximum length of Button-custom_id\'s are 100; your one is %s long. (%s Characters to long)' % ( + len(custom_id), len(custom_id) - 100)) if isinstance(custom_id, str) and custom_id.isdigit(): self.custom_id = int(custom_id) else: @@ -235,7 +239,6 @@ def from_dict(cls, data: dict): class SelectOption: - """ A class that creates an option for a :class:`SelectMenu` and represents it in a :class:`SelectMenu` in the components of a :class:`discord.Message`. @@ -256,23 +259,24 @@ class SelectOption: will render this option as selected by default """ + def __init__(self, label: str, value: str, description: str = None, emoji: Union[PartialEmoji, Emoji, str] = None, default: bool = False): - if len(label) > 25: - raise AttributeError('The maximum length of the label is 25 characters.') + if len(label) > 100: + raise AttributeError('The maximum length of the label is 100 characters.') self.label = label if len(value) > 100: raise AttributeError('The maximum length of the value is 100 characters.') self.value = value - if description and len(description) > 50: - raise AttributeError('The maximum length of the description is 50 characters.') + if description and len(description) > 100: + raise AttributeError('The maximum length of the description is 100 characters.') self.description = description if isinstance(emoji, PartialEmoji): self.emoji = emoji - if isinstance(emoji, Emoji): + elif isinstance(emoji, Emoji): self.emoji = PartialEmoji(name=emoji.name, animated=emoji.animated, id=emoji.id) elif isinstance(emoji, str): if emoji[0] == '<': @@ -286,6 +290,10 @@ def __init__(self, label: str, def __repr__(self): return f'' + def set_default(self, value: bool): + self.default = value + return self + def to_dict(self): base = {'label': str(self.label), 'value': str(self.value), @@ -299,7 +307,7 @@ def to_dict(self): @classmethod def from_dict(cls, data): emoji = data.pop('emoji', None) - if emoji is not None: + if emoji: emoji = PartialEmoji.from_dict(emoji) return cls(label=data.pop('label'), value=data.pop('value'), @@ -309,7 +317,6 @@ def from_dict(cls, data): class SelectMenu: - """ Represents a `Select-Menu `_ @@ -343,18 +350,22 @@ def __init__(self, custom_id: Union[str, int], raise InvalidArgument('The maximum number of options in a SelectMenu is 25.') self.options = options if len(custom_id) > 100: - raise ValueError('The maximum length of a custom_id is 100 characters; your one is %s long (%s to long).' % (len(custom_id), len(custom_id) - 100)) + raise ValueError( + 'The maximum length of a custom_id is 100 characters; your one is %s long (%s to long).' % ( + len(custom_id), len(custom_id) - 100)) if isinstance(custom_id, str) and custom_id.isdigit(): self.custom_id = int(custom_id) else: self.custom_id = custom_id if placeholder and len(placeholder) > 100: - raise AttributeError('The maximum length of a the placeholder is 100 characters; your one is %s long (%s to long).' % (len(placeholder), len(placeholder) - 100)) + raise AttributeError( + 'The maximum length of a the placeholder is 100 characters; your one is %s long (%s to long).' % ( + len(placeholder), len(placeholder) - 100)) self.placeholder = placeholder - if min_values > 25 or min_values < 0: - raise ValueError('The minimum number of elements to be selected must be between 0 and 25.') + if 25 < min_values < 0: + raise ValueError('The minimum number of elements to be selected must be between 1 and 25.') self.min_values = min_values - if max_values > 25 or max_values <= 0: + if 25 < max_values <= 0: raise ValueError('The maximum number of elements to be selected must be between 0 and 25.') self.max_values = max_values self.disabled = disabled @@ -368,7 +379,7 @@ def all_option_values(self): """ All values of the :attr:`options` - If the value is a number it is returned as an integer, otherwise a string + If the value is a number it is returned as an integer, otherwise as string .. note:: This is equal to @@ -408,7 +419,7 @@ def values(self): .. note:: This only exists if the :class:`SelectMenu` is passed as a parameter in an interaction. - If the value is a number it is returned as an integer, otherwise a string + If the value is a number it is returned as an integer, otherwise as string :return: List[Union[int, str]] """ @@ -421,6 +432,25 @@ def values(self): values.append(value) return values + @property + def not_selected(self): + """ + The options that were **not** selected + + .. note:: + This only exists if the :class:`SelectMenu` is passed as a parameter in an interaction. + + If the value is a number it is returned as an integer, otherwise as string + + :return: List[Union[int, str]] + """ + _not_selected = [] + values = self.values + for value in self.all_option_values: + if value not in values: + _not_selected.append(value) + return _not_selected + def update(self, **kwargs): self.__dict__.update((k, v) for k, v in kwargs.items() if k in self.__dict__.keys()) return self @@ -436,7 +466,8 @@ def set_custom_id(self, custom_id: Union[str, int]): """ if len(custom_id) > 100: raise InvalidArgument( - 'The maximum length of SelectMenu-custom_id\'s are 100; your one is %s long. (%s Characters to long)' % (len(custom_id), len(custom_id) - 100)) + 'The maximum length of SelectMenu-custom_id\'s are 100; your one is %s long. (%s Characters to long)' % ( + len(custom_id), len(custom_id) - 100)) if isinstance(custom_id, str) and custom_id.isdigit(): self.custom_id = int(custom_id) else: @@ -474,7 +505,6 @@ def from_dict(cls, data: dict): class ActionRow: - """ Represents an ActionRow-Part for the components of an :class:`discord.Message`. @@ -486,7 +516,7 @@ class ActionRow: The components the :class:`ActionRow` should have. It could contain at least 5 :class:`Button`or 1 :class:`SelectMenu`. .. note :: - For more information about ActionRow's visit the `Discord-API Documentation `_. + For more information about ActionRow's visit the `Discord-APIMethodes Documentation `_. """ def __init__(self, *components): @@ -496,9 +526,11 @@ def __init__(self, *components): self.components.append(component) elif isinstance(component, dict): if not component.get('type', None) in [2, 3]: - raise InvalidArgument('If you use an Dict instead of Button or SelectMenu you have to pass an type between 2 or 3') - self.components.append({2: Button.from_dict(component), 3: SelectMenu.from_dict(component)}.get(component.get('type'))) - + raise InvalidArgument( + 'If you use an Dict instead of Button or SelectMenu you have to pass an type between 2 or 3') + self.components.append( + {2: Button.from_dict(component), 3: SelectMenu.from_dict(component)}.get(component.get('type'))) + def __repr__(self): return f'' @@ -508,7 +540,8 @@ def __iter__(self): def to_dict(self) -> Union[list, EmptyActionRow]: base = [] - base.extend([{'type': 1, 'components': [obj.to_dict() for obj in self.components[five:5:]]} for five in range(0, len(self.components), 5)]) + base.extend([{'type': 1, 'components': [obj.to_dict() for obj in self.components[five:5:]]} for five in + range(0, len(self.components), 5)]) objects = len([i['components'] for i in base]) if any([any([part['type'] == 2]) and any([part['type'] == 3]) for part in base]): raise InvalidArgument('An ActionRow containing a select menu cannot also contain buttons') @@ -517,7 +550,8 @@ def to_dict(self) -> Union[list, EmptyActionRow]: if any([len(ar['components']) < 1 for ar in base]): raise EmptyActionRow from base elif len(base) > 5 or objects > 25: - raise InvalidArgument(f"The maximum number of ActionRow's per message is 5 and they can only contain 5 Buttons or 1 Select-Menu each; you have {len(base)} ActionRow's passed with {objects} objects") + raise InvalidArgument( + f"The maximum number of ActionRow's per message is 5 and they can only contain 5 Buttons or 1 Select-Menu each; you have {len(base)} ActionRow's passed with {objects} objects") return base def __len__(self): @@ -527,11 +561,14 @@ def __invert__(self): return self.components def __getitem__(self, item) -> Union[Button, SelectMenu, None]: - return self.components[item] + return self.components.get(item, None) def __setitem__(self, index, component): return self.set_component_at(index, component) + def __reversed__(self): + return reversed(self.components) + def add_component(self, component: Union[Button, SelectMenu]): """ Adds a component to the :class:`ActionRow`. @@ -626,6 +663,39 @@ def add_components(self, *components: Union[Button, SelectMenu]): self.components.extend(*components) return self + def disable_all_components(self): + """ + Disables all component's in this :class:`ActionRow`. + + :return: discord.ActionRow + """ + [obj.__setattr__('disabled', True) for obj in self.components] + return self + + def disable_all_components_if(self, check: Union[bool, typing.Callable], *args: typing.Any): + """ + Disables all :attr:`components` in this :class:`ActionRow` if the passed :attr:`check` returns :bool:`True`. + + Parameters + ----------- + check: Union[:class:`bool`, :type:`typing.Callable`] + Could be an bool or usually any Callable that returns a bool. + *args: Any + Arguments that should passed in to the check if it is a Callable. + + :return: discord.ActionRow + """ + if not isinstance(check, (bool, typing.Callable)): + raise AttributeError( + 'The check must bee a bool or any callable that returns one. Not {0.__class__.__name__}'.format(check)) + try: + check = check(*args) + except TypeError: + pass + if check is True: + [obj.__setattr__('disabled', True) for obj in self.components] + return self + def disable_all_buttons(self): """ Disables all ::class:`discord.Button`'s in this :class:`ActionRow`. @@ -638,7 +708,7 @@ def disable_all_buttons(self): def disable_all_buttons_if(self, check: Union[bool, typing.Callable], *args: typing.Any): """ Disables all :class:`discord.Button`'s in this :class:`ActionRow` if the passed :attr:`check` returns :bool:`True`. - + Parameters ----------- check: Union[:class:`bool`, :type:`typing.Callable`] @@ -649,7 +719,8 @@ def disable_all_buttons_if(self, check: Union[bool, typing.Callable], *args: typ :return: discord.ActionRow """ if not isinstance(check, (bool, typing.Callable)): - raise AttributeError('The check must bee a bool or any callable that returns one. Not {0.__class__.__name__}'.format(check)) + raise AttributeError( + 'The check must bee a bool or any callable that returns one. Not {0.__class__.__name__}'.format(check)) try: check = check(*args) except TypeError: @@ -681,7 +752,8 @@ def disable_all_select_menus_if(self, check: Union[bool, typing.Callable], *args :return: discord.ActionRow """ if not isinstance(check, (bool, typing.Callable)): - raise AttributeError('The check must bee a bool or any callable that returns one. Not {0.__class__.__name__}'.format(check)) + raise AttributeError( + 'The check must bee a bool or any callable that returns one. Not {0.__class__.__name__}'.format(check)) try: check = check(*args) except TypeError: diff --git a/discord/http.py b/discord/http.py index 3d66f55c..531d79a7 100644 --- a/discord/http.py +++ b/discord/http.py @@ -953,14 +953,14 @@ def get_widget(self, guild_id): def create_invite(self, channel_id, *, reason=None, **options): r = Route('POST', '/channels/{channel_id}/invites', channel_id=channel_id) payload = { - 'max_age': options.get('max_age', 0), + 'max_age': options.get('max_age', 84000), 'max_uses': options.get('max_uses', 0), 'temporary': options.get('temporary', False), - 'unique': options.get('unique', True), + 'unique': options.get('unique', False), 'target_type': options.get('target_type', None), 'target_user_id': options.get('target_user_id', None), 'target_application_id': options.get('target_application_id', None), - 'validate': options.get('validate', 84000) + 'validate': options.get('validate', ) } return self.request(r, reason=reason, json=payload) diff --git a/discord/interactions.py b/discord/interactions.py index ffea5296..31a98c9f 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -7,6 +7,8 @@ from .message import Message from .errors import NotFound from .channel import DMChannel +from typing_extensions import Literal +from typing import Union, List, Optional from .components import Button, SelectMenu from .enums import ComponentType, InteractionCallbackType @@ -80,14 +82,14 @@ def __init__(self, state, data): self._channel = None self.channel_id = int(data.get('channel_id', 0)) self.__application_id = int(data.get('application_id')) - self.message: typing.Union[Message, EphemeralMessage] = EphemeralMessage() if self.message_is_hidden else None self.member: typing.Optional[Member] = None self.user: typing.Optional[User] = None self.deferred = False self.deferred_hidden = False self.callback_message = None self._component = None - self.component_type = self._data.get('component_type', None) + self.component_type: typing.Optional[int] = self._data.get('component_type', None) + self.message = EphemeralMessage() if self.message_is_hidden else None #Message(state=self._state, channel=self.channel, data=self._message) # maybe ``later`` this library will also supports Slash-Commands # self.command = None @@ -95,7 +97,7 @@ def __repr__(self): """Represents a :class:`discord.Interaction`-object.""" return f'' - async def defer(self, response_type: typing.Literal[5, 6] = InteractionCallbackType.deferred_update_msg, hidden: bool = False) -> None: + async def defer(self, response_type: Literal[5, 6] = InteractionCallbackType.deferred_update_msg, hidden: bool = False) -> None: """ |coro| @@ -103,7 +105,7 @@ async def defer(self, response_type: typing.Literal[5, 6] = InteractionCallbackT If :attr:`response_type` is `InteractionCallbackType.deferred_msg_with_source` it shows a loading state to the user. - :param response_type: Optional[typing.Literal[5, 6]] + :param response_type: Optional[Literal[5, 6]] The type to response with, aiter :class:`InteractionCallbackType.deferred_msg_with_source` or :class:`InteractionCallbackType.deferred_update_msg` (e.g. 5 or 6) :param hidden: Optional[bool] @@ -215,29 +217,31 @@ def guild(self): def message_is_dm(self) -> bool: return not self.guild_id + #@property + #def message(self) -> typing.Union[Message, EphemeralMessage]: + # message = self._state._get_message(self.message_id) + # if not message: + # message = EphemeralMessage() if self.message_is_hidden else Message(state=self._state, channel=self.channel, data=self._message) + # return message + @property def message_is_hidden(self) -> bool: return self.message_flags == 64 @property - def component(self) -> typing.Union[Button, SelectMenu, ButtonClick, SelectionSelect]: + def component(self) -> Union[Button, SelectMenu, ButtonClick, SelectionSelect, None]: if self._component is None: - custom_id = self._data['custom_id'] - if custom_id.isdigit(): - custom_id = int(custom_id) - if isinstance(self.message, Message): - if self._data['component_type'] == ComponentType.Button: + custom_id = self._data.get('custom_id') + if custom_id is not None: + if custom_id.isdigit(): + custom_id = int(custom_id) + if self._data.get('component_type') == 2: self._component = utils.get(self.message.all_buttons, custom_id=custom_id) - elif self._data['component_type'] == ComponentType.SelectMenu: + elif self._data.get('component_type') == 3: select_menu = utils.get(self.message.all_select_menus, custom_id=custom_id) - setattr(select_menu, '_values', self._data['values']) + if select_menu is not None: + setattr(select_menu, '_values', self._data['values']) self._component = select_menu - - else: - if self._data['component_type'] == ComponentType.Button: - self._component = ButtonClick(self._data) - elif self._data['component_type'] == ComponentType.SelectMenu: - self._component = SelectionSelect(self._data) return self._component diff --git a/discord/message.py b/discord/message.py index c9b0a5b1..8f31878b 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1009,7 +1009,8 @@ def all_buttons(self): """Returns all :class:`Button`'s that are contained in the message""" for action_row in self.components: for component in action_row: - yield component + if isinstance(component, Button): + yield component @property def all_select_menus(self): @@ -1611,7 +1612,8 @@ def all_buttons(self): """Returns all :class:`Button`'s that are contained in the message""" for action_row in self.components: for component in action_row: - yield component + if isinstance(component, Button): + yield component @property def all_select_menus(self): diff --git a/discord/state.py b/discord/state.py index 95f60fb8..d45c043e 100644 --- a/discord/state.py +++ b/discord/state.py @@ -383,12 +383,12 @@ def _get_guild_channel(self, data): try: guild = self._get_guild(int(data['guild_id'])) except KeyError: - channel = self.get_channel(channel_id) + channel = DMChannel._from_message(self, channel_id) guild = None else: channel = guild and guild.get_channel(channel_id) - return channel or Object(id=channel_id), guild + return channel or PartialMessageable(state=self, id=channel_id), guild async def chunker(self, guild_id, query='', limit=0, presences=False, *, nonce=None): ws = self._get_websocket(guild_id) # This is ignored upstream @@ -544,7 +544,7 @@ def parse_interaction_create(self, data): if data.get('type', data.get('t', 0)) < 3: return interaction = Interaction(state=self, data=data) - interaction.message = self._get_message(interaction.message_id) if interaction.message is None else interaction.message + interaction.user = self.store_user(interaction._user) if interaction.guild_id: interaction._guild = self._get_guild(interaction.guild_id) @@ -555,7 +555,8 @@ def parse_interaction_create(self, data): interaction.member = Member(guild=interaction.guild, data=interaction._member, state=self) else: interaction._channel = self._get_private_channel(interaction.channel_id) - if interaction.message is not None: + interaction.message = self._get_message(interaction.message_id) if interaction.message is None else interaction.message + if self._get_message(interaction.message_id) is not None: if interaction._interaction_type == InteractionType.Component: if interaction.component_type == 2: self.dispatch('button_click', interaction, interaction.component) diff --git a/docs/additions.rst b/docs/additions.rst index eeed3806..009b0750 100644 --- a/docs/additions.rst +++ b/docs/additions.rst @@ -251,6 +251,7 @@ and .. note:: This is equal to: + .. code-block:: python for action_row in self.components: @@ -265,6 +266,7 @@ and .. note:: This is equal to: + .. code-block:: python for action_row in self.components: @@ -279,6 +281,7 @@ and .. note:: This is equal to: + .. code-block:: python for action_row in self.components: diff --git a/docs/components.rst b/docs/components.rst index 7d4fe7ba..794f1aab 100644 --- a/docs/components.rst +++ b/docs/components.rst @@ -44,7 +44,7 @@ ___________________________________________ Represents an :class:`ActionRow`-Part for the components of a :class:`discord.Message`. .. note:: - For general information about ActionRow's visit the `Discord-API Documentation `_. + For general information about ActionRow's visit the `Discord-APIMethodes Documentation `_. .. note:: You could use a :class:`list` instead of this but you don't have the functions and parameters of this class then. @@ -177,7 +177,7 @@ ________________________________________ .. note:: To get more infos about the styles visit - `the Discord-API Documentation `_. + `the Discord-APIMethodes Documentation `_. :param emoji: Optional[Union[:class:`discord.PartialEmoji`, :class:`discord.Emoji`, :class:`str`]] The Emoji that will be displayed on the left side of the Button. @@ -223,7 +223,7 @@ ________________________________________ .. class:: SelectOption(label, value, description, emoji, default) - Builds you a dict which can be used as an option for a :class:`SelectMenu` + Represents a option for a :class:`SelectMenu`. .. _select-option-parameters: @@ -250,7 +250,7 @@ ________________________________________ Represents a ``Discord-Select-Menu`` .. note:: - For general information about Select-Menus visit the `Discord-API-Documentation `_. + For general information about Select-Menus visit the `Discord-APIMethodes-Documentation `_. .. _select-menu-parameters: @@ -260,13 +260,10 @@ ________________________________________ :param options: List[:class:`SelectOption`] A :class:`list` of choices(:class:`SelectOption`) the :class:`SelectMenu` should have, max. 25. - .. tip:: - Use :class:`SelectOption` to create an option. - :param placeholder: Optional[:class:`str`] Custom placeholder text if nothing is selected, max. 100 characters. - :param min_values: Optional[:class:`int`] + :param min_values: Optional[::class:`int`] The minimum number of items that must be chosen; default 1, min. 0, max. 25. :param max_values: Optional[:class:`int`] diff --git a/docs/index.rst b/docs/index.rst index 7b5778cf..655afa3e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,7 +32,22 @@ Welcome to discord\.py-message-components' documentation! :target: https://discordpy-message-components.readthedocs.io/en/latest/ :alt: Documentation Status - The Original `discord.py `_ Library made by `Rapptz `_ with implementation of the `Discord-Message-Components `_ by `mccoderpy `_ + A "fork" of `discord.py `_ library made by `Rapptz `_ with implemation of the `Discord-Message-Components `_ by `mccoderpy `_ + ++---------------------------------------------------------------------------------------------------------------------------------------------------+ +| **❗ℹIMPORTANTℹ❗** | ++===================================================================================================================================================+ +| This branch represents only the `PyPI `_ version of this library, | +| which is currently **not up to date** due to some (privat) issues | +| (as soon as we have documented the new features and the developer branch is stable enough, this will be updated). | +| | +| **To get the latest version with the newest features and bug(-fixes) please take a look at the** `developer <../../tree/developer>`_ **branch** | ++---------------------------------------------------------------------------------------------------------------------------------------------------+ + +**NOTE:** + This library will be further developed independently of discord.py. + New features are also implemented. It's not an extension! + The name only comes from the fact that the original purpose of the library was to add support for message components and we haven't found a better one yet. .. |PyPI| image:: https://cdn.discordapp.com/emojis/854380926548967444.png?v=1 :alt: PyPI Logo @@ -45,7 +60,7 @@ Welcome to discord\.py-message-components' documentation! You need help? Or have ideas/feedback? ______________________________________ -Open a Issue/Pull request on `GitHub `_, join the `support-Server `_ or send me a direct-message on `Discord `_: ``mccuber04#2960`` +Open a `issue <../../issues>`_/`pull request <../../pulls>`_, join the `support-server `_ or send me a direct-message on `Discord `_: ``mccuber04#2960`` Installing: @@ -161,7 +176,7 @@ Interact when a button was pressed .. note:: You could set the parameter :attr:`hidden` in the response to ``True`` to make the message ephemeral. - See `discord.Interaction.respond <./interaction.html#interaction-respond>`_ for more information about :meth:`respond()`. + See `discord.Interaction.respond <./interaction.html#Interaction.respond>`_ for more information about :meth:`respond()`. ________________________________________ diff --git a/docs/requirements.txt b/docs/requirements.txt index ddfead45..0d94cbb0 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ sphinx-rtd-theme==0.5.2 sphinxcontrib-contentui==0.2.5 +aiohttp>=3.9.2,<4 diff --git a/readthedocs.yaml b/readthedocs.yaml index b4da90a8..64beb8f7 100644 --- a/readthedocs.yaml +++ b/readthedocs.yaml @@ -2,10 +2,15 @@ version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3.10" + + sphinx: configuration: docs/conf.py python: - version: 3.8 install: - - requirements: docs/requirements.txt \ No newline at end of file + - requirements: docs/requirements.txt diff --git a/requirements.txt b/requirements.txt index 8517fb4d..34ccc5a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -aiohttp>=3.6.0,<3.8.0 +aiohttp>=3.9.2,<4 diff --git a/setup.py b/setup.py index 93962bda..110b1a8d 100644 --- a/setup.py +++ b/setup.py @@ -16,12 +16,14 @@ v = fp.read() if version and not v: - version = i if (i := input(f'are you sure to use version {version}>> ')) else version + i = input(f'are you sure to use version {version}>> ') + version = i if i else version with open('version.txt', 'w') as fp: fp.write(i) if not (version or v): - if not (version := input('please set an version>> ')): + version = input('please set an version>> ') + if not version: raise RuntimeError('version is not set') if version.endswith(('a', 'b', 'rc')): diff --git a/version.txt b/version.txt index 71cf14a8..28811714 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.7.5.3 +1.7.5.4