Skip to Content
Command System

Command System

The ZwyLib command registration system allows you to easily register commands, subcommands, and error handlers in just a few lines — and also dynamically add or remove them at runtime.

Getting Started

Let’s register a basic command:

# ... metadata and zwylib import ... from zwylib import command_manager def register_commands(): prefix = "!" # command prefix for your plugin commands_priority = 10 # your commands' execution priority over others # commands are registered through a dispatcher dispatcher = command_manager.get_dispatcher(__id__, prefix, commands_priority) # register the "!test" command @dispatcher.register_command("test") def test_command(params: Any, account: int) -> HookResult: # https://plugins.exteragram.app/docs/plugin-class#message-sending-hook params.message = "Command '!test' executed successfully!" return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params) class MyPlugin(BasePlugin): def on_plugin_load(self): # register commands when the plugin loads register_commands() def on_plugin_unload(self): # on unload, deregister commands to avoid issues with plugin updates/validation command_manager.remove_dispatcher(__id__) ... # rest of plugin logic

The arguments params and account are mandatory — ZwyLib will raise a MissingRequiredArguments error if these are missing.

ZwyLib also enforces the return type to be HookResult. If a different type is returned, an InvalidTypeError will be thrown and the command won’t be registered.

Aliases

Both register_command and subcommand accept an optional aliases list, letting a command respond to more than one name:

@dispatcher.register_command("test", aliases=["t", "tst"]) def test_command(params: Any, account: int) -> HookResult: ...

Calling !test, !t, or !tst will all invoke test_command. Names and aliases share the same namespace — ZwyLib will refuse to register a name or alias that’s already taken by another command.

Subcommands

ZwyLib allows you to register as many nested subcommands as you like:

# ... metadata and zwylib import ... def register_commands(): dispatcher = zwylib.command_manager.get_dispatcher(__id__, "!") # called as "!test" @dispatcher.register_command("test") def test_command(params: Any, account: int) -> HookResult: ... # called as "!test sub" @test_command.subcommand("sub") def test_subcommand(params: Any, account: int) -> HookResult: params.message = "Command '!test sub' executed successfully!" return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params) # called as "!test sub new" @test_subcommand.subcommand("new") def test_sub_new_command(params: Any, account: int) -> HookResult: params.message = "Command '!test sub new' executed successfully!" return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params)

Stub Commands

Sometimes you only want a command to act as a namespace for its subcommands, with no behavior of its own. Use register_stub_command (on a Dispatcher) or stub_subcommand (on a _Command) instead of providing a function — invoking the stub directly shows a hint bulletin instead of doing nothing silently:

dispatcher = zwylib.command_manager.get_dispatcher(__id__, "!") # "!settings" has no behavior of its own... settings_cmd = dispatcher.register_stub_command("settings") # ...but "!settings reset" does @settings_cmd.subcommand("reset") def settings_reset(params: Any, account: int) -> HookResult: params.message = "Settings were reset." return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params)

Arguments

ZwyLib automatically parses the message text and attempts to match parameters based on function arguments.

The function must have required params and account parameters and if a command function includes additional typed parameters, ZwyLib will try to parse and cast arguments to the expected types. Supported types include: str, int, float, bool, and generic Any, Union, Optional from the typing module (see Python typing documentation ).

Note: For boolean conversion, values like true, 1, yes, on map to True, and false, 0, no, off map to False.

If casting fails, a CannotCastError is raised. If the number of provided arguments is less than the required (non-Optional, non-default, non-variadic) arguments or more than the expected arguments (when no variadic arguments are present), a WrongArgumentAmountError is raised. Arguments annotated as Optional[T] (or Union[T, None]) or with a default value (e.g., arg: str = None) are automatically assigned None or their default value if no value is provided.

ZwyLib also supports variadic arguments (*args), which must be annotated as *args: T, where T is one of the supported types (str, int, float, bool, Any, or a Union of these types). Variadic arguments are passed as a tuple to the command function:

  • If no extra arguments are provided, *args is an empty tuple ().
  • If one extra argument is provided, *args is a single-item tuple (arg,).
  • If multiple extra arguments are provided, *args is a tuple of all extra arguments (arg1, arg2, ...).

Examples

Example 1: Required and Variadic Arguments

