From 81b818d6915b8663e521490d639e90e3319980ee Mon Sep 17 00:00:00 2001 From: EdVraz <88881326+EdVraz@users.noreply.github.com> Date: Fri, 25 Mar 2022 21:43:31 +0100 Subject: [PATCH 1/3] refactor: Change command synchronization layout to stop constant command synchronization and to sync all commands at once. --- interactions/client.py | 82 +++++++++++++++++++++++++++++++---------- interactions/client.pyi | 4 ++ 2 files changed, 66 insertions(+), 20 deletions(-) diff --git a/interactions/client.py b/interactions/client.py index e5040ed38..9f5b3845a 100644 --- a/interactions/client.py +++ b/interactions/client.py @@ -84,6 +84,10 @@ def __init__( self._token = token self._extensions = {} self._scopes = set([]) + self.__to_sync = [] + self.__to_delete = [] + self.__has_commands = False + # determines if any command has been found in the code. if not, all existing commands will be deleted. self.me = None _token = self._token # noqa: F841 _cache = self._http.cache # noqa: F841 @@ -132,14 +136,34 @@ async def __compare_sync(self, data: dict, pool: List[dict]) -> bool: :return: Whether the command has changed or not. :rtype: bool """ - attrs: List[str] = ["type", "name", "description", "options", "guild_id"] + attrs: List[str] = [ + "type", + "name", + "description", + "options", + "guild_id", + "default_permission", + ] + # TODO: implement permission/localization check when available. log.info(f"Current attributes to compare: {', '.join(attrs)}.") clean: bool = True for command in pool: if command["name"] == data["name"]: + if not isinstance(command.get("options"), list): + command["options"] = [] + # this will ensure that the option will be an emtpy list, since discord returns `None` + # when no options are present, but they're in the data as `[]` + if command.get("guild_id") and not isinstance(command.get("guild_id"), int): + if isinstance(command.get("guild_id"), list): + command["guild_id"] = [int(_) for _ in command["guild_id"]] + else: + command["guild_id"] = int(command["guild_id"]) + # ensure that IDs are present as integers since discord returns strings. for attr in attrs: - if hasattr(data, attr) and command.get(attr) == data.get(attr): + + if data.get(attr, None) and command.get(attr) == data.get(attr): + # hasattr checks `dict` not `dict[attr]` continue else: clean = False @@ -196,7 +220,8 @@ async def __bulk_update_sync(self, data: List[dict], delete: Optional[bool] = Fa for guild, commands in guild_commands.items(): log.info( - f"Guild commands {', '.join(command['name'] for command in commands)} under ID {guild} have been {'deleted' if delete else 'synced'}." + f"Guild commands {', '.join(command['name'] for command in commands)} under ID {guild} " + f"have been {'deleted' if delete else 'synced'}." ) await self._http.overwrite_application_command( application_id=self.me.id, @@ -206,17 +231,18 @@ async def __bulk_update_sync(self, data: List[dict], delete: Optional[bool] = Fa if global_commands: log.info( - f"Global commands {', '.join(command['name'] for command in global_commands)} have been {'deleted' if delete else 'synced'}." + f"Global commands {', '.join(command['name'] for command in global_commands)} " + f"have been {'deleted' if delete else 'synced'}." ) await self._http.overwrite_application_command( application_id=self.me.id, data=[] if delete else global_commands ) - async def _synchronize(self, payload: Optional[dict] = None) -> None: + async def __prepare_sync(self, payload: Optional[dict] = None) -> None: """ - Synchronizes a command from the client-facing API to the Web API. + Prepares commands to be synced. - :ivar payload?: The application command to synchronize. Defaults to ``None`` where a global synchronization process begins. + :ivar payload?: The application command to prepare. Defaults to ``None`` where a global synchronization process begins. :type payload: Optional[dict] """ cache: Optional[List[dict]] = self._http.cache.interactions.view @@ -234,7 +260,7 @@ async def _synchronize(self, payload: Optional[dict] = None) -> None: if isinstance(commands, dict): if commands.get("code"): # Error exists. raise JSONException(commands["code"], message=f'{commands["message"]} |') - # TODO: redo error handling. + elif isinstance(commands, list): for command in commands: if command.get("code"): @@ -244,20 +270,23 @@ async def _synchronize(self, payload: Optional[dict] = None) -> None: names: List[str] = ( [command["name"] for command in commands if command.get("name")] if commands else [] ) - to_sync: list = [] - to_delete: list = [] if payload: log.info(f"Checking command {payload['name']}.") if payload["name"] in names: if not await self.__compare_sync(payload, commands): - to_sync.append(payload) + self.__to_sync.append(payload) else: await self.__create_sync(payload) else: - to_delete.extend(command for command in commands if command not in cache) - await self.__bulk_update_sync(to_sync) - await self.__bulk_update_sync(to_delete, delete=True) + self.__to_delete.extend(command for command in commands if command not in cache) + + async def _synchronize(self) -> None: + """ + Synchronizes all commands at once. + """ + await self.__bulk_update_sync(self.__to_sync) + await self.__bulk_update_sync(self.__to_delete, delete=True) async def _ready(self) -> None: """ @@ -303,6 +332,14 @@ async def _ready(self) -> None: self.__register_events() if self._automate_sync: + if not self.__has_commands: + # I don't know how I could check for guild commands since the guild objects are not available before + # login, so I can only get all existing global commands and delete them, + # if there are no commands in the code. + + cmds = await self._http.get_application_commands(application_id=self.me.id) + for cmd in cmds: + self.__to_delete.append(cmd) await self._synchronize() ready = True except Exception as error: @@ -602,6 +639,7 @@ async def message_command(ctx): def decorator(coro: Coroutine) -> Callable[..., Any]: + self.__has_commands = True commands: List[ApplicationCommand] = command( type=type, name=name, @@ -614,13 +652,15 @@ def decorator(coro: Coroutine) -> Callable[..., Any]: if self._automate_sync: if self._loop.is_running(): - [self._loop.create_task(self._synchronize(command)) for command in commands] + [self._loop.create_task(self.__prepare_sync(command)) for command in commands] else: [ - self._loop.run_until_complete(self._synchronize(command)) + self._loop.run_until_complete(self.__prepare_sync(command)) for command in commands ] + # no sync call, _ready should handle that(?) + if scope is not MISSING: if isinstance(scope, List): [self._scopes.add(_ if isinstance(_, int) else _.id) for _ in scope] @@ -666,6 +706,7 @@ async def context_menu_name(ctx): def decorator(coro: Coroutine) -> Callable[..., Any]: + self.__has_commands = True commands: List[ApplicationCommand] = command( type=ApplicationCommandType.MESSAGE, name=name, @@ -676,10 +717,10 @@ def decorator(coro: Coroutine) -> Callable[..., Any]: if self._automate_sync: if self._loop.is_running(): - [self._loop.create_task(self._synchronize(command)) for command in commands] + [self._loop.create_task(self.__prepare_sync(command)) for command in commands] else: [ - self._loop.run_until_complete(self._synchronize(command)) + self._loop.run_until_complete(self.__prepare_sync(command)) for command in commands ] @@ -722,6 +763,7 @@ async def context_menu_name(ctx): def decorator(coro: Coroutine) -> Callable[..., Any]: + self.__has_commands = True commands: List[ApplicationCommand] = command( type=ApplicationCommandType.USER, name=name, @@ -733,10 +775,10 @@ def decorator(coro: Coroutine) -> Callable[..., Any]: if self._automate_sync: if self._loop.is_running(): - [self._loop.create_task(self._synchronize(command)) for command in commands] + [self._loop.create_task(self.__prepare_sync(command)) for command in commands] else: [ - self._loop.run_until_complete(self._synchronize(command)) + self._loop.run_until_complete(self.__prepare_sync(command)) for command in commands ] diff --git a/interactions/client.pyi b/interactions/client.pyi index 561387598..a5d13ffd2 100644 --- a/interactions/client.pyi +++ b/interactions/client.pyi @@ -28,6 +28,9 @@ class Client: _scopes: set[List[Union[int, Snowflake]]] _automate_sync: bool _extensions: Optional[Dict[str, Union[ModuleType, Extension]]] + __to_delete: list + __to_sync: list + __has_commands: bool me: Optional[Application] def __init__( self, @@ -45,6 +48,7 @@ class Client: async def __bulk_update_sync( self, data: List[dict], delete: Optional[bool] = False ) -> None: ... + async def __prepare_sync(self, payload: Optional[dict] = None) -> None: ... async def _synchronize(self, payload: Optional[dict] = None) -> None: ... async def _ready(self) -> None: ... async def _login(self) -> None: ... From 4af491b055a001a426680aa902bd763e7b23ed1e Mon Sep 17 00:00:00 2001 From: EdVraz <88881326+EdVraz@users.noreply.github.com> Date: Sun, 27 Mar 2022 21:35:22 +0200 Subject: [PATCH 2/3] revert: sync changes, keep only fix for comparing sync states --- interactions/client.py | 59 +++++++++++++---------------------------- interactions/client.pyi | 4 --- 2 files changed, 18 insertions(+), 45 deletions(-) diff --git a/interactions/client.py b/interactions/client.py index 9f5b3845a..8fa1661ca 100644 --- a/interactions/client.py +++ b/interactions/client.py @@ -84,10 +84,6 @@ def __init__( self._token = token self._extensions = {} self._scopes = set([]) - self.__to_sync = [] - self.__to_delete = [] - self.__has_commands = False - # determines if any command has been found in the code. if not, all existing commands will be deleted. self.me = None _token = self._token # noqa: F841 _cache = self._http.cache # noqa: F841 @@ -163,7 +159,7 @@ async def __compare_sync(self, data: dict, pool: List[dict]) -> bool: for attr in attrs: if data.get(attr, None) and command.get(attr) == data.get(attr): - # hasattr checks `dict` not `dict[attr]` + # hasattr checks `dict.attr` not `dict[attr]` continue else: clean = False @@ -220,8 +216,7 @@ async def __bulk_update_sync(self, data: List[dict], delete: Optional[bool] = Fa for guild, commands in guild_commands.items(): log.info( - f"Guild commands {', '.join(command['name'] for command in commands)} under ID {guild} " - f"have been {'deleted' if delete else 'synced'}." + f"Guild commands {', '.join(command['name'] for command in commands)} under ID {guild} have been {'deleted' if delete else 'synced'}." ) await self._http.overwrite_application_command( application_id=self.me.id, @@ -231,18 +226,17 @@ async def __bulk_update_sync(self, data: List[dict], delete: Optional[bool] = Fa if global_commands: log.info( - f"Global commands {', '.join(command['name'] for command in global_commands)} " - f"have been {'deleted' if delete else 'synced'}." + f"Global commands {', '.join(command['name'] for command in global_commands)} have been {'deleted' if delete else 'synced'}." ) await self._http.overwrite_application_command( application_id=self.me.id, data=[] if delete else global_commands ) - async def __prepare_sync(self, payload: Optional[dict] = None) -> None: + async def _synchronize(self, payload: Optional[dict] = None) -> None: """ - Prepares commands to be synced. + Synchronizes a command from the client-facing API to the Web API. - :ivar payload?: The application command to prepare. Defaults to ``None`` where a global synchronization process begins. + :ivar payload?: The application command to synchronize. Defaults to ``None`` where a global synchronization process begins. :type payload: Optional[dict] """ cache: Optional[List[dict]] = self._http.cache.interactions.view @@ -260,7 +254,6 @@ async def __prepare_sync(self, payload: Optional[dict] = None) -> None: if isinstance(commands, dict): if commands.get("code"): # Error exists. raise JSONException(commands["code"], message=f'{commands["message"]} |') - elif isinstance(commands, list): for command in commands: if command.get("code"): @@ -270,23 +263,20 @@ async def __prepare_sync(self, payload: Optional[dict] = None) -> None: names: List[str] = ( [command["name"] for command in commands if command.get("name")] if commands else [] ) + to_sync: list = [] + to_delete: list = [] if payload: log.info(f"Checking command {payload['name']}.") if payload["name"] in names: if not await self.__compare_sync(payload, commands): - self.__to_sync.append(payload) + to_sync.append(payload) else: await self.__create_sync(payload) else: - self.__to_delete.extend(command for command in commands if command not in cache) - - async def _synchronize(self) -> None: - """ - Synchronizes all commands at once. - """ - await self.__bulk_update_sync(self.__to_sync) - await self.__bulk_update_sync(self.__to_delete, delete=True) + to_delete.extend(command for command in commands if command not in cache) + await self.__bulk_update_sync(to_sync) + await self.__bulk_update_sync(to_delete, delete=True) async def _ready(self) -> None: """ @@ -332,14 +322,6 @@ async def _ready(self) -> None: self.__register_events() if self._automate_sync: - if not self.__has_commands: - # I don't know how I could check for guild commands since the guild objects are not available before - # login, so I can only get all existing global commands and delete them, - # if there are no commands in the code. - - cmds = await self._http.get_application_commands(application_id=self.me.id) - for cmd in cmds: - self.__to_delete.append(cmd) await self._synchronize() ready = True except Exception as error: @@ -639,7 +621,6 @@ async def message_command(ctx): def decorator(coro: Coroutine) -> Callable[..., Any]: - self.__has_commands = True commands: List[ApplicationCommand] = command( type=type, name=name, @@ -652,15 +633,13 @@ def decorator(coro: Coroutine) -> Callable[..., Any]: if self._automate_sync: if self._loop.is_running(): - [self._loop.create_task(self.__prepare_sync(command)) for command in commands] + [self._loop.create_task(self._synchronize(command)) for command in commands] else: [ - self._loop.run_until_complete(self.__prepare_sync(command)) + self._loop.run_until_complete(self._synchronize(command)) for command in commands ] - # no sync call, _ready should handle that(?) - if scope is not MISSING: if isinstance(scope, List): [self._scopes.add(_ if isinstance(_, int) else _.id) for _ in scope] @@ -706,7 +685,6 @@ async def context_menu_name(ctx): def decorator(coro: Coroutine) -> Callable[..., Any]: - self.__has_commands = True commands: List[ApplicationCommand] = command( type=ApplicationCommandType.MESSAGE, name=name, @@ -717,10 +695,10 @@ def decorator(coro: Coroutine) -> Callable[..., Any]: if self._automate_sync: if self._loop.is_running(): - [self._loop.create_task(self.__prepare_sync(command)) for command in commands] + [self._loop.create_task(self._synchronize(command)) for command in commands] else: [ - self._loop.run_until_complete(self.__prepare_sync(command)) + self._loop.run_until_complete(self._synchronize(command)) for command in commands ] @@ -763,7 +741,6 @@ async def context_menu_name(ctx): def decorator(coro: Coroutine) -> Callable[..., Any]: - self.__has_commands = True commands: List[ApplicationCommand] = command( type=ApplicationCommandType.USER, name=name, @@ -775,10 +752,10 @@ def decorator(coro: Coroutine) -> Callable[..., Any]: if self._automate_sync: if self._loop.is_running(): - [self._loop.create_task(self.__prepare_sync(command)) for command in commands] + [self._loop.create_task(self._synchronize(command)) for command in commands] else: [ - self._loop.run_until_complete(self.__prepare_sync(command)) + self._loop.run_until_complete(self._synchronize(command)) for command in commands ] diff --git a/interactions/client.pyi b/interactions/client.pyi index a5d13ffd2..561387598 100644 --- a/interactions/client.pyi +++ b/interactions/client.pyi @@ -28,9 +28,6 @@ class Client: _scopes: set[List[Union[int, Snowflake]]] _automate_sync: bool _extensions: Optional[Dict[str, Union[ModuleType, Extension]]] - __to_delete: list - __to_sync: list - __has_commands: bool me: Optional[Application] def __init__( self, @@ -48,7 +45,6 @@ class Client: async def __bulk_update_sync( self, data: List[dict], delete: Optional[bool] = False ) -> None: ... - async def __prepare_sync(self, payload: Optional[dict] = None) -> None: ... async def _synchronize(self, payload: Optional[dict] = None) -> None: ... async def _ready(self) -> None: ... async def _login(self) -> None: ... From f9df9b52ad9cf21c2e9f0de41a041f670e02b7a3 Mon Sep 17 00:00:00 2001 From: EdVraz <88881326+EdVraz@users.noreply.github.com> Date: Thu, 31 Mar 2022 18:48:19 +0200 Subject: [PATCH 3/3] refactor: attribute check --- interactions/client.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/interactions/client.py b/interactions/client.py index 8fa1661ca..16a6e52dd 100644 --- a/interactions/client.py +++ b/interactions/client.py @@ -133,14 +133,8 @@ async def __compare_sync(self, data: dict, pool: List[dict]) -> bool: :rtype: bool """ attrs: List[str] = [ - "type", - "name", - "description", - "options", - "guild_id", - "default_permission", + name for name in ApplicationCommand.__slots__ if not name.startswith("_") ] - # TODO: implement permission/localization check when available. log.info(f"Current attributes to compare: {', '.join(attrs)}.") clean: bool = True