Skip to content

Commit efbd720

Browse files
EepyElvyraToricanepre-commit-ci[bot]FayeDel
authored
fix(channel, sync): Prevent bot from crashing if it is invited without application.commands scope; allow modifying channels outside of a category (#780)
* refactor: optimize sync behavior * ooops * Update interactions/api/http/interaction.py Co-authored-by: Toricane <73972068+Toricane@users.noreply.github.com> * Update interactions/client/bot.pyi Co-authored-by: Toricane <73972068+Toricane@users.noreply.github.com> * Update interactions/api/http/scheduledEvent.py Co-authored-by: Toricane <73972068+Toricane@users.noreply.github.com> * Update interactions/client/bot.py Co-authored-by: Toricane <73972068+Toricane@users.noreply.github.com> * Update bot.py * ci: correct from checks. * fix: Fix command check for user and member decorator * Update interactions/client/bot.py Co-authored-by: Toricane <73972068+Toricane@users.noreply.github.com> * fix!: Fix autocomplete when sync is disabled * Update bot.py * Update bot.py * Update bot.pyi * ci: correct from checks. * Update bot.py * fix: fix option checks and autocomplete dispatch with command names * ci * refactor: unnecessary if checks * fix!: Fix synchronisation by properly checking attributes and their logic comparators. * refactor: Remove print statements. * refactor: move sync and autocomplete into _ready * doc: add warning * Update interactions/client/bot.py Co-authored-by: Toricane <73972068+Toricane@users.noreply.github.com> * refactor!: Consider extensions in the sync process * ci: correct from checks. * i hate merge conflicts * purge: remove debugging changes * Update interactions/client/bot.py Co-authored-by: Toricane <73972068+Toricane@users.noreply.github.com> * Update interactions/client/bot.py Co-authored-by: Toricane <73972068+Toricane@users.noreply.github.com> * fix: consider loading after bot start * ci: correct from checks. * refactor: make compare sync static * ci: correct from checks. * fix: consider sync toggle on load * fix: Fix attribute reference typo. * fix!: Attempt to fix critical error on startup with JSONException * add check * fix * refactor: change raise to warning if sync is off * ci: correct from checks. * fix!: continue * fix!: fix snyc bug if the bot does not have the `application.commands` scope; include checks for subcommands * fix: fix permission overwrite serialization and allow modifying channels without category * ci Co-authored-by: Toricane <73972068+Toricane@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: DeltaX <33706469+DeltaXWizard@users.noreply.github.com>
1 parent 570c671 commit efbd720

File tree

3 files changed

+132
-87
lines changed

3 files changed

+132
-87
lines changed

interactions/api/models/channel.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,10 @@ def __init__(self, **kwargs):
181181
else None
182182
)
183183
self.permission_overwrites = (
184-
[Overwrite(**overwrite) for overwrite in self._json.get("permission_overwrites")]
184+
[
185+
Overwrite(**overwrite) if isinstance(overwrite, dict) else overwrite
186+
for overwrite in self._json.get("permission_overwrites")
187+
]
185188
if self._json.get("permission_overwrites")
186189
else None
187190
)
@@ -349,10 +352,14 @@ async def modify(
349352
self.rate_limit_per_user if rate_limit_per_user is MISSING else rate_limit_per_user
350353
)
351354
_position = self.position if position is MISSING else position
352-
_parent_id = int(self.parent_id) if parent_id is MISSING else int(parent_id)
355+
_parent_id = (
356+
(int(self.parent_id) if self.parent_id else None)
357+
if parent_id is MISSING
358+
else int(parent_id)
359+
)
353360
_nsfw = self.nsfw if nsfw is MISSING else nsfw
354361
_permission_overwrites = (
355-
self.permission_overwrites
362+
[overwrite._json for overwrite in self.permission_overwrites]
356363
if permission_overwrites is MISSING
357364
else [overwrite._json for overwrite in permission_overwrites]
358365
)

interactions/api/models/guild.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -834,10 +834,12 @@ async def modify_channel(
834834
ch.rate_limit_per_user if rate_limit_per_user is MISSING else rate_limit_per_user
835835
)
836836
_position = ch.position if position is MISSING else position
837-
_parent_id = ch.parent_id if parent_id is MISSING else parent_id
837+
_parent_id = (
838+
(int(ch.parent_id) if ch.parent_id else None) if parent_id is MISSING else parent_id
839+
)
838840
_nsfw = ch.nsfw if nsfw is MISSING else nsfw
839841
_permission_overwrites = (
840-
ch.permission_overwrites
842+
[overwrite._json for overwrite in ch.permission_overwrites]
841843
if permission_overwrites is MISSING
842844
else [overwrite._json for overwrite in permission_overwrites]
843845
)