from typing import Union def register_commands(): dispatcher = zwylib.command_manager.get_dispatcher(...) @dispatcher.register_command("numbers") def numbers_command(params: Any, account: int, first: int, *args: int) -> HookResult: params.message = f"First: {first}, additional numbers: {args}" return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params)
  • !numbers 42first = 42, args = () → Output: First: 42, additional numbers: ()
  • !numbers 42 100first = 42, args = (100,) → Output: First: 42, additional numbers: (100,)
  • !numbers 42 100 200 300first = 42, args = (100, 200, 300) → Output: First: 42, additional numbers: (100, 200, 300)
  • !numbers → Error: Expected at least 3 arguments, got 2

Example 2: Optional Argument

from typing import Optional def register_commands(): dispatcher = zwylib.command_manager.get_dispatcher(...) @dispatcher.register_command("test") def test_command(params: Any, account: int, option: Optional[str]) -> HookResult: params.message = f"Option: {option}" return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params)
  • !test hello 123account = 123, option = None → Output: Option: None
  • !test hello 123 abcaccount = 123, option = "abc" → Output: Option: abc
  • !test hello → Error: Expected at least 2 arguments, got 1
  • !test hello 123 abc def → Error: Expected at most 3 arguments, got 4

Example 3: Optional Argument with Default Value

from typing import Optional def register_commands(): dispatcher = zwylib.command_manager.get_dispatcher(...) @dispatcher.register_command("test") def test_command(params: Any, account: int, option: Optional[str] = None) -> HookResult: params.message = f"Option: {option}" return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params)
  • !test hello 123account = 123, option = None → Output: Option: None
  • !test hello 123 abcaccount = 123, option = "abc" → Output: Option: abc
  • !test hello → Error: Expected at least 2 arguments, got 1
  • !test hello 123 abc def → Error: Expected at most 3 arguments, got 4

Example 4: Only Variadic Arguments

from typing import Union def register_commands(): dispatcher = zwylib.command_manager.get_dispatcher(...) @dispatcher.register_command("echo") def echo_command(params: Any, account: int, *args: Union[str, int]) -> HookResult: params.message = f"Echo: {list(args)}" return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params)
  • !echoargs = () → Output: Echo: []
  • !echo helloargs = ('hello',) → Output: Echo: ['hello']
  • !echo hello 42args = ('hello', 42) → Output: Echo: ['hello', 42]

If the *args parameter’s type or any argument type is not one of the supported types or a valid Union/Optional of supported types, an InvalidTypeError is raised during command registration.

Async Commands

Command functions may be defined with async def. Async commands must not return a HookResult — their return annotation must be omitted or None, since ZwyLib immediately cancels the outgoing message (HookStrategy.CANCEL) and schedules the coroutine on zwylib.async_manager instead of waiting for it:

from zwylib import Requests, build_bulletin_helper @dispatcher.register_command("ping") async def ping_command(params: Any, account: int): response = await Requests.async_send(...) build_bulletin_helper(__id__).show_info(f"Got: {response}")

An error handler registered for an async command may itself be sync or async — ZwyLib schedules it the same way if it’s a coroutine function.

Error Handling

If an exception occurs during command or subcommand execution, it can be caught using the @command.register_error_handler decorator:

def register_commands(): dispatcher = zwylib.command_manager.get_dispatcher(...) @dispatcher.register_command("number") def number_command(params: Any, account: int, number: int) -> HookResult: params.message = f"number: {type(number)}" return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params) @number_command.register_error_handler def number_command_error_handler(params: Any, account: int, error: Exception) -> HookResult: params.message = f"An error occurred in 'number': {error}" return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params)

The error handler must accept exactly three arguments: params, account, and error. Otherwise, ZwyLib won’t register the handler.

Unhandled exceptions in a command will cause ZwyLib to send the stack trace to chat.

Command Deregistration

To manually remove a command, use:

dispatcher = zwylib.command_manager.get_dispatcher(__id__) dispatcher.unregister_command("my_command")

This will also remove all subcommands associated with the removed command.

zwylib.CommandManager

zwylib.command_manager: CommandManager

This global object is created during ZwyLib initialization and is used to manage all dispatchers. You should only use its documented methods.

Methods

get_dispatcher

