mproved name conflict handling in custom commands.
CI / Formatting (push) Successful in 6s
CI / Linting (push) Successful in 5s
CI / Tests (Python 3.12) (push) Failing after 14s
CI / Tests (Python 3.13) (push) Failing after 14s
CI / Tests (Python 3.14) (push) Failing after 10s
CI / Type Checking (push) Successful in 9s
CI / Spelling (push) Successful in 8s
CI / Formatting (push) Successful in 6s
CI / Linting (push) Successful in 5s
CI / Tests (Python 3.12) (push) Failing after 14s
CI / Tests (Python 3.13) (push) Failing after 14s
CI / Tests (Python 3.14) (push) Failing after 10s
CI / Type Checking (push) Successful in 9s
CI / Spelling (push) Successful in 8s
This commit is contained in:
@@ -25,6 +25,7 @@ from .types import (
|
||||
CannotDeleteByAliasError,
|
||||
CommandAlreadyExistsError,
|
||||
CommandNotFoundError,
|
||||
InactiveCommandError,
|
||||
InvalidNameError,
|
||||
NotCustomCommandError,
|
||||
)
|
||||
@@ -112,6 +113,12 @@ async def editcommand(ctx: CommandContext) -> None:
|
||||
"That command is not a custom command and cannot be modified."
|
||||
)
|
||||
return
|
||||
except InactiveCommandError as e:
|
||||
await ctx.owncast_client.send_message(
|
||||
f"Custom command {prefix}{e.name} is disabled because another module "
|
||||
f"registered the same name. Delete it or resolve the conflict to re-enable."
|
||||
)
|
||||
return
|
||||
|
||||
await ctx.owncast_client.send_message(f"Command {prefix}{cmd.name} updated.")
|
||||
|
||||
@@ -192,6 +199,12 @@ async def commandmodonly(ctx: CommandContext) -> None:
|
||||
"That command is not a custom command and cannot be modified."
|
||||
)
|
||||
return
|
||||
except InactiveCommandError as e:
|
||||
await ctx.owncast_client.send_message(
|
||||
f"Custom command {prefix}{e.name} is disabled because another module "
|
||||
f"registered the same name. Delete it or resolve the conflict to re-enable."
|
||||
)
|
||||
return
|
||||
|
||||
status = "moderator-only" if cmd.requires_moderator else "public"
|
||||
await ctx.owncast_client.send_message(
|
||||
@@ -227,6 +240,12 @@ async def resetcommand(ctx: CommandContext) -> None:
|
||||
"That command is not a custom command and cannot be modified."
|
||||
)
|
||||
return
|
||||
except InactiveCommandError as e:
|
||||
await ctx.owncast_client.send_message(
|
||||
f"Custom command {prefix}{e.name} is disabled because another module "
|
||||
f"registered the same name. Delete it or resolve the conflict to re-enable."
|
||||
)
|
||||
return
|
||||
|
||||
await ctx.owncast_client.send_message(f"Command {prefix}{cmd.name} counter reset.")
|
||||
|
||||
@@ -323,6 +342,12 @@ async def commandcooldown(ctx: CommandContext) -> None:
|
||||
"That command is not a custom command and cannot be modified."
|
||||
)
|
||||
return
|
||||
except InactiveCommandError as e:
|
||||
await ctx.owncast_client.send_message(
|
||||
f"Custom command {prefix}{e.name} is disabled because another module "
|
||||
f"registered the same name. Delete it or resolve the conflict to re-enable."
|
||||
)
|
||||
return
|
||||
|
||||
if cmd.cooldown == 0:
|
||||
await ctx.owncast_client.send_message(
|
||||
@@ -363,6 +388,12 @@ async def addalias(ctx: CommandContext) -> None:
|
||||
"That command is not a custom command and cannot be modified."
|
||||
)
|
||||
return
|
||||
except InactiveCommandError as e:
|
||||
await ctx.owncast_client.send_message(
|
||||
f"Custom command {prefix}{e.name} is disabled because another module "
|
||||
f"registered the same name. Delete it or resolve the conflict to re-enable."
|
||||
)
|
||||
return
|
||||
except InvalidNameError:
|
||||
await ctx.owncast_client.send_message(
|
||||
"Invalid alias name. Only letters, numbers, and underscores are allowed."
|
||||
|
||||
@@ -31,6 +31,7 @@ from .types import (
|
||||
CannotDeleteByAliasError,
|
||||
CommandAlreadyExistsError,
|
||||
CommandNotFoundError,
|
||||
InactiveCommandError,
|
||||
InvalidNameError,
|
||||
NotCustomCommandError,
|
||||
)
|
||||
@@ -73,11 +74,47 @@ class CommandManager:
|
||||
raise NotCustomCommandError(name) from err
|
||||
raise
|
||||
|
||||
def _reregister(self, command: Command) -> None:
|
||||
"""Unregister and re-register a command with current snapshot settings.
|
||||
async def _resolve_active(self, name: str) -> Command:
|
||||
"""Resolve a name/alias and refuse if the command is inactive.
|
||||
|
||||
Both operations are synchronous, preventing interleaved state.
|
||||
A command is inactive when its canonical name is not currently owned
|
||||
by this module in the registry, typically because another module
|
||||
holds the same name. If the conflict has cleared since startup,
|
||||
attempts to re-register with the current snapshot before giving up.
|
||||
|
||||
:param name: Command name or alias.
|
||||
:return: The resolved Command snapshot.
|
||||
:raises InactiveCommandError: If the command is inactive and cannot
|
||||
be reactivated.
|
||||
"""
|
||||
command = await self._resolve(name)
|
||||
if self._commands.get(command.name) is not None:
|
||||
return command
|
||||
try:
|
||||
self._commands.register(
|
||||
name=command.name,
|
||||
handler=custom_command_handler,
|
||||
aliases=list(command.aliases),
|
||||
requires_moderator=command.requires_moderator,
|
||||
cooldown=command.cooldown,
|
||||
)
|
||||
except ValueError:
|
||||
raise InactiveCommandError(command.name) from None
|
||||
self._ctx.logger.info(
|
||||
"Custom command '%s' reactivated after conflict cleared.",
|
||||
command.name,
|
||||
)
|
||||
return command
|
||||
|
||||
def _reregister(self, command: Command) -> None:
|
||||
"""Unregister and re-register a command when owned by this module.
|
||||
|
||||
No-op when another module currently holds the name, so mutation
|
||||
paths (for example ``remove_alias``) can still keep the database
|
||||
in sync without disturbing the active registration.
|
||||
"""
|
||||
if self._commands.get(command.name) is None:
|
||||
return
|
||||
self._commands.unregister(command.name)
|
||||
self._commands.register(
|
||||
name=command.name,
|
||||
@@ -144,7 +181,7 @@ class CommandManager:
|
||||
:param response: New response template.
|
||||
:return: Updated command snapshot.
|
||||
"""
|
||||
command = await self._resolve(name)
|
||||
command = await self._resolve_active(name)
|
||||
updated = await self._repo.update_response(command.name, response)
|
||||
self._ctx.logger.info("Custom command '%s' updated.", command.name)
|
||||
return updated
|
||||
@@ -171,7 +208,7 @@ class CommandManager:
|
||||
:param enabled: True for moderator-only, False for public.
|
||||
:return: Updated command snapshot.
|
||||
"""
|
||||
command = await self._resolve(name)
|
||||
command = await self._resolve_active(name)
|
||||
updated = await self._repo.update_moderator_flag(command.name, enabled=enabled)
|
||||
self._reregister(updated)
|
||||
self._ctx.logger.info(
|
||||
@@ -188,7 +225,7 @@ class CommandManager:
|
||||
:param seconds: Cooldown in seconds (0 to disable).
|
||||
:return: Updated command snapshot.
|
||||
"""
|
||||
command = await self._resolve(name)
|
||||
command = await self._resolve_active(name)
|
||||
updated = await self._repo.update_cooldown(command.name, seconds=seconds)
|
||||
self._reregister(updated)
|
||||
self._ctx.logger.info(
|
||||
@@ -204,7 +241,7 @@ class CommandManager:
|
||||
:param name: Command name or alias.
|
||||
:return: Updated command snapshot.
|
||||
"""
|
||||
command = await self._resolve(name)
|
||||
command = await self._resolve_active(name)
|
||||
reset = await self._repo.reset_use_count(command.name)
|
||||
self._ctx.logger.info("Custom command '%s' counter reset.", command.name)
|
||||
return reset
|
||||
@@ -218,7 +255,7 @@ class CommandManager:
|
||||
"""
|
||||
if not NAME_RE.match(alias):
|
||||
raise InvalidNameError(alias)
|
||||
command = await self._resolve(command_name)
|
||||
command = await self._resolve_active(command_name)
|
||||
if alias == command.name:
|
||||
raise AliasIsCanonicalNameError(alias)
|
||||
if self._commands.exists(alias):
|
||||
|
||||
@@ -181,3 +181,15 @@ class CannotDeleteByAliasError(CommandError):
|
||||
self.alias = alias
|
||||
self.canonical = canonical
|
||||
super().__init__(f"cannot delete alias {alias}, belongs to {canonical}")
|
||||
|
||||
|
||||
class InactiveCommandError(CommandError):
|
||||
"""The command exists in the database but is not registered."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialize with the inactive command name.
|
||||
|
||||
:param name: The name of the inactive command.
|
||||
"""
|
||||
self.name = name
|
||||
super().__init__(f"command is inactive: {name}")
|
||||
|
||||
Reference in New Issue
Block a user