diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
deleted file mode 100644
index 4f0af8ed63..0000000000
--- a/.github/CODE_OF_CONDUCT.md
+++ /dev/null
@@ -1,120 +0,0 @@
-# Contributor Covenant Code of Conduct
-
-## Our Pledge
-
-We as members, contributors, and leaders pledge to make participation in our community a
-harassment-free experience for everyone, regardless of age, body size, visible or
-invisible disability, ethnicity, sex characteristics, gender identity and expression,
-level of experience, education, socio-economic status, nationality, personal appearance,
-race, religion, or sexual identity and orientation.
-
-We pledge to act and interact in ways that contribute to an open, welcoming, diverse,
-inclusive, and healthy community.
-
-## Our Standards
-
-Examples of behavior that contributes to a positive environment for our community
-include:
-
-- Demonstrating empathy and kindness toward other people
-- Being respectful of differing opinions, viewpoints, and experiences
-- Giving and gracefully accepting constructive feedback
-- Accepting responsibility and apologizing to those affected by our mistakes, and
- learning from the experience
-- Focusing on what is best not just for us as individuals, but for the overall community
-
-Examples of unacceptable behavior include:
-
-- The use of sexualized language or imagery, and sexual attention or advances of any
- kind
-- Trolling, insulting or derogatory comments, and personal or political attacks
-- Public or private harassment
-- Publishing others' private information, such as a physical or email address, without
- their explicit permission
-- Other conduct which could reasonably be considered inappropriate in a professional
- setting
-
-## Enforcement Responsibilities
-
-Community leaders are responsible for clarifying and enforcing our standards of
-acceptable behavior and will take appropriate and fair corrective action in response to
-any behavior that they deem inappropriate, threatening, offensive, or harmful.
-
-Community leaders have the right and responsibility to remove, edit, or reject comments,
-commits, code, wiki edits, issues, and other contributions that are not aligned to this
-Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
-
-## Scope
-
-This Code of Conduct applies within all community spaces, and also applies when an
-individual is officially representing the community in public spaces. Examples of
-representing our community include using an official e-mail address, posting via an
-official social media account, or acting as an appointed representative at an online or
-offline event.
-
-## Enforcement
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to
-the community leaders responsible for enforcement at Our Discord server. All complaints
-will be reviewed and investigated promptly and fairly.
-
-All community leaders are obligated to respect the privacy and security of the reporter
-of any incident.
-
-## Enforcement Guidelines
-
-Community leaders will follow these Community Impact Guidelines in determining the
-consequences for any action they deem in violation of this Code of Conduct:
-
-### 1. Correction
-
-**Community Impact**: Use of inappropriate language or other behavior deemed
-unprofessional or unwelcome in the community.
-
-**Consequence**: A private, written warning from community leaders, providing clarity
-around the nature of the violation and an explanation of why the behavior was
-inappropriate. A public apology may be requested.
-
-### 2. Warning
-
-**Community Impact**: A violation through a single incident or series of actions.
-
-**Consequence**: A warning with consequences for continued behavior. No interaction with
-the people involved, including unsolicited interaction with those enforcing the Code of
-Conduct, for a specified period of time. This includes avoiding interactions in
-community spaces as well as external channels like social media. Violating these terms
-may lead to a temporary or permanent ban.
-
-### 3. Temporary Ban
-
-**Community Impact**: A serious violation of community standards, including sustained
-inappropriate behavior.
-
-**Consequence**: A temporary ban from any sort of interaction or public communication
-with the community for a specified period of time. No public or private interaction with
-the people involved, including unsolicited interaction with those enforcing the Code of
-Conduct, is allowed during this period. Violating these terms may lead to a permanent
-ban.
-
-### 4. Permanent Ban
-
-**Community Impact**: Demonstrating a pattern of violation of community standards,
-including sustained inappropriate behavior, harassment of an individual, or aggression
-toward or disparagement of classes of individuals.
-
-**Consequence**: A permanent ban from any sort of public interaction within the
-community.
-
-## Attribution
-
-This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
-available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
-
-Community Impact Guidelines were inspired by
-[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
-
-[homepage]: https://www.contributor-covenant.org
-
-For answers to common questions about this code of conduct, see the FAQ at
-https://www.contributor-covenant.org/faq. Translations are available at
-https://www.contributor-covenant.org/translations.
diff --git a/.github/DEVELOPER_CERTIFICATE_OF_ORIGIN.md b/.github/DEVELOPER_CERTIFICATE_OF_ORIGIN.md
deleted file mode 100644
index e003b3bd6d..0000000000
--- a/.github/DEVELOPER_CERTIFICATE_OF_ORIGIN.md
+++ /dev/null
@@ -1,37 +0,0 @@
-# Developer Certificate of Origin (DCO)
-
-```
-Version 1.1
-
-Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
-
-Everyone is permitted to copy and distribute verbatim copies of this
-license document, but changing it is not allowed.
-
-
-Developer's Certificate of Origin 1.1
-
-By making a contribution to this project, I certify that:
-
-(a) The contribution was created in whole or in part by me and I
- have the right to submit it under the open source license
- indicated in the file; or
-
-(b) The contribution is based upon previous work that, to the best
- of my knowledge, is covered under an appropriate open source
- license and I have the right under that license to submit that
- work with modifications, whether created in whole or in part
- by me, under the same open source license (unless I am
- permitted to submit under a different license), as indicated
- in the file; or
-
-(c) The contribution was provided directly to me by some other
- person who certified (a), (b) or (c) and I have not modified
- it.
-
-(d) I understand and agree that this project and the contribution
- are public and that a record of the contribution (including all
- personal information I submit with it, including my sign-off) is
- maintained indefinitely and may be redistributed consistent with
- this project or the open source license(s) involved.
-```
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
deleted file mode 100644
index e171b7955b..0000000000
--- a/.github/FUNDING.yml
+++ /dev/null
@@ -1 +0,0 @@
-patreon: pycord
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
deleted file mode 100644
index d6ae4ca6aa..0000000000
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ /dev/null
@@ -1,23 +0,0 @@
-## Summary
-
-
-
-## Information
-
-
-
-- [ ] This PR fixes an issue.
-- [ ] This PR adds something new (e.g. new method or parameters).
-- [ ] This PR is a breaking change (e.g. methods or parameters removed/renamed).
-- [ ] This PR is **not** a code change (e.g. documentation, README, typehinting,
- examples, ...).
-
-## Checklist
-
-
-
-- [ ] I have searched the open pull requests for duplicates.
-- [ ] If code changes were made then they have been tested.
- - [ ] I have updated the documentation to reflect the changes.
-- [ ] If `type: ignore` comments were used, a comment is also left explaining why.
-- [ ] I have updated the changelog to include these changes.
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 0fe3702d7d..9ad535184a 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -45,20 +45,6 @@ jobs:
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings.
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=120 --statistics
- - name: Run code coverage with pytest
- run: |
- coverage run -m pytest
- coverage xml
- - name: Upload code coverage to codecov.io
- uses: codecov/codecov-action@v3
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
- env_vars: OS,PYTHON
- files: coverage.xml
- flags: ${{ matrix.os }}-${{ matrix.python-version }}
- name: codecov-umbrella
- fail_ci_if_error: true
- verbose: true
docs:
runs-on: ubuntu-latest
steps:
diff --git a/.github/workflows/todo.yml b/.github/workflows/todo.yml
index 15d85e18b3..fec76a1f57 100644
--- a/.github/workflows/todo.yml
+++ b/.github/workflows/todo.yml
@@ -6,7 +6,7 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Run tdg-github-action
- uses: ribtoks/tdg-github-action@v0.4.6-beta
+ uses: ribtoks/tdg-github-action@v0.4.7-beta
with:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
diff --git a/.github/workflows/version-updates.yml b/.github/workflows/version-updates.yml
new file mode 100644
index 0000000000..f98f1f34ed
--- /dev/null
+++ b/.github/workflows/version-updates.yml
@@ -0,0 +1,20 @@
+name: Version Update Auto Merge
+on: pull_request
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ auto-merge:
+ runs-on: ubuntu-latest
+ if: ${{ github.actor == 'dependabot[bot]' }}
+ steps:
+ - run: gh pr review --approve "$PR_URL"
+ env:
+ PR_URL: ${{github.event.pull_request.html_url}}
+ GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
+ - run: gh pr merge --auto --squash "$PR_URL"
+ env:
+ PR_URL: ${{github.event.pull_request.html_url}}
+ GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4f51e49c19..43519b50cd 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -9,7 +9,7 @@ repos:
- id: trailing-whitespace
- id: end-of-file-fixer
- repo: https://github.com/PyCQA/autoflake
- rev: v2.0.2
+ rev: v2.2.0
hooks:
- id: autoflake
# args:
@@ -19,7 +19,7 @@ repos:
# - --remove-duplicate-keys
# - --remove-unused-variables
- repo: https://github.com/asottile/pyupgrade
- rev: v3.3.1
+ rev: v3.9.0
hooks:
- id: pyupgrade
args: [--py38-plus]
@@ -28,7 +28,7 @@ repos:
hooks:
- id: isort
- repo: https://github.com/psf/black
- rev: 23.3.0
+ rev: 23.7.0
hooks:
- id: black
args: [--safe, --quiet]
@@ -77,7 +77,7 @@ repos:
# - id: mypy
- repo: https://github.com/pre-commit/mirrors-prettier
- rev: v3.0.0-alpha.6
+ rev: v3.0.0
hooks:
- id: prettier
args: [--prose-wrap=always, --print-width=88]
diff --git a/.readthedocs.yml b/.readthedocs.yml
index b12d1ab002..59842ef8bb 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -2,7 +2,9 @@ version: 2
formats: []
build:
- image: latest
+ os: ubuntu-22.04
+ tools:
+ python: "3.8"
sphinx:
configuration: docs/conf.py
@@ -10,7 +12,6 @@ sphinx:
builder: html
python:
- version: "3.8"
install:
- method: pip
path: .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0e65373326..45f8d1e2fc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,9 @@ These changes are available on the `master` branch, but have not yet been releas
- Added possibility to start bot via async context manager.
([#1801](https://github.com/Pycord-Development/pycord/pull/1801))
+- Change default for all `name_localizations` & `description_localizations` attributes
+ from being `None` to be `MISSING`.
+ ([#1866](https://github.com/Pycord-Development/pycord/pull/1866))
- Added new parameters (`author`, `footer`, `image`, `thumbnail`) to `discord.Embed`.
([#1996](https://github.com/Pycord-Development/pycord/pull/1996))
- Added new events `on_bridge_command`, `on_bridge_command_completion`, and
@@ -29,21 +32,84 @@ These changes are available on the `master` branch, but have not yet been releas
([#1983](https://github.com/Pycord-Development/pycord/pull/1983))
- Added new `application_auto_moderation_rule_create_badge` to `ApplicationFlags`.
([#1992](https://github.com/Pycord-Development/pycord/pull/1992))
+- Added `sync_start` argument to `VoiceClient.start_recording()`. This adds silence to
+ the start of audio recordings.
+ ([#1984](https://github.com/Pycord-Development/pycord/pull/1984))
+- Added `custom_message` to AutoModActionMetadata.
+ ([#2029](https://github.com/Pycord-Development/pycord/pull/2029))
+- Added support for
+ [voice messages](https://github.com/discord/discord-api-docs/pull/6082).
+ ([#2016](https://github.com/Pycord-Development/pycord/pull/2016))
+- Added the `data` attribute to all
+ [Raw Event payloads](https://docs.pycord.dev/en/master/api/models.html#events).
+ ([#2023](https://github.com/Pycord-Development/pycord/pull/2023))
+- Added and documented missing `AuditLogAction` enums.
+ ([#2030](https://github.com/Pycord-Development/pycord/pull/2030),
+ [#2171](https://github.com/Pycord-Development/pycord/pull/2171))
+- `AuditLogDiff` now supports AutoMod related models.
+ ([#2030](https://github.com/Pycord-Development/pycord/pull/2030))
+- Added `Interaction.respond` and `Interaction.edit` as shortcut responses.
+ ([#2026](https://github.com/Pycord-Development/pycord/pull/2026))
+- Added `view.parent` which is set when the view was sent by
+ `interaction.response.send_message`.
+ ([#2036](https://github.com/Pycord-Development/pycord/pull/2036))
+- Added functions (`bridge.Bot.walk_bridge_commands` &
+ `BridgeCommandGroup.walk_commands`) to cycle through all bridge commands and their
+ children/subcommands.
+ ([#1867](https://github.com/Pycord-Development/pycord/pull/1867))
+- Added support for usernames and modified multiple methods accordingly.
+ ([#2042](https://github.com/Pycord-Development/pycord/pull/2042))
+- Added `icon` and `unicode_emoji` to `Guild.create_role`.
+ ([#2086](https://github.com/Pycord-Development/pycord/pull/2086))
+- Added `cooldown` and `max_concurrency` to `SlashCommandGroup`.
+ ([#2091](https://github.com/Pycord-Development/pycord/pull/2091))
+- Added new embedded activities, Gartic Phone and Jamspace.
+ ([#2102](https://github.com/Pycord-Development/pycord/pull/2102))
+- Added `bridge.Context` as a shortcut to `Union` of subclasses.
+ ([#2106](https://github.com/Pycord-Development/pycord/pull/2106))
+- Added Annotated forms support for typehinting slash command options.
+ ([#2124](https://github.com/Pycord-Development/pycord/pull/2124))
+- Added `suppress` and `allowed_mentions` parameters to `Webhook` and
+ `InteractionResponse` edit methods.
+ ([#2138](https://github.com/Pycord-Development/pycord/pull/2138))
+- Added `wait_finish` parameter to `VoiceClient.play` for awaiting the end of a play.
+ ([#2194](https://github.com/Pycord-Development/pycord/pull/2194))
+- Added support for custom bot status.
+ ([#2206](https://github.com/Pycord-Development/pycord/pull/2206))
### Changed
+- Suppressed FFMPEG output when recording voice channels.
+ ([#1993](https://github.com/Pycord-Development/pycord/pull/1993))
- Changed file-upload size limit from 8 MB to 25 MB accordingly.
([#2014](https://github.com/Pycord-Development/pycord/pull/2014))
+- `Interaction.channel` is received from the gateway, so it can now be `DMChannel` and
+ `GroupChannel`. ([#2025](https://github.com/Pycord-Development/pycord/pull/2025))
+- `DMChannel.recipients` can now be `None`.
+ ([#2025](https://github.com/Pycord-Development/pycord/pull/2025))
+- Store `view.message` on receiving Interaction for a component.
+ ([#2036](https://github.com/Pycord-Development/pycord/pull/2036))
+- Attributes shared between ext and slash commands are now dynamically fetched on bridge
+ commands. ([#1867](https://github.com/Pycord-Development/pycord/pull/1867))
+- Embed attribues like author, footer, etc now return `None` when not set, and return
+ their respective classes when set.
+ ([#2063](https://github.com/Pycord-Development/pycord/pull/2063))
+- `default_avatar` behavior changes depending on the user's username migration status.
+ ([#2087](https://github.com/Pycord-Development/pycord/pull/2087))
+- Typehinted `command_prefix` and `help_command` arguments properly.
+ ([#2099](https://github.com/Pycord-Development/pycord/pull/2099))
+- Replace `orjson` support with `msgspec` support.
+ ([#2170](https://github.com/Pycord-Development/pycord/pull/2170))
### Removed
- Removed `@client.once()` in favour of `@client.listen(once=True)`.
([#1957](https://github.com/Pycord-Development/pycord/pull/1957))
-
-### Changed
-
-- Suppressed FFMPEG output when recording voice channels.
- ([#1993](https://github.com/Pycord-Development/pycord/pull/1993))
+- Removed `view.message` being set when the view was sent by
+ `interaction.response.send_message`.
+ ([#2036](https://github.com/Pycord-Development/pycord/pull/2036))
+- Removed `Embed.Empty` in favour of `None`, and `EmbedProxy` in favour of individual
+ classes. ([#2063](https://github.com/Pycord-Development/pycord/pull/2063))
### Fixed
@@ -63,6 +129,43 @@ These changes are available on the `master` branch, but have not yet been releas
working. ([#1999](https://github.com/Pycord-Development/pycord/pull/1999))
- Fixed `TypeError` being raised when passing `name` argument to bridge groups.
([#2000](https://github.com/Pycord-Development/pycord/pull/2000))
+- Fixed `TypeError` in AutoModRule.
+ ([#2029](https://github.com/Pycord-Development/pycord/pull/2029))
+- Reflecting the api for gettings bans correctly.
+ ([#1922](https://github.com/Pycord-Development/pycord/pull/1922))
+- Restored functionality for overriding default `on_application_command_error` via
+ listeners. ([#2044](https://github.com/Pycord-Development/pycord/pull/2044))
+- Fixed unloading of cogs having bridge commands.
+ ([#2048](https://github.com/Pycord-Development/pycord/pull/2048))
+- Fixed the Slash command syncronization method `indiviual`.
+ ([#1925](https://github.com/Pycord-Development/pycord/pull/1925))
+- Fixed an issue that occurred when `webhooks_update` event payload channel ID was
+ `None`. ([#2078](https://github.com/Pycord-Development/pycord/pull/2078))
+- Fixed major TypeError when an AuditLogEntry has no user.
+ ([#2079](https://github.com/Pycord-Development/pycord/pull/2079))
+- Fixed `HTTPException` when trying to create a forum thread with files.
+ ([#2075](https://github.com/Pycord-Development/pycord/pull/2075))
+- Fixed `before_invoke` not being run for `SlashCommandGroup`.
+ ([#2091](https://github.com/Pycord-Development/pycord/pull/2091))
+- Fixed `AttributeError` when accessing a `Select`'s values when it hasn't been
+ interacted with. ([#2104](https://github.com/Pycord-Development/pycord/pull/2104))
+- Fixed `before_invoke` being run twice for slash subcommands.
+ ([#2139](https://github.com/Pycord-Development/pycord/pull/2139))
+- Fixed `Guild._member_count` sometimes not being set.
+ ([#2145](https://github.com/Pycord-Development/pycord/pull/2145))
+- Fixed `Thread.applied_tags` not being updated.
+ ([#2146](https://github.com/Pycord-Development/pycord/pull/2146))
+- Fixed type-hinting of `author` property of `ApplicationContext` to include typehinting
+ of `User` or `Member`.
+ ([#2148](https://github.com/Pycord-Development/pycord/pull/2148))
+- Fixed missing `delete_after` parameter in overload type-hinting for `send` method in
+ `Webhook` class. ([#2156](https://github.com/Pycord-Development/pycord/pull/2156))
+- Fixed `ScheduledEvent.creator_id` returning `str` instead of `int`.
+ ([#2162](https://github.com/Pycord-Development/pycord/pull/2162))
+- Fixed initial message inside of the create thread payload sending legacy beta payload.
+ ([#2191](https://github.com/Pycord-Development/pycord/pull/2191))
+- Fixed a misplaced payload object inside of the thread creation payload.
+ ([#2192](https://github.com/Pycord-Development/pycord/pull/2192))
## [2.4.1] - 2023-03-20
diff --git a/.github/CONTRIBUTING.md b/CONTRIBUTING.md
similarity index 100%
rename from .github/CONTRIBUTING.md
rename to CONTRIBUTING.md
diff --git a/README.rst b/README.rst
index 72eea5a0ef..278ee90ab5 100644
--- a/README.rst
+++ b/README.rst
@@ -87,7 +87,7 @@ Optional Packages
* `PyNaCl `__ (for voice support)
* `aiodns `__, `brotlipy `__, `cchardet `__ (for aiohttp speedup)
-* `orjson `__ (for json speedup)
+* `msgspec `__ (for json speedup)
Please note that while installing voice support on Linux, you must install the following packages via your preferred package manager (e.g. ``apt``, ``dnf``, etc) BEFORE running the above commands:
diff --git a/discord/abc.py b/discord/abc.py
index 5aa94840eb..71a307ab87 100644
--- a/discord/abc.py
+++ b/discord/abc.py
@@ -215,6 +215,14 @@ class User(Snowflake, Protocol):
The user's username.
discriminator: :class:`str`
The user's discriminator.
+
+ .. note::
+
+ If the user has migrated to the new username system, this will always be "0".
+ global_name: :class:`str`
+ The user's global name.
+
+ .. versionadded:: 2.5
avatar: :class:`~discord.Asset`
The avatar asset the user has.
bot: :class:`bool`
@@ -225,6 +233,7 @@ class User(Snowflake, Protocol):
name: str
discriminator: str
+ global_name: str | None
avatar: Asset
bot: bool
diff --git a/discord/activity.py b/discord/activity.py
index 52e7df08e2..96b20255a8 100644
--- a/discord/activity.py
+++ b/discord/activity.py
@@ -160,7 +160,7 @@ class Activity(BaseActivity):
type: :class:`ActivityType`
The type of activity currently being done.
state: Optional[:class:`str`]
- The user's current state. For example, "In Game".
+ The user's current party status or text used for a custom status.
details: Optional[:class:`str`]
The detail of the user's current activity.
timestamps: Dict[:class:`str`, :class:`int`]
@@ -229,7 +229,6 @@ def __init__(self, **kwargs):
self.assets: ActivityAssets = kwargs.pop("assets", {})
self.party: ActivityParty = kwargs.pop("party", {})
self.application_id: int | None = _get_as_snowflake(kwargs, "application_id")
- self.name: str | None = kwargs.pop("name", None)
self.url: str | None = kwargs.pop("url", None)
self.flags: int = kwargs.pop("flags", 0)
self.sync_id: str | None = kwargs.pop("sync_id", None)
@@ -242,6 +241,9 @@ def __init__(self, **kwargs):
if isinstance(activity_type, ActivityType)
else try_enum(ActivityType, activity_type)
)
+ self.name: str | None = kwargs.pop(
+ "name", "Custom Status" if self.type == ActivityType.custom else None
+ )
emoji = kwargs.pop("emoji", None)
self.emoji: PartialEmoji | None = (
@@ -252,6 +254,7 @@ def __repr__(self) -> str:
attrs = (
("type", self.type),
("name", self.name),
+ ("state", self.state),
("url", self.url),
("details", self.details),
("application_id", self.application_id),
@@ -432,9 +435,6 @@ def to_dict(self) -> dict[str, Any]:
def __eq__(self, other: Any) -> bool:
return isinstance(other, Game) and other.name == self.name
- def __ne__(self, other: Any) -> bool:
- return not self.__eq__(other)
-
def __hash__(self) -> int:
return hash(self.name)
@@ -542,9 +542,6 @@ def __eq__(self, other: Any) -> bool:
and other.url == self.url
)
- def __ne__(self, other: Any) -> bool:
- return not self.__eq__(other)
-
def __hash__(self) -> int:
return hash(self.name)
@@ -654,9 +651,6 @@ def __eq__(self, other: Any) -> bool:
and other.start == self.start
)
- def __ne__(self, other: Any) -> bool:
- return not self.__eq__(other)
-
def __hash__(self) -> int:
return hash(self._session_id)
@@ -769,6 +763,8 @@ class CustomActivity(BaseActivity):
The custom activity's name.
emoji: Optional[:class:`PartialEmoji`]
The emoji to pass to the activity, if any.
+ state: Optional[:class:`str`]
+ The text used for the custom activity.
"""
__slots__ = ("name", "emoji", "state")
@@ -778,7 +774,7 @@ def __init__(
):
super().__init__(**extra)
self.name: str | None = name
- self.state: str | None = extra.pop("state", None)
+ self.state: str | None = extra.pop("state", name)
if self.name == "Custom Status":
self.name = self.state
@@ -829,9 +825,6 @@ def __eq__(self, other: Any) -> bool:
and other.emoji == self.emoji
)
- def __ne__(self, other: Any) -> bool:
- return not self.__eq__(other)
-
def __hash__(self) -> int:
return hash((self.name, str(self.emoji)))
diff --git a/discord/audit_logs.py b/discord/audit_logs.py
index c427534ea7..27a4f740b2 100644
--- a/discord/audit_logs.py
+++ b/discord/audit_logs.py
@@ -29,6 +29,7 @@
from . import enums, utils
from .asset import Asset
+from .automod import AutoModAction, AutoModTriggerMetadata
from .colour import Colour
from .invite import Invite
from .mixins import Hashable
@@ -57,6 +58,8 @@
from .threads import Thread
from .types.audit_log import AuditLogChange as AuditLogChangePayload
from .types.audit_log import AuditLogEntry as AuditLogEntryPayload
+ from .types.automod import AutoModAction as AutoModActionPayload
+ from .types.automod import AutoModTriggerMetadata as AutoModTriggerMetadataPayload
from .types.channel import PermissionOverwrite as PermissionOverwritePayload
from .types.role import Role as RolePayload
from .types.snowflake import Snowflake
@@ -83,6 +86,22 @@ def _transform_channel(
return entry.guild.get_channel(int(data)) or Object(id=data)
+def _transform_channels(
+ entry: AuditLogEntry, data: list[Snowflake] | None
+) -> list[abc.GuildChannel | Object] | None:
+ if data is None:
+ return None
+ return [_transform_channel(entry, channel) for channel in data]
+
+
+def _transform_roles(
+ entry: AuditLogEntry, data: list[Snowflake] | None
+) -> list[Role | Object] | None:
+ if data is None:
+ return None
+ return [entry.guild.get_role(int(r)) or Object(id=r) for r in data]
+
+
def _transform_member_id(
entry: AuditLogEntry, data: Snowflake | None
) -> Member | User | None:
@@ -172,6 +191,24 @@ def _transform_type(
return enums.try_enum(enums.ChannelType, data)
+def _transform_actions(
+ entry: AuditLogEntry, data: list[AutoModActionPayload] | None
+) -> AutoModAction | None:
+ if data is None:
+ return None
+ else:
+ return [AutoModAction.from_dict(d) for d in data]
+
+
+def _transform_trigger_metadata(
+ entry: AuditLogEntry, data: list[AutoModActionPayload] | None
+) -> AutoModAction | None:
+ if data is None:
+ return None
+ else:
+ return AutoModTriggerMetadata.from_dict(data)
+
+
class AuditLogDiff:
def __len__(self) -> int:
return len(self.__dict__)
@@ -240,6 +277,12 @@ class AuditLogChanges:
),
"command_id": ("command_id", _transform_snowflake),
"image_hash": ("cover", _transform_scheduled_event_cover),
+ "trigger_type": (None, _enum_transformer(enums.AutoModTriggerType)),
+ "event_type": (None, _enum_transformer(enums.AutoModEventType)),
+ "actions": (None, _transform_actions),
+ "trigger_metadata": (None, _transform_trigger_metadata),
+ "exempt_roles": (None, _transform_roles),
+ "exempt_channels": (None, _transform_channels),
}
def __init__(
@@ -255,13 +298,31 @@ def __init__(
for elem in sorted(data, key=lambda i: i["key"]):
attr = elem["key"]
- # special cases for role add/remove
+ # special cases for role/trigger_metadata add/remove
if attr == "$add":
self._handle_role(self.before, self.after, entry, elem["new_value"]) # type: ignore
continue
elif attr == "$remove":
self._handle_role(self.after, self.before, entry, elem["new_value"]) # type: ignore
continue
+ elif attr in [
+ "$add_keyword_filter",
+ "$add_regex_patterns",
+ "$add_allow_list",
+ ]:
+ self._handle_trigger_metadata(
+ self.before, self.after, entry, elem["new_value"], attr
+ )
+ continue
+ elif attr in [
+ "$remove_keyword_filter",
+ "$remove_regex_patterns",
+ "$remove_allow_list",
+ ]:
+ self._handle_trigger_metadata(
+ self.after, self.before, entry, elem["new_value"], attr
+ )
+ continue
try:
key, transformer = self.TRANSFORMERS[attr]
@@ -355,6 +416,23 @@ def _handle_role(
setattr(second, "roles", data)
+ def _handle_trigger_metadata(
+ self,
+ first: AuditLogDiff,
+ second: AuditLogDiff,
+ entry: AuditLogEntry,
+ elem: list[AutoModTriggerMetadataPayload],
+ attr: str,
+ ) -> None:
+ if not hasattr(first, "trigger_metadata"):
+ setattr(first, "trigger_metadata", None)
+
+ key = attr.split("_", 1)[-1]
+ data = {key: elem}
+ tm = AutoModTriggerMetadata.from_dict(data)
+
+ setattr(second, "trigger_metadata", tm)
+
class _AuditLogProxyMemberPrune:
delete_member_days: int
@@ -405,7 +483,7 @@ class AuditLogEntry(Hashable):
-----------
action: :class:`AuditLogAction`
The action that was done.
- user: :class:`abc.User`
+ user: Optional[:class:`abc.User`]
The user who initiated this action. Usually a :class:`Member`\, unless gone
then it's a :class:`User`.
id: :class:`int`
diff --git a/discord/automod.py b/discord/automod.py
index 5f091d0f65..c4b1e23c0e 100644
--- a/discord/automod.py
+++ b/discord/automod.py
@@ -76,6 +76,10 @@ class AutoModActionMetadata:
timeout_duration: :class:`datetime.timedelta`
How long the member that triggered the action should be timed out for.
Only for actions of type :attr:`AutoModActionType.timeout`.
+ custom_message: :class:`str`
+ An additional message shown to members when their message is blocked.
+ Maximum 150 characters.
+ Only for actions of type :attr:`AutoModActionType.block_message`.
"""
# maybe add a table of action types and attributes?
@@ -83,13 +87,18 @@ class AutoModActionMetadata:
__slots__ = (
"channel_id",
"timeout_duration",
+ "custom_message",
)
def __init__(
- self, channel_id: int = MISSING, timeout_duration: timedelta = MISSING
+ self,
+ channel_id: int = MISSING,
+ timeout_duration: timedelta = MISSING,
+ custom_message: str = MISSING,
):
self.channel_id: int = channel_id
self.timeout_duration: timedelta = timeout_duration
+ self.custom_message: str = custom_message
def to_dict(self) -> dict:
data = {}
@@ -100,6 +109,9 @@ def to_dict(self) -> dict:
if self.timeout_duration is not MISSING:
data["duration_seconds"] = self.timeout_duration.total_seconds()
+ if self.custom_message is not MISSING:
+ data["custom_message"] = self.custom_message
+
return data
@classmethod
@@ -113,12 +125,16 @@ def from_dict(cls, data: AutoModActionMetadataPayload):
# might need an explicit int cast
kwargs["timeout_duration"] = timedelta(seconds=duration_seconds)
+ if (custom_message := data.get("custom_message")) is not None:
+ kwargs["custom_message"] = custom_message
+
return cls(**kwargs)
def __repr__(self) -> str:
repr_attrs = (
"channel_id",
"timeout_duration",
+ "custom_message",
)
inner = []
@@ -301,7 +317,7 @@ def __repr__(self) -> str:
inner.append(f"{attr}={value}")
inner = " ".join(inner)
- return f""
+ return f""
class AutoModRule(Hashable):
@@ -352,6 +368,7 @@ class AutoModRule(Hashable):
"""
__slots__ = (
+ "__dict__",
"_state",
"id",
"guild_id",
diff --git a/discord/bot.py b/discord/bot.py
index b7bf968ea4..5587aef34b 100644
--- a/discord/bot.py
+++ b/discord/bot.py
@@ -501,7 +501,11 @@ def _register(
)
def register(
- method: Literal["bulk", "upsert", "delete", "edit"], *args, **kwargs
+ method: Literal["bulk", "upsert", "delete", "edit"],
+ *args,
+ cmd_name: str = None,
+ guild_id: int | None = None,
+ **kwargs,
):
if kwargs.pop("_log", True):
if method == "bulk":
@@ -509,13 +513,12 @@ def register(
f"Bulk updating commands {[c['name'] for c in args[0]]} for"
f" guild {guild_id}"
)
- # TODO: Find where "cmd" is defined
elif method == "upsert":
- _log.debug(f"Creating command {cmd['name']} for guild {guild_id}") # type: ignore
+ _log.debug(f"Creating command {cmd_name} for guild {guild_id}") # type: ignore
elif method == "edit":
- _log.debug(f"Editing command {cmd['name']} for guild {guild_id}") # type: ignore
+ _log.debug(f"Editing command {cmd_name} for guild {guild_id}") # type: ignore
elif method == "delete":
- _log.debug(f"Deleting command {cmd['name']} for guild {guild_id}") # type: ignore
+ _log.debug(f"Deleting command {cmd_name} for guild {guild_id}") # type: ignore
return _register(method, *args, **kwargs)
pending_actions = []
@@ -602,15 +605,31 @@ def register(
registered = []
for cmd in filtered_no_action:
if cmd["action"] == "delete":
- await register("delete", cmd["command"])
+ await register(
+ "delete",
+ cmd["id"],
+ cmd_name=cmd["command"].name,
+ guild_id=guild_id,
+ )
continue
if cmd["action"] == "edit":
registered.append(
- await register("edit", cmd["id"], cmd["command"].to_dict())
+ await register(
+ "edit",
+ cmd["id"],
+ cmd["command"].to_dict(),
+ cmd_name=cmd["command"].name,
+ guild_id=guild_id,
+ )
)
elif cmd["action"] == "upsert":
registered.append(
- await register("upsert", cmd["command"].to_dict())
+ await register(
+ "upsert",
+ cmd["command"].to_dict(),
+ cmd_name=cmd["command"].name,
+ guild_id=guild_id,
+ )
)
else:
raise ValueError(f"Unknown action: {cmd['action']}")
@@ -628,7 +647,7 @@ def register(
)
else:
data = [cmd.to_dict() for cmd in pending]
- registered = await register("bulk", data)
+ registered = await register("bulk", data, guild_id=guild_id)
for i in registered:
cmd = get(
@@ -1177,6 +1196,8 @@ async def on_application_command_error(
This only fires if you do not specify any listeners for command error.
"""
+ if self._event_handlers.get("on_application_command_error", None):
+ return
command = context.command
if command and command.has_error_handler():
return
diff --git a/discord/channel.py b/discord/channel.py
index 88ba717db5..076b2c704f 100644
--- a/discord/channel.py
+++ b/discord/channel.py
@@ -751,7 +751,7 @@ def _repr_attrs(self) -> tuple[str, ...]:
def _update(self, guild: Guild, data: TextChannelPayload) -> None:
super()._update(guild, data)
- async def _get_channel(self) -> "TextChannel":
+ async def _get_channel(self) -> TextChannel:
return self
def is_news(self) -> bool:
@@ -1064,11 +1064,11 @@ async def edit(
available_tags: list[ForumTag] = ...,
require_tag: bool = ...,
overwrites: Mapping[Role | Member | Snowflake, PermissionOverwrite] = ...,
- ) -> "ForumChannel" | None:
+ ) -> ForumChannel | None:
...
@overload
- async def edit(self) -> "ForumChannel" | None:
+ async def edit(self) -> ForumChannel | None:
...
async def edit(self, *, reason=None, **options):
@@ -1274,26 +1274,7 @@ async def create_thread(
if file is not None and files is not None:
raise InvalidArgument("cannot pass both file and files parameter to send()")
- if file is not None:
- if not isinstance(file, File):
- raise InvalidArgument("file parameter must be File")
-
- try:
- data = await state.http.send_files(
- self.id,
- files=[file],
- allowed_mentions=allowed_mentions,
- content=message_content,
- embed=embed,
- embeds=embeds,
- nonce=nonce,
- stickers=stickers,
- components=components,
- )
- finally:
- file.close()
-
- elif files is not None:
+ if files is not None:
if len(files) > 10:
raise InvalidArgument(
"files parameter must be a list of up to 10 elements"
@@ -1301,26 +1282,17 @@ async def create_thread(
elif not all(isinstance(file, File) for file in files):
raise InvalidArgument("files parameter must be a list of File")
- try:
- data = await state.http.send_files(
- self.id,
- files=files,
- content=message_content,
- embed=embed,
- embeds=embeds,
- nonce=nonce,
- allowed_mentions=allowed_mentions,
- stickers=stickers,
- components=components,
- )
- finally:
- for f in files:
- f.close()
- else:
+ if file is not None:
+ if not isinstance(file, File):
+ raise InvalidArgument("file parameter must be File")
+ files = [file]
+
+ try:
data = await state.http.start_forum_thread(
self.id,
content=message_content,
name=name,
+ files=files,
embed=embed,
embeds=embeds,
nonce=nonce,
@@ -1333,6 +1305,11 @@ async def create_thread(
applied_tags=applied_tags,
reason=reason,
)
+ finally:
+ if files is not None:
+ for f in files:
+ f.close()
+
ret = Thread(guild=self.guild, state=self._state, data=data)
msg = ret.get_partial_message(data["last_message_id"])
if view:
@@ -2838,7 +2815,9 @@ def __init__(
self, *, me: ClientUser, state: ConnectionState, data: DMChannelPayload
):
self._state: ConnectionState = state
- self.recipient: User | None = state.store_user(data["recipients"][0])
+ self.recipient: User | None = None
+ if r := data.get("recipients"):
+ self.recipient: state.store_user(r[0])
self.me: ClientUser = me
self.id: int = int(data["id"])
diff --git a/discord/client.py b/discord/client.py
index 27e94d7c2f..5a025aa660 100644
--- a/discord/client.py
+++ b/discord/client.py
@@ -1049,7 +1049,9 @@ def get_all_members(self) -> Generator[Member, None, None]:
yield from guild.members
async def get_or_fetch_user(self, id: int, /) -> User | None:
- """Looks up a user in the user cache or fetches if not found.
+ """|coro|
+
+ Looks up a user in the user cache or fetches if not found.
Parameters
----------
diff --git a/discord/cog.py b/discord/cog.py
index 6f6a573869..686575db6f 100644
--- a/discord/cog.py
+++ b/discord/cog.py
@@ -586,7 +586,10 @@ def _eject(self, bot) -> None:
try:
for command in self.__cog_commands__:
- if isinstance(command, ApplicationCommand):
+ if hasattr(command, "add_to"):
+ bot.bridge_commands.remove(command)
+ continue
+ elif isinstance(command, ApplicationCommand):
bot.remove_application_command(command)
elif command.parent is None:
bot.remove_command(command.name)
diff --git a/discord/colour.py b/discord/colour.py
index c202351859..4472f028bf 100644
--- a/discord/colour.py
+++ b/discord/colour.py
@@ -86,9 +86,6 @@ def _get_byte(self, byte: int) -> int:
def __eq__(self, other: Any) -> bool:
return isinstance(other, Colour) and self.value == other.value
- def __ne__(self, other: Any) -> bool:
- return not self.__eq__(other)
-
def __str__(self) -> str:
return f"#{self.value:0>6x}"
@@ -335,8 +332,8 @@ def nitro_pink(cls: type[CT]) -> CT:
@classmethod
def embed_background(cls: type[CT], theme: str = "dark") -> CT:
- """A factory method that returns a :class:`Color` corresponding to the
- embed colors on discord clients, with a value of:
+ """A factory method that returns a :class:`Colour` corresponding to the
+ embed colours on discord clients, with a value of:
- ``0x2B2D31`` (dark)
- ``0xEEEFF1`` (light)
@@ -347,7 +344,7 @@ def embed_background(cls: type[CT], theme: str = "dark") -> CT:
Parameters
----------
theme: :class:`str`
- The theme color to apply, must be one of "dark", "light", or "amoled".
+ The theme colour to apply, must be one of "dark", "light", or "amoled".
"""
themes_cls = {
"dark": 0x2B2D31,
diff --git a/discord/commands/context.py b/discord/commands/context.py
index f20db34e9f..576a804494 100644
--- a/discord/commands/context.py
+++ b/discord/commands/context.py
@@ -207,7 +207,7 @@ def user(self) -> Member | User:
"""
return self.interaction.user # type: ignore # command user will never be None
- author = user
+ author: Member | User = user
@property
def voice_client(self) -> VoiceProtocol | None:
@@ -266,26 +266,12 @@ def unselected_options(self) -> list[Option] | None:
def send_modal(self) -> Callable[..., Awaitable[Interaction]]:
return self.interaction.response.send_modal
- async def respond(self, *args, **kwargs) -> Interaction | WebhookMessage:
- """|coro|
-
- Sends either a response or a message using the followup webhook determined by whether the interaction
- has been responded to or not.
-
- Returns
- -------
- Union[:class:`discord.Interaction`, :class:`discord.WebhookMessage`]:
- The response, its type depending on whether it's an interaction response or a followup.
- """
- try:
- if not self.interaction.response.is_done():
- return await self.interaction.response.send_message(
- *args, **kwargs
- ) # self.response
- else:
- return await self.followup.send(*args, **kwargs) # self.send_followup
- except discord.errors.InteractionResponded:
- return await self.followup.send(*args, **kwargs)
+ @property
+ @discord.utils.copy_doc(Interaction.respond)
+ def respond(
+ self, *args, **kwargs
+ ) -> Callable[..., Awaitable[Interaction | WebhookMessage]]:
+ return self.interaction.respond
@property
@discord.utils.copy_doc(InteractionResponse.send_message)
diff --git a/discord/commands/core.py b/discord/commands/core.py
index d4f7c0ff96..d53bac0d71 100644
--- a/discord/commands/core.py
+++ b/discord/commands/core.py
@@ -30,6 +30,7 @@
import functools
import inspect
import re
+import sys
import types
from collections import OrderedDict
from enum import Enum
@@ -64,6 +65,11 @@
from .context import ApplicationContext, AutocompleteContext
from .options import Option, OptionChoice
+if sys.version_info >= (3, 11):
+ from typing import Annotated, get_args, get_origin
+else:
+ from typing_extensions import Annotated, get_args, get_origin
+
__all__ = (
"_BaseCommand",
"ApplicationCommand",
@@ -84,6 +90,7 @@
from .. import Permissions
from ..cog import Cog
+ from ..ext.commands.cooldowns import CooldownMapping, MaxConcurrency
T = TypeVar("T")
CogT = TypeVar("CogT", bound="Cog")
@@ -294,18 +301,17 @@ async def prepare(self, ctx: ApplicationContext) -> None:
f"The check functions for the command {self.name} failed"
)
- if hasattr(self, "_max_concurrency"):
- if self._max_concurrency is not None:
- # For this application, context can be duck-typed as a Message
- await self._max_concurrency.acquire(ctx) # type: ignore # ctx instead of non-existent message
+ if self._max_concurrency is not None:
+ # For this application, context can be duck-typed as a Message
+ await self._max_concurrency.acquire(ctx) # type: ignore # ctx instead of non-existent message
- try:
- self._prepare_cooldowns(ctx)
- await self.call_before_hooks(ctx)
- except:
- if self._max_concurrency is not None:
- await self._max_concurrency.release(ctx) # type: ignore # ctx instead of non-existent message
- raise
+ try:
+ self._prepare_cooldowns(ctx)
+ await self.call_before_hooks(ctx)
+ except:
+ if self._max_concurrency is not None:
+ await self._max_concurrency.release(ctx) # type: ignore # ctx instead of non-existent message
+ raise
def is_on_cooldown(self, ctx: ApplicationContext) -> bool:
"""Checks whether the command is currently on cooldown.
@@ -647,10 +653,10 @@ class SlashCommand(ApplicationCommand):
cooldown: Optional[:class:`~discord.ext.commands.Cooldown`]
The cooldown applied when the command is invoked. ``None`` if the command
doesn't have a cooldown.
- name_localizations: Optional[Dict[:class:`str`, :class:`str`]]
+ name_localizations: Dict[:class:`str`, :class:`str`]
The name localizations for this command. The values of this should be ``"locale": "name"``. See
`here `_ for a list of valid locales.
- description_localizations: Optional[Dict[:class:`str`, :class:`str`]]
+ description_localizations: Dict[:class:`str`, :class:`str`]
The description localizations for this command. The values of this should be ``"locale": "description"``.
See `here `_ for a list of valid locales.
"""
@@ -668,8 +674,8 @@ def __init__(self, func: Callable, *args, **kwargs) -> None:
raise TypeError("Callback must be a coroutine.")
self.callback = func
- self.name_localizations: dict[str, str] | None = kwargs.get(
- "name_localizations", None
+ self.name_localizations: dict[str, str] = kwargs.get(
+ "name_localizations", MISSING
)
_validate_names(self)
@@ -680,8 +686,8 @@ def __init__(self, func: Callable, *args, **kwargs) -> None:
)
self.description: str = description
- self.description_localizations: dict[str, str] | None = kwargs.get(
- "description_localizations", None
+ self.description_localizations: dict[str, str] = kwargs.get(
+ "description_localizations", MISSING
)
_validate_descriptions(self)
@@ -732,6 +738,19 @@ def _parse_options(self, params, *, check_params: bool = True) -> list[Option]:
if option == inspect.Parameter.empty:
option = str
+ if self._is_typing_annotated(option):
+ type_hint = get_args(option)[0]
+ metadata = option.__metadata__
+ # If multiple Options in metadata, the first will be used.
+ option_gen = (elem for elem in metadata if isinstance(elem, Option))
+ option = next(option_gen, Option())
+ # Handle Optional
+ if self._is_typing_optional(type_hint):
+ option.input_type = get_args(type_hint)[0]
+ option.default = None
+ else:
+ option.input_type = type_hint
+
if self._is_typing_union(option):
if self._is_typing_optional(option):
option = Option(option.__args__[0], default=None)
@@ -820,6 +839,9 @@ def _is_typing_union(self, annotation):
def _is_typing_optional(self, annotation):
return self._is_typing_union(annotation) and type(None) in annotation.__args__ # type: ignore
+ def _is_typing_annotated(self, annotation):
+ return get_origin(annotation) is Annotated
+
@property
def cog(self):
return getattr(self, "_cog", MISSING)
@@ -843,9 +865,9 @@ def to_dict(self) -> dict:
"description": self.description,
"options": [o.to_dict() for o in self.options],
}
- if self.name_localizations is not None:
+ if self.name_localizations is not MISSING:
as_dict["name_localizations"] = self.name_localizations
- if self.description_localizations is not None:
+ if self.description_localizations is not MISSING:
as_dict["description_localizations"] = self.description_localizations
if self.is_subcommand:
as_dict["type"] = SlashCommandOptionType.sub_command.value
@@ -1081,10 +1103,10 @@ class SlashCommandGroup(ApplicationCommand):
:exc:`.ApplicationCommandError` should be used. Note that if the checks fail then
:exc:`.CheckFailure` exception is raised to the :func:`.on_application_command_error`
event.
- name_localizations: Optional[Dict[:class:`str`, :class:`str`]]
+ name_localizations: Dict[:class:`str`, :class:`str`]
The name localizations for this command. The values of this should be ``"locale": "name"``. See
`here `_ for a list of valid locales.
- description_localizations: Optional[Dict[:class:`str`, :class:`str`]]
+ description_localizations: Dict[:class:`str`, :class:`str`]
The description localizations for this command. The values of this should be ``"locale": "description"``.
See `here `_ for a list of valid locales.
"""
@@ -1119,6 +1141,8 @@ def __init__(
description: str | None = None,
guild_ids: list[int] | None = None,
parent: SlashCommandGroup | None = None,
+ cooldown: CooldownMapping | None = None,
+ max_concurrency: MaxConcurrency | None = None,
**kwargs,
) -> None:
self.name = str(name)
@@ -1146,13 +1170,40 @@ def __init__(
self.guild_only: bool | None = kwargs.get("guild_only", None)
self.nsfw: bool | None = kwargs.get("nsfw", None)
- self.name_localizations: dict[str, str] | None = kwargs.get(
- "name_localizations", None
+ self.name_localizations: dict[str, str] = kwargs.get(
+ "name_localizations", MISSING
)
- self.description_localizations: dict[str, str] | None = kwargs.get(
- "description_localizations", None
+ self.description_localizations: dict[str, str] = kwargs.get(
+ "description_localizations", MISSING
)
+ # similar to ApplicationCommand
+ from ..ext.commands.cooldowns import BucketType, CooldownMapping, MaxConcurrency
+
+ # no need to getattr, since slash cmds groups cant be created using a decorator
+
+ if cooldown is None:
+ buckets = CooldownMapping(cooldown, BucketType.default)
+ elif isinstance(cooldown, CooldownMapping):
+ buckets = cooldown
+ else:
+ raise TypeError(
+ "Cooldown must be a an instance of CooldownMapping or None."
+ )
+
+ self._buckets: CooldownMapping = buckets
+
+ # no need to getattr, since slash cmds groups cant be created using a decorator
+
+ if max_concurrency is not None and not isinstance(
+ max_concurrency, MaxConcurrency
+ ):
+ raise TypeError(
+ "max_concurrency must be an instance of MaxConcurrency or None"
+ )
+
+ self._max_concurrency: MaxConcurrency | None = max_concurrency
+
@property
def module(self) -> str | None:
return self.__module__
@@ -1163,9 +1214,9 @@ def to_dict(self) -> dict:
"description": self.description,
"options": [c.to_dict() for c in self.subcommands],
}
- if self.name_localizations is not None:
+ if self.name_localizations is not MISSING:
as_dict["name_localizations"] = self.name_localizations
- if self.description_localizations is not None:
+ if self.description_localizations is not MISSING:
as_dict["description_localizations"] = self.description_localizations
if self.parent is not None:
@@ -1236,10 +1287,10 @@ def create_subgroup(
:exc:`.ApplicationCommandError` should be used. Note that if the checks fail then
:exc:`.CheckFailure` exception is raised to the :func:`.on_application_command_error`
event.
- name_localizations: Optional[Dict[:class:`str`, :class:`str`]]
+ name_localizations: Dict[:class:`str`, :class:`str`]
The name localizations for this command. The values of this should be ``"locale": "name"``. See
`here `_ for a list of valid locales.
- description_localizations: Optional[Dict[:class:`str`, :class:`str`]]
+ description_localizations: Dict[:class:`str`, :class:`str`]
The description localizations for this command. The values of this should be ``"locale": "description"``.
See `here `_ for a list of valid locales.
@@ -1317,6 +1368,28 @@ async def invoke_autocomplete_callback(self, ctx: AutocompleteContext) -> None:
ctx.interaction.data = option
await command.invoke_autocomplete_callback(ctx)
+ async def call_before_hooks(self, ctx: ApplicationContext) -> None:
+ # only call local hooks
+ cog = self.cog
+ if self._before_invoke is not None:
+ # should be cog if @commands.before_invoke is used
+ instance = getattr(self._before_invoke, "__self__", cog)
+ # __self__ only exists for methods, not functions
+ # however, if @command.before_invoke is used, it will be a function
+ if instance:
+ await self._before_invoke(instance, ctx) # type: ignore
+ else:
+ await self._before_invoke(ctx) # type: ignore
+
+ async def call_after_hooks(self, ctx: ApplicationContext) -> None:
+ cog = self.cog
+ if self._after_invoke is not None:
+ instance = getattr(self._after_invoke, "__self__", cog)
+ if instance:
+ await self._after_invoke(instance, ctx) # type: ignore
+ else:
+ await self._after_invoke(ctx) # type: ignore
+
def walk_commands(self) -> Generator[SlashCommand | SlashCommandGroup, None, None]:
"""An iterator that recursively walks through all slash commands and groups in this group.
@@ -1413,7 +1486,7 @@ class ContextMenuCommand(ApplicationCommand):
cooldown: Optional[:class:`~discord.ext.commands.Cooldown`]
The cooldown applied when the command is invoked. ``None`` if the command
doesn't have a cooldown.
- name_localizations: Optional[Dict[:class:`str`, :class:`str`]]
+ name_localizations: Dict[:class:`str`, :class:`str`]
The name localizations for this command. The values of this should be ``"locale": "name"``. See
`here `_ for a list of valid locales.
"""
@@ -1430,8 +1503,8 @@ def __init__(self, func: Callable, *args, **kwargs) -> None:
raise TypeError("Callback must be a coroutine.")
self.callback = func
- self.name_localizations: dict[str, str] | None = kwargs.get(
- "name_localizations", None
+ self.name_localizations: dict[str, str] = kwargs.get(
+ "name_localizations", MISSING
)
# Discord API doesn't support setting descriptions for context menu commands, so it must be empty
@@ -1506,7 +1579,7 @@ def to_dict(self) -> dict[str, str | int]:
"default_member_permissions"
] = self.default_member_permissions.value
- if self.name_localizations is not None:
+ if self.name_localizations:
as_dict["name_localizations"] = self.name_localizations
return as_dict
diff --git a/discord/commands/options.py b/discord/commands/options.py
index d6c04faf72..73cda77dc9 100644
--- a/discord/commands/options.py
+++ b/discord/commands/options.py
@@ -41,6 +41,7 @@
from ..enums import ChannelType
from ..enums import Enum as DiscordEnum
from ..enums import SlashCommandOptionType
+from ..utils import MISSING
if TYPE_CHECKING:
from ..ext.commands import Converter
@@ -141,9 +142,9 @@ class Option:
The maximum length of the string that can be entered. Must be between 1 and 6000 (inclusive).
Only applies to Options with an :attr:`input_type` of :class:`str`.
autocomplete: Optional[:class:`Any`]
- The autocomplete handler for the option. Accepts an iterable of :class:`str`, a callable (sync or async)
+ The autocomplete handler for the option. Accepts an iterable of :class:`str` or :class:`OptionChoice`, a callable (sync or async)
that takes a single argument of :class:`AutocompleteContext`, or a coroutine.
- Must resolve to an iterable of :class:`str`.
+ Must resolve to an iterable of :class:`str` or :class:`OptionChoice`.
.. note::
@@ -152,10 +153,10 @@ class Option:
A list of channel types that can be selected in this option.
Only applies to Options with an :attr:`input_type` of :class:`discord.SlashCommandOptionType.channel`.
If this argument is used, :attr:`input_type` will be ignored.
- name_localizations: Optional[Dict[:class:`str`, :class:`str`]]
+ name_localizations: Dict[:class:`str`, :class:`str`]
The name localizations for this option. The values of this should be ``"locale": "name"``.
See `here `_ for a list of valid locales.
- description_localizations: Optional[Dict[:class:`str`, :class:`str`]]
+ description_localizations: Dict[:class:`str`, :class:`str`]
The description localizations for this option. The values of this should be ``"locale": "description"``.
See `here `_ for a list of valid locales.
@@ -323,8 +324,10 @@ def __init__(
self.autocomplete = kwargs.pop("autocomplete", None)
- self.name_localizations = kwargs.pop("name_localizations", None)
- self.description_localizations = kwargs.pop("description_localizations", None)
+ self.name_localizations = kwargs.pop("name_localizations", MISSING)
+ self.description_localizations = kwargs.pop(
+ "description_localizations", MISSING
+ )
def to_dict(self) -> dict:
as_dict = {
@@ -335,9 +338,9 @@ def to_dict(self) -> dict:
"choices": [c.to_dict() for c in self.choices],
"autocomplete": bool(self.autocomplete),
}
- if self.name_localizations is not None:
+ if self.name_localizations is not MISSING:
as_dict["name_localizations"] = self.name_localizations
- if self.description_localizations is not None:
+ if self.description_localizations is not MISSING:
as_dict["description_localizations"] = self.description_localizations
if self.channel_types:
as_dict["channel_types"] = [t.value for t in self.channel_types]
@@ -368,7 +371,7 @@ class OptionChoice:
The name of the choice. Shown in the UI when selecting an option.
value: Optional[Union[:class:`str`, :class:`int`, :class:`float`]]
The value of the choice. If not provided, will use the value of ``name``.
- name_localizations: Optional[Dict[:class:`str`, :class:`str`]]
+ name_localizations: Dict[:class:`str`, :class:`str`]
The name localizations for this choice. The values of this should be ``"locale": "name"``.
See `here `_ for a list of valid locales.
"""
@@ -377,7 +380,7 @@ def __init__(
self,
name: str,
value: str | int | float | None = None,
- name_localizations: dict[str, str] | None = None,
+ name_localizations: dict[str, str] = MISSING,
):
self.name = str(name)
self.value = value if value is not None else name
@@ -385,16 +388,22 @@ def __init__(
def to_dict(self) -> dict[str, str | int | float]:
as_dict = {"name": self.name, "value": self.value}
- if self.name_localizations is not None:
+ if self.name_localizations is not MISSING:
as_dict["name_localizations"] = self.name_localizations
return as_dict
def option(name, type=None, **kwargs):
- """A decorator that can be used instead of typehinting :class:`Option`.
+ """A decorator that can be used instead of typehinting :class:`.Option`.
.. versionadded:: 2.0
+
+ Attributes
+ ----------
+ parameter_name: :class:`str`
+ The name of the target parameter this option is mapped to.
+ This allows you to have a separate UI ``name`` and parameter name.
"""
def decorator(func):
diff --git a/discord/embeds.py b/discord/embeds.py
index 1d6a7d11f2..81424050e2 100644
--- a/discord/embeds.py
+++ b/discord/embeds.py
@@ -26,7 +26,7 @@
from __future__ import annotations
import datetime
-from typing import TYPE_CHECKING, Any, Final, Mapping, Protocol, TypeVar, Union
+from typing import TYPE_CHECKING, Any, Mapping, TypeVar
from . import utils
from .colour import Colour
@@ -36,76 +36,19 @@
"EmbedField",
"EmbedAuthor",
"EmbedFooter",
+ "EmbedMedia",
+ "EmbedProvider",
)
-class _EmptyEmbed:
- def __bool__(self) -> bool:
- return False
-
- def __repr__(self) -> str:
- return "Embed.Empty"
-
- def __len__(self) -> int:
- return 0
-
-
-EmptyEmbed: Final = _EmptyEmbed()
-
-
-class EmbedProxy:
- def __init__(self, layer: dict[str, Any]):
- self.__dict__.update(layer)
-
- def __len__(self) -> int:
- return len(self.__dict__)
-
- def __repr__(self) -> str:
- inner = ", ".join(
- (f"{k}={v!r}" for k, v in self.__dict__.items() if not k.startswith("_"))
- )
- return f"{type(self).__name__}({inner})"
-
- def __getattr__(self, attr: str) -> _EmptyEmbed:
- return EmptyEmbed
-
-
E = TypeVar("E", bound="Embed")
if TYPE_CHECKING:
from discord.types.embed import Embed as EmbedData
from discord.types.embed import EmbedType
- T = TypeVar("T")
- MaybeEmpty = Union[T, _EmptyEmbed]
-
- class _EmbedFooterProxy(Protocol):
- text: MaybeEmpty[str]
- icon_url: MaybeEmpty[str]
-
- class _EmbedMediaProxy(Protocol):
- url: MaybeEmpty[str]
- proxy_url: MaybeEmpty[str]
- height: MaybeEmpty[int]
- width: MaybeEmpty[int]
-
- class _EmbedVideoProxy(Protocol):
- url: MaybeEmpty[str]
- height: MaybeEmpty[int]
- width: MaybeEmpty[int]
-
- class _EmbedProviderProxy(Protocol):
- name: MaybeEmpty[str]
- url: MaybeEmpty[str]
- class _EmbedAuthorProxy(Protocol):
- name: MaybeEmpty[str]
- url: MaybeEmpty[str]
- icon_url: MaybeEmpty[str]
- proxy_icon_url: MaybeEmpty[str]
-
-
-class EmbedAuthor(EmbedProxy):
+class EmbedAuthor:
"""Represents the author on the :class:`Embed` object.
.. versionadded:: 2.5
@@ -118,25 +61,50 @@ class EmbedAuthor(EmbedProxy):
The URL of the hyperlink created in the author's name.
icon_url: :class:`str`
The URL of the author icon image.
+ proxy_icon_url: :class:`str`
+ The proxied URL of the author icon image. This can't be set directly, it is set by Discord.
"""
def __init__(
self,
name: str,
- url: MaybeEmpty[str] = EmptyEmbed,
- icon_url: MaybeEmpty[str] = EmptyEmbed,
- proxy_icon_url: MaybeEmpty[str] = EmptyEmbed,
+ url: str | None = None,
+ icon_url: str | None = None,
) -> None:
- layer = {
- k: v
- for k, v in locals().items()
- if k in {"name", "url", "icon_url", "proxy_icon_url"}
- and v is not EmptyEmbed
- }
- super().__init__(layer)
+ self.name: str = name
+ self.url: str | None = url
+ self.icon_url: str | None = icon_url
+ self.proxy_icon_url: str | None = None
+ @classmethod
+ def from_dict(cls, data: dict[str, str | None]) -> EmbedAuthor:
+ self = cls.__new__(cls)
+ name = data.get("name")
+ if not name:
+ raise ValueError("name field is required")
+ self.name = name
+ self.url = data.get("url")
+ self.icon_url = data.get("icon_url")
+ self.proxy_icon_url = data.get("proxy_icon_url")
+ return self
-class EmbedFooter(EmbedProxy):
+ def to_dict(self) -> dict[str, str]:
+ d = {"name": str(self.name)}
+ if self.url:
+ d["url"] = str(self.url)
+ if self.icon_url:
+ d["icon_url"] = str(self.icon_url)
+ return d
+
+ def __len__(self) -> int:
+ """Returns the total number of characters in the author name."""
+ return len(self.name)
+
+ def __repr__(self) -> str:
+ return f""
+
+
+class EmbedFooter:
"""Represents the footer on the :class:`Embed` object.
.. versionadded:: 2.5
@@ -147,20 +115,113 @@ class EmbedFooter(EmbedProxy):
The text inside the footer.
icon_url: :class:`str`
The URL of the footer icon image.
+ proxy_icon_url: :class:`str`
+ The proxied URL of the footer icon image. This can't be set directly, it is set by Discord.
"""
def __init__(
self,
text: str,
- icon_url: MaybeEmpty[str] = EmptyEmbed,
- proxy_icon_url: MaybeEmpty[str] = EmptyEmbed,
+ icon_url: str | None = None,
) -> None:
- layer = {
- k: v
- for k, v in locals().items()
- if k in {"text", "icon_url", "proxy_icon_url"} and v is not EmptyEmbed
- }
- super().__init__(layer)
+ self.text: str = text
+ self.icon_url: str | None = icon_url
+ self.proxy_icon_url: str | None = None
+
+ @classmethod
+ def from_dict(cls, data: dict[str, str | None]) -> EmbedFooter:
+ self = cls.__new__(cls)
+ text = data.get("text")
+ if not text:
+ raise ValueError("text field is required")
+ self.text = text
+ self.icon_url = data.get("icon_url")
+ self.proxy_icon_url = data.get("proxy_icon_url")
+ return self
+
+ def to_dict(self) -> dict[str, Any]:
+ d = {"text": str(self.text)}
+ if self.icon_url:
+ d["icon_url"] = str(self.icon_url)
+ return d
+
+ def __len__(self) -> int:
+ """Returns the total number of characters in the footer text."""
+ return len(self.text)
+
+ def __repr__(self) -> str:
+ return f""
+
+
+class EmbedMedia: # Thumbnail, Image, Video
+ """Represents a media on the :class:`Embed` object.
+ This includes thumbnails, images, and videos.
+
+ .. versionadded:: 2.5
+
+ Attributes
+ ----------
+ url: :class:`str`
+ The source URL of the media.
+ proxy_url: :class:`str`
+ The proxied URL of the media.
+ height: :class:`int`
+ The height of the media.
+ width: :class:`int`
+ The width of the media.
+ """
+
+ url: str
+ proxy_url: str | None
+ height: int | None
+ width: int | None
+
+ def __init__(self, url: str):
+ self.url = url
+ self.proxy_url = None
+ self.height = None
+ self.width = None
+
+ @classmethod
+ def from_dict(cls, data: dict[str, str | int]) -> EmbedMedia:
+ self = cls.__new__(cls)
+ self.url = str(data.get("url"))
+ self.proxy_url = (
+ str(proxy_url) if (proxy_url := data.get("proxy_url")) else None
+ )
+ self.height = int(height) if (height := data.get("height")) else None
+ self.width = int(width) if (width := data.get("width")) else None
+ return self
+
+ def __repr__(self) -> str:
+ return f" height={self.height!r} width={self.width!r}>"
+
+
+class EmbedProvider:
+ """Represents a provider on the :class:`Embed` object.
+
+ .. versionadded:: 2.5
+
+ Attributes
+ ----------
+ name: :class:`str`
+ The name of the provider.
+ url: :class:`str`
+ The URL of the provider.
+ """
+
+ name: str | None
+ url: str | None
+
+ @classmethod
+ def from_dict(cls, data: dict[str, str | None]) -> EmbedProvider:
+ self = cls.__new__(cls)
+ self.name = data.get("name")
+ self.url = data.get("url")
+ return self
+
+ def __repr__(self) -> str:
+ return f""
class EmbedField:
@@ -184,7 +245,7 @@ def __init__(self, name: str, value: str, inline: bool | None = False):
self.inline = inline
@classmethod
- def from_dict(cls: type[E], data: Mapping[str, Any]) -> E:
+ def from_dict(cls, data: dict[str, str | bool]) -> EmbedField:
"""Converts a :class:`dict` to a :class:`EmbedField` provided it is in the
format that Discord expects it to be in.
@@ -199,7 +260,7 @@ def from_dict(cls: type[E], data: Mapping[str, Any]) -> E:
data: :class:`dict`
The dictionary to convert into an EmbedField object.
"""
- self: E = cls.__new__(cls)
+ self = cls.__new__(cls)
self.name = data["name"]
self.value = data["value"]
@@ -207,7 +268,7 @@ def from_dict(cls: type[E], data: Mapping[str, Any]) -> E:
return self
- def to_dict(self) -> dict[str, str | bool]:
+ def to_dict(self) -> dict[str, str | bool | None]:
"""Converts this EmbedField object into a dict.
Returns
@@ -221,6 +282,9 @@ def to_dict(self) -> dict[str, str | bool]:
"inline": self.inline,
}
+ def __repr__(self) -> str:
+ return f""
+
class Embed:
"""Represents a Discord embed.
@@ -238,12 +302,6 @@ class Embed:
.. versionadded:: 2.0
- Certain properties return an ``EmbedProxy``, a type
- that acts similar to a regular :class:`dict` except using dotted access,
- e.g. ``embed.author.icon_url``. If the attribute
- is invalid or empty, then a special sentinel value is returned,
- :attr:`Embed.Empty`.
-
For ease of use, all parameters that expect a :class:`str` are implicitly
cast to :class:`str` for you.
@@ -272,9 +330,6 @@ class Embed:
colour: Union[:class:`Colour`, :class:`int`]
The colour code of the embed. Aliased to ``color`` as well.
This can be set during initialisation.
- Empty
- A special sentinel value used by ``EmbedProxy`` and this class
- to denote that the value or attribute is empty.
"""
__slots__ = (
@@ -293,54 +348,46 @@ class Embed:
"description",
)
- Empty: Final = EmptyEmbed
-
def __init__(
self,
*,
- colour: int | Colour | _EmptyEmbed = EmptyEmbed,
- color: int | Colour | _EmptyEmbed = EmptyEmbed,
- title: MaybeEmpty[Any] = EmptyEmbed,
+ colour: int | Colour | None = None,
+ color: int | Colour | None = None,
+ title: Any | None = None,
type: EmbedType = "rich",
- url: MaybeEmpty[Any] = EmptyEmbed,
- description: MaybeEmpty[Any] = EmptyEmbed,
- timestamp: datetime.datetime = None,
+ url: Any | None = None,
+ description: Any | None = None,
+ timestamp: datetime.datetime | None = None,
fields: list[EmbedField] | None = None,
- author: MaybeEmpty[EmbedAuthor] = EmptyEmbed,
- footer: MaybeEmpty[EmbedFooter] = EmptyEmbed,
- image: MaybeEmpty[str] = EmptyEmbed,
- thumbnail: MaybeEmpty[str] = EmptyEmbed,
+ author: EmbedAuthor | None = None,
+ footer: EmbedFooter | None = None,
+ image: str | EmbedMedia | None = None,
+ thumbnail: str | EmbedMedia | None = None,
):
- self.colour = colour if colour is not EmptyEmbed else color
+ self.colour = colour if colour else color
self.title = title
self.type = type
self.url = url
self.description = description
- if self.title is not EmptyEmbed and self.title is not None:
+ if self.title:
self.title = str(self.title)
- if self.description is not EmptyEmbed and self.description is not None:
+ if self.description:
self.description = str(self.description)
- if self.url is not EmptyEmbed and self.url is not None:
+ if self.url:
self.url = str(self.url)
if timestamp:
self.timestamp = timestamp
- self._fields: list[EmbedField] = fields or []
- if author is not EmptyEmbed:
- self.set_author(**author.__dict__)
+ self._fields: list[EmbedField] = fields if fields is not None else []
- if footer is not EmptyEmbed:
- self.set_footer(**footer.__dict__)
-
- if image is not EmptyEmbed:
- self.set_image(url=image)
-
- if thumbnail is not EmptyEmbed:
- self.set_thumbnail(url=thumbnail)
+ self.author = author
+ self.footer = footer
+ self.image = image
+ self.thumbnail = thumbnail
@classmethod
def from_dict(cls: type[E], data: Mapping[str, Any]) -> E:
@@ -368,18 +415,18 @@ def from_dict(cls: type[E], data: Mapping[str, Any]) -> E:
# fill in the basic fields
- self.title = data.get("title", EmptyEmbed)
- self.type = data.get("type", EmptyEmbed)
- self.description = data.get("description", EmptyEmbed)
- self.url = data.get("url", EmptyEmbed)
+ self.title = data.get("title", None)
+ self.type = data.get("type", None)
+ self.description = data.get("description", None)
+ self.url = data.get("url", None)
- if self.title is not EmptyEmbed:
+ if self.title:
self.title = str(self.title)
- if self.description is not EmptyEmbed:
+ if self.description:
self.description = str(self.description)
- if self.url is not EmptyEmbed:
+ if self.url:
self.url = str(self.url)
# try to fill in the more rich fields
@@ -427,7 +474,11 @@ def copy(self: E) -> E:
return self.__class__.from_dict(self.to_dict())
def __len__(self) -> int:
- total = len(self.title) + len(self.description)
+ total = 0
+ if self.title:
+ total += len(self.title)
+ if self.description:
+ total += len(self.description)
for field in getattr(self, "_fields", []):
total += len(field.name) + len(field.value)
@@ -466,56 +517,71 @@ def __bool__(self) -> bool:
)
@property
- def colour(self) -> MaybeEmpty[Colour]:
- return getattr(self, "_colour", EmptyEmbed)
+ def colour(self) -> Colour | None:
+ return getattr(self, "_colour", None)
@colour.setter
- def colour(self, value: int | Colour | _EmptyEmbed): # type: ignore
- if isinstance(value, (Colour, _EmptyEmbed)):
+ def colour(self, value: int | Colour | None): # type: ignore
+ if value is None or isinstance(value, Colour):
self._colour = value
elif isinstance(value, int):
self._colour = Colour(value=value)
else:
raise TypeError(
- "Expected discord.Colour, int, or Embed.Empty but received"
+ "Expected discord.Colour, int, or None but received"
f" {value.__class__.__name__} instead."
)
color = colour
@property
- def timestamp(self) -> MaybeEmpty[datetime.datetime]:
- return getattr(self, "_timestamp", EmptyEmbed)
+ def timestamp(self) -> datetime.datetime | None:
+ return getattr(self, "_timestamp", None)
@timestamp.setter
- def timestamp(self, value: MaybeEmpty[datetime.datetime]):
+ def timestamp(self, value: datetime.datetime | None):
if isinstance(value, datetime.datetime):
if value.tzinfo is None:
value = value.astimezone()
self._timestamp = value
- elif isinstance(value, _EmptyEmbed):
+ elif value is None:
self._timestamp = value
else:
raise TypeError(
- "Expected datetime.datetime or Embed.Empty received"
+ "Expected datetime.datetime or None. Received"
f" {value.__class__.__name__} instead"
)
@property
- def footer(self) -> EmbedFooter:
- """Returns an ``EmbedProxy`` denoting the footer contents.
+ def footer(self) -> EmbedFooter | None:
+ """Returns an :class:`EmbedFooter` denoting the footer contents.
See :meth:`set_footer` for possible values you can access.
- If the attribute has no value then :attr:`Empty` is returned.
+ If the footer is not set then `None` is returned.
"""
- return EmbedFooter(**getattr(self, "_footer", {}))
+ foot = getattr(self, "_footer", None)
+ if not foot:
+ return None
+ return EmbedFooter.from_dict(foot)
+
+ @footer.setter
+ def footer(self, value: EmbedFooter | None):
+ if value is None:
+ self.remove_footer()
+ elif isinstance(value, EmbedFooter):
+ self._footer = value.to_dict()
+ else:
+ raise TypeError(
+ "Expected EmbedFooter or None. Received"
+ f" {value.__class__.__name__} instead"
+ )
def set_footer(
self: E,
*,
- text: MaybeEmpty[Any] = EmptyEmbed,
- icon_url: MaybeEmpty[Any] = EmptyEmbed,
+ text: Any | None = None,
+ icon_url: Any | None = None,
) -> E:
"""Sets the footer for the embed content.
@@ -532,10 +598,10 @@ def set_footer(
"""
self._footer = {}
- if text is not EmptyEmbed:
+ if text:
self._footer["text"] = str(text)
- if icon_url is not EmptyEmbed:
+ if icon_url:
self._footer["icon_url"] = str(icon_url)
return self
@@ -556,28 +622,45 @@ def remove_footer(self: E) -> E:
return self
@property
- def image(self) -> _EmbedMediaProxy:
- """Returns an ``EmbedProxy`` denoting the image contents.
+ def image(self) -> EmbedMedia | None:
+ """Returns an :class:`EmbedMedia` denoting the image contents.
- Possible attributes you can access are:
+ Attributes you can access are:
- ``url``
- ``proxy_url``
- ``width``
- ``height``
- If the attribute has no value then :attr:`Empty` is returned.
+ If the image is not set then `None` is returned.
"""
- return EmbedProxy(getattr(self, "_image", {})) # type: ignore
+ img = getattr(self, "_image", None)
+ if not img:
+ return None
+ return EmbedMedia.from_dict(img)
+
+ @image.setter
+ def image(self, value: str | EmbedMedia | None):
+ if value is None:
+ self.remove_image()
+ elif isinstance(value, str):
+ self.set_image(url=value)
+ elif isinstance(value, EmbedMedia):
+ self.set_image(url=value.url)
+ else:
+ raise TypeError(
+ "Expected discord.EmbedMedia, or None but received"
+ f" {value.__class__.__name__} instead."
+ )
- def set_image(self: E, *, url: MaybeEmpty[Any]) -> E:
+ def set_image(self: E, *, url: Any | None) -> E:
"""Sets the image for the embed content.
This function returns the class instance to allow for fluent-style
chaining.
.. versionchanged:: 1.4
- Passing :attr:`Empty` removes the image.
+ Passing `None` removes the image.
Parameters
----------
@@ -585,7 +668,7 @@ def set_image(self: E, *, url: MaybeEmpty[Any]) -> E:
The source URL for the image. Only HTTP(S) is supported.
"""
- if url is EmptyEmbed:
+ if url is None:
try:
del self._image
except AttributeError:
@@ -613,28 +696,45 @@ def remove_image(self: E) -> E:
return self
@property
- def thumbnail(self) -> _EmbedMediaProxy:
- """Returns an ``EmbedProxy`` denoting the thumbnail contents.
+ def thumbnail(self) -> EmbedMedia | None:
+ """Returns an :class:`EmbedMedia` denoting the thumbnail contents.
- Possible attributes you can access are:
+ Attributes you can access are:
- ``url``
- ``proxy_url``
- ``width``
- ``height``
- If the attribute has no value then :attr:`Empty` is returned.
+ If the thumbnail is not set then `None` is returned.
"""
- return EmbedProxy(getattr(self, "_thumbnail", {})) # type: ignore
+ thumb = getattr(self, "_thumbnail", None)
+ if not thumb:
+ return None
+ return EmbedMedia.from_dict(thumb)
+
+ @thumbnail.setter
+ def thumbnail(self, value: str | EmbedMedia | None):
+ if value is None:
+ self.remove_thumbnail()
+ elif isinstance(value, str):
+ self.set_thumbnail(url=value)
+ elif isinstance(value, EmbedMedia):
+ self.set_thumbnail(url=value.url)
+ else:
+ raise TypeError(
+ "Expected discord.EmbedMedia, or None but received"
+ f" {value.__class__.__name__} instead."
+ )
- def set_thumbnail(self: E, *, url: MaybeEmpty[Any]) -> E:
+ def set_thumbnail(self: E, *, url: Any | None) -> E:
"""Sets the thumbnail for the embed content.
This function returns the class instance to allow for fluent-style
chaining.
.. versionchanged:: 1.4
- Passing :attr:`Empty` removes the thumbnail.
+ Passing `None` removes the thumbnail.
Parameters
----------
@@ -642,7 +742,7 @@ def set_thumbnail(self: E, *, url: MaybeEmpty[Any]) -> E:
The source URL for the thumbnail. Only HTTP(S) is supported.
"""
- if url is EmptyEmbed:
+ if url is None:
try:
del self._thumbnail
except AttributeError:
@@ -670,45 +770,66 @@ def remove_thumbnail(self: E) -> E:
return self
@property
- def video(self) -> _EmbedVideoProxy:
- """Returns an ``EmbedProxy`` denoting the video contents.
+ def video(self) -> EmbedMedia | None:
+ """Returns an :class:`EmbedMedia` denoting the video contents.
- Possible attributes include:
+ Attributes include:
- ``url`` for the video URL.
- ``height`` for the video height.
- ``width`` for the video width.
- If the attribute has no value then :attr:`Empty` is returned.
+ If the video is not set then `None` is returned.
"""
- return EmbedProxy(getattr(self, "_video", {})) # type: ignore
+ vid = getattr(self, "_video", None)
+ if not vid:
+ return None
+ return EmbedMedia.from_dict(vid)
@property
- def provider(self) -> _EmbedProviderProxy:
- """Returns an ``EmbedProxy`` denoting the provider contents.
+ def provider(self) -> EmbedProvider | None:
+ """Returns an :class:`EmbedProvider` denoting the provider contents.
The only attributes that might be accessed are ``name`` and ``url``.
- If the attribute has no value then :attr:`Empty` is returned.
+ If the provider is not set then `None` is returned.
"""
- return EmbedProxy(getattr(self, "_provider", {})) # type: ignore
+ prov = getattr(self, "_provider", None)
+ if not prov:
+ return None
+ return EmbedProvider.from_dict(prov)
@property
- def author(self) -> EmbedAuthor:
- """Returns an ``EmbedProxy`` denoting the author contents.
+ def author(self) -> EmbedAuthor | None:
+ """Returns an :class:`EmbedAuthor` denoting the author contents.
See :meth:`set_author` for possible values you can access.
- If the attribute has no value then :attr:`Empty` is returned.
+ If the author is not set then `None` is returned.
"""
- return EmbedAuthor(**getattr(self, "_author", {})) # type: ignore
+ auth = getattr(self, "_author", None)
+ if not auth:
+ return None
+ return EmbedAuthor.from_dict(auth)
+
+ @author.setter
+ def author(self, value: EmbedAuthor | None):
+ if value is None:
+ self.remove_author()
+ elif isinstance(value, EmbedAuthor):
+ self._author = value.to_dict()
+ else:
+ raise TypeError(
+ "Expected discord.EmbedAuthor, or None but received"
+ f" {value.__class__.__name__} instead."
+ )
def set_author(
self: E,
*,
name: Any,
- url: MaybeEmpty[Any] = EmptyEmbed,
- icon_url: MaybeEmpty[Any] = EmptyEmbed,
+ url: Any | None = None,
+ icon_url: Any | None = None,
) -> E:
"""Sets the author for the embed content.
@@ -730,10 +851,10 @@ def set_author(
"name": str(name),
}
- if url is not EmptyEmbed:
+ if url:
self._author["url"] = str(url)
- if icon_url is not EmptyEmbed:
+ if icon_url:
self._author["icon_url"] = str(icon_url)
return self
diff --git a/discord/emoji.py b/discord/emoji.py
index 002aa1586f..cbb51eee57 100644
--- a/discord/emoji.py
+++ b/discord/emoji.py
@@ -147,9 +147,6 @@ def __repr__(self) -> str:
def __eq__(self, other: Any) -> bool:
return isinstance(other, _EmojiTag) and self.id == other.id
- def __ne__(self, other: Any) -> bool:
- return not self.__eq__(other)
-
def __hash__(self) -> int:
return self.id >> 22
diff --git a/discord/enums.py b/discord/enums.py
index 75af244382..f4b44623b0 100644
--- a/discord/enums.py
+++ b/discord/enums.py
@@ -37,7 +37,6 @@
"VerificationLevel",
"ContentFilter",
"Status",
- "DefaultAvatar",
"AuditLogAction",
"AuditLogActionCategory",
"UserFlags",
@@ -354,20 +353,6 @@ def __str__(self):
return self.value
-class DefaultAvatar(Enum):
- """Default avatar"""
-
- blurple = 0
- grey = 1
- gray = 1
- green = 2
- orange = 3
- red = 4
-
- def __str__(self):
- return self.name
-
-
class NotificationLevel(Enum, comparable=True):
"""Notification level"""
@@ -438,6 +423,10 @@ class AuditLogAction(Enum):
auto_moderation_rule_update = 141
auto_moderation_rule_delete = 142
auto_moderation_block_message = 143
+ auto_moderation_flag_to_channel = 144
+ auto_moderation_user_communication_disabled = 145
+ creator_monetization_request_created = 150
+ creator_monetization_terms_accepted = 151
@property
def category(self) -> AuditLogActionCategory | None:
@@ -496,6 +485,10 @@ def category(self) -> AuditLogActionCategory | None:
AuditLogAction.auto_moderation_rule_update: AuditLogActionCategory.update,
AuditLogAction.auto_moderation_rule_delete: AuditLogActionCategory.delete,
AuditLogAction.auto_moderation_block_message: None,
+ AuditLogAction.auto_moderation_flag_to_channel: None,
+ AuditLogAction.auto_moderation_user_communication_disabled: None,
+ AuditLogAction.creator_monetization_request_created: None,
+ AuditLogAction.creator_monetization_terms_accepted: None,
}
return lookup[self]
@@ -534,7 +527,7 @@ def target_type(self) -> str | None:
return "thread"
elif v < 122:
return "application_command_permission"
- elif v < 144:
+ elif v < 146:
return "auto_moderation_rule"
@@ -848,6 +841,8 @@ class EmbeddedActivity(Enum):
doodle_crew = 878067389634314250
doodle_crew_dev = 878067427668275241
fishington = 814288819477020702
+ gartic_phone = 1007373802981822582
+ jamspace = 1070087967294631976
know_what_i_meme = 950505761862189096
land = 903769130790969345
letter_league = 879863686565621790
diff --git a/discord/ext/bridge/bot.py b/discord/ext/bridge/bot.py
index 1d9a36f37d..e44927e735 100644
--- a/discord/ext/bridge/bot.py
+++ b/discord/ext/bridge/bot.py
@@ -25,6 +25,7 @@
from __future__ import annotations
from abc import ABC
+from collections.abc import Iterator
from discord.commands import ApplicationContext
from discord.errors import CheckFailure, DiscordException
@@ -60,6 +61,21 @@ def bridge_commands(self) -> list[BridgeCommand | BridgeCommandGroup]:
return cmds
+ def walk_bridge_commands(
+ self,
+ ) -> Iterator[BridgeCommand | BridgeCommandGroup]:
+ """An iterator that recursively walks through all the bot's bridge commands.
+
+ Yields
+ ------
+ Union[:class:`.BridgeCommand`, :class:`.BridgeCommandGroup`]
+ A bridge command or bridge group of the bot.
+ """
+ for cmd in self._bridge_commands:
+ yield cmd
+ if isinstance(cmd, BridgeCommandGroup):
+ yield from cmd.walk_commands()
+
async def get_application_context(
self, interaction: Interaction, cls=None
) -> BridgeApplicationContext:
diff --git a/discord/ext/bridge/context.py b/discord/ext/bridge/context.py
index 3ba5989886..033fdd6caf 100644
--- a/discord/ext/bridge/context.py
+++ b/discord/ext/bridge/context.py
@@ -25,7 +25,7 @@
from __future__ import annotations
from abc import ABC, abstractmethod
-from typing import TYPE_CHECKING, Any, overload
+from typing import TYPE_CHECKING, Any, Union, overload
from discord.commands import ApplicationContext
from discord.interactions import Interaction, InteractionMessage
@@ -38,7 +38,7 @@
from .core import BridgeExtCommand, BridgeSlashCommand
-__all__ = ("BridgeContext", "BridgeExtContext", "BridgeApplicationContext")
+__all__ = ("BridgeContext", "BridgeExtContext", "BridgeApplicationContext", "Context")
class BridgeContext(ABC):
@@ -195,3 +195,10 @@ async def delete(
"""
if self._original_response_message:
await self._original_response_message.delete(delay=delay, reason=reason)
+
+
+Context = Union[BridgeExtContext, BridgeApplicationContext]
+"""
+A Union class for either :class:`BridgeExtContext` or :class:`BridgeApplicationContext`.
+Can be used as a type hint for Context for bridge commands.
+"""
diff --git a/discord/ext/bridge/core.py b/discord/ext/bridge/core.py
index 4f53389d05..427bd0269f 100644
--- a/discord/ext/bridge/core.py
+++ b/discord/ext/bridge/core.py
@@ -25,6 +25,7 @@
from __future__ import annotations
import inspect
+from collections.abc import Iterator
from typing import TYPE_CHECKING, Any, Callable
import discord.commands.options
@@ -38,7 +39,7 @@
SlashCommandOptionType,
)
-from ...utils import filter_params, find, get
+from ...utils import MISSING, find, get
from ..commands import BadArgument
from ..commands import Bot as ExtBot
from ..commands import (
@@ -156,6 +157,8 @@ class BridgeCommand:
The prefix-based version of this bridge command.
"""
+ __special_attrs__ = ["slash_variant", "ext_variant", "parent"]
+
def __init__(self, callback, **kwargs):
self.parent = kwargs.pop("parent", None)
self.slash_variant: BridgeSlashCommand = kwargs.pop(
@@ -166,13 +169,10 @@ def __init__(self, callback, **kwargs):
) or BridgeExtCommand(callback, **kwargs)
@property
- def name_localizations(self) -> dict[str, str]:
+ def name_localizations(self) -> dict[str, str] | None:
"""Returns name_localizations from :attr:`slash_variant`
-
You can edit/set name_localizations directly with
-
.. code-block:: python3
-
bridge_command.name_localizations["en-UK"] = ... # or any other locale
# or
bridge_command.name_localizations = {"en-UK": ..., "fr-FR": ...}
@@ -184,13 +184,10 @@ def name_localizations(self, value):
self.slash_variant.name_localizations = value
@property
- def description_localizations(self) -> dict[str, str]:
+ def description_localizations(self) -> dict[str, str] | None:
"""Returns description_localizations from :attr:`slash_variant`
-
You can edit/set description_localizations directly with
-
.. code-block:: python3
-
bridge_command.description_localizations["en-UK"] = ... # or any other locale
# or
bridge_command.description_localizations = {"en-UK": ..., "fr-FR": ...}
@@ -201,9 +198,34 @@ def description_localizations(self) -> dict[str, str]:
def description_localizations(self, value):
self.slash_variant.description_localizations = value
- @property
- def qualified_name(self) -> str:
- return self.slash_variant.qualified_name
+ def __getattribute__(self, name):
+ try:
+ # first, look for the attribute on the bridge command
+ return super().__getattribute__(name)
+ except AttributeError as e:
+ # if it doesn't exist, check this list, if the name of
+ # the parameter is here
+ if name is self.__special_attrs__:
+ raise e
+
+ # looks up the result in the variants.
+ # slash cmd prioritized
+ result = getattr(self.slash_variant, name, MISSING)
+ try:
+ if result is MISSING:
+ return getattr(self.ext_variant, name)
+ return result
+ except AttributeError:
+ raise AttributeError(
+ f"'{self.__class__.__name__}' object has no attribute '{name}'"
+ )
+
+ def __setattr__(self, name, value) -> None:
+ if name not in self.__special_attrs__:
+ setattr(self.slash_variant, name, value)
+ setattr(self.ext_variant, name, value)
+
+ return super().__setattr__(name, value)
def add_to(self, bot: ExtBot) -> None:
"""Adds the command to a bot. This method is inherited by :class:`.BridgeCommandGroup`.
@@ -321,6 +343,14 @@ class BridgeCommandGroup(BridgeCommand):
If :func:`map_to` is used, the mapped slash command.
"""
+ __special_attrs__ = [
+ "slash_variant",
+ "ext_variant",
+ "parent",
+ "subcommands",
+ "mapped",
+ ]
+
ext_variant: BridgeExtGroup
slash_variant: BridgeSlashGroup
@@ -341,6 +371,16 @@ def __init__(self, callback, *args, **kwargs):
kwargs.update(map_to)
self.mapped = self.slash_variant.command(**kwargs)(callback)
+ def walk_commands(self) -> Iterator[BridgeCommand]:
+ """An iterator that recursively walks through all the bridge group's subcommands.
+
+ Yields
+ ------
+ :class:`.BridgeCommand`
+ A bridge command of this bridge group.
+ """
+ yield from self.subcommands
+
def command(self, *args, **kwargs):
"""A decorator to register a function as a subcommand.
diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py
index 4f1753e49a..cd70a2393f 100644
--- a/discord/ext/commands/bot.py
+++ b/discord/ext/commands/bot.py
@@ -29,7 +29,7 @@
import collections.abc
import sys
import traceback
-from typing import TYPE_CHECKING, Any, Callable, TypeVar
+from typing import TYPE_CHECKING, Any, Callable, Coroutine, Iterable, TypeVar
import discord
@@ -109,28 +109,28 @@ def _is_submodule(parent: str, child: str) -> bool:
return parent == child or child.startswith(f"{parent}.")
-class _DefaultRepr:
- def __repr__(self):
- return ""
-
-
-_default = _DefaultRepr()
-
-
class BotBase(GroupMixin, discord.cog.CogMixin):
+ _help_command = None
_supports_prefixed_commands = True
- def __init__(self, command_prefix=when_mentioned, help_command=_default, **options):
+ def __init__(
+ self,
+ command_prefix: str
+ | Iterable[str]
+ | Callable[
+ [Bot | AutoShardedBot, Message],
+ str | Iterable[str] | Coroutine[Any, Any, str | Iterable[str]],
+ ] = when_mentioned,
+ help_command: HelpCommand | None = MISSING,
+ **options,
+ ):
super().__init__(**options)
self.command_prefix = command_prefix
- self._help_command = None
+ self.help_command = (
+ DefaultHelpCommand() if help_command is MISSING else help_command
+ )
self.strip_after_prefix = options.get("strip_after_prefix", False)
- if help_command is _default:
- self.help_command = DefaultHelpCommand()
- else:
- self.help_command = help_command
-
@discord.utils.copy_doc(discord.Client.close)
async def close(self) -> None:
for extension in tuple(self.__extensions):
diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py
index ee131f0424..7a4bbabf46 100644
--- a/discord/ext/commands/converter.py
+++ b/discord/ext/commands/converter.py
@@ -199,11 +199,11 @@ async def query_member_named(self, guild, argument):
return discord.utils.get(
members, name=username, discriminator=discriminator
)
- else:
- members = await guild.query_members(argument, limit=100, cache=cache)
- return discord.utils.find(
- lambda m: m.name == argument or m.nick == argument, members
- )
+ members = await guild.query_members(argument, limit=100, cache=cache)
+ return discord.utils.find(
+ lambda m: argument in (m.nick, m.name, m.global_name),
+ members,
+ )
async def query_member_by_id(self, bot, guild, user_id):
ws = bot._get_websocket(shard_id=guild.shard_id)
@@ -320,7 +320,7 @@ async def convert(self, ctx: Context, argument: str) -> discord.User:
if result is not None:
return result
- predicate = lambda u: u.name == arg
+ predicate = lambda u: arg in (u.name, u.global_name)
result = discord.utils.find(predicate, state._users.values())
if result is None:
diff --git a/discord/ext/commands/flags.py b/discord/ext/commands/flags.py
index d265442bc0..54e7e0c37c 100644
--- a/discord/ext/commands/flags.py
+++ b/discord/ext/commands/flags.py
@@ -33,6 +33,11 @@
from discord.utils import MISSING, MissingField, maybe_coroutine, resolve_annotation
+if sys.version_info >= (3, 11):
+ _MISSING = MissingField
+else:
+ _MISSING = MISSING
+
from .converter import run_converters
from .errors import (
BadFlagArgument,
@@ -81,13 +86,13 @@ class Flag:
Whether multiple given values overrides the previous value.
"""
- name: str = MissingField
+ name: str = _MISSING
aliases: list[str] = field(default_factory=list)
- attribute: str = MissingField
- annotation: Any = MissingField
- default: Any = MissingField
- max_args: int = MissingField
- override: bool = MissingField
+ attribute: str = _MISSING
+ annotation: Any = _MISSING
+ default: Any = _MISSING
+ max_args: int = _MISSING
+ override: bool = _MISSING
cast_to_dict: bool = False
@property
diff --git a/discord/flags.py b/discord/flags.py
index ae5d74da82..308083d673 100644
--- a/discord/flags.py
+++ b/discord/flags.py
@@ -116,9 +116,6 @@ def _from_value(cls, value):
def __eq__(self, other: Any) -> bool:
return isinstance(other, self.__class__) and self.value == other.value
- def __ne__(self, other: Any) -> bool:
- return not self.__eq__(other)
-
def __hash__(self) -> int:
return hash(self.value)
@@ -404,6 +401,14 @@ def suppress_notifications(self):
return 4096
+ @flag_value
+ def is_voice_message(self):
+ """:class:`bool`: Returns ``True`` if this message is a voice message.
+
+ .. versionadded:: 2.5
+ """
+ return 8192
+
@fill_with_flags()
class PublicUserFlags(BaseFlags):
diff --git a/discord/guild.py b/discord/guild.py
index a84f0d2eba..2bd77b2dd5 100644
--- a/discord/guild.py
+++ b/discord/guild.py
@@ -202,50 +202,8 @@ class Guild(Hashable):
The guild's notification settings.
features: List[:class:`str`]
A list of features that the guild has. The features that a guild can have are
- subject to arbitrary change by Discord.
-
- They are currently as follows:
-
- - ``ANIMATED_BANNER``: Guild can upload an animated banner.
- - ``ANIMATED_ICON``: Guild can upload an animated icon.
- - ``APPLICATION_COMMAND_PERMISSIONS_V2``: Guild is using the old command permissions behavior.
- - ``AUTO_MODERATION``: Guild has enabled the auto moderation system.
- - ``BANNER``: Guild can upload and use a banner. (i.e. :attr:`.banner`)
- - ``CHANNEL_BANNER``: Guild can upload and use a channel banners.
- - ``COMMERCE``: Guild can sell things using store channels, which have now been removed.
- - ``COMMUNITY``: Guild is a community server.
- - ``DEVELOPER_SUPPORT_SERVER``: Guild has been set as a support server on the App Directory.
- - ``DISCOVERABLE``: Guild shows up in Server Discovery.
- - ``FEATURABLE``: Guild can be featured in the Server Directory.
- - ``HAS_DIRECTORY_ENTRY``: Unknown.
- - ``HUB``: Hubs contain a directory channel that let you find school-related, student-run servers for your school or university.
- - ``INTERNAL_EMPLOYEE_ONLY``: Indicates that only users with the staff badge can join the guild.
- - ``INVITES_DISABLED``: Guild Invites are disabled.
- - ``INVITE_SPLASH``: Guild's invite page can have a special splash.
- - ``LINKED_TO_HUB``: 'Guild is linked to a hub.
- - ``MEMBER_PROFILES``: Unknown.
- - ``MEMBER_VERIFICATION_GATE_ENABLED``: Guild has Membership Screening enabled.
- - ``MONETIZATION_ENABLED``: Guild has enabled monetization.
- - ``MORE_EMOJI``: Guild has increased custom emoji slots.
- - ``MORE_STICKERS``: Guild has increased custom sticker slots.
- - ``NEWS``: Guild can create news channels.
- - ``NEW_THREAD_PERMISSIONS``: Guild has new thread permissions.
- - ``PARTNERED``: Guild is a partnered server.
- - ``PREMIUM_TIER_3_OVERRIDE``: Forces the server to server boosting level 3 (specifically created by Discord Staff Member "Jethro" for their personal server).
- - ``PREVIEW_ENABLED``: Guild can be viewed before being accepted via Membership Screening.
- - ``ROLE_ICONS``: Guild can set an image or emoji as a role icon.
- - ``ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE``: Role subscriptions are available for purchasing.
- - ``ROLE_SUBSCRIPTIONS_ENABLED``: Guild is able to view and manage role subscriptions.
- - ``SEVEN_DAY_THREAD_ARCHIVE``: Users can set the thread archive time to 7 days.
- - ``TEXT_IN_VOICE_ENABLED``: Guild has a chat button inside voice channels that opens a dedicated text channel in a sidebar similar to thread view.
- - ``THREADS_ENABLED_TESTING``: Used by bot developers to test their bots with threads in guilds with 5 or fewer members and a bot. Also gives the premium thread features.
- - ``THREE_DAY_THREAD_ARCHIVE``: Users can set the thread archive time to 3 days.
- - ``TICKETED_EVENTS_ENABLED``: Guild has enabled ticketed events.
- - ``VANITY_URL``: Guild can have a vanity invite URL (e.g. discord.gg/discord-api).
- - ``VERIFIED``: Guild is a verified server.
- - ``VIP_REGIONS``: Guild has VIP voice regions.
- - ``WELCOME_SCREEN_ENABLED``: Guild has enabled the welcome screen.
-
+ subject to arbitrary change by Discord. You can find a catalog of guild features
+ `here `_.
premium_tier: :class:`int`
The premium tier for this guild. Corresponds to "Nitro Server" in the official UI.
The number goes from 0 to 3 inclusive.
@@ -425,7 +383,7 @@ def __repr__(self) -> str:
("name", self.name),
("shard_id", self.shard_id),
("chunked", self.chunked),
- ("member_count", getattr(self, "_member_count", None)),
+ ("member_count", self._member_count),
)
inner = " ".join("%s=%r" % t for t in attrs)
return f""
@@ -483,11 +441,11 @@ def _remove_role(self, role_id: int, /) -> Role:
return role
def _from_data(self, guild: GuildPayload) -> None:
- # according to Stan, this is always available even if the guild is unavailable
- # I don't have this guarantee when someone updates the guild.
- member_count = guild.get("member_count", None)
- if member_count is not None:
- self._member_count: int = member_count
+ member_count = guild.get("member_count")
+ # Either the payload includes member_count, or it hasn't been set yet.
+ # Prevents valid _member_count from suddenly changing to None
+ if member_count is not None or not hasattr(self, "_member_count"):
+ self._member_count: int | None = member_count
self.name: str = guild.get("name")
self.verification_level: VerificationLevel = try_enum(
@@ -574,7 +532,7 @@ def _from_data(self, guild: GuildPayload) -> None:
self._sync(guild)
self._large: bool | None = (
- None if member_count is None else self._member_count >= 250
+ None if self._member_count is None else self._member_count >= 250
)
self.owner_id: int | None = utils._get_as_snowflake(guild, "owner_id")
@@ -640,10 +598,7 @@ def large(self) -> bool:
members, which for this library is set to the maximum of 250.
"""
if self._large is None:
- try:
- return self._member_count >= 250
- except AttributeError:
- return len(self._members) >= 250
+ return (self._member_count or len(self._members)) >= 250
return self._large
@property
@@ -1044,10 +999,9 @@ def chunked(self) -> bool:
If this value returns ``False``, then you should request for
offline members.
"""
- count = getattr(self, "_member_count", None)
- if count is None:
+ if self._member_count is None:
return False
- return count == len(self._members)
+ return self._member_count == len(self._members)
@property
def shard_id(self) -> int:
@@ -1110,10 +1064,7 @@ def get_member_named(self, name: str, /) -> Member | None:
if result is not None:
return result
- def pred(m: Member) -> bool:
- return m.nick == name or m.name == name
-
- return utils.find(pred, members)
+ return utils.find(lambda m: name in (m.nick, m.name, m.global_name), members)
def _create_channel(
self,
@@ -2158,13 +2109,19 @@ async def fetch_channel(self, channel_id: int, /) -> GuildChannel | Thread:
def bans(
self,
limit: int | None = None,
- before: SnowflakeTime | None = None,
- after: SnowflakeTime | None = None,
+ before: Snowflake | None = None,
+ after: Snowflake | None = None,
) -> BanIterator:
"""|coro|
Retrieves an :class:`.AsyncIterator` that enables receiving the guild's bans. In order to use this, you must
have the :attr:`~Permissions.ban_members` permission.
+ Users will always be returned in ascending order sorted by user ID.
+ If both the ``before`` and ``after`` parameters are provided, only before is respected.
+
+ .. versionchanged:: 2.5
+ The ``before``. and ``after`` parameters were changed. They are now of the type :class:`.abc.Snowflake` instead of
+ `SnowflakeTime` to comply with the discord api.
.. versionchanged:: 2.0
The ``limit``, ``before``. and ``after`` parameters were added. Now returns a :class:`.BanIterator` instead
@@ -2176,14 +2133,10 @@ def bans(
----------
limit: Optional[:class:`int`]
The number of bans to retrieve. Defaults to 1000.
- before: Optional[Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]]
- Retrieve bans before this date or object.
- If a datetime is provided, it is recommended to use a UTC aware datetime.
- If the datetime is naive, it is assumed to be local time.
- after: Optional[Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]]
- Retrieve bans after this date or object.
- If a datetime is provided, it is recommended to use a UTC aware datetime.
- If the datetime is naive, it is assumed to be local time.
+ before: Optional[:class:`.abc.Snowflake`]
+ Retrieve bans before the given user.
+ after: Optional[:class:`.abc.Snowflake`]
+ Retrieve bans after the given user.
Yields
------
@@ -2846,6 +2799,8 @@ async def create_role(
colour: Colour | int = ...,
hoist: bool = ...,
mentionable: bool = ...,
+ icon: bytes | None = MISSING,
+ unicode_emoji: str | None = MISSING,
) -> Role:
...
@@ -2859,6 +2814,8 @@ async def create_role(
color: Colour | int = ...,
hoist: bool = ...,
mentionable: bool = ...,
+ icon: bytes | None = ...,
+ unicode_emoji: str | None = ...,
) -> Role:
...
@@ -2872,6 +2829,8 @@ async def create_role(
hoist: bool = MISSING,
mentionable: bool = MISSING,
reason: str | None = None,
+ icon: bytes | None = MISSING,
+ unicode_emoji: str | None = MISSING,
) -> Role:
"""|coro|
@@ -2902,6 +2861,13 @@ async def create_role(
Defaults to ``False``.
reason: Optional[:class:`str`]
The reason for creating this role. Shows up on the audit log.
+ icon: Optional[:class:`bytes`]
+ A :term:`py:bytes-like object` representing the icon. Only PNG/JPEG/WebP is supported.
+ If this argument is passed, ``unicode_emoji`` is set to None.
+ Only available to guilds that contain ``ROLE_ICONS`` in :attr:`Guild.features`.
+ unicode_emoji: Optional[:class:`str`]
+ The role's unicode emoji. If this argument is passed, ``icon`` is set to None.
+ Only available to guilds that contain ``ROLE_ICONS`` in :attr:`Guild.features`.
Returns
-------
@@ -2938,6 +2904,17 @@ async def create_role(
if name is not MISSING:
fields["name"] = name
+ if icon is not MISSING:
+ if icon is None:
+ fields["icon"] = None
+ else:
+ fields["icon"] = _bytes_to_base64_data(icon)
+ fields["unicode_emoji"] = None
+
+ if unicode_emoji is not MISSING:
+ fields["unicode_emoji"] = unicode_emoji
+ fields["icon"] = None
+
data = await self._state.http.create_role(self.id, reason=reason, **fields)
role = Role(guild=self, data=data, state=self._state)
diff --git a/discord/http.py b/discord/http.py
index d70380cd4f..9eb7837d13 100644
--- a/discord/http.py
+++ b/discord/http.py
@@ -1170,6 +1170,7 @@ def start_forum_thread(
invitable: bool = True,
applied_tags: SnowflakeList | None = None,
reason: str | None = None,
+ files: Sequence[File] | None = None,
embed: embed.Embed | None = None,
embeds: list[embed.Embed] | None = None,
nonce: str | None = None,
@@ -1177,43 +1178,74 @@ def start_forum_thread(
stickers: list[sticker.StickerItem] | None = None,
components: list[components.Component] | None = None,
) -> Response[threads.Thread]:
- payload = {
+ payload: dict[str, Any] = {
"name": name,
"auto_archive_duration": auto_archive_duration,
"invitable": invitable,
}
- if content:
- payload["content"] = content
if applied_tags:
payload["applied_tags"] = applied_tags
+ if rate_limit_per_user:
+ payload["rate_limit_per_user"] = rate_limit_per_user
+
+ message = {}
+
+ if content:
+ message["content"] = content
+
if embed:
- payload["embeds"] = [embed]
+ message["embeds"] = [embed]
if embeds:
- payload["embeds"] = embeds
+ message["embeds"] = embeds
if nonce:
- payload["nonce"] = nonce
+ message["nonce"] = nonce
if allowed_mentions:
- payload["allowed_mentions"] = allowed_mentions
+ message["allowed_mentions"] = allowed_mentions
if components:
- payload["components"] = components
+ message["components"] = components
if stickers:
- payload["sticker_ids"] = stickers
+ message["sticker_ids"] = stickers
+
+ if message != {}:
+ payload["message"] = message
- if rate_limit_per_user:
- payload["rate_limit_per_user"] = rate_limit_per_user
- # TODO: Once supported by API, remove has_message=true query parameter
route = Route(
"POST",
- "/channels/{channel_id}/threads?has_message=true",
+ "/channels/{channel_id}/threads",
channel_id=channel_id,
)
+
+ if files:
+ form = [{"name": "payload_json"}]
+
+ attachments = []
+ for index, file in enumerate(files):
+ attachments.append(
+ {
+ "id": index,
+ "filename": file.filename,
+ "description": file.description,
+ }
+ )
+ form.append(
+ {
+ "name": f"files[{index}]",
+ "value": file.fp,
+ "filename": file.filename,
+ "content_type": "application/octet-stream",
+ }
+ )
+
+ payload["attachments"] = attachments
+ form[0]["value"] = utils._to_json(payload)
+ return self.request(route, form=form, reason=reason)
return self.request(route, json=payload, reason=reason)
def join_thread(self, channel_id: Snowflake) -> Response[None]:
diff --git a/discord/interactions.py b/discord/interactions.py
index 2cd4b740b8..83ca14f128 100644
--- a/discord/interactions.py
+++ b/discord/interactions.py
@@ -29,16 +29,22 @@
from typing import TYPE_CHECKING, Any, Coroutine, Union
from . import utils
-from .channel import ChannelType, PartialMessageable
+from .channel import ChannelType, PartialMessageable, _threaded_channel_factory
from .enums import InteractionResponseType, InteractionType, try_enum
from .errors import ClientException, InteractionResponded, InvalidArgument
from .file import File
+from .flags import MessageFlags
from .member import Member
from .message import Attachment, Message
from .object import Object
from .permissions import Permissions
from .user import User
-from .webhook.async_ import Webhook, async_context, handle_message_parameters
+from .webhook.async_ import (
+ Webhook,
+ WebhookMessage,
+ async_context,
+ handle_message_parameters,
+)
__all__ = (
"Interaction",
@@ -52,7 +58,9 @@
from .channel import (
CategoryChannel,
+ DMChannel,
ForumChannel,
+ GroupChannel,
StageChannel,
TextChannel,
VoiceChannel,
@@ -77,6 +85,8 @@
ForumChannel,
CategoryChannel,
Thread,
+ DMChannel,
+ GroupChannel,
PartialMessageable,
]
@@ -99,8 +109,10 @@ class Interaction:
The interaction type.
guild_id: Optional[:class:`int`]
The guild ID the interaction was sent from.
+ channel: Optional[Union[:class:`abc.GuildChannel`, :class:`abc.PrivateChannel`, :class:`Thread`]]
+ The channel the interaction was sent from.
channel_id: Optional[:class:`int`]
- The channel ID the interaction was sent from.
+ The ID of the channel the interaction was sent from.
application_id: :class:`int`
The application ID that the interaction was for.
user: Optional[Union[:class:`User`, :class:`Member`]]
@@ -124,6 +136,7 @@ class Interaction:
"id",
"type",
"guild_id",
+ "channel",
"channel_id",
"data",
"application_id",
@@ -134,6 +147,7 @@ class Interaction:
"token",
"version",
"custom_id",
+ "_channel_data",
"_message_data",
"_permissions",
"_app_permissions",
@@ -169,13 +183,7 @@ def _from_data(self, data: InteractionPayload):
self._app_permissions: int = int(data.get("app_permissions", 0))
self.message: Message | None = None
-
- if message_data := data.get("message"):
- self.message = Message(
- state=self._state, channel=self.channel, data=message_data
- )
-
- self._message_data = message_data
+ self.channel = None
self.user: User | Member | None = None
self._permissions: int = 0
@@ -206,6 +214,30 @@ def _from_data(self, data: InteractionPayload):
except KeyError:
pass
+ if channel := data.get("channel"):
+ if (ch_type := channel.get("type")) is not None:
+ factory, ch_type = _threaded_channel_factory(ch_type)
+
+ if ch_type in (ChannelType.group, ChannelType.private):
+ self.channel = factory(
+ me=self.user, data=channel, state=self._state
+ )
+ elif self.guild:
+ self.channel = factory(
+ guild=self.guild, state=self._state, data=channel
+ )
+ else:
+ self.channel = self.cached_channel
+
+ self._channel_data = channel
+
+ if message_data := data.get("message"):
+ self.message = Message(
+ state=self._state, channel=self.channel, data=message_data
+ )
+
+ self._message_data = message_data
+
@property
def client(self) -> Client:
"""Returns the client that sent the interaction."""
@@ -225,7 +257,7 @@ def is_component(self) -> bool:
return self.type == InteractionType.component
@utils.cached_slot_property("_cs_channel")
- def channel(self) -> InteractionChannel | None:
+ def cached_channel(self) -> InteractionChannel | None:
"""The channel the
interaction was sent from.
@@ -355,6 +387,7 @@ async def edit_original_response(
view: View | None = MISSING,
allowed_mentions: AllowedMentions | None = None,
delete_after: float | None = None,
+ suppress: bool = False,
) -> InteractionMessage:
"""|coro|
@@ -393,6 +426,8 @@ async def edit_original_response(
If provided, the number of seconds to wait in the background
before deleting the message we just edited. If the deletion fails,
then it is silently ignored.
+ suppress: :class:`bool`
+ Whether to suppress embeds for the message.
Returns
-------
@@ -422,6 +457,7 @@ async def edit_original_response(
view=view,
allowed_mentions=allowed_mentions,
previous_allowed_mentions=previous_mentions,
+ suppress=suppress,
)
adapter = async_context.get()
http = self._state.http
@@ -519,6 +555,44 @@ async def delete_original_message(self, **kwargs):
"""
return await self.delete_original_response(**kwargs)
+ async def respond(self, *args, **kwargs) -> Interaction | WebhookMessage:
+ """|coro|
+
+ Sends either a response or a message using the followup webhook determined by whether the interaction
+ has been responded to or not.
+
+ Returns
+ -------
+ Union[:class:`discord.Interaction`, :class:`discord.WebhookMessage`]:
+ The response, its type depending on whether it's an interaction response or a followup.
+ """
+ try:
+ if not self.response.is_done():
+ return await self.response.send_message(*args, **kwargs)
+ else:
+ return await self.followup.send(*args, **kwargs)
+ except InteractionResponded:
+ return await self.followup.send(*args, **kwargs)
+
+ async def edit(self, *args, **kwargs) -> InteractionMessage | None:
+ """|coro|
+
+ Either respond to the interaction with an edit_message or edits the existing response, determined by
+ whether the interaction has been responded to or not.
+
+ Returns
+ -------
+ Union[:class:`discord.InteractionMessage`, :class:`discord.WebhookMessage`]:
+ The response, its type depending on whether it's an interaction response or a followup.
+ """
+ try:
+ if not self.response.is_done():
+ return await self.response.edit_message(*args, **kwargs)
+ else:
+ return await self.edit_original_response(*args, **kwargs)
+ except InteractionResponded:
+ return await self.edit_original_response(*args, **kwargs)
+
def to_dict(self) -> dict[str, Any]:
"""
Converts this interaction object into a dict.
@@ -607,6 +681,10 @@ async def defer(self, *, ephemeral: bool = False, invisible: bool = True) -> Non
- :attr:`InteractionType.component`
- :attr:`InteractionType.modal_submit`
+ .. note::
+ The follow-up response will also be non-ephemeral if the `ephemeral`
+ argument is ``False``, and ephemeral if ``True``.
+
Parameters
----------
ephemeral: :class:`bool`
@@ -844,7 +922,7 @@ async def send_message(
if ephemeral and view.timeout is None:
view.timeout = 15 * 60.0
- view.message = await self._parent.original_response()
+ view.parent = self._parent
self._parent._state.store_view(view)
self._responded = True
@@ -863,6 +941,8 @@ async def edit_message(
attachments: list[Attachment] = MISSING,
view: View | None = MISSING,
delete_after: float | None = None,
+ suppress: bool | None = MISSING,
+ allowed_mentions: AllowedMentions | None = None,
) -> None:
"""|coro|
@@ -893,6 +973,15 @@ async def edit_message(
If provided, the number of seconds to wait in the background
before deleting the message we just edited. If the deletion fails,
then it is silently ignored.
+ suppress: Optional[:class:`bool`]
+ Whether to suppress embeds for the message.
+ allowed_mentions: Optional[:class:`~discord.AllowedMentions`]
+ Controls the mentions being processed in this message. If this is
+ passed, then the object is merged with :attr:`~discord.Client.allowed_mentions`.
+ The merging behaviour only overrides attributes that have been explicitly passed
+ to the object, otherwise it uses the attributes set in :attr:`~discord.Client.allowed_mentions`.
+ If no object is passed at all then the defaults given by :attr:`~discord.Client.allowed_mentions`
+ are used instead.
Raises
------
@@ -956,6 +1045,23 @@ async def edit_message(
# we keep previous attachments when adding new files
payload["attachments"] = [a.to_dict() for a in msg.attachments]
+ if suppress is not MISSING:
+ flags = MessageFlags._from_value(self._parent.message.flags.value)
+ flags.suppress_embeds = suppress
+ payload["flags"] = flags.value
+
+ if allowed_mentions is None:
+ payload["allowed_mentions"] = (
+ state.allowed_mentions and state.allowed_mentions.to_dict()
+ )
+
+ elif state.allowed_mentions is not None:
+ payload["allowed_mentions"] = state.allowed_mentions.merge(
+ allowed_mentions
+ ).to_dict()
+ else:
+ payload["allowed_mentions"] = allowed_mentions.to_dict()
+
adapter = async_context.get()
http = parent._state.http
try:
@@ -1142,6 +1248,7 @@ async def edit(
view: View | None = MISSING,
allowed_mentions: AllowedMentions | None = None,
delete_after: float | None = None,
+ suppress: bool | None = MISSING,
) -> InteractionMessage:
"""|coro|
@@ -1174,6 +1281,8 @@ async def edit(
If provided, the number of seconds to wait in the background
before deleting the message we just edited. If the deletion fails,
then it is silently ignored.
+ suppress: Optional[:class:`bool`]
+ Whether to suppress embeds for the message.
Returns
-------
@@ -1193,6 +1302,8 @@ async def edit(
"""
if attachments is MISSING:
attachments = self.attachments or MISSING
+ if suppress is MISSING:
+ suppress = self.flags.suppress_embeds
return await self._state._interaction.edit_original_response(
content=content,
embeds=embeds,
@@ -1203,6 +1314,7 @@ async def edit(
view=view,
allowed_mentions=allowed_mentions,
delete_after=delete_after,
+ suppress=suppress,
)
async def delete(self, *, delay: float | None = None) -> None:
diff --git a/discord/iterators.py b/discord/iterators.py
index f4ec6cda28..78edb7570d 100644
--- a/discord/iterators.py
+++ b/discord/iterators.py
@@ -689,12 +689,6 @@ def create_member(self, data):
class BanIterator(_AsyncIterator["BanEntry"]):
def __init__(self, guild, limit=None, before=None, after=None):
- if isinstance(after, datetime.datetime):
- after = Object(id=time_snowflake(after, high=True))
-
- if isinstance(before, datetime.datetime):
- before = Object(id=time_snowflake(before, high=True))
-
self.guild = guild
self.limit = limit
self.after = after
diff --git a/discord/member.py b/discord/member.py
index 0c798a725d..3e60fc1365 100644
--- a/discord/member.py
+++ b/discord/member.py
@@ -230,7 +230,7 @@ class Member(discord.abc.Messageable, _UserTag):
.. describe:: str(x)
- Returns the member's name with the discriminator.
+ Returns the member's name with the discriminator or global_name.
Attributes
----------
@@ -322,6 +322,19 @@ def __str__(self) -> str:
return str(self._user)
def __repr__(self) -> str:
+ if self._user.is_migrated:
+ if self.global_name is not None:
+ return (
+ ""
+ )
+ else:
+ return (
+ ""
+ )
return (
" str:
def __eq__(self, other: Any) -> bool:
return isinstance(other, _UserTag) and other.id == self.id
- def __ne__(self, other: Any) -> bool:
- return not self.__eq__(other)
-
def __hash__(self) -> int:
return hash(self._user)
@@ -427,17 +437,24 @@ def _presence_update(
def _update_inner_user(self, user: UserPayload) -> tuple[User, User] | None:
u = self._user
- original = (u.name, u._avatar, u.discriminator, u._public_flags)
+ original = (u.name, u._avatar, u.discriminator, u.global_name, u._public_flags)
# These keys seem to always be available
modified = (
user["username"],
user["avatar"],
user["discriminator"],
+ user.get("global_name", None) or None,
user.get("public_flags", 0),
)
if original != modified:
to_return = User._copy(self._user)
- u.name, u._avatar, u.discriminator, u._public_flags = modified
+ (
+ u.name,
+ u._avatar,
+ u.discriminator,
+ u.global_name,
+ u._public_flags,
+ ) = modified
# Signal to dispatch on_user_update
return to_return, u
@@ -476,6 +493,11 @@ def web_status(self) -> Status:
"""The member's status on the web client, if applicable."""
return try_enum(Status, self._client_status.get("web", "offline"))
+ @property
+ def global_name(self) -> str | None:
+ """The member's global name, if applicable."""
+ return self._user.global_name
+
def is_on_mobile(self) -> bool:
"""A helper function that determines if a member is active on a mobile device."""
return "mobile" in self._client_status
@@ -535,12 +557,9 @@ def mention(self) -> str:
@property
def display_name(self) -> str:
"""Returns the user's display name.
-
- For regular users this is just their username, but
- if they have a guild specific nickname then that
- is returned instead.
+ This will either be their guild specific nickname, global name or username.
"""
- return self.nick or self.name
+ return self.nick or self.global_name or self.name
@property
def display_avatar(self) -> Asset:
diff --git a/discord/message.py b/discord/message.py
index 77193b2920..c74f40c288 100644
--- a/discord/message.py
+++ b/discord/message.py
@@ -159,7 +159,7 @@ class Attachment(Hashable):
case of images. When the message is deleted, this URL might be valid for a few
minutes or not valid at all.
content_type: Optional[:class:`str`]
- The attachment's `media type `_
+ The attachment's `media type `_.
ephemeral: :class:`bool`
Whether the attachment is ephemeral or not.
@@ -169,6 +169,16 @@ class Attachment(Hashable):
The attachment's description.
.. versionadded:: 2.0
+
+ duration_secs: Optional[:class:`float`]
+ The duration of the audio file (currently for voice messages).
+
+ .. versionadded:: 2.5
+
+ waveform: Optional[:class:`str`]
+ The base64 encoded bytearray representing a sampled waveform (currently for voice messages).
+
+ .. versionadded:: 2.5
"""
__slots__ = (
@@ -183,6 +193,8 @@ class Attachment(Hashable):
"content_type",
"ephemeral",
"description",
+ "duration_secs",
+ "waveform",
)
def __init__(self, *, data: AttachmentPayload, state: ConnectionState):
@@ -197,6 +209,8 @@ def __init__(self, *, data: AttachmentPayload, state: ConnectionState):
self.content_type: str | None = data.get("content_type")
self.ephemeral: bool = data.get("ephemeral", False)
self.description: str | None = data.get("description")
+ self.duration_secs: float | None = data.get("duration_secs")
+ self.waveform: str | None = data.get("waveform")
def is_spoiler(self) -> bool:
"""Whether this attachment contains a spoiler."""
diff --git a/discord/mixins.py b/discord/mixins.py
index cfbb030bbd..acf451361d 100644
--- a/discord/mixins.py
+++ b/discord/mixins.py
@@ -38,9 +38,7 @@ def __eq__(self, other: object) -> bool:
return isinstance(other, self.__class__) and other.id == self.id
def __ne__(self, other: object) -> bool:
- if isinstance(other, self.__class__):
- return other.id != self.id
- return True
+ return not self.__eq__(other)
class Hashable(EqualityComparable):
diff --git a/discord/partial_emoji.py b/discord/partial_emoji.py
index dd2d6fa04d..ec5495c7af 100644
--- a/discord/partial_emoji.py
+++ b/discord/partial_emoji.py
@@ -202,9 +202,6 @@ def __eq__(self, other: Any) -> bool:
return self.id == other.id
return False
- def __ne__(self, other: Any) -> bool:
- return not self.__eq__(other)
-
def __hash__(self) -> int:
return hash((self.id, self.name))
diff --git a/discord/permissions.py b/discord/permissions.py
index 43dc6f2f7d..e94e6c2116 100644
--- a/discord/permissions.py
+++ b/discord/permissions.py
@@ -610,6 +610,14 @@ def moderate_members(self) -> int:
"""
return 1 << 40
+ @flag_value
+ def send_voice_messages(self) -> int:
+ """:class:`bool`: Returns ``True`` if a member can send voice messages.
+
+ .. versionadded:: 2.5
+ """
+ return 1 << 46
+
PO = TypeVar("PO", bound="PermissionOverwrite")
@@ -727,6 +735,7 @@ class PermissionOverwrite:
use_external_stickers: bool | None
start_embedded_activities: bool | None
moderate_members: bool | None
+ send_voice_messages: bool | None
def __init__(self, **kwargs: bool | None):
self._values: dict[str, bool | None] = {}
diff --git a/discord/raw_models.py b/discord/raw_models.py
index 78f4d01903..1cd88ea0c2 100644
--- a/discord/raw_models.py
+++ b/discord/raw_models.py
@@ -97,9 +97,13 @@ class RawMessageDeleteEvent(_RawReprMixin):
The message ID that got deleted.
cached_message: Optional[:class:`Message`]
The cached message, if found in the internal message cache.
+ data: :class:`dict`
+ The raw data sent by the `gateway `_.
+
+ .. versionadded:: 2.5
"""
- __slots__ = ("message_id", "channel_id", "guild_id", "cached_message")
+ __slots__ = ("message_id", "channel_id", "guild_id", "cached_message", "data")
def __init__(self, data: MessageDeleteEvent) -> None:
self.message_id: int = int(data["id"])
@@ -109,6 +113,7 @@ def __init__(self, data: MessageDeleteEvent) -> None:
self.guild_id: int | None = int(data["guild_id"])
except KeyError:
self.guild_id: int | None = None
+ self.data: MessageDeleteEvent = data
class RawBulkMessageDeleteEvent(_RawReprMixin):
@@ -124,9 +129,13 @@ class RawBulkMessageDeleteEvent(_RawReprMixin):
The guild ID where the message got deleted, if applicable.
cached_messages: List[:class:`Message`]
The cached messages, if found in the internal message cache.
+ data: :class:`dict`
+ The raw data sent by the `gateway `_.
+
+ .. versionadded:: 2.5
"""
- __slots__ = ("message_ids", "channel_id", "guild_id", "cached_messages")
+ __slots__ = ("message_ids", "channel_id", "guild_id", "cached_messages", "data")
def __init__(self, data: BulkMessageDeleteEvent) -> None:
self.message_ids: set[int] = {int(x) for x in data.get("ids", [])}
@@ -137,6 +146,7 @@ def __init__(self, data: BulkMessageDeleteEvent) -> None:
self.guild_id: int | None = int(data["guild_id"])
except KeyError:
self.guild_id: int | None = None
+ self.data: BulkMessageDeleteEvent = data
class RawMessageUpdateEvent(_RawReprMixin):
@@ -156,7 +166,7 @@ class RawMessageUpdateEvent(_RawReprMixin):
.. versionadded:: 1.7
data: :class:`dict`
- The raw data given by the `gateway `_
+ The raw data sent by the `gateway `_
cached_message: Optional[:class:`Message`]
The cached message, if found in the internal message cache. Represents the message before
it is modified by the data in :attr:`RawMessageUpdateEvent.data`.
@@ -204,6 +214,10 @@ class RawReactionActionEvent(_RawReprMixin):
``REACTION_REMOVE`` for reaction removal.
.. versionadded:: 1.3
+ data: :class:`dict`
+ The raw data sent by the `gateway `_.
+
+ .. versionadded:: 2.5
"""
__slots__ = (
@@ -214,6 +228,7 @@ class RawReactionActionEvent(_RawReprMixin):
"emoji",
"event_type",
"member",
+ "data",
)
def __init__(
@@ -230,6 +245,7 @@ def __init__(
self.guild_id: int | None = int(data["guild_id"])
except KeyError:
self.guild_id: int | None = None
+ self.data: ReactionActionEvent = data
class RawReactionClearEvent(_RawReprMixin):
@@ -243,9 +259,13 @@ class RawReactionClearEvent(_RawReprMixin):
The channel ID where the reactions got cleared.
guild_id: Optional[:class:`int`]
The guild ID where the reactions got cleared.
+ data: :class:`dict`
+ The raw data sent by the `gateway `_.
+
+ .. versionadded:: 2.5
"""
- __slots__ = ("message_id", "channel_id", "guild_id")
+ __slots__ = ("message_id", "channel_id", "guild_id", "data")
def __init__(self, data: ReactionClearEvent) -> None:
self.message_id: int = int(data["message_id"])
@@ -255,6 +275,7 @@ def __init__(self, data: ReactionClearEvent) -> None:
self.guild_id: int | None = int(data["guild_id"])
except KeyError:
self.guild_id: int | None = None
+ self.data: ReactionClearEvent = data
class RawReactionClearEmojiEvent(_RawReprMixin):
@@ -272,9 +293,13 @@ class RawReactionClearEmojiEvent(_RawReprMixin):
The guild ID where the reactions got cleared.
emoji: :class:`PartialEmoji`
The custom or unicode emoji being removed.
+ data: :class:`dict`
+ The raw data sent by the `gateway `_.
+
+ .. versionadded:: 2.5
"""
- __slots__ = ("message_id", "channel_id", "guild_id", "emoji")
+ __slots__ = ("message_id", "channel_id", "guild_id", "emoji", "data")
def __init__(self, data: ReactionClearEmojiEvent, emoji: PartialEmoji) -> None:
self.emoji: PartialEmoji = emoji
@@ -285,6 +310,7 @@ def __init__(self, data: ReactionClearEmojiEvent, emoji: PartialEmoji) -> None:
self.guild_id: int | None = int(data["guild_id"])
except KeyError:
self.guild_id: int | None = None
+ self.data: ReactionClearEmojiEvent = data
class RawIntegrationDeleteEvent(_RawReprMixin):
@@ -300,9 +326,13 @@ class RawIntegrationDeleteEvent(_RawReprMixin):
The ID of the bot/OAuth2 application for this deleted integration.
guild_id: :class:`int`
The guild ID where the integration got deleted.
+ data: :class:`dict`
+ The raw data sent by the `gateway `_.
+
+ .. versionadded:: 2.5
"""
- __slots__ = ("integration_id", "application_id", "guild_id")
+ __slots__ = ("integration_id", "application_id", "guild_id", "data")
def __init__(self, data: IntegrationDeleteEvent) -> None:
self.integration_id: int = int(data["id"])
@@ -312,6 +342,7 @@ def __init__(self, data: IntegrationDeleteEvent) -> None:
self.application_id: int | None = int(data["application_id"])
except KeyError:
self.application_id: int | None = None
+ self.data: IntegrationDeleteEvent = data
class RawThreadUpdateEvent(_RawReprMixin):
@@ -330,7 +361,7 @@ class RawThreadUpdateEvent(_RawReprMixin):
parent_id: :class:`int`
The ID of the channel the thread belongs to.
data: :class:`dict`
- The raw data given by the `gateway `_.
+ The raw data sent by the `gateway `_.
thread: :class:`discord.Thread` | None
The thread, if it could be found in the internal cache.
"""
@@ -364,9 +395,13 @@ class RawThreadDeleteEvent(_RawReprMixin):
The ID of the channel the thread belonged to.
thread: Optional[:class:`discord.Thread`]
The thread that was deleted. This may be ``None`` if deleted thread is not found in internal cache.
+ data: :class:`dict`
+ The raw data sent by the `gateway `_.
+
+ .. versionadded:: 2.5
"""
- __slots__ = ("thread_id", "thread_type", "guild_id", "parent_id", "thread")
+ __slots__ = ("thread_id", "thread_type", "guild_id", "parent_id", "thread", "data")
def __init__(self, data: ThreadDeleteEvent) -> None:
self.thread_id: int = int(data["id"])
@@ -374,6 +409,7 @@ def __init__(self, data: ThreadDeleteEvent) -> None:
self.guild_id: int = int(data["guild_id"])
self.parent_id: int = int(data["parent_id"])
self.thread: Thread | None = None
+ self.data: ThreadDeleteEvent = data
class RawTypingEvent(_RawReprMixin):
@@ -393,9 +429,13 @@ class RawTypingEvent(_RawReprMixin):
The guild ID where the typing originated from, if applicable.
member: Optional[:class:`Member`]
The member who started typing. Only available if the member started typing in a guild.
+ data: :class:`dict`
+ The raw data sent by the `gateway `_.
+
+ .. versionadded:: 2.5
"""
- __slots__ = ("channel_id", "user_id", "when", "guild_id", "member")
+ __slots__ = ("channel_id", "user_id", "when", "guild_id", "member", "data")
def __init__(self, data: TypingEvent) -> None:
self.channel_id: int = int(data["channel_id"])
@@ -409,6 +449,7 @@ def __init__(self, data: TypingEvent) -> None:
self.guild_id: int | None = int(data["guild_id"])
except KeyError:
self.guild_id: int | None = None
+ self.data: TypingEvent = data
class RawMemberRemoveEvent(_RawReprMixin):
@@ -422,13 +463,18 @@ class RawMemberRemoveEvent(_RawReprMixin):
The user that left the guild.
guild_id: :class:`int`
The ID of the guild the user left.
+ data: :class:`dict`
+ The raw data sent by the `gateway `_.
+
+ .. versionadded:: 2.5
"""
- __slots__ = ("user", "guild_id")
+ __slots__ = ("user", "guild_id", "data")
def __init__(self, data: MemberRemoveEvent, user: User):
self.user: User = user
self.guild_id: int = int(data["guild_id"])
+ self.data: MemberRemoveEvent = data
class RawScheduledEventSubscription(_RawReprMixin):
@@ -448,15 +494,20 @@ class RawScheduledEventSubscription(_RawReprMixin):
event_type: :class:`str`
Can be either ``USER_ADD`` or ``USER_REMOVE`` depending on
the event called.
+ data: :class:`dict`
+ The raw data sent by the `gateway `_.
+
+ .. versionadded:: 2.5
"""
- __slots__ = ("event_id", "guild", "user_id", "event_type")
+ __slots__ = ("event_id", "guild", "user_id", "event_type", "data")
def __init__(self, data: ScheduledEventSubscription, event_type: str):
self.event_id: int = int(data["guild_scheduled_event_id"])
self.user_id: int = int(data["user_id"])
self.guild: Guild | None = None
self.event_type: str = event_type
+ self.data: ScheduledEventSubscription = data
class AutoModActionExecutionEvent:
@@ -503,6 +554,10 @@ class AutoModActionExecutionEvent:
The word or phrase configured that was matched in the content.
matched_content: :class:`str`
The substring in the content that was matched.
+ data: :class:`dict`
+ The raw data sent by the `gateway `_.
+
+ .. versionadded:: 2.5
"""
__slots__ = (
@@ -522,6 +577,7 @@ class AutoModActionExecutionEvent:
"message",
"alert_system_message_id",
"alert_system_message",
+ "data",
)
def __init__(self, state: ConnectionState, data: AutoModActionExecution) -> None:
@@ -570,6 +626,7 @@ def __init__(self, state: ConnectionState, data: AutoModActionExecution) -> None
except KeyError:
self.alert_system_message_id: int | None = None
self.alert_system_message: Message | None = None
+ self.data: AutoModActionExecution = data
def __repr__(self) -> str:
return (
@@ -593,7 +650,9 @@ class RawThreadMembersUpdateEvent(_RawReprMixin):
member_count: :class:`int`
The approximate number of members in the thread. Maximum of 50.
data: :class:`dict`
- The raw data given by the `gateway `_.
+ The raw data sent by the `gateway `_.
+
+ .. versionadded:: 2.5
"""
__slots__ = ("thread_id", "guild_id", "member_count", "data")
@@ -602,7 +661,7 @@ def __init__(self, data: ThreadMembersUpdateEvent) -> None:
self.thread_id = int(data["id"])
self.guild_id = int(data["guild_id"])
self.member_count = int(data["member_count"])
- self.data = data
+ self.data: ThreadMembersUpdateEvent = data
class RawAuditLogEntryEvent(_RawReprMixin):
@@ -618,7 +677,7 @@ class RawAuditLogEntryEvent(_RawReprMixin):
The entry ID.
guild_id: :class:`int`
The ID of the guild this action came from.
- user_id: :class:`int`
+ user_id: Optional[:class:`int`]
The ID of the user who initiated this action.
target_id: Optional[:class:`int`]
The ID of the target that got changed.
@@ -632,7 +691,7 @@ class RawAuditLogEntryEvent(_RawReprMixin):
contains extra information. See :class:`AuditLogAction` for
which actions have this field filled out.
data: :class:`dict`
- The raw data given by the `gateway `_.
+ The raw data sent by the `gateway `_.
"""
__slots__ = (
@@ -644,11 +703,14 @@ class RawAuditLogEntryEvent(_RawReprMixin):
"reason",
"extra",
"changes",
+ "data",
)
def __init__(self, data: AuditLogEntryEvent) -> None:
self.id = int(data["id"])
- self.user_id = int(data["user_id"])
+ self.user_id = data.get("user_id")
+ if self.user_id:
+ self.user_id = int(self.user_id)
self.guild_id = int(data["guild_id"])
self.target_id = data.get("target_id")
if self.target_id:
@@ -657,4 +719,4 @@ def __init__(self, data: AuditLogEntryEvent) -> None:
self.reason = data.get("reason")
self.extra = data.get("options")
self.changes = data.get("changes")
- self.data = data
+ self.data: AuditLogEntryEvent = data
diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py
index 6f70ff88c6..f7c0f898d9 100644
--- a/discord/scheduled_events.py
+++ b/discord/scheduled_events.py
@@ -158,7 +158,7 @@ class ScheduledEvent(Hashable):
The number of users that have marked themselves as interested in the event.
creator_id: Optional[:class:`int`]
The ID of the user who created the event.
- It may be ``None`` because events created before October 25th, 2021, haven't
+ It may be ``None`` because events created before October 25th, 2021 haven't
had their creators tracked.
creator: Optional[:class:`User`]
The resolved user object of who created the event.
@@ -209,7 +209,7 @@ def __init__(
ScheduledEventStatus, data.get("status")
)
self.subscriber_count: int | None = data.get("user_count", None)
- self.creator_id = data.get("creator_id", None)
+ self.creator_id: int | None = utils._get_as_snowflake(data, "creator_id")
self.creator: Member | None = creator
entity_metadata = data.get("entity_metadata")
diff --git a/discord/sinks/core.py b/discord/sinks/core.py
index 479acbe179..05d090a07a 100644
--- a/discord/sinks/core.py
+++ b/discord/sinks/core.py
@@ -34,7 +34,7 @@
from .errors import SinkException
if TYPE_CHECKING:
- from ..channel import VoiceChannel
+ from ..voice_client import VoiceClient
__all__ = (
"Filters",
@@ -115,6 +115,7 @@ def __init__(self, data, client):
self.decoded_data = None
self.user_id = None
+ self.receive_time = time.perf_counter()
class AudioData:
@@ -200,11 +201,11 @@ def __init__(self, *, filters=None):
filters = default_filters
self.filters = filters
Filters.__init__(self, **self.filters)
- self.vc: VoiceChannel = None
+ self.vc: VoiceClient = None
self.audio_data = {}
def init(self, vc): # called under listen
- self.vc: VoiceChannel = vc
+ self.vc: VoiceClient = vc
super().init()
@Filters.container
diff --git a/discord/state.py b/discord/state.py
index 90fd97ab53..d222ba4518 100644
--- a/discord/state.py
+++ b/discord/state.py
@@ -1139,10 +1139,8 @@ def parse_guild_member_add(self, data) -> None:
if self.member_cache_flags.joined:
guild._add_member(member)
- try:
+ if guild._member_count is not None:
guild._member_count += 1
- except AttributeError:
- pass
self.dispatch("member_join", member)
@@ -1152,10 +1150,8 @@ def parse_guild_member_remove(self, data) -> None:
guild = self._get_guild(int(data["guild_id"]))
if guild is not None:
- try:
+ if guild._member_count is not None:
guild._member_count -= 1
- except AttributeError:
- pass
member = guild.get_member(user.id)
if member is not None:
@@ -1256,8 +1252,10 @@ def is_guild_evicted(self, guild) -> bool:
return guild.id not in self._guilds
async def chunk_guild(self, guild, *, wait=True, cache=None):
+ # Note: This method makes an API call without timeout, and should be used in
+ # conjunction with `asyncio.wait_for(..., timeout=...)`.
cache = cache or self.member_cache_flags.joined
- request = self._chunk_requests.get(guild.id)
+ request = self._chunk_requests.get(guild.id) # nosec B113
if request is None:
self._chunk_requests[guild.id] = request = ChunkRequest(
guild.id, self.loop, self._get_guild, cache=cache
@@ -1635,13 +1633,20 @@ def parse_webhooks_update(self, data) -> None:
)
return
- channel = guild.get_channel(int(data["channel_id"]))
- if channel is not None:
- self.dispatch("webhooks_update", channel)
+ channel_id = data["channel_id"]
+ if channel_id is not None:
+ channel = guild.get_channel(int(channel_id))
+ if channel is not None:
+ self.dispatch("webhooks_update", channel)
+ else:
+ _log.debug(
+ "WEBHOOKS_UPDATE referencing an unknown channel ID: %s. Discarding.",
+ data["channel_id"],
+ )
else:
_log.debug(
- "WEBHOOKS_UPDATE referencing an unknown channel ID: %s. Discarding.",
- data["channel_id"],
+ "WEBHOOKS_UPDATE channel ID was null for guild: %s. Discarding.",
+ data["guild_id"],
)
def parse_stage_instance_create(self, data) -> None:
diff --git a/discord/team.py b/discord/team.py
index 10432a4d01..58f638e425 100644
--- a/discord/team.py
+++ b/discord/team.py
@@ -108,7 +108,7 @@ class TeamMember(BaseUser):
.. describe:: str(x)
- Returns the team member's name with discriminator.
+ Returns the team member's name with discriminator or global_name.
.. versionadded:: 1.3
@@ -120,6 +120,14 @@ class TeamMember(BaseUser):
The team member's unique ID.
discriminator: :class:`str`
The team member's discriminator. This is given when the username has conflicts.
+
+ .. note::
+
+ If the user has migrated to the new username system, this will always be "0".
+ global_name: :class:`str`
+ The team member's global name.
+
+ .. versionadded:: 2.5
avatar: Optional[:class:`str`]
The avatar hash the team member has. Could be ``None``.
bot: :class:`bool`
@@ -141,6 +149,16 @@ def __init__(self, team: Team, state: ConnectionState, data: TeamMemberPayload):
super().__init__(state=state, data=data["user"])
def __repr__(self) -> str:
+ if self.is_migrated:
+ if self.global_name is not None:
+ return (
+ f"<{self.__class__.__name__} id={self.id} username={self.name!r} "
+ f"global_name={self.global_name!r} membership_state={self.membership_state!r}>"
+ )
+ return (
+ f"<{self.__class__.__name__} id={self.id} username={self.name!r} "
+ f"membership_state={self.membership_state!r}>"
+ )
return (
f"<{self.__class__.__name__} id={self.id} name={self.name!r} "
f"discriminator={self.discriminator!r} membership_state={self.membership_state!r}>"
diff --git a/discord/threads.py b/discord/threads.py
index 6eb1bfc0a9..9a65997e52 100644
--- a/discord/threads.py
+++ b/discord/threads.py
@@ -247,6 +247,9 @@ def _update(self, data):
except KeyError:
pass
+ self._applied_tags: list[int] = [
+ int(tag_id) for tag_id in data.get("applied_tags", [])
+ ]
self.flags: ChannelFlags = ChannelFlags._from_value(data.get("flags", 0))
self.slowmode_delay = data.get("rate_limit_per_user", 0)
@@ -611,7 +614,7 @@ async def edit(
Editing the thread requires :attr:`.Permissions.manage_threads`. The thread
creator can also edit ``name``, ``archived`` or ``auto_archive_duration``.
Note that if the thread is locked then only those with :attr:`.Permissions.manage_threads`
- can unarchive a thread.
+ can send messages in it or unarchive a thread.
The thread must be unarchived to be edited.
diff --git a/discord/types/audit_log.py b/discord/types/audit_log.py
index 7e00526e6b..c0b1303947 100644
--- a/discord/types/audit_log.py
+++ b/discord/types/audit_log.py
@@ -267,6 +267,9 @@ class AuditEntryInfo(TypedDict):
id: Snowflake
type: Literal["0", "1"]
role_name: str
+ application_id: Snowflake
+ auto_moderation_rule_name: str
+ auto_moderation_rule_trigger_type: str
class AuditLogEntry(TypedDict):
diff --git a/discord/types/automod.py b/discord/types/automod.py
index 4632c8d8c4..4f13b46ae0 100644
--- a/discord/types/automod.py
+++ b/discord/types/automod.py
@@ -47,6 +47,7 @@ class AutoModTriggerMetadata(TypedDict, total=False):
class AutoModActionMetadata(TypedDict, total=False):
channel_id: Snowflake
duration_seconds: int
+ custom_message: str
class AutoModAction(TypedDict):
diff --git a/discord/types/interactions.py b/discord/types/interactions.py
index 458247709d..ad891e3203 100644
--- a/discord/types/interactions.py
+++ b/discord/types/interactions.py
@@ -39,6 +39,7 @@
if TYPE_CHECKING:
from .message import AllowedMentions, Message
+ from ..interactions import InteractionChannel
from .._typed_dict import NotRequired, TypedDict
@@ -196,6 +197,7 @@ class Interaction(TypedDict):
data: NotRequired[InteractionData]
guild_id: NotRequired[Snowflake]
channel_id: NotRequired[Snowflake]
+ channel: NotRequired[InteractionChannel]
member: NotRequired[Member]
user: NotRequired[User]
message: NotRequired[Message]
diff --git a/discord/types/message.py b/discord/types/message.py
index 4d6d295d09..b141531e9a 100644
--- a/discord/types/message.py
+++ b/discord/types/message.py
@@ -66,6 +66,8 @@ class Attachment(TypedDict):
size: int
url: str
proxy_url: str
+ duration_secs: NotRequired[float]
+ waveform: NotRequired[str]
MessageActivityType = Literal[1, 2, 3, 5]
diff --git a/discord/types/raw_models.py b/discord/types/raw_models.py
index 405cce4123..453e75cf17 100644
--- a/discord/types/raw_models.py
+++ b/discord/types/raw_models.py
@@ -134,7 +134,7 @@ class ThreadMembersUpdateEvent(TypedDict):
class AuditLogEntryEvent(TypedDict):
id: Snowflake
- user_id: Snowflake
+ user_id: NotRequired[Snowflake]
guild_id: Snowflake
target_id: NotRequired[Snowflake]
action_type: int
diff --git a/discord/types/user.py b/discord/types/user.py
index 233eb4e4aa..6398fe1fc8 100644
--- a/discord/types/user.py
+++ b/discord/types/user.py
@@ -33,10 +33,11 @@ class PartialUser(TypedDict):
id: Snowflake
username: str
discriminator: str
+ global_name: str | None
avatar: str | None
-PremiumType = Literal[0, 1, 2]
+PremiumType = Literal[0, 1, 2, 3]
class User(PartialUser, total=False):
diff --git a/discord/ui/select.py b/discord/ui/select.py
index 5b373bd188..fb55d39d1d 100644
--- a/discord/ui/select.py
+++ b/discord/ui/select.py
@@ -334,10 +334,13 @@ def values(
| list[Member | User | Role]
| list[GuildChannel | Thread]
):
- """Union[List[:class:`str`], List[Union[:class:`discord.Member`, :class:`discord.User`]], List[:class:`discord.Role`]],
- List[Union[:class:`discord.Member`, :class:`discord.User`, :class:`discord.Role`]], List[:class:`discord.abc.GuildChannel`]]:
- A list of values that have been selected by the user.
+ """List[:class:`str`] | List[:class:`discord.Member` | :class:`discord.User`]] | List[:class:`discord.Role`]] |
+ List[:class:`discord.Member` | :class:`discord.User` | :class:`discord.Role`]] | List[:class:`discord.abc.GuildChannel`] | None:
+ A list of values that have been selected by the user. This will be ``None`` if the select has not been interacted with yet.
"""
+ if self._interaction is None:
+ # The select has not been interacted with yet
+ return None
select_type = self._underlying.type
if select_type is ComponentType.string_select:
return self._selected_values
diff --git a/discord/ui/view.py b/discord/ui/view.py
index 8172e726f8..5bf96f2612 100644
--- a/discord/ui/view.py
+++ b/discord/ui/view.py
@@ -141,6 +141,9 @@ class View:
message: Optional[:class:`.Message`]
The message that this view is attached to.
If ``None`` then the view has not been sent with a message.
+ parent: Optional[:class:`.Interaction`]
+ The parent interaction which this view was sent from.
+ If ``None`` then the view was not sent using :meth:`InteractionResponse.send_message`.
"""
__discord_ui_view__: ClassVar[bool] = True
@@ -187,6 +190,7 @@ def __init__(
self.__timeout_task: asyncio.Task[None] | None = None
self.__stopped: asyncio.Future[bool] = loop.create_future()
self._message: Message | InteractionMessage | None = None
+ self.parent: Interaction | None = None
def __repr__(self) -> str:
return f"<{self.__class__.__name__} timeout={self.timeout} children={len(self.children)}>"
@@ -363,9 +367,12 @@ async def on_timeout(self) -> None:
A callback that is called when a view's timeout elapses without being explicitly stopped.
"""
if self.disable_on_timeout:
- if self._message:
- self.disable_all_items()
- await self._message.edit(view=self)
+ self.disable_all_items()
+ message = self._message or self.parent
+ if message:
+ m = await message.edit(view=self)
+ if m:
+ self._message = m
async def on_check_failure(self, interaction: Interaction) -> None:
"""|coro|
@@ -438,6 +445,9 @@ def _dispatch_item(self, item: Item, interaction: Interaction):
if self.__stopped.done():
return
+ if interaction.message:
+ self.message = interaction.message
+
asyncio.create_task(
self._scheduled_task(item, interaction),
name=f"discord-ui-view-dispatch-{self.id}",
diff --git a/discord/user.py b/discord/user.py
index 540032f0b9..c3dee8eeb7 100644
--- a/discord/user.py
+++ b/discord/user.py
@@ -31,7 +31,6 @@
from .asset import Asset
from .colour import Colour
-from .enums import DefaultAvatar
from .flags import PublicUserFlags
from .utils import MISSING, _bytes_to_base64_data, snowflake_time
@@ -64,6 +63,7 @@ class BaseUser(_UserTag):
"name",
"id",
"discriminator",
+ "global_name",
"_avatar",
"_banner",
"_accent_colour",
@@ -77,6 +77,7 @@ class BaseUser(_UserTag):
name: str
id: int
discriminator: str
+ global_name: str | None
bot: bool
system: bool
_state: ConnectionState
@@ -90,6 +91,18 @@ def __init__(self, *, state: ConnectionState, data: UserPayload) -> None:
self._update(data)
def __repr__(self) -> str:
+ if self.is_migrated:
+ if self.global_name is not None:
+ return (
+ ""
+ )
+ return (
+ ""
+ )
return (
" str:
)
def __str__(self) -> str:
- return f"{self.name}#{self.discriminator}"
+ return (
+ f"{self.name}#{self.discriminator}"
+ if not self.is_migrated
+ else (
+ f"{self.name} ({self.global_name})"
+ if self.global_name is not None
+ else self.name
+ )
+ )
def __eq__(self, other: Any) -> bool:
return isinstance(other, _UserTag) and other.id == self.id
- def __ne__(self, other: Any) -> bool:
- return not self.__eq__(other)
-
def __hash__(self) -> int:
return self.id >> 22
@@ -112,6 +130,7 @@ def _update(self, data: UserPayload) -> None:
self.name = data["username"]
self.id = int(data["id"])
self.discriminator = data["discriminator"]
+ self.global_name = data.get("global_name", None) or None
self._avatar = data["avatar"]
self._banner = data.get("banner", None)
self._accent_colour = data.get("accent_color", None)
@@ -126,6 +145,7 @@ def _copy(cls: type[BU], user: BU) -> BU:
self.name = user.name
self.id = user.id
self.discriminator = user.discriminator
+ self.global_name = user.global_name
self._avatar = user._avatar
self._banner = user._banner
self._accent_colour = user._accent_colour
@@ -141,6 +161,7 @@ def _to_minimal_user_json(self) -> dict[str, Any]:
"id": self.id,
"avatar": self._avatar,
"discriminator": self.discriminator,
+ "global_name": self.global_name,
"bot": self.bot,
}
@@ -171,11 +192,11 @@ def avatar(self) -> Asset | None:
@property
def default_avatar(self) -> Asset:
"""Returns the default avatar for a given user.
- This is calculated by the user's discriminator.
+ This is calculated by the user's ID if they're on the new username system, otherwise their discriminator.
"""
- return Asset._from_default_avatar(
- self._state, int(self.discriminator) % len(DefaultAvatar)
- )
+ eq = (self.id >> 22) if self.is_migrated else int(self.discriminator)
+ perc = 6 if self.is_migrated else 5
+ return Asset._from_default_avatar(self._state, eq % perc)
@property
def display_avatar(self) -> Asset:
@@ -264,12 +285,9 @@ def created_at(self) -> datetime:
@property
def display_name(self) -> str:
"""Returns the user's display name.
-
- For regular users this is just their username, but
- if they have a guild specific nickname then that
- is returned instead.
+ This will be their global name if set, otherwise their username.
"""
- return self.name
+ return self.global_name or self.name
def mentioned_in(self, message: Message) -> bool:
"""Checks if the user is mentioned in the specified message.
@@ -290,6 +308,11 @@ def mentioned_in(self, message: Message) -> bool:
return any(user.id == self.id for user in message.mentions)
+ @property
+ def is_migrated(self) -> bool:
+ """Checks whether the user is already migrated to global name."""
+ return self.discriminator == "0"
+
class ClientUser(BaseUser):
"""Represents your Discord user.
@@ -310,7 +333,7 @@ class ClientUser(BaseUser):
.. describe:: str(x)
- Returns the user's name with discriminator.
+ Returns the user's name with discriminator or global_name.
Attributes
----------
@@ -320,13 +343,20 @@ class ClientUser(BaseUser):
The user's unique ID.
discriminator: :class:`str`
The user's discriminator. This is given when the username has conflicts.
+
+ .. note::
+
+ If the user has migrated to the new username system, this will always be 0.
+ global_name: :class:`str`
+ The user's global name.
+
+ .. versionadded:: 2.5
bot: :class:`bool`
Specifies if the user is a bot account.
system: :class:`bool`
Specifies if the user is a system user (i.e. represents Discord officially).
.. versionadded:: 1.3
-
verified: :class:`bool`
Specifies if the user's email is verified.
locale: Optional[:class:`str`]
@@ -347,6 +377,18 @@ def __init__(self, *, state: ConnectionState, data: UserPayload) -> None:
super().__init__(state=state, data=data)
def __repr__(self) -> str:
+ if self.is_migrated:
+ if self.global_name is not None:
+ return (
+ ""
+ )
+ return (
+ ""
+ )
return (
" None:
self._flags = data.get("flags", 0)
self.mfa_enabled = data.get("mfa_enabled", False)
+ # TODO: Username might not be able to edit anymore.
async def edit(
self, *, username: str = MISSING, avatar: bytes = MISSING
) -> ClientUser:
@@ -432,7 +475,7 @@ class User(BaseUser, discord.abc.Messageable):
.. describe:: str(x)
- Returns the user's name with discriminator.
+ Returns the user's name with discriminator or global_name.
Attributes
----------
@@ -442,6 +485,14 @@ class User(BaseUser, discord.abc.Messageable):
The user's unique ID.
discriminator: :class:`str`
The user's discriminator. This is given when the username has conflicts.
+
+ .. note::
+
+ If the user has migrated to the new username system, this will always be "0".
+ global_name: :class:`str`
+ The user's global name.
+
+ .. versionadded:: 2.5
bot: :class:`bool`
Specifies if the user is a bot account.
system: :class:`bool`
@@ -455,6 +506,13 @@ def __init__(self, *, state: ConnectionState, data: UserPayload) -> None:
self._stored: bool = False
def __repr__(self) -> str:
+ if self.is_migrated:
+ if self.global_name is not None:
+ return (
+ ""
+ )
+ return ""
return (
""
diff --git a/discord/utils.py b/discord/utils.py
index 20add93227..7e41b6d04f 100644
--- a/discord/utils.py
+++ b/discord/utils.py
@@ -66,11 +66,11 @@
from .errors import HTTPException, InvalidArgument
try:
- import orjson
+ import msgspec
except ModuleNotFoundError:
- HAS_ORJSON = False
+ HAS_MSGSPEC = False
else:
- HAS_ORJSON = True
+ HAS_MSGSPEC = True
__all__ = (
@@ -142,6 +142,7 @@ def __get__(self, instance, owner):
from .abc import Snowflake
from .commands.context import AutocompleteContext
+ from .commands.options import OptionChoice
from .invite import Invite
from .permissions import Permissions
from .template import Template
@@ -156,6 +157,7 @@ class _RequestLike(Protocol):
else:
cached_property = _cached_property
AutocompleteContext = Any
+ OptionChoice = Any
T = TypeVar("T")
@@ -660,12 +662,12 @@ def _bytes_to_base64_data(data: bytes) -> str:
return fmt.format(mime=mime, data=b64)
-if HAS_ORJSON:
+if HAS_MSGSPEC:
def _to_json(obj: Any) -> str: # type: ignore
- return orjson.dumps(obj).decode("utf-8")
+ return msgspec.json.encode(obj).decode("utf-8")
- _from_json = orjson.loads # type: ignore
+ _from_json = msgspec.json.decode # type: ignore
else:
@@ -1298,7 +1300,7 @@ def generate_snowflake(dt: datetime.datetime | None = None) -> int:
return int(dt.timestamp() * 1000 - DISCORD_EPOCH) << 22 | 0x3FFFFF
-V = Union[Iterable[str], Iterable[int], Iterable[float]]
+V = Union[Iterable[OptionChoice], Iterable[str], Iterable[int], Iterable[float]]
AV = Awaitable[V]
Values = Union[V, Callable[[AutocompleteContext], Union[V, AV]], AV]
AutocompleteFunc = Callable[[AutocompleteContext], AV]
diff --git a/discord/voice_client.py b/discord/voice_client.py
index 110977d214..437311122a 100644
--- a/discord/voice_client.py
+++ b/discord/voice_client.py
@@ -624,7 +624,11 @@ def get_ssrc(self, user_id):
]
def play(
- self, source: AudioSource, *, after: Callable[[Exception | None], Any] = None
+ self,
+ source: AudioSource,
+ *,
+ after: Callable[[Exception | None], Any] = None,
+ wait_finish: bool = False,
) -> None:
"""Plays an :class:`AudioSource`.
@@ -643,6 +647,14 @@ def play(
The finalizer that is called after the stream is exhausted.
This function must have a single parameter, ``error``, that
denotes an optional exception that was raised during playing.
+ wait_finish: bool
+ If True, an awaitable will be returned, which can be used to wait for
+ audio to stop playing. This awaitable will return an exception if raised,
+ or None when no exception is raised.
+
+ If False, None is returned and the function does not block.
+
+ .. versionadded:: v2.5
Raises
------
@@ -668,8 +680,22 @@ def play(
if not self.encoder and not source.is_opus():
self.encoder = opus.Encoder()
+ future = None
+ if wait_finish:
+ future = asyncio.Future()
+ after_callback = after
+
+ def _after(exc: Exception | None):
+ if callable(after_callback):
+ after_callback(exc)
+
+ future.set_result(exc)
+
+ after = _after
+
self._player = AudioPlayer(source, self, after=after)
self._player.start()
+ return future
def unpack_audio(self, data):
"""Takes an audio packet received from Discord and decodes it into pcm audio data.
@@ -700,7 +726,7 @@ def unpack_audio(self, data):
self.decoder.decode(data)
- def start_recording(self, sink, callback, *args):
+ def start_recording(self, sink, callback, *args, sync_start: bool = False):
"""The bot will begin recording audio from the current voice channel it is in.
This function uses a thread so the current code line will not be stopped.
Must be in a voice channel to use.
@@ -716,6 +742,9 @@ def start_recording(self, sink, callback, *args):
A function which is called after the bot has stopped recording.
*args:
Args which will be passed to the callback function.
+ sync_start: :class:`bool`
+ If True, the recordings of subsequent users will start with silence.
+ This is useful for recording audio just as it was heard.
Raises
------
@@ -738,6 +767,7 @@ def start_recording(self, sink, callback, *args):
self.decoder = opus.DecodeManager(self)
self.decoder.start()
self.recording = True
+ self.sync_start = sync_start
self.sink = sink
sink.init(self)
@@ -796,8 +826,9 @@ def recv_audio(self, sink, callback, *args):
# it by user, handles pcm files and
# silence that should be added.
- self.user_timestamps = {}
+ self.user_timestamps: dict[int, tuple[int, float]] = {}
self.starting_time = time.perf_counter()
+ self.first_packet_timestamp: float
while self.recording:
ready, _, err = select.select([self.socket], [], [self.socket], 0.01)
if not ready:
@@ -815,27 +846,46 @@ def recv_audio(self, sink, callback, *args):
self.stopping_time = time.perf_counter()
self.sink.cleanup()
- callback = asyncio.run_coroutine_threadsafe(
- callback(self.sink, *args), self.loop
- )
+ callback = asyncio.run_coroutine_threadsafe(callback(sink, *args), self.loop)
result = callback.result()
if result is not None:
print(result)
- def recv_decoded_audio(self, data):
- if data.ssrc not in self.user_timestamps:
- self.user_timestamps.update({data.ssrc: data.timestamp})
- # Add silence when they were not being recorded.
- silence = 0
- else:
- silence = data.timestamp - self.user_timestamps[data.ssrc] - 960
- self.user_timestamps[data.ssrc] = data.timestamp
+ def recv_decoded_audio(self, data: RawData):
+ # Add silence when they were not being recorded.
+ if data.ssrc not in self.user_timestamps: # First packet from user
+ if (
+ not self.user_timestamps or not self.sync_start
+ ): # First packet from anyone
+ self.first_packet_timestamp = data.receive_time
+ silence = 0
+
+ else: # Previously received a packet from someone else
+ silence = (
+ (data.receive_time - self.first_packet_timestamp) * 48000
+ ) - 960
+
+ else: # Previously received a packet from user
+ dRT = (
+ data.receive_time - self.user_timestamps[data.ssrc][1]
+ ) * 48000 # delta receive time
+ dT = data.timestamp - self.user_timestamps[data.ssrc][0] # delta timestamp
+ diff = abs(100 - dT * 100 / dRT)
+ if (
+ diff > 60 and dT != 960
+ ): # If the difference in change is more than 60% threshold
+ silence = dRT - 960
+ else:
+ silence = dT - 960
+
+ self.user_timestamps.update({data.ssrc: (data.timestamp, data.receive_time)})
data.decoded_data = (
- struct.pack(" ExecuteWebhookParameters:
if files is not MISSING and file is not MISSING:
raise TypeError("Cannot mix file and files keyword arguments.")
@@ -648,8 +650,9 @@ def handle_message_parameters(
payload["avatar_url"] = str(avatar_url)
if username:
payload["username"] = username
- if ephemeral:
- payload["flags"] = 64
+
+ flags = MessageFlags(suppress_embeds=suppress, ephemeral=ephemeral)
+ payload["flags"] = flags.value
if allowed_mentions:
if previous_allowed_mentions is not None:
@@ -827,6 +830,7 @@ async def edit(
attachments: list[Attachment] = MISSING,
view: View | None = MISSING,
allowed_mentions: AllowedMentions | None = None,
+ suppress: bool | None = MISSING,
) -> WebhookMessage:
"""|coro|
@@ -868,6 +872,8 @@ async def edit(
the view is removed.
.. versionadded:: 2.0
+ suppress: Optional[:class:`bool`]
+ Whether to suppress embeds for the message.
Returns
-------
@@ -898,6 +904,9 @@ async def edit(
if attachments is MISSING:
attachments = self.attachments or MISSING
+ if suppress is MISSING:
+ suppress = self.flags.suppress_embeds
+
return await self._state._webhook.edit_message(
self.id,
content=content,
@@ -909,6 +918,7 @@ async def edit(
view=view,
allowed_mentions=allowed_mentions,
thread=thread,
+ suppress=suppress,
)
async def delete(self, *, delay: float | None = None) -> None:
@@ -1267,6 +1277,7 @@ def _as_follower(cls, data, *, channel, user) -> Webhook:
"user": {
"username": user.name,
"discriminator": user.discriminator,
+ "global_name": user.global_name,
"id": user.id,
"avatar": user._avatar,
},
@@ -1556,6 +1567,7 @@ async def send(
thread: Snowflake = MISSING,
thread_name: str | None = None,
wait: Literal[True],
+ delete_after: float = None,
) -> WebhookMessage:
...
@@ -1577,6 +1589,7 @@ async def send(
thread: Snowflake = MISSING,
thread_name: str | None = None,
wait: Literal[False] = ...,
+ delete_after: float = None,
) -> None:
...
@@ -1844,6 +1857,7 @@ async def edit_message(
view: View | None = MISSING,
allowed_mentions: AllowedMentions | None = None,
thread: Snowflake | None = MISSING,
+ suppress: bool = False,
) -> WebhookMessage:
"""|coro|
@@ -1891,6 +1905,8 @@ async def edit_message(
.. versionadded:: 2.0
thread: Optional[:class:`~discord.abc.Snowflake`]
The thread that contains the message.
+ suppress: :class:`bool`
+ Whether to suppress embeds for the message.
Returns
-------
@@ -1938,6 +1954,7 @@ async def edit_message(
view=view,
allowed_mentions=allowed_mentions,
previous_allowed_mentions=previous_mentions,
+ suppress=suppress,
)
thread_id: int | None = None
diff --git a/discord/webhook/sync.py b/discord/webhook/sync.py
index 042130ac25..9e812b5709 100644
--- a/discord/webhook/sync.py
+++ b/discord/webhook/sync.py
@@ -472,6 +472,7 @@ def edit(
file: File = MISSING,
files: list[File] = MISSING,
allowed_mentions: AllowedMentions | None = None,
+ suppress: bool | None = MISSING,
) -> SyncWebhookMessage:
"""Edits the message.
@@ -492,6 +493,8 @@ def edit(
allowed_mentions: :class:`AllowedMentions`
Controls the mentions being processed in this message.
See :meth:`.abc.Messageable.send` for more information.
+ suppress: Optional[:class:`bool`]
+ Whether to suppress embeds for the message.
Returns
-------
@@ -517,6 +520,9 @@ def edit(
elif isinstance(self.channel, Thread):
thread = Object(self.channel.id)
+ if suppress is MISSING:
+ suppress = self.flags.suppress_embeds
+
return self._state._webhook.edit_message(
self.id,
content=content,
@@ -526,6 +532,7 @@ def edit(
files=files,
allowed_mentions=allowed_mentions,
thread=thread,
+ suppress=suppress,
)
def delete(self, *, delay: float | None = None) -> None:
@@ -952,6 +959,7 @@ def send(
thread: Snowflake = MISSING,
thread_name: str | None = None,
wait: Literal[False] = ...,
+ suppress: bool = MISSING,
) -> None:
...
@@ -970,6 +978,7 @@ def send(
thread: Snowflake = MISSING,
thread_name: str | None = None,
wait: bool = False,
+ suppress: bool = False,
) -> SyncWebhookMessage | None:
"""Sends a message using the webhook.
@@ -1022,6 +1031,8 @@ def send(
The name of the thread to create. Only works for forum channels.
.. versionadded:: 2.0
+ suppress: :class:`bool`
+ Whether to suppress embeds for the message.
Returns
-------
@@ -1070,6 +1081,7 @@ def send(
embeds=embeds,
allowed_mentions=allowed_mentions,
previous_allowed_mentions=previous_mentions,
+ suppress=suppress,
)
adapter: WebhookAdapter = _get_webhook_adapter()
thread_id: int | None = None
@@ -1151,6 +1163,7 @@ def edit_message(
files: list[File] = MISSING,
allowed_mentions: AllowedMentions | None = None,
thread: Snowflake | None = MISSING,
+ suppress: bool = False,
) -> SyncWebhookMessage:
"""Edits a message owned by this webhook.
@@ -1211,6 +1224,7 @@ def edit_message(
embeds=embeds,
allowed_mentions=allowed_mentions,
previous_allowed_mentions=previous_mentions,
+ suppress=suppress,
)
adapter: WebhookAdapter = _get_webhook_adapter()
diff --git a/discord/widget.py b/discord/widget.py
index 257846092a..b6bcf0f013 100644
--- a/discord/widget.py
+++ b/discord/widget.py
@@ -202,6 +202,16 @@ def __init__(
self.connected_channel: WidgetChannel | None = connected_channel
def __repr__(self) -> str:
+ if self.is_migrated:
+ if self.global_name is not None:
+ return (
+ f""
+ )
+ return (
+ f""
+ )
return (
f""
@@ -210,7 +220,9 @@ def __repr__(self) -> str:
@property
def display_name(self) -> str:
"""Returns the member's display name."""
- return self.nick or self.name
+ return self.nick or (
+ self.global_name or self.name if self.is_migrated else self.name
+ )
class Widget:
diff --git a/docs/api/data_classes.rst b/docs/api/data_classes.rst
index f05b38d646..ab9e8d678d 100644
--- a/docs/api/data_classes.rst
+++ b/docs/api/data_classes.rst
@@ -83,6 +83,16 @@ Embed
.. autoclass:: EmbedFooter
:members:
+.. attributetable:: EmbedMedia
+
+.. autoclass:: EmbedMedia
+ :members:
+
+.. attributetable:: EmbedProvider
+
+.. autoclass:: EmbedProvider
+ :members:
+
Flags
diff --git a/docs/api/enums.rst b/docs/api/enums.rst
index 49c105878f..e2620ed8e5 100644
--- a/docs/api/enums.rst
+++ b/docs/api/enums.rst
@@ -1512,6 +1512,86 @@ of :class:`enum.Enum`.
.. versionadded:: 2.0
+ .. attribute:: auto_moderation_rule_create
+
+ A guild auto moderation rule was created.
+
+ Possible attributes for :class:`AuditLogDiff`:
+
+ - :attr:`~AuditLogDiff.name`
+ - :attr:`~AuditLogDiff.enabled`
+ - :attr:`~AuditLogDiff.trigger_type`
+ - :attr:`~AuditLogDiff.event_type`
+ - :attr:`~AuditLogDiff.trigger_metadata`
+ - :attr:`~AuditLogDiff.actions`
+ - :attr:`~AuditLogDiff.exempt_roles`
+ - :attr:`~AuditLogDiff.exempt_channels`
+
+ .. versionadded:: 2.5
+
+ .. attribute:: auto_moderation_rule_update
+
+ A guild auto moderation rule was updated.
+
+ Possible attributes for :class:`AuditLogDiff`:
+
+ - :attr:`~AuditLogDiff.name`
+ - :attr:`~AuditLogDiff.enabled`
+ - :attr:`~AuditLogDiff.trigger_type`
+ - :attr:`~AuditLogDiff.trigger_metadata`
+ - :attr:`~AuditLogDiff.actions`
+ - :attr:`~AuditLogDiff.exempt_roles`
+ - :attr:`~AuditLogDiff.exempt_channels`
+
+ .. versionadded:: 2.5
+
+ .. attribute:: auto_moderation_rule_delete
+
+ A guild auto moderation rule was deleted.
+
+ Possible attributes for :class:`AuditLogDiff`:
+
+ - :attr:`~AuditLogDiff.name`
+ - :attr:`~AuditLogDiff.enabled`
+ - :attr:`~AuditLogDiff.trigger_type`
+ - :attr:`~AuditLogDiff.event_type`
+ - :attr:`~AuditLogDiff.trigger_metadata`
+ - :attr:`~AuditLogDiff.actions`
+ - :attr:`~AuditLogDiff.exempt_roles`
+ - :attr:`~AuditLogDiff.exempt_channels`
+
+ .. versionadded:: 2.5
+
+ .. attribute:: auto_moderation_block_message
+
+ A message was blocked by auto moderation.
+
+ .. versionadded:: 2.5
+
+ .. attribute:: auto_moderation_flag_to_channel
+
+ A message was flagged by auto moderation.
+
+ .. versionadded:: 2.5
+
+ .. attribute:: auto_moderation_user_communication_disabled
+
+ A member was timed out by auto moderation.
+
+ .. versionadded:: 2.5
+
+ .. attribute:: creator_monetization_request_created
+
+ A creator monetization request was created.
+
+ .. versionadded:: 2.5
+
+ .. attribute:: creator_monetization_terms_accepted
+
+ The creator monetization terms were accepted.
+
+ .. versionadded:: 2.5
+
.. class:: AuditLogActionCategory
@@ -1878,6 +1958,18 @@ of :class:`enum.Enum`.
Represents the embedded application Fishington.io.
+ .. attribute:: gartic_phone
+
+ Represents the embedded application Gartic Phone.
+
+ .. versionadded:: 2.5
+
+ .. attribute:: jamspace
+
+ Represents the embedded application Jamspace.
+
+ .. versionadded:: 2.5
+
.. attribute:: know_what_i_meme
Represents the embedded application Know What I Meme.
diff --git a/docs/api/events.rst b/docs/api/events.rst
index 4903ae8f92..2eb92c19b5 100644
--- a/docs/api/events.rst
+++ b/docs/api/events.rst
@@ -7,11 +7,11 @@ Event Reference
This section outlines the different types of events listened by :class:`Client`.
-There are 4 ways to register an event, the first way is through the use of
+There are 3 ways to register an event, the first way is through the use of
:meth:`Client.event`. The second way is through subclassing :class:`Client` and
overriding the specific events. The third way is through the use of :meth:`Client.listen`,
which can be used to assign multiple event handlers instead of only one like in :meth:`Client.event`.
-The fourth way is through the use of :meth:`Client.once`, which serves as a one-time event listener. For example:
+For example:
.. code-block:: python
:emphasize-lines: 17, 22
@@ -41,10 +41,10 @@ The fourth way is through the use of :meth:`Client.once`, which serves as a one-
async def on_message(message: discord.Message):
print(f"Received {message.content}")
- # Runs only for the 1st 'on_message' event. Can be useful for listening to 'on_ready'
- @client.once()
- async def message(message: discord.Message):
- print(f"Received {message.content}")
+ # Runs only for the 1st event dispatch. Can be useful for listening to 'on_ready'
+ @client.listen(once=True)
+ async def on_ready():
+ print("Client is ready!")
If an event handler raises an exception, :func:`on_error` will be called
@@ -735,6 +735,7 @@ Members/Users
- avatar
- username
- discriminator
+ - global_name
This requires :attr:`Intents.members` to be enabled.
diff --git a/docs/ext/bridge/api.rst b/docs/ext/bridge/api.rst
index 327e6600fb..d8f4d78821 100644
--- a/docs/ext/bridge/api.rst
+++ b/docs/ext/bridge/api.rst
@@ -151,3 +151,9 @@ BridgeContext Subclasses
.. autoclass:: discord.ext.bridge.BridgeExtContext
:members:
+
+.. attributetable:: discord.ext.bridge.Context
+
+.. data:: discord.ext.bridge.Context
+
+ Alias of :data:`typing.Union` [ :class:`.BridgeExtContext`, :class:`.BridgeApplicationContext` ] for typing convenience.
diff --git a/examples/audio_recording_merged.py b/examples/audio_recording_merged.py
new file mode 100644
index 0000000000..9148a67a43
--- /dev/null
+++ b/examples/audio_recording_merged.py
@@ -0,0 +1,113 @@
+import io
+
+import pydub # pip install pydub==0.25.1
+
+import discord
+from discord.sinks import MP3Sink
+
+bot = discord.Bot()
+
+
+@bot.event
+async def on_ready():
+ print(f"Logged in as {bot.user}")
+
+
+async def finished_callback(sink: MP3Sink, channel: discord.TextChannel):
+ mention_strs = []
+ audio_segs: list[pydub.AudioSegment] = []
+ files: list[discord.File] = []
+
+ longest = pydub.AudioSegment.empty()
+
+ for user_id, audio in sink.audio_data.items():
+ mention_strs.append(f"<@{user_id}>")
+
+ seg = pydub.AudioSegment.from_file(audio.file, format="mp3")
+
+ # Determine the longest audio segment
+ if len(seg) > len(longest):
+ audio_segs.append(longest)
+ longest = seg
+ else:
+ audio_segs.append(seg)
+
+ audio.file.seek(0)
+ files.append(discord.File(audio.file, filename=f"{user_id}.mp3"))
+
+ for seg in audio_segs:
+ longest = longest.overlay(seg)
+
+ with io.BytesIO() as f:
+ longest.export(f, format="mp3")
+ await channel.send(
+ f"Finished! Recorded audio for {', '.join(mention_strs)}.",
+ files=files + [discord.File(f, filename="recording.mp3")],
+ )
+
+
+@bot.command()
+async def join(ctx: discord.ApplicationContext):
+ """Join the voice channel!"""
+ voice = ctx.author.voice
+
+ if not voice:
+ return await ctx.respond("You're not in a vc right now")
+
+ await voice.channel.connect()
+
+ await ctx.respond("Joined!")
+
+
+@bot.command()
+async def start(ctx: discord.ApplicationContext):
+ """Record the voice channel!"""
+ voice = ctx.author.voice
+
+ if not voice:
+ return await ctx.respond("You're not in a vc right now")
+
+ vc: discord.VoiceClient = ctx.voice_client
+
+ if not vc:
+ return await ctx.respond(
+ "I'm not in a vc right now. Use `/join` to make me join!"
+ )
+
+ vc.start_recording(
+ MP3Sink(),
+ finished_callback,
+ ctx.channel,
+ sync_start=True, # WARNING: This feature is very unstable and may break at any time.
+ )
+
+ await ctx.respond("The recording has started!")
+
+
+@bot.command()
+async def stop(ctx: discord.ApplicationContext):
+ """Stop the recording"""
+ vc: discord.VoiceClient = ctx.voice_client
+
+ if not vc:
+ return await ctx.respond("There's no recording going on right now")
+
+ vc.stop_recording()
+
+ await ctx.respond("The recording has stopped!")
+
+
+@bot.command()
+async def leave(ctx: discord.ApplicationContext):
+ """Leave the voice channel!"""
+ vc: discord.VoiceClient = ctx.voice_client
+
+ if not vc:
+ return await ctx.respond("I'm not in a vc right now")
+
+ await vc.disconnect()
+
+ await ctx.respond("Left!")
+
+
+bot.run("TOKEN")
diff --git a/examples/converters.py b/examples/converters.py
index 4d80f5fe86..bbf0fb9e51 100644
--- a/examples/converters.py
+++ b/examples/converters.py
@@ -21,7 +21,7 @@ async def userinfo(ctx: commands.Context, user: discord.User):
# the value passed as `user` to a `discord.User` instance.
# The documentation notes what can be converted and, in the case of `discord.User`,
# you pass an ID, mention or username (discriminator optional)
- # E.g. 80088516616269824, @Danny or Danny#0007
+ # E.g. 90339695967350784, @Desch or Desch#3091
# NOTE: Type hinting acts as a converter within the `commands` framework only.
# In standard Python, it is used for documentation and IDE assistance purposes.
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 696ec91da3..2e9fac2a7c 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -1,11 +1,11 @@
-r _.txt
-pylint~=2.17.2
-pytest~=7.3.1
-pytest-asyncio~=0.21.0
+pylint~=2.17.5
+pytest~=7.4.0
+pytest-asyncio~=0.21.1
# pytest-order~=1.0.1
-mypy~=1.2.0
+mypy~=1.5.0
coverage~=7.2
-pre-commit==3.2.2
-codespell==2.2.4
-bandit==1.7.4
-flake8==6.0.0
+pre-commit==3.3.3
+codespell==2.2.5
+bandit==1.7.5
+flake8==6.1.0
diff --git a/requirements/speed.txt b/requirements/speed.txt
index f3830485ba..32c37acb7a 100644
--- a/requirements/speed.txt
+++ b/requirements/speed.txt
@@ -1,2 +1,2 @@
-orjson>=3.5.4
+msgspec~=0.18.0
aiohttp[speedups]
diff --git a/tests/test_typing_annotated.py b/tests/test_typing_annotated.py
new file mode 100644
index 0000000000..582bd4f8a0
--- /dev/null
+++ b/tests/test_typing_annotated.py
@@ -0,0 +1,86 @@
+from typing import Optional
+
+import pytest
+from typing_extensions import Annotated
+
+import discord
+from discord import ApplicationContext
+from discord.commands.core import SlashCommand, slash_command
+
+
+def test_typing_annotated():
+ async def echo(ctx, txt: Annotated[str, discord.Option()]):
+ await ctx.respond(txt)
+
+ cmd = SlashCommand(echo)
+ bot = discord.Bot()
+ bot.add_application_command(cmd)
+
+
+def test_typing_annotated_decorator():
+ bot = discord.Bot()
+
+ @bot.slash_command()
+ async def echo(ctx, txt: Annotated[str, discord.Option(description="Some text")]):
+ await ctx.respond(txt)
+
+
+def test_typing_annotated_cog():
+ class echoCog(discord.Cog):
+ def __init__(self, bot_) -> None:
+ self.bot = bot_
+ super().__init__()
+
+ @slash_command()
+ async def echo(
+ self, ctx, txt: Annotated[str, discord.Option(description="Some text")]
+ ):
+ await ctx.respond(txt)
+
+ bot = discord.Bot()
+ bot.add_cog(echoCog(bot))
+
+
+def test_typing_annotated_cog_slashgroup():
+ class echoCog(discord.Cog):
+ grp = discord.commands.SlashCommandGroup("echo")
+
+ def __init__(self, bot_) -> None:
+ self.bot = bot_
+ super().__init__()
+
+ @grp.command()
+ async def echo(
+ self, ctx, txt: Annotated[str, discord.Option(description="Some text")]
+ ):
+ await ctx.respond(txt)
+
+ bot = discord.Bot()
+ bot.add_cog(echoCog(bot))
+
+
+def test_typing_annotated_optional():
+ async def echo(ctx, txt: Annotated[Optional[str], discord.Option()]):
+ await ctx.respond(txt)
+
+ cmd = SlashCommand(echo)
+ bot = discord.Bot()
+ bot.add_application_command(cmd)
+
+
+def test_no_annotation():
+ async def echo(ctx, txt: str):
+ await ctx.respond(txt)
+
+ cmd = SlashCommand(echo)
+ bot = discord.Bot()
+ bot.add_application_command(cmd)
+
+
+def test_annotated_no_option():
+ async def echo(ctx, txt: Annotated[str, "..."]):
+ await ctx.respond(txt)
+
+ cmd = SlashCommand(echo)
+ bot = discord.Bot()
+ bot.add_application_command(cmd)