Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
f7e4821
fix: Initial Commit To Fix Command Auto Syncing
Icebluewolf Nov 5, 2025
6cabb6f
Add support for editing application info and new fields
Lumabots Nov 7, 2025
2f54fe7
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 7, 2025
fab1478
Add AppInfo.edit() and missing fields support
Lumabots Nov 7, 2025
bfa2940
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 7, 2025
9893239
Update discord/appinfo.py
Lumabots Nov 7, 2025
884cd6c
Update discord/appinfo.py
Lumabots Nov 7, 2025
41f12a2
Update discord/appinfo.py
Lumabots Nov 7, 2025
714c68a
Update discord/appinfo.py
Lumabots Nov 7, 2025
734b5ce
Update discord/appinfo.py
Lumabots Nov 7, 2025
49c90b9
Update discord/appinfo.py
Lumabots Nov 7, 2025
76eb883
Restrict icon and cover_image types to bytes or None
Lumabots Nov 7, 2025
bd91789
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 7, 2025
7e5ff54
feat(emoji): add mention property to BaseEmoji for easier emoji refer…
Lumabots Nov 7, 2025
6fc48da
Merge branch 'appinf' of https://github.com/Lumabots/pycord into appinf
Lumabots Nov 7, 2025
bde7ec3
Merge branch 'master' into appinf
Lumabots Nov 11, 2025
89d3011
refactor(AppInfo): improve integration_types_config handling and simp…
Lumabots Nov 11, 2025
dfb7907
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 11, 2025
9eecbfa
refactor(AppInfo): remove deprecated flags attribute from AppInfo class
Lumabots Nov 14, 2025
ab000ac
Merge branch 'master' into appinf
Paillat-dev Nov 16, 2025
8c0dcd7
Merge branch 'master' into appinf
Paillat-dev Nov 23, 2025
03a46e8
refactor(AppInfo): rename event_webhooks_status to _event_webhooks_st…
Lumabots Nov 25, 2025
0b603f5
refactor(AppInfo): remove setter for event_webhooks_enabled and assoc…
Lumabots Nov 25, 2025
3c398cc
Update CHANGELOG.md
Lumabots Nov 29, 2025
1b0017c
refactor: update method names and types in AppInfo and HTTPClient
Lumabots Dec 2, 2025
20cd5ed
Merge branch 'master' into appinf
Lumabots Dec 2, 2025
ef0cb75
feat: add ApplicationEventWebhookStatus enum and update AppInfo to us…
Lumabots Dec 5, 2025
5f939dd
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 5, 2025
27b5b01
Merge branch 'master' into appinf
Lumabots Dec 8, 2025
ae03d85
Update discord/appinfo.py
Lumabots Dec 8, 2025
f2d461d
refactor: simplify icon and cover_image type annotations in AppInfo
Lumabots Dec 8, 2025
7afeed1
feat: implement FlagCommand for enhanced command argument parsing
Lumabots Dec 9, 2025
630bdcd
Merge branch 'master' into appinf
Lumabots Dec 9, 2025
03700ad
Update discord/appinfo.py
Paillat-dev Dec 9, 2025
9f6d9bc
Merge branch 'master' into appinf
Paillat-dev Dec 23, 2025
10cc2a6
Merge branch 'master' into appinf
Lumabots Dec 23, 2025
30e6fa0
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 23, 2025
fc63c77
docs: add versionadded directive for 2.7 in AppInfo and enums
Lumabots Dec 23, 2025
ab26c2a
docs: 📝 Add versionadded directive for ApplicationEventWebhookStatus …
Lumabots Dec 23, 2025
92a9616
docs: 📝 Update versionadded directive to 2.7.1 for AppInfo attributes…
Lumabots Dec 29, 2025
eae7a46
Merge branch 'master' into appinf
Lumabots Dec 29, 2025
143d451
Merge branch 'master' into fix-auto-sync
Paillat-dev Dec 29, 2025
94ea2c6
docs: 📝 Update attribute names for ApplicationEventWebhookStatus to l…
Lumabots Dec 30, 2025
4f7bbcc
Update discord/appinfo.py
Lumabots Jan 1, 2026
9afae37
Update discord/appinfo.py
Lumabots Jan 1, 2026
b499895
Update discord/appinfo.py
Lumabots Jan 1, 2026
97e9ab8
Update discord/appinfo.py
Lumabots Jan 1, 2026
d37080a
Update discord/appinfo.py
Lumabots Jan 1, 2026
ed843bb
Merge branch 'master' into appinf
Lumabots Jan 1, 2026
c98a56b
Update discord/appinfo.py
Lumabots Jan 4, 2026
fc3c746
Update discord/appinfo.py
Lumabots Jan 4, 2026
587921a
Update discord/appinfo.py
Lumabots Jan 4, 2026
a61568b
Merge branch 'master' into appinf
Lumabots Jan 4, 2026
5589372
Update enums.rst
Lumabots Jan 13, 2026
e871952
Update CHANGELOG.md
Lumabots Jan 13, 2026
80b65cb
Update appinfo.py
Lumabots Jan 13, 2026
8f5a9cd
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 13, 2026
066b5aa
Merge branch 'master' into appinf
Lumabots Feb 15, 2026
c90e24e
fix(appinfo): update versionadded annotations from 2.7.1 to 2.8
Lumabots Feb 15, 2026
6b4f274
fix: Apply Suggestions From Code Review
Icebluewolf Feb 17, 2026
b62ed9e
Merge branch 'fix-auto-sync' of https://github.com/Icebluewolf/pycord…
Icebluewolf Feb 17, 2026
90b0648
Merge branch 'master' into fix-auto-sync
NeloBlivion Feb 18, 2026
8a5e7f0
refactor: Apply Suggestions From Code Review
Icebluewolf Feb 22, 2026
c6d2f28
Merge branch 'master' into fix-auto-sync
Paillat-dev Feb 22, 2026
66292d8
fix: Subcommand Checking
Icebluewolf Feb 22, 2026
8914531
Merge branch 'fix-auto-sync' of https://github.com/Icebluewolf/pycord…
Icebluewolf Feb 22, 2026
b8b75b9
refactor: Use Pipe Union
Icebluewolf Feb 22, 2026
e3752ed
Merge branch 'master' into appinf
Paillat-dev Feb 23, 2026
90e69d4
Update docs/api/enums.rst
Paillat-dev Feb 23, 2026
988788a
Update discord/appinfo.py
Paillat-dev Feb 23, 2026
a8d8922
Merge branch 'master' into fix-auto-sync
Paillat-dev Feb 28, 2026
2d3df29
Merge branch 'master' into fix-auto-sync
Paillat-dev Feb 28, 2026
b3864c9
refactor: :wrench: New test setup and modernize dev installl (#3128)
Paillat-dev Feb 28, 2026
34b80a8
Merge branch 'master' into fix-auto-sync
Paillat-dev Feb 28, 2026
ae9a6c4
Merge branch 'master' into fix-auto-sync
Paillat-dev Feb 28, 2026
1118414
Merge branch 'refs/heads/pr-2994' into fix-auto-sync
Icebluewolf Mar 2, 2026
e7d0670
fix(appinfo): set default value for flags and update flags property type
Lumabots Mar 2, 2026
b20de1e
Merge branch 'master' into appinf
Lumabots Mar 2, 2026
d7424c2
Update discord/appinfo.py
Paillat-dev Mar 2, 2026
9ef07a0
Apply suggestion from @Paillat-dev
Paillat-dev Mar 2, 2026
a385c80
fix(appinfo): update types for guild and user in IntegrationTypesConfig
Lumabots Mar 2, 2026
1e3567d
Merge branch 'refs/heads/pr-2994' into fix-auto-sync
Icebluewolf Mar 2, 2026
2cdbcc3
feat: Support IntegrationTypes Defaults Via AppInfo
Icebluewolf Mar 2, 2026
da483b1
chore: Changelog
Icebluewolf Mar 2, 2026
370e485
Merge branch 'master' of https://github.com/Pycord-Development/pycord…
Icebluewolf Mar 2, 2026
5845999
feat: Implement Tests For Command Comparison
Icebluewolf Mar 3, 2026
06c12dd
chore: Apply Minor Changes From Code Review
Icebluewolf Mar 3, 2026
2f7892c
fix: Use Dummy Data In Place Of Fetching AppInfo
Icebluewolf Mar 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ These changes are available on the `master` branch, but have not yet been releas
([#3105](https://github.com/Pycord-Development/pycord/pull/3105))
- Fixed the update of a user's `avatar_decoration` to now cause an `on_user_update`
event to fire. ([#3103](https://github.com/Pycord-Development/pycord/pull/3103))
- Fixed backend logic for `sync_commands` to only sync when needed.
([#2990](https://github.com/Pycord-Development/pycord/pull/2990))

### Deprecated

Expand Down
272 changes: 200 additions & 72 deletions discord/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,12 @@
Generator,
Literal,
Mapping,
TypeAlias,
TypeVar,
)

from typing_extensions import override

from .client import Client
from .cog import CogMixin
from .commands import (
Expand Down Expand Up @@ -81,6 +84,175 @@
_log = logging.getLogger(__name__)


class DefaultComparison:
"""
Comparison rule for when there are multiple default values that should be considered equivalent when comparing 2 objects.
Allows for a custom check to be passed for further control over equality.

Attributes
----------
defaults: :class:`tuple`
The values that should be considered equivalent to each other
callback: Callable[[Any, Any], bool]
A callable that will do additional comparison on the objects if neither are a default value.
Defaults to a `==` comparison.
It should accept the 2 objects as arguments and return True if they should be considered equivalent
and False otherwise.
"""

def __init__(
self,
defaults: tuple[Any, ...],
callback: Callable[[Any, Any], bool] = lambda x, y: x == y,
):
self.defaults = defaults
self.callback = callback

def _check_defaults(self, local: Any, remote: Any) -> bool | None:
defaults = (local in self.defaults) + (remote in self.defaults)
if defaults == 2:
# Both are COMMAND_DEFAULTS, so they can be counted as the same
return True
elif defaults == 0:
# Neither are COMMAND_DEFAULTS so the callback has to be used
return None
else:
# Only one is a default, so the command must be out of sync
return False

def check(self, local: Any, remote: Any) -> bool:
"""
Compares the local and remote objects.

Returns
-------
bool
True if local and remote are deemed to be equivalent. False otherwise.
"""
if (rtn := self._check_defaults(local, remote)) is not None:
return rtn
else:
return self.callback(local, remote)


class DefaultSetComparison(DefaultComparison):
@override
def check(self, local: Any, remote: Any) -> bool:
try:
local = set(local)
except TypeError:
pass
try:
remote = set(remote)
except TypeError:
pass
return super().check(local, remote)


NestedComparison: TypeAlias = dict[str, "NestedComparison | DefaultComparison"]


def _compare_defaults(
obj: Mapping[str, Any] | Any,
match: Mapping[str, Any] | Any,
schema: NestedComparison,
) -> bool:
if not isinstance(match, Mapping) or not isinstance(obj, Mapping):
return obj == match
for field, comparison in schema.items():
remote = match.get(field, MISSING)
local = obj.get(field, MISSING)
if isinstance(comparison, dict):
if not _compare_defaults(local, remote, comparison):
return False
elif isinstance(comparison, DefaultComparison):
if not comparison.check(local, remote):
return False
return True


OPTION_DEFAULT_VALUES = ([], MISSING)


def _option_comparison_check(local: Any, remote: Any) -> bool:
matching = (local in OPTION_DEFAULT_VALUES) + (remote in OPTION_DEFAULT_VALUES)
if matching == 2:
return True
elif matching == 1:
return False
else:
return len(local) == len(remote) and all(
[
_compare_defaults(local[x], remote[x], COMMAND_OPTION_DEFAULTS)
for x in range(len(local))
]
)


CHOICES_DEFAULT_VALUES = ([], MISSING)


def _choices_comparison_check(local: Any, remote: Any) -> bool:
matching = (local in CHOICES_DEFAULT_VALUES) + (remote in CHOICES_DEFAULT_VALUES)
if matching == 2:
return True
elif matching == 1:
return False
else:
return len(local) == len(remote) and all(
[
_compare_defaults(local[x], remote[x], OPTIONS_CHOICES_DEFAULTS)
for x in range(len(local))
]
)


COMMAND_DEFAULTS: NestedComparison = {
"type": DefaultComparison((1, MISSING)),
"name": DefaultComparison(()),
"description": DefaultComparison((MISSING,)),
"name_localizations": DefaultComparison((None, {}, MISSING)),
"description_localizations": DefaultComparison((None, {}, MISSING)),
"options": DefaultComparison(OPTION_DEFAULT_VALUES, _option_comparison_check),
"default_member_permissions": DefaultComparison((None, MISSING)),
"nsfw": DefaultComparison((False, MISSING)),
# TODO: Change the below default if needed to use the correct default integration types
# Discord States That This Defaults To "your app's configured contexts"
"integration_types": DefaultSetComparison(
(MISSING, {0, 1}), lambda x, y: set(x) == set(y)
),
"contexts": DefaultSetComparison((None, MISSING), lambda x, y: set(x) == set(y)),
}
SUBCOMMAND_DEFAULTS: NestedComparison = {
"type": DefaultComparison(()),
"name": DefaultComparison(()),
"description": DefaultComparison(()),
"name_localizations": DefaultComparison((None, {}, MISSING)),
"description_localizations": DefaultComparison((None, {}, MISSING)),
"options": DefaultComparison(OPTION_DEFAULT_VALUES, _option_comparison_check),
}
COMMAND_OPTION_DEFAULTS: NestedComparison = {
"type": DefaultComparison(()),
"name": DefaultComparison(()),
"description": DefaultComparison(()),
"name_localizations": DefaultComparison((None, {}, MISSING)),
"description_localizations": DefaultComparison((None, {}, MISSING)),
"required": DefaultComparison((False, MISSING)),
"choices": DefaultComparison(CHOICES_DEFAULT_VALUES, _choices_comparison_check),
"channel_types": DefaultComparison(([], MISSING)),
"min_value": DefaultComparison((MISSING,)),
"max_value": DefaultComparison((MISSING,)),
"min_length": DefaultComparison((MISSING,)),
"max_length": DefaultComparison((MISSING,)),
"autocomplete": DefaultComparison((MISSING, False)),
}
OPTIONS_CHOICES_DEFAULTS: NestedComparison = {
"name": DefaultComparison(()),
"name_localizations": DefaultComparison((None, {}, MISSING)),
"value": DefaultComparison(()),
}


class ApplicationCommandMixin(ABC):
"""A mixin that implements common functionality for classes that need
application command compatibility.
Expand Down Expand Up @@ -239,6 +411,16 @@ def get_application_command(
return
return command

async def _get_command_defaults(self):
app_info = await self._bot.application_info()
integration_contexts = app_info.integration_types_config._to_payload().keys()

command_defaults = COMMAND_DEFAULTS.copy()
command_defaults["integration_types"] = DefaultSetComparison(
(MISSING, integration_contexts), lambda x, y: set(x) == set(y)
)
return command_defaults

async def get_desynced_commands(
self,
guild_id: int | None = None,
Expand Down Expand Up @@ -270,82 +452,27 @@ async def get_desynced_commands(
the action, including ``id``.
"""

# We can suggest the user to upsert, edit, delete, or bulk upsert the commands
updated_command_defaults = await self._get_command_defaults()

# We can suggest the user to upsert, edit, delete, or bulk upsert the commands
def _check_command(cmd: ApplicationCommand, match: Mapping[str, Any]) -> bool:
"""Returns True If Commands Are Equivalent"""
if isinstance(cmd, SlashCommandGroup):
if len(cmd.subcommands) != len(match.get("options", [])):
return True
return False
for i, subcommand in enumerate(cmd.subcommands):
match_ = next(
(
data
for data in match["options"]
if data["name"] == subcommand.name
),
MISSING,
match_ = find(
lambda x: x["name"] == subcommand.name, match["options"]
)
if match_ is not MISSING and _check_command(subcommand, match_):
return True
if match_ is not None:
return _check_command(subcommand, match_)
else:
as_dict = cmd.to_dict()
to_check = {
"nsfw": None,
"default_member_permissions": None,
"name": None,
"description": None,
"name_localizations": None,
"description_localizations": None,
"options": [
"type",
"name",
"description",
"autocomplete",
"choices",
"name_localizations",
"description_localizations",
],
"contexts": None,
"integration_types": None,
}
for check, value in to_check.items():
if type(to_check[check]) == list:
# We need to do some falsy conversion here
# The API considers False (autocomplete) and [] (choices) to be falsy values
falsy_vals = (False, [])
for opt in value:
cmd_vals = (
[val.get(opt, MISSING) for val in as_dict[check]]
if check in as_dict
else []
)
for i, val in enumerate(cmd_vals):
if val in falsy_vals:
cmd_vals[i] = MISSING
if match.get(
check, MISSING
) is not MISSING and cmd_vals != [
val.get(opt, MISSING) for val in match[check]
]:
# We have a difference
return True
elif (attr := getattr(cmd, check, None)) != (
found := match.get(check)
):
# We might have a difference
if "localizations" in check and bool(attr) == bool(found):
# unlike other attrs, localizations are MISSING by default
continue
elif (
check == "default_permission"
and attr is True
and found is None
):
# This is a special case
# TODO: Remove for perms v2
continue
return True
return False
if cmd.parent is None:
return _compare_defaults(
cmd.to_dict(), match, updated_command_defaults
)
else:
return _compare_defaults(cmd.to_dict(), match, SUBCOMMAND_DEFAULTS)

return_value = []
cmds = self.pending_application_commands.copy()
Expand Down Expand Up @@ -376,19 +503,20 @@ def _check_command(cmd: ApplicationCommand, match: Mapping[str, Any]) -> bool:
# First let's check if the commands we have locally are the same as the ones on discord
for cmd in pending:
match = registered_commands_dict.get(cmd.name)
# We don't have this command registered
if match is None:
# We don't have this command registered
return_value.append({"command": cmd, "action": "upsert"})
elif _check_command(cmd, match):
# We have a different version of the command then Discord
elif not _check_command(cmd, match):
return_value.append(
{
"command": cmd,
"action": "edit",
"id": int(registered_commands_dict[cmd.name]["id"]),
}
)
# We have this command registered and it's the same
else:
# We have this command registered but it's the same
return_value.append(
{"command": cmd, "action": None, "id": int(match["id"])}
)
Expand Down
Loading