interactions/client/bot.py

Lines changed: 118 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ async def __compare_sync(
155155
"""
156156

157157
# sourcery skip: none-compare
158+
158159
attrs: List[str] = [
159160
name
160161
for name in ApplicationCommand.__slots__
@@ -170,6 +171,82 @@ async def __compare_sync(
170171

171172
_command: dict = {}
172173

174+
def __check_options(command, data):
175+
# sourcery skip: none-compare
176+
# sourcery no-metrics
177+
_command_option_names = [option["name"] for option in command.get("options")]
178+
_data_option_names = [option["name"] for option in data.get("options")]
179+
180+
if any(option not in _command_option_names for option in _data_option_names) or len(
181+
_data_option_names
182+
) != len(_command_option_names):
183+
return False, command
184+
185+
for option in command.get("options"):
186+
for _option in data.get("options"):
187+
if _option["name"] == option["name"]:
188+
for option_attr in option_attrs:
189+
if (
190+
option.get(option_attr)
191+
and not _option.get(option_attr)
192+
or not option.get(option_attr)
193+
and _option.get(option_attr)
194+
):
195+
return False, command
196+
elif option_attr == "choices":
197+
if not option.get("choices") or not _option.get("choices"):
198+
continue
199+
200+
_option_choice_names = [
201+
choice["name"] for choice in option.get("choices")
202+
]
203+
_data_choice_names = [
204+
choice["name"] for choice in _option.get("choices")
205+
]
206+
207+
if any(
208+
_ not in _option_choice_names for _ in _data_choice_names
209+
) or len(_data_choice_names) != len(_option_choice_names):
210+
return False, command
211+
212+
for choice in option.get("choices"):
213+
for _choice in _option.get("choices"):
214+
if choice["name"] == _choice["name"]:
215+
for choice_attr in choice_attrs:
216+
if (
217+
choice.get(choice_attr)
218+
and not _choice.get(choice_attr)
219+
or not choice.get(choice_attr)
220+
and _choice.get(choice_attr)
221+
):
222+
return False, command
223+
elif choice.get(choice_attr) != _choice.get(
224+
choice_attr
225+
):
226+
return False, command
227+
else:
228+
continue
229+
elif option_attr == "required":
230+
if (
231+
option.get(option_attr) == None # noqa: E711
232+
and _option.get(option_attr) == False # noqa: E712
233+
):
234+
# API not including if False
235+
continue
236+
237+
elif option_attr == "options":
238+
if not option.get(option_attr) and not _option.get("options"):
239+
continue
240+
_clean, _command = __check_options(option, _option)
241+
if not _clean:
242+
return _clean, _command
243+
244+
elif option.get(option_attr) != _option.get(option_attr):
245+
return False, command
246+
else:
247+
continue
248+
return True, command
249+
173250
for command in pool:
174251
if command["name"] == data["name"]:
175252
_command = command
@@ -197,83 +274,7 @@ async def __compare_sync(
197274

198275
elif command.get("options") and data.get("options"):
199276

200-
_command_option_names = [_["name"] for _ in command.get("options")]
201-
_data_option_names = [_["name"] for _ in data.get("options")]
202-
203-
if any(
204-
_ not in _command_option_names for _ in _data_option_names
205-
) or len(_data_option_names) != len(_command_option_names):
206-
clean = False
207-
return clean, _command
208-
209-
for option in command.get("options"):
210-
for _option in data.get("options"):
211-
if _option["name"] == option["name"]:
212-
for option_attr in option_attrs:
213-
if (
214-
option.get(option_attr)
215-
and not _option.get(option_attr)
216-
or not option.get(option_attr)
217-
and _option.get(option_attr)
218-
):
219-
clean = False
220-
return clean, _command
221-
elif option_attr == "choices":
222-
if not option.get("choices") or not _option.get(
223-
"choices"
224-
):
225-
continue
226-
227-
_option_choice_names = [
228-
_["name"] for _ in option.get("choices")
229-
]
230-
_data_choice_names = [
231-
_["name"] for _ in _option.get("choices")
232-
]
233-
234-
if any(
235-
_ not in _option_choice_names
236-
for _ in _data_choice_names
237-
) or len(_data_choice_names) != len(
238-
_option_choice_names
239-
):
240-
clean = False
241-
return clean, _command
242-
243-
for choice in option.get("choices"):
244-
for _choice in _option.get("choices"):
245-
if choice["name"] == _choice["name"]:
246-
for choice_attr in choice_attrs:
247-
if (
248-
choice.get(choice_attr)
249-
and not _choice.get(choice_attr)
250-
or not choice.get(choice_attr)
251-
and _choice.get(choice_attr)
252-
):
253-
clean = False
254-
return clean, _command
255-
elif choice.get(
256-
choice_attr
257-
) != _choice.get(choice_attr):
258-
clean = False
259-
return clean, _command
260-
else:
261-
continue
262-
elif option_attr == "required":
263-
if (
264-
option.get(option_attr) == None # noqa: E711
265-
and _option.get(option_attr)
266-
== False # noqa: E712
267-
):
268-
# API not including if False
269-
continue
270-
elif option.get(option_attr) != _option.get(
271-
option_attr
272-
):
273-
clean = False
274-
return clean, _command
275-
else:
276-
continue
277+
clean, _command = __check_options(command, data)
277278

278279
if not clean:
279280
return clean, _command
@@ -354,8 +355,8 @@ async def _ready(self) -> None:
354355
await self.__register_name_autocomplete()
355356

356357
ready = True
357-
except Exception as error:
358-
log.critical(f"Could not prepare the client: {error}")
358+
except Exception:
359+
log.exception("Could not prepare the client:")
359360
finally:
360361
if ready:
361362
log.debug("Client is now ready.")
@@ -398,6 +399,17 @@ async def __get_all_commands(self) -> None:
398399
application_id=self.me.id, guild_id=_id, with_localizations=True
399400
)
400401

402+
if isinstance(_cmds, dict) and _cmds.get("code"):
403+
if int(_cmds.get("code")) != 50001:
404+
raise JSONException(_cmds["code"], message=f'{_cmds["message"]} |')
405+
406+
log.warning(
407+
f"Your bot is missing access to guild with corresponding id {_id}! "
408+
"Syncing commands will not be possible until it is invited with "
409+
"`application.commands` scope!"
410+
)
411+
continue
412+
401413
for command in _cmds:
402414
if command.get("code"):
403415
# Error exists.
@@ -431,6 +443,7 @@ async def __sync(self) -> None: # sourcery no-metrics
431443

432444
__check_global_commands: List[str] = [cmd["name"] for cmd in _cmds]
433445
__check_guild_commands: Dict[int, List[str]] = {}
446+
__blocked_guilds: set = set()
434447

435448
# responsible for checking if a command is in the cache but not a coro -> allowing removal
436449

@@ -439,6 +452,19 @@ async def __sync(self) -> None: # sourcery no-metrics
439452
application_id=self.me.id, guild_id=_id, with_localizations=True
440453
)
441454

455+
if isinstance(_cmds, dict) and _cmds.get("code"):
456+
# Error exists.
457+
if int(_cmds.get("code")) != 50001:
458+
raise JSONException(_cmds["code"], message=f'{_cmds["message"]} |')
459+
460+
log.warning(
461+
f"Your bot is missing access to guild with corresponding id {_id}! "
462+
"Adding commands will not be possible until it is invited with "
463+
"`application.commands` scope!"
464+
)
465+
__blocked_guilds.add(_id)
466+
continue
467+
442468
for command in _cmds:
443469
if command.get("code"):
444470
# Error exists.
@@ -453,7 +479,9 @@ async def __sync(self) -> None: # sourcery no-metrics
453479
_guild_command: dict
454480
for _guild_command in coro._command_data:
455481
_guild_id = _guild_command.get("guild_id")
456-
482+
if _guild_id in __blocked_guilds:
483+
log.fatal(f"Cannot sync commands on guild with id {_guild_id}!")
484+
raise JSONException(50001, message="Missing Access |")
457485
if _guild_command["name"] not in __check_guild_commands[_guild_id]:
458486
self.__guild_commands[_guild_id]["clean"] = False
459487
self.__guild_commands[_guild_id]["commands"].append(_guild_command)
@@ -999,7 +1027,11 @@ def decorator(coro: Coroutine) -> Callable[..., Any]:
9991027
),
10001028
coro=coro,
10011029
)
1002-
coro._command_data = commands
1030+
if hasattr(coro, "__func__"):
1031+
coro.__func__._command_data = commands
1032+
else:
1033+
coro._command_data = commands
1034+
10031035
self.__command_coroutines.append(coro)
10041036

10051037
return self.event(coro, name=f"command_{name}")
@@ -1064,7 +1096,11 @@ def decorator(coro: Coroutine) -> Callable[..., Any]:
10641096
),
10651097
coro=coro,
10661098
)
1067-
coro._command_data = commands
1099+
if hasattr(coro, "__func__"):
1100+
coro.__func__._command_data = commands
1101+
else:
1102+
coro._command_data = commands
1103+
10681104
self.__command_coroutines.append(coro)
10691105

10701106
return self.event(coro, name=f"command_{name}")

0 commit comments

Comments
 (0)