CommandManager.get_dispatcher( plugin_id: Optional[str] = None, prefix="default", # defaults to "." commands_priority=-1 ) -> Dispatcher

Creates (if necessary) and returns a Dispatcher instance for the given plugin_id.

Parameters

  • plugin_id (Optional[str]): Your plugin’s unique ID. If omitted (or None), ZwyLib infers it from the call stack of the caller’s plugin file.
  • prefix (str): Prefix for all commands of this plugin. "default" means ".". Ignored if a prefix was previously saved for this plugin via set_prefix.
  • commands_priority (int): Execution priority. Default is -1.

Example

zwylib.command_manager.get_dispatcher("MyPluginID", "!", 10)

remove_dispatcher

CommandManager.remove_dispatcher(plugin_id: Optional[str])

Removes the dispatcher associated with the given plugin.

Parameters

  • plugin_id (Optional[str]): ID of the plugin whose dispatcher is being removed. Pass None to infer it from the caller’s plugin file.

Example

zwylib.command_manager.remove_dispatcher(__id__)

sort_dispatchers

CommandManager.sort_dispatchers()

Re-sorts all registered dispatchers by their commands_priority, highest first. Called automatically whenever a new dispatcher is created — you shouldn’t normally need to call this yourself.

zwylib.Dispatcher

zwylib.command_manager.get_dispatcher(__id__): Dispatcher

A class returned by zwylib.command_manager.get_dispatcher, responsible for registering commands under the current plugin ID. Should only be obtained via get_dispatcher.

Methods

set_prefix

Dispatcher.set_prefix(prefix: str)

Sets the prefix for all commands registered via this dispatcher. The prefix saves between exteraGram sessions. Fails silently (with a log entry) if prefix is empty or contains spaces.

Parameters

  • prefix (str): New command prefix.

Example

dispatcher.set_prefix("/")

register_command

@dispatcher.register_command(name: str, aliases: List[str] = None)

Decorator to register a command.

Arguments params and account are required. The return type must be HookResult, unless the command function is a coroutine function — see Async Commands.

Parameters

  • name (str): Command name. Cannot be empty or contain spaces.
  • aliases (List[str], optional): Additional names that also invoke this command.

Raises

  • MissingRequiredArguments: If params or account are missing.
  • InvalidTypeError: If parameter types are unsupported or return type is not HookResult (for sync commands).

Example

@dispatcher.register_command("hello", aliases=["hi"]) def test_command(params: Any, account: int) -> HookResult: params.message = "Hi!" return HookResult(strategy=HookStrategy.MODIFY, params=params)

register_stub_command

Dispatcher.register_stub_command(name: str) -> _Command

Registers a command with no behavior of its own, meant purely as a namespace for subcommands. Invoking it directly shows an informational bulletin instead of doing nothing. See Stub Commands.

Parameters

  • name (str): Command name.

Example

settings_cmd = dispatcher.register_stub_command("settings")

unregister_command

Dispatcher.unregister_command(name: str)

Removes a previously registered command (and all of its subcommands) from this dispatcher.

Parameters

  • name (str): Name of the command to remove.

Example

dispatcher.unregister_command("my_command")

zwylib._Command

@dispatcher.register_command(...): _Command

The object returned by register_command, representing a single registered command. Used to attach subcommands and error handlers to it.

Methods

subcommand

@command.subcommand(name: str, aliases: List[str] = None)

Decorator to register a nested subcommand under this command. Follows the same argument and return-type rules as register_command.

Parameters

  • name (str): Subcommand name. Cannot be empty or contain spaces.
  • aliases (List[str], optional): Additional names that also invoke this subcommand.

Example

@test_command.subcommand("sub") def test_subcommand(params: Any, account: int) -> HookResult: ...

stub_subcommand

_Command.stub_subcommand(name: str) -> _Command

Registers a subcommand with no behavior of its own, meant purely as a namespace for further nested subcommands. See Stub Commands.

Parameters

  • name (str): Subcommand name.

Example

group_cmd = test_command.stub_subcommand("group")

register_error_handler

@command.register_error_handler

Decorator to register an exception handler for this command (and any of its subcommands that don’t define their own). See Error Handling.

Raises

  • MissingRequiredArguments: If the handler doesn’t accept exactly params, account, and error.

Example

@test_command.register_error_handler def on_error(params: Any, account: int, error: Exception) -> HookResult: params.message = f"Error: {error}" return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params)
Last updated on