Added pydocstyle (D) rules to Ruff and fixed all violations.
CI / Formatting (push) Successful in 12s
CI / Linting (push) Successful in 13s
CI / Tests (Python 3.12) (push) Successful in 26s
CI / Tests (Python 3.13) (push) Successful in 25s
CI / Tests (Python 3.14) (push) Successful in 25s
CI / Type Checking (push) Successful in 26s
CI / Formatting (push) Successful in 12s
CI / Linting (push) Successful in 13s
CI / Tests (Python 3.12) (push) Successful in 26s
CI / Tests (Python 3.13) (push) Successful in 25s
CI / Tests (Python 3.14) (push) Successful in 25s
CI / Type Checking (push) Successful in 26s
This commit is contained in:
+1
-2
@@ -12,8 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
"""
|
"""Owlbot - A modular chat bot for Owncast.
|
||||||
Owlbot - A modular chat bot for Owncast.
|
|
||||||
|
|
||||||
The public API for building modules is in the `owlbot.api` subpackage:
|
The public API for building modules is in the `owlbot.api` subpackage:
|
||||||
|
|
||||||
|
|||||||
@@ -83,8 +83,7 @@ def on_command(
|
|||||||
requires_moderator: bool = False,
|
requires_moderator: bool = False,
|
||||||
cooldown: int | float = 0,
|
cooldown: int | float = 0,
|
||||||
) -> Callable[[CommandHandler], CommandHandler]:
|
) -> Callable[[CommandHandler], CommandHandler]:
|
||||||
"""
|
"""Register a command handler.
|
||||||
Decorator to register a command handler.
|
|
||||||
|
|
||||||
:param name: Primary command name (case-insensitive).
|
:param name: Primary command name (case-insensitive).
|
||||||
:param aliases: Optional list of alternative names.
|
:param aliases: Optional list of alternative names.
|
||||||
|
|||||||
+19
-35
@@ -36,8 +36,7 @@ class Config:
|
|||||||
config_path: str | Path = "config.yaml",
|
config_path: str | Path = "config.yaml",
|
||||||
overrides: dict[str, Any] | None = None,
|
overrides: dict[str, Any] | None = None,
|
||||||
):
|
):
|
||||||
"""
|
"""Initialize the configuration manager.
|
||||||
Initialize the configuration manager.
|
|
||||||
|
|
||||||
:param config_path: Path to the YAML config file.
|
:param config_path: Path to the YAML config file.
|
||||||
:param overrides: CLI overrides (keys match property names).
|
:param overrides: CLI overrides (keys match property names).
|
||||||
@@ -91,9 +90,9 @@ class Config:
|
|||||||
default: Any = _UNSET,
|
default: Any = _UNSET,
|
||||||
type_fn: type = str,
|
type_fn: type = str,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""
|
"""Resolve a setting through the priority chain.
|
||||||
Resolve a setting through the priority chain:
|
|
||||||
CLI arg > env var > config file > default.
|
Priority order: CLI arg > env var > config file > default.
|
||||||
|
|
||||||
When *default* is a ``str``, the resolved value is coerced to ``str``
|
When *default* is a ``str``, the resolved value is coerced to ``str``
|
||||||
(the implicit *type_fn*). For non-string types, pass both *default*
|
(the implicit *type_fn*). For non-string types, pass both *default*
|
||||||
@@ -123,8 +122,7 @@ class Config:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def webhook_secret(self) -> str:
|
def webhook_secret(self) -> str:
|
||||||
"""
|
"""Secret string for the webhook URL path.
|
||||||
Secret string for the webhook URL path.
|
|
||||||
|
|
||||||
The webhook endpoint is always /webhook/<secret>. A cryptographically
|
The webhook endpoint is always /webhook/<secret>. A cryptographically
|
||||||
secure value is generated on first run if not explicitly configured.
|
secure value is generated on first run if not explicitly configured.
|
||||||
@@ -139,8 +137,7 @@ class Config:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def webhook_path(self) -> str:
|
def webhook_path(self) -> str:
|
||||||
"""
|
"""URL path where Owncast sends webhooks.
|
||||||
URL path where Owncast sends webhooks.
|
|
||||||
|
|
||||||
Always returns /webhook/<secret>. The secret is auto-generated
|
Always returns /webhook/<secret>. The secret is auto-generated
|
||||||
on first run if not configured.
|
on first run if not configured.
|
||||||
@@ -207,8 +204,7 @@ class Config:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def public_base_url(self) -> str:
|
def public_base_url(self) -> str:
|
||||||
"""
|
"""Public base URL for Owlbot's web server.
|
||||||
Public base URL for Owlbot's web server.
|
|
||||||
|
|
||||||
Used to construct URLs for module routes and the webhook endpoint.
|
Used to construct URLs for module routes and the webhook endpoint.
|
||||||
Falls back to ``owncast.url`` if not explicitly set.
|
Falls back to ``owncast.url`` if not explicitly set.
|
||||||
@@ -224,8 +220,7 @@ class Config:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def storage_dir(self) -> Path:
|
def storage_dir(self) -> Path:
|
||||||
"""
|
"""Directory for module database files.
|
||||||
Directory for module database files.
|
|
||||||
|
|
||||||
Each module gets its own database file named '<module_name>.db'.
|
Each module gets its own database file named '<module_name>.db'.
|
||||||
Defaults to 'data/' in the working directory.
|
Defaults to 'data/' in the working directory.
|
||||||
@@ -242,8 +237,7 @@ class Config:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def modules_dir(self) -> Path:
|
def modules_dir(self) -> Path:
|
||||||
"""
|
"""Directory containing user modules.
|
||||||
Directory containing user modules.
|
|
||||||
|
|
||||||
Built-in modules are loaded from the package regardless of this setting.
|
Built-in modules are loaded from the package regardless of this setting.
|
||||||
Defaults to 'modules/' in the working directory.
|
Defaults to 'modules/' in the working directory.
|
||||||
@@ -260,8 +254,7 @@ class Config:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def log_dir(self) -> Path | None:
|
def log_dir(self) -> Path | None:
|
||||||
"""
|
"""Directory for the log file.
|
||||||
Directory for the log file.
|
|
||||||
|
|
||||||
When set, an ``owlbot.log`` file is written to this directory in
|
When set, an ``owlbot.log`` file is written to this directory in
|
||||||
addition to stdout. Returns ``None`` when unset (stdout only).
|
addition to stdout. Returns ``None`` when unset (stdout only).
|
||||||
@@ -405,8 +398,7 @@ class Config:
|
|||||||
logger.info(f"Configuration saved to: {self.config_path.absolute()}")
|
logger.info(f"Configuration saved to: {self.config_path.absolute()}")
|
||||||
|
|
||||||
def is_module_enabled(self, module_name: str) -> bool:
|
def is_module_enabled(self, module_name: str) -> bool:
|
||||||
"""
|
"""Check if a module is enabled.
|
||||||
Check if a module is enabled.
|
|
||||||
|
|
||||||
Modules are enabled by default unless explicitly disabled with
|
Modules are enabled by default unless explicitly disabled with
|
||||||
``modules.<name>.enabled: false`` in the config file.
|
``modules.<name>.enabled: false`` in the config file.
|
||||||
@@ -424,8 +416,7 @@ class Config:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def get_module_config(self, module_name: str) -> dict[str, Any]:
|
def get_module_config(self, module_name: str) -> dict[str, Any]:
|
||||||
"""
|
"""Get the configuration dict for a module.
|
||||||
Get the configuration dict for a module.
|
|
||||||
|
|
||||||
Returns merged defaults and config file values, with config file
|
Returns merged defaults and config file values, with config file
|
||||||
values taking precedence.
|
values taking precedence.
|
||||||
@@ -443,8 +434,7 @@ class Config:
|
|||||||
return {**defaults, **config}
|
return {**defaults, **config}
|
||||||
|
|
||||||
def set_module_config(self, module_name: str, config: dict[str, Any]) -> None:
|
def set_module_config(self, module_name: str, config: dict[str, Any]) -> None:
|
||||||
"""
|
"""Update the configuration for a module at runtime and persist to disk.
|
||||||
Update the configuration for a module at runtime and persist to disk.
|
|
||||||
|
|
||||||
:param module_name: The module name.
|
:param module_name: The module name.
|
||||||
:param config: Dict of configuration values to set.
|
:param config: Dict of configuration values to set.
|
||||||
@@ -459,8 +449,7 @@ class Config:
|
|||||||
def register_module_defaults(
|
def register_module_defaults(
|
||||||
self, module_name: str, defaults: dict[str, Any]
|
self, module_name: str, defaults: dict[str, Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Register default configuration values for a module.
|
||||||
Register default configuration values for a module.
|
|
||||||
|
|
||||||
Called by modules during setup to declare their expected config keys
|
Called by modules during setup to declare their expected config keys
|
||||||
and default values. Missing keys are backfilled into the config file
|
and default values. Missing keys are backfilled into the config file
|
||||||
@@ -516,8 +505,7 @@ class ModuleConfig:
|
|||||||
"""Pre-scoped configuration for a specific module."""
|
"""Pre-scoped configuration for a specific module."""
|
||||||
|
|
||||||
def __init__(self, config: Config, module_name: str):
|
def __init__(self, config: Config, module_name: str):
|
||||||
"""
|
"""Initialize a module-scoped configuration.
|
||||||
Initialize a module-scoped configuration.
|
|
||||||
|
|
||||||
:param config: The parent Config object.
|
:param config: The parent Config object.
|
||||||
:param module_name: The name of the module this config
|
:param module_name: The name of the module this config
|
||||||
@@ -537,8 +525,7 @@ class ModuleConfig:
|
|||||||
return self._config.public_base_url
|
return self._config.public_base_url
|
||||||
|
|
||||||
def get(self, key: str, default: Any = None) -> Any:
|
def get(self, key: str, default: Any = None) -> Any:
|
||||||
"""
|
"""Get a config value by key.
|
||||||
Get a config value by key.
|
|
||||||
|
|
||||||
:param key: The configuration key.
|
:param key: The configuration key.
|
||||||
:param default: Value to return if key is not found.
|
:param default: Value to return if key is not found.
|
||||||
@@ -547,16 +534,14 @@ class ModuleConfig:
|
|||||||
return self.as_dict().get(key, default)
|
return self.as_dict().get(key, default)
|
||||||
|
|
||||||
def as_dict(self) -> dict[str, Any]:
|
def as_dict(self) -> dict[str, Any]:
|
||||||
"""
|
"""Get the full config dict for this module.
|
||||||
Get the full config dict for this module.
|
|
||||||
|
|
||||||
:return: Dict of all configuration values.
|
:return: Dict of all configuration values.
|
||||||
"""
|
"""
|
||||||
return self._config.get_module_config(self._module_name)
|
return self._config.get_module_config(self._module_name)
|
||||||
|
|
||||||
def set(self, key: str, value: Any) -> None:
|
def set(self, key: str, value: Any) -> None:
|
||||||
"""
|
"""Set a config value at runtime and persist to disk.
|
||||||
Set a config value at runtime and persist to disk.
|
|
||||||
|
|
||||||
:param key: The configuration key.
|
:param key: The configuration key.
|
||||||
:param value: The value to set.
|
:param value: The value to set.
|
||||||
@@ -566,8 +551,7 @@ class ModuleConfig:
|
|||||||
self._config.set_module_config(self._module_name, current)
|
self._config.set_module_config(self._module_name, current)
|
||||||
|
|
||||||
def register_defaults(self, defaults: dict[str, Any]) -> None:
|
def register_defaults(self, defaults: dict[str, Any]) -> None:
|
||||||
"""
|
"""Register default values for this module's config.
|
||||||
Register default values for this module's config.
|
|
||||||
|
|
||||||
Called during setup() to declare expected config keys and their
|
Called during setup() to declare expected config keys and their
|
||||||
default values.
|
default values.
|
||||||
|
|||||||
+7
-12
@@ -44,8 +44,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ModuleContext:
|
class ModuleContext:
|
||||||
"""
|
"""Shared services available to all module handlers.
|
||||||
Shared services available to all module handlers.
|
|
||||||
|
|
||||||
Created once per module during loading and reused for all handler
|
Created once per module during loading and reused for all handler
|
||||||
invocations. This bundles the common dependencies that every handler needs.
|
invocations. This bundles the common dependencies that every handler needs.
|
||||||
@@ -83,6 +82,7 @@ class ModuleContext:
|
|||||||
logger: logging.Logger = field(init=False)
|
logger: logging.Logger = field(init=False)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
|
"""Derive the module-scoped logger from the module name."""
|
||||||
self.logger = logging.getLogger(f"owlbot.modules.{self.module_name}")
|
self.logger = logging.getLogger(f"owlbot.modules.{self.module_name}")
|
||||||
|
|
||||||
|
|
||||||
@@ -96,8 +96,7 @@ class PropagationState:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class EventContext[E]:
|
class EventContext[E]:
|
||||||
"""
|
"""Context passed to event handlers.
|
||||||
Context passed to event handlers.
|
|
||||||
|
|
||||||
Each handler invocation receives its own EventContext instance with the event
|
Each handler invocation receives its own EventContext instance with the event
|
||||||
data and access to shared services via the module context.
|
data and access to shared services via the module context.
|
||||||
@@ -165,16 +164,14 @@ class EventContext[E]:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def propagation_stopped(self) -> bool:
|
def propagation_stopped(self) -> bool:
|
||||||
"""
|
"""Check if event propagation has been stopped by a handler.
|
||||||
Check if event propagation has been stopped by a handler.
|
|
||||||
|
|
||||||
:return: True if stop_propagation() was called by any handler.
|
:return: True if stop_propagation() was called by any handler.
|
||||||
"""
|
"""
|
||||||
return self._propagation.stopped
|
return self._propagation.stopped
|
||||||
|
|
||||||
def stop_propagation(self, reason: str | None = None) -> None:
|
def stop_propagation(self, reason: str | None = None) -> None:
|
||||||
"""
|
"""Stop event from being dispatched to remaining handlers and commands.
|
||||||
Stop event from being dispatched to remaining handlers and commands.
|
|
||||||
|
|
||||||
Once called, no further handlers will be invoked for this event, and
|
Once called, no further handlers will be invoked for this event, and
|
||||||
command dispatch (for CHAT events) will be skipped.
|
command dispatch (for CHAT events) will be skipped.
|
||||||
@@ -189,8 +186,7 @@ class EventContext[E]:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CommandContext:
|
class CommandContext:
|
||||||
"""
|
"""Context passed to command handlers.
|
||||||
Context passed to command handlers.
|
|
||||||
|
|
||||||
Provides access to the parsed command data, original chat event context,
|
Provides access to the parsed command data, original chat event context,
|
||||||
and shared services. Like EventContext and RouteContext, all ModuleContext
|
and shared services. Like EventContext and RouteContext, all ModuleContext
|
||||||
@@ -289,8 +285,7 @@ class CommandContext:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RouteContext:
|
class RouteContext:
|
||||||
"""
|
"""Context passed to HTTP route handlers.
|
||||||
Context passed to HTTP route handlers.
|
|
||||||
|
|
||||||
Similar to EventContext but includes the aiohttp request object
|
Similar to EventContext but includes the aiohttp request object
|
||||||
for accessing HTTP-specific data (body, headers, query params).
|
for accessing HTTP-specific data (body, headers, query params).
|
||||||
|
|||||||
+14
-28
@@ -43,8 +43,7 @@ class EventType(StrEnum):
|
|||||||
|
|
||||||
|
|
||||||
def _parse_timestamp(ts: str | None) -> datetime | None:
|
def _parse_timestamp(ts: str | None) -> datetime | None:
|
||||||
"""
|
"""Parse an ISO 8601 timestamp string into a datetime.
|
||||||
Parse an ISO 8601 timestamp string into a datetime.
|
|
||||||
|
|
||||||
Handles timezone suffixes and the Go zero-value timestamp that Owncast
|
Handles timezone suffixes and the Go zero-value timestamp that Owncast
|
||||||
sends for missing/unset timestamps.
|
sends for missing/unset timestamps.
|
||||||
@@ -91,8 +90,7 @@ class User:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict[str, Any]) -> User:
|
def from_dict(cls, data: dict[str, Any]) -> User:
|
||||||
"""
|
"""Create a User from webhook JSON data.
|
||||||
Create a User from webhook JSON data.
|
|
||||||
|
|
||||||
:param data: The user data from the webhook payload.
|
:param data: The user data from the webhook payload.
|
||||||
:return: A populated User instance.
|
:return: A populated User instance.
|
||||||
@@ -111,8 +109,7 @@ class User:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_moderator(self) -> bool:
|
def is_moderator(self) -> bool:
|
||||||
"""
|
"""Check if the user has moderator privileges.
|
||||||
Check if the user has moderator privileges.
|
|
||||||
|
|
||||||
:return: True if the user has the MODERATOR scope.
|
:return: True if the user has the MODERATOR scope.
|
||||||
"""
|
"""
|
||||||
@@ -133,8 +130,7 @@ class ChatEvent:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict[str, Any]) -> ChatEvent:
|
def from_dict(cls, data: dict[str, Any]) -> ChatEvent:
|
||||||
"""
|
"""Create a ChatEvent from webhook JSON data.
|
||||||
Create a ChatEvent from webhook JSON data.
|
|
||||||
|
|
||||||
:param data: The event data from the webhook payload.
|
:param data: The event data from the webhook payload.
|
||||||
:return: A populated ChatEvent instance.
|
:return: A populated ChatEvent instance.
|
||||||
@@ -168,8 +164,7 @@ class UserJoinedEvent:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict[str, Any]) -> UserJoinedEvent:
|
def from_dict(cls, data: dict[str, Any]) -> UserJoinedEvent:
|
||||||
"""
|
"""Create a UserJoinedEvent from webhook JSON data.
|
||||||
Create a UserJoinedEvent from webhook JSON data.
|
|
||||||
|
|
||||||
:param data: The event data from the webhook payload.
|
:param data: The event data from the webhook payload.
|
||||||
:return: A populated UserJoinedEvent instance.
|
:return: A populated UserJoinedEvent instance.
|
||||||
@@ -193,8 +188,7 @@ class UserPartedEvent:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict[str, Any]) -> UserPartedEvent:
|
def from_dict(cls, data: dict[str, Any]) -> UserPartedEvent:
|
||||||
"""
|
"""Create a UserPartedEvent from webhook JSON data.
|
||||||
Create a UserPartedEvent from webhook JSON data.
|
|
||||||
|
|
||||||
:param data: The event data from the webhook payload.
|
:param data: The event data from the webhook payload.
|
||||||
:return: A populated UserPartedEvent instance.
|
:return: A populated UserPartedEvent instance.
|
||||||
@@ -219,8 +213,7 @@ class NameChangedEvent:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict[str, Any]) -> NameChangedEvent:
|
def from_dict(cls, data: dict[str, Any]) -> NameChangedEvent:
|
||||||
"""
|
"""Create a NameChangedEvent from webhook JSON data.
|
||||||
Create a NameChangedEvent from webhook JSON data.
|
|
||||||
|
|
||||||
Note: The user object contains the OLD display name in user.display_name.
|
Note: The user object contains the OLD display name in user.display_name.
|
||||||
|
|
||||||
@@ -248,8 +241,7 @@ class StreamStartedEvent:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict[str, Any]) -> StreamStartedEvent:
|
def from_dict(cls, data: dict[str, Any]) -> StreamStartedEvent:
|
||||||
"""
|
"""Create a StreamStartedEvent from webhook JSON data.
|
||||||
Create a StreamStartedEvent from webhook JSON data.
|
|
||||||
|
|
||||||
:param data: The event data from the webhook payload.
|
:param data: The event data from the webhook payload.
|
||||||
:return: A populated StreamStartedEvent instance.
|
:return: A populated StreamStartedEvent instance.
|
||||||
@@ -275,8 +267,7 @@ class StreamStoppedEvent:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict[str, Any]) -> StreamStoppedEvent:
|
def from_dict(cls, data: dict[str, Any]) -> StreamStoppedEvent:
|
||||||
"""
|
"""Create a StreamStoppedEvent from webhook JSON data.
|
||||||
Create a StreamStoppedEvent from webhook JSON data.
|
|
||||||
|
|
||||||
:param data: The event data from the webhook payload.
|
:param data: The event data from the webhook payload.
|
||||||
:return: A populated StreamStoppedEvent instance.
|
:return: A populated StreamStoppedEvent instance.
|
||||||
@@ -305,8 +296,7 @@ class StreamStatus:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict[str, Any]) -> StreamStatus:
|
def from_dict(cls, data: dict[str, Any]) -> StreamStatus:
|
||||||
"""
|
"""Create a StreamStatus from webhook JSON data.
|
||||||
Create a StreamStatus from webhook JSON data.
|
|
||||||
|
|
||||||
:param data: The status data from the webhook payload.
|
:param data: The status data from the webhook payload.
|
||||||
:return: A populated StreamStatus instance.
|
:return: A populated StreamStatus instance.
|
||||||
@@ -336,8 +326,7 @@ class StreamTitleUpdatedEvent:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict[str, Any]) -> StreamTitleUpdatedEvent:
|
def from_dict(cls, data: dict[str, Any]) -> StreamTitleUpdatedEvent:
|
||||||
"""
|
"""Create a StreamTitleUpdatedEvent from webhook JSON data.
|
||||||
Create a StreamTitleUpdatedEvent from webhook JSON data.
|
|
||||||
|
|
||||||
:param data: The event data from the webhook payload.
|
:param data: The event data from the webhook payload.
|
||||||
:return: A populated StreamTitleUpdatedEvent instance.
|
:return: A populated StreamTitleUpdatedEvent instance.
|
||||||
@@ -364,8 +353,7 @@ class VisibilityUpdateEvent:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict[str, Any]) -> VisibilityUpdateEvent:
|
def from_dict(cls, data: dict[str, Any]) -> VisibilityUpdateEvent:
|
||||||
"""
|
"""Create a VisibilityUpdateEvent from webhook JSON data.
|
||||||
Create a VisibilityUpdateEvent from webhook JSON data.
|
|
||||||
|
|
||||||
:param data: The event data from the webhook payload.
|
:param data: The event data from the webhook payload.
|
||||||
:return: A populated VisibilityUpdateEvent instance.
|
:return: A populated VisibilityUpdateEvent instance.
|
||||||
@@ -407,8 +395,7 @@ _EVENT_TYPE_MAP: dict[EventType, type[Event]] = {
|
|||||||
|
|
||||||
|
|
||||||
def parse_event(data: dict[str, Any]) -> _ParsedEvent | None:
|
def parse_event(data: dict[str, Any]) -> _ParsedEvent | None:
|
||||||
"""
|
"""Parse a raw webhook payload into a typed event.
|
||||||
Parse a raw webhook payload into a typed event.
|
|
||||||
|
|
||||||
:param data: The JSON payload from Owncast's webhook.
|
:param data: The JSON payload from Owncast's webhook.
|
||||||
:return: Tuple of (event_type, event_instance), or None if the event type
|
:return: Tuple of (event_type, event_instance), or None if the event type
|
||||||
@@ -431,8 +418,7 @@ def parse_event(data: dict[str, Any]) -> _ParsedEvent | None:
|
|||||||
|
|
||||||
|
|
||||||
def log_event(event_type: EventType, event: Event) -> None:
|
def log_event(event_type: EventType, event: Event) -> None:
|
||||||
"""
|
"""Log event details for debugging and monitoring.
|
||||||
Log event details for debugging and monitoring.
|
|
||||||
|
|
||||||
:param event_type: The type of event being logged.
|
:param event_type: The type of event being logged.
|
||||||
:param event: The parsed event instance.
|
:param event: The parsed event instance.
|
||||||
|
|||||||
@@ -43,8 +43,7 @@ type EventHandler = "Callable[[EventContext[Any]], Awaitable[None]]"
|
|||||||
|
|
||||||
|
|
||||||
class Priority(IntEnum):
|
class Priority(IntEnum):
|
||||||
"""
|
"""Standard priority levels for event handlers.
|
||||||
Standard priority levels for event handlers.
|
|
||||||
|
|
||||||
Higher values run first. Handlers at the same priority level run in
|
Higher values run first. Handlers at the same priority level run in
|
||||||
registration order. Custom numeric values can be used for fine-grained control.
|
registration order. Custom numeric values can be used for fine-grained control.
|
||||||
@@ -61,8 +60,7 @@ def on_event(
|
|||||||
*event_types: EventType,
|
*event_types: EventType,
|
||||||
priority: int = Priority.NORMAL,
|
priority: int = Priority.NORMAL,
|
||||||
) -> Callable[[EventHandler], EventHandler]:
|
) -> Callable[[EventHandler], EventHandler]:
|
||||||
"""
|
"""Register a function as a handler for one or more event types.
|
||||||
Decorator to register a function as a handler for one or more event types.
|
|
||||||
|
|
||||||
The decorated function will be called whenever an event of the specified
|
The decorated function will be called whenever an event of the specified
|
||||||
type(s) is received. Handlers are executed sequentially in priority order
|
type(s) is received. Handlers are executed sequentially in priority order
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ type LifecycleHandler = "Callable[[ModuleContext], Awaitable[None]]"
|
|||||||
|
|
||||||
|
|
||||||
def on_setup(func: LifecycleHandler) -> LifecycleHandler:
|
def on_setup(func: LifecycleHandler) -> LifecycleHandler:
|
||||||
"""
|
"""Mark a function as a module setup hook.
|
||||||
Decorator to mark a function as a module setup hook.
|
|
||||||
|
|
||||||
The decorated function will be called during module loading with a
|
The decorated function will be called during module loading with a
|
||||||
``ModuleContext``. Setup hooks run inside a storage transaction that
|
``ModuleContext``. Setup hooks run inside a storage transaction that
|
||||||
@@ -52,8 +51,7 @@ def on_setup(func: LifecycleHandler) -> LifecycleHandler:
|
|||||||
|
|
||||||
|
|
||||||
def on_teardown(func: LifecycleHandler) -> LifecycleHandler:
|
def on_teardown(func: LifecycleHandler) -> LifecycleHandler:
|
||||||
"""
|
"""Mark a function as a module teardown hook.
|
||||||
Decorator to mark a function as a module teardown hook.
|
|
||||||
|
|
||||||
The decorated function will be called during module unload or bot
|
The decorated function will be called during module unload or bot
|
||||||
shutdown with the same ``ModuleContext`` from setup.
|
shutdown with the same ``ModuleContext`` from setup.
|
||||||
|
|||||||
@@ -186,8 +186,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self, base_url: str, username: str, password: str, http_client: HttpClient
|
self, base_url: str, username: str, password: str, http_client: HttpClient
|
||||||
):
|
):
|
||||||
"""
|
"""Initialize the admin client.
|
||||||
Initialize the admin client.
|
|
||||||
|
|
||||||
:param base_url: The Owncast server URL (e.g., "https://stream.logal.dev").
|
:param base_url: The Owncast server URL (e.g., "https://stream.logal.dev").
|
||||||
:param username: Admin username.
|
:param username: Admin username.
|
||||||
@@ -203,24 +202,21 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def get_status(self) -> dict[str, Any]:
|
async def get_status(self) -> dict[str, Any]:
|
||||||
"""
|
"""Get the current server status including stream info and viewer count.
|
||||||
Get the current server status including stream info and viewer count.
|
|
||||||
|
|
||||||
:return: Server status dict.
|
:return: Server status dict.
|
||||||
"""
|
"""
|
||||||
return dict(await self._get("/api/admin/status"))
|
return dict(await self._get("/api/admin/status"))
|
||||||
|
|
||||||
async def get_active_viewers(self) -> list[dict[str, Any]]:
|
async def get_active_viewers(self) -> list[dict[str, Any]]:
|
||||||
"""
|
"""Get a list of currently active viewers.
|
||||||
Get a list of currently active viewers.
|
|
||||||
|
|
||||||
:return: Viewer list as returned by the Owncast API.
|
:return: Viewer list as returned by the Owncast API.
|
||||||
"""
|
"""
|
||||||
return list(await self._get("/api/admin/viewers"))
|
return list(await self._get("/api/admin/viewers"))
|
||||||
|
|
||||||
async def get_viewers_over_time(self, window_start: int) -> list[dict[str, Any]]:
|
async def get_viewers_over_time(self, window_start: int) -> list[dict[str, Any]]:
|
||||||
"""
|
"""Get viewer count data over time for charting.
|
||||||
Get viewer count data over time for charting.
|
|
||||||
|
|
||||||
:param window_start: Unix timestamp (seconds since epoch)
|
:param window_start: Unix timestamp (seconds since epoch)
|
||||||
for the start of the window.
|
for the start of the window.
|
||||||
@@ -233,24 +229,21 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def get_hardware_stats(self) -> dict[str, Any]:
|
async def get_hardware_stats(self) -> dict[str, Any]:
|
||||||
"""
|
"""Get server hardware statistics (CPU, memory, disk).
|
||||||
Get server hardware statistics (CPU, memory, disk).
|
|
||||||
|
|
||||||
:return: Hardware stats dict.
|
:return: Hardware stats dict.
|
||||||
"""
|
"""
|
||||||
return dict(await self._get("/api/admin/hardwarestats"))
|
return dict(await self._get("/api/admin/hardwarestats"))
|
||||||
|
|
||||||
async def get_server_config(self) -> dict[str, Any]:
|
async def get_server_config(self) -> dict[str, Any]:
|
||||||
"""
|
"""Get the full server configuration.
|
||||||
Get the full server configuration.
|
|
||||||
|
|
||||||
:return: Server configuration dict.
|
:return: Server configuration dict.
|
||||||
"""
|
"""
|
||||||
return dict(await self._get("/api/admin/serverconfig"))
|
return dict(await self._get("/api/admin/serverconfig"))
|
||||||
|
|
||||||
async def disconnect_stream(self) -> dict[str, Any]:
|
async def disconnect_stream(self) -> dict[str, Any]:
|
||||||
"""
|
"""Disconnect the current inbound stream.
|
||||||
Disconnect the current inbound stream.
|
|
||||||
|
|
||||||
:return: API response confirming the disconnect.
|
:return: API response confirming the disconnect.
|
||||||
"""
|
"""
|
||||||
@@ -258,16 +251,14 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
return dict(await self._get("/api/admin/disconnect"))
|
return dict(await self._get("/api/admin/disconnect"))
|
||||||
|
|
||||||
async def get_chat_messages(self) -> list[dict[str, Any]]:
|
async def get_chat_messages(self) -> list[dict[str, Any]]:
|
||||||
"""
|
"""Get chat messages from the admin perspective.
|
||||||
Get chat messages from the admin perspective.
|
|
||||||
|
|
||||||
:return: Chat message list as returned by the Owncast API.
|
:return: Chat message list as returned by the Owncast API.
|
||||||
"""
|
"""
|
||||||
return list(await self._get("/api/admin/chat/messages"))
|
return list(await self._get("/api/admin/chat/messages"))
|
||||||
|
|
||||||
async def get_connected_chat_clients(self) -> list[dict[str, Any]]:
|
async def get_connected_chat_clients(self) -> list[dict[str, Any]]:
|
||||||
"""
|
"""Get currently connected chat clients.
|
||||||
Get currently connected chat clients.
|
|
||||||
|
|
||||||
:return: Client list as returned by the Owncast API.
|
:return: Client list as returned by the Owncast API.
|
||||||
"""
|
"""
|
||||||
@@ -276,8 +267,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
async def set_message_visibility(
|
async def set_message_visibility(
|
||||||
self, message_ids: list[str], visible: bool
|
self, message_ids: list[str], visible: bool
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""Hide or show chat messages.
|
||||||
Hide or show chat messages.
|
|
||||||
|
|
||||||
:param message_ids: List of message IDs to modify.
|
:param message_ids: List of message IDs to modify.
|
||||||
:param visible: True to show messages, False to hide them.
|
:param visible: True to show messages, False to hide them.
|
||||||
@@ -292,8 +282,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_user_enabled(self, user_id: str, enabled: bool) -> str:
|
async def set_user_enabled(self, user_id: str, enabled: bool) -> str:
|
||||||
"""
|
"""Enable or disable a chat user.
|
||||||
Enable or disable a chat user.
|
|
||||||
|
|
||||||
:param user_id: The user ID to modify.
|
:param user_id: The user ID to modify.
|
||||||
:param enabled: True to enable, False to disable the user.
|
:param enabled: True to enable, False to disable the user.
|
||||||
@@ -307,16 +296,14 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def get_disabled_users(self) -> list[dict[str, Any]]:
|
async def get_disabled_users(self) -> list[dict[str, Any]]:
|
||||||
"""
|
"""Get a list of disabled chat users.
|
||||||
Get a list of disabled chat users.
|
|
||||||
|
|
||||||
:return: User list as returned by the Owncast API.
|
:return: User list as returned by the Owncast API.
|
||||||
"""
|
"""
|
||||||
return list(await self._get("/api/admin/chat/users/disabled"))
|
return list(await self._get("/api/admin/chat/users/disabled"))
|
||||||
|
|
||||||
async def set_user_moderator(self, user_id: str, is_mod: bool) -> str:
|
async def set_user_moderator(self, user_id: str, is_mod: bool) -> str:
|
||||||
"""
|
"""Grant or revoke moderator status for a user.
|
||||||
Grant or revoke moderator status for a user.
|
|
||||||
|
|
||||||
:param user_id: The user ID to modify.
|
:param user_id: The user ID to modify.
|
||||||
:param is_mod: True to grant moderator, False to revoke.
|
:param is_mod: True to grant moderator, False to revoke.
|
||||||
@@ -330,16 +317,14 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def get_moderators(self) -> list[dict[str, Any]]:
|
async def get_moderators(self) -> list[dict[str, Any]]:
|
||||||
"""
|
"""Get a list of moderator users.
|
||||||
Get a list of moderator users.
|
|
||||||
|
|
||||||
:return: User list as returned by the Owncast API.
|
:return: User list as returned by the Owncast API.
|
||||||
"""
|
"""
|
||||||
return list(await self._get("/api/admin/chat/users/moderators"))
|
return list(await self._get("/api/admin/chat/users/moderators"))
|
||||||
|
|
||||||
async def ban_ip_address(self, ip: str) -> str:
|
async def ban_ip_address(self, ip: str) -> str:
|
||||||
"""
|
"""Ban an IP address from chat.
|
||||||
Ban an IP address from chat.
|
|
||||||
|
|
||||||
:param ip: The IP address to ban.
|
:param ip: The IP address to ban.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -348,8 +333,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
return await self._post("/api/admin/chat/users/ipbans/create", {"value": ip})
|
return await self._post("/api/admin/chat/users/ipbans/create", {"value": ip})
|
||||||
|
|
||||||
async def unban_ip_address(self, ip: str) -> str:
|
async def unban_ip_address(self, ip: str) -> str:
|
||||||
"""
|
"""Remove an IP address ban.
|
||||||
Remove an IP address ban.
|
|
||||||
|
|
||||||
:param ip: The IP address to unban.
|
:param ip: The IP address to unban.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -358,16 +342,14 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
return await self._post("/api/admin/chat/users/ipbans/remove", {"value": ip})
|
return await self._post("/api/admin/chat/users/ipbans/remove", {"value": ip})
|
||||||
|
|
||||||
async def get_ip_address_bans(self) -> list[dict[str, Any]]:
|
async def get_ip_address_bans(self) -> list[dict[str, Any]]:
|
||||||
"""
|
"""Get a list of banned IP addresses.
|
||||||
Get a list of banned IP addresses.
|
|
||||||
|
|
||||||
:return: IP ban list as returned by the Owncast API.
|
:return: IP ban list as returned by the Owncast API.
|
||||||
"""
|
"""
|
||||||
return list(await self._get("/api/admin/chat/users/ipbans"))
|
return list(await self._get("/api/admin/chat/users/ipbans"))
|
||||||
|
|
||||||
async def set_stream_title(self, title: str) -> str:
|
async def set_stream_title(self, title: str) -> str:
|
||||||
"""
|
"""Set the stream title.
|
||||||
Set the stream title.
|
|
||||||
|
|
||||||
:param title: The new stream title.
|
:param title: The new stream title.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -377,8 +359,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_server_name(self, name: str) -> str:
|
async def set_server_name(self, name: str) -> str:
|
||||||
"""
|
"""Set the server name.
|
||||||
Set the server name.
|
|
||||||
|
|
||||||
:param name: The new server name.
|
:param name: The new server name.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -388,8 +369,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_server_summary(self, summary: str) -> str:
|
async def set_server_summary(self, summary: str) -> str:
|
||||||
"""
|
"""Set the server summary.
|
||||||
Set the server summary.
|
|
||||||
|
|
||||||
:param summary: The new server summary.
|
:param summary: The new server summary.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -399,8 +379,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_welcome_message(self, message: str) -> str:
|
async def set_welcome_message(self, message: str) -> str:
|
||||||
"""
|
"""Set the welcome message shown to new viewers.
|
||||||
Set the welcome message shown to new viewers.
|
|
||||||
|
|
||||||
:param message: The new welcome message.
|
:param message: The new welcome message.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -410,8 +389,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_offline_message(self, message: str) -> str:
|
async def set_offline_message(self, message: str) -> str:
|
||||||
"""
|
"""Set the message shown when the stream is offline.
|
||||||
Set the message shown when the stream is offline.
|
|
||||||
|
|
||||||
:param message: The new offline message.
|
:param message: The new offline message.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -421,8 +399,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_page_content(self, content: str) -> str:
|
async def set_page_content(self, content: str) -> str:
|
||||||
"""
|
"""Set the custom page content (HTML/markdown below the player).
|
||||||
Set the custom page content (HTML/markdown below the player).
|
|
||||||
|
|
||||||
:param content: The page content.
|
:param content: The page content.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -432,8 +409,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_admin_password(self, password: str) -> str:
|
async def set_admin_password(self, password: str) -> str:
|
||||||
"""
|
"""Change the admin password.
|
||||||
Change the admin password.
|
|
||||||
|
|
||||||
:param password: The new admin password.
|
:param password: The new admin password.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -443,8 +419,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_server_url(self, url: str) -> str:
|
async def set_server_url(self, url: str) -> str:
|
||||||
"""
|
"""Set the public server URL.
|
||||||
Set the public server URL.
|
|
||||||
|
|
||||||
:param url: The new server URL.
|
:param url: The new server URL.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -454,8 +429,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_custom_styles(self, css: str) -> str:
|
async def set_custom_styles(self, css: str) -> str:
|
||||||
"""
|
"""Set custom CSS styles for the web interface.
|
||||||
Set custom CSS styles for the web interface.
|
|
||||||
|
|
||||||
:param css: The CSS string.
|
:param css: The CSS string.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -465,8 +439,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_custom_javascript(self, js: str) -> str:
|
async def set_custom_javascript(self, js: str) -> str:
|
||||||
"""
|
"""Set custom JavaScript for the web interface.
|
||||||
Set custom JavaScript for the web interface.
|
|
||||||
|
|
||||||
:param js: The JavaScript string.
|
:param js: The JavaScript string.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -476,8 +449,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_socket_host_override(self, host: str) -> str:
|
async def set_socket_host_override(self, host: str) -> str:
|
||||||
"""
|
"""Set the WebSocket host override.
|
||||||
Set the WebSocket host override.
|
|
||||||
|
|
||||||
:param host: The WebSocket host override value.
|
:param host: The WebSocket host override value.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -487,8 +459,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_ffmpeg_path(self, path: str) -> str:
|
async def set_ffmpeg_path(self, path: str) -> str:
|
||||||
"""
|
"""Set the path to the ffmpeg binary.
|
||||||
Set the path to the ffmpeg binary.
|
|
||||||
|
|
||||||
:param path: The ffmpeg binary path.
|
:param path: The ffmpeg binary path.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -498,8 +469,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_video_serving_endpoint(self, endpoint: str) -> str:
|
async def set_video_serving_endpoint(self, endpoint: str) -> str:
|
||||||
"""
|
"""Set the video serving endpoint (e.g., for CDN).
|
||||||
Set the video serving endpoint (e.g., for CDN).
|
|
||||||
|
|
||||||
:param endpoint: The video serving endpoint URL.
|
:param endpoint: The video serving endpoint URL.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -509,8 +479,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_video_codec(self, codec: str) -> str:
|
async def set_video_codec(self, codec: str) -> str:
|
||||||
"""
|
"""Set the video codec.
|
||||||
Set the video codec.
|
|
||||||
|
|
||||||
:param codec: The video codec name.
|
:param codec: The video codec name.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -520,8 +489,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_chat_disabled(self, disabled: bool) -> str:
|
async def set_chat_disabled(self, disabled: bool) -> str:
|
||||||
"""
|
"""Enable or disable the chat.
|
||||||
Enable or disable the chat.
|
|
||||||
|
|
||||||
:param disabled: True to disable chat, False to enable it.
|
:param disabled: True to disable chat, False to enable it.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -531,8 +499,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_chat_join_messages_enabled(self, enabled: bool) -> str:
|
async def set_chat_join_messages_enabled(self, enabled: bool) -> str:
|
||||||
"""
|
"""Enable or disable chat join messages.
|
||||||
Enable or disable chat join messages.
|
|
||||||
|
|
||||||
:param enabled: True to show join messages, False to hide them.
|
:param enabled: True to show join messages, False to hide them.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -542,8 +509,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_chat_established_mode(self, enabled: bool) -> str:
|
async def set_chat_established_mode(self, enabled: bool) -> str:
|
||||||
"""
|
"""Enable or disable established user mode for chat.
|
||||||
Enable or disable established user mode for chat.
|
|
||||||
|
|
||||||
:param enabled: True to enable established mode.
|
:param enabled: True to enable established mode.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -555,8 +521,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_chat_spam_protection(self, enabled: bool) -> str:
|
async def set_chat_spam_protection(self, enabled: bool) -> str:
|
||||||
"""
|
"""Enable or disable chat spam protection.
|
||||||
Enable or disable chat spam protection.
|
|
||||||
|
|
||||||
:param enabled: True to enable spam protection.
|
:param enabled: True to enable spam protection.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -568,8 +533,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_chat_slur_filter(self, enabled: bool) -> str:
|
async def set_chat_slur_filter(self, enabled: bool) -> str:
|
||||||
"""
|
"""Enable or disable the chat slur filter.
|
||||||
Enable or disable the chat slur filter.
|
|
||||||
|
|
||||||
:param enabled: True to enable the slur filter.
|
:param enabled: True to enable the slur filter.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -579,8 +543,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_nsfw(self, nsfw: bool) -> str:
|
async def set_nsfw(self, nsfw: bool) -> str:
|
||||||
"""
|
"""Set the NSFW flag for the server.
|
||||||
Set the NSFW flag for the server.
|
|
||||||
|
|
||||||
:param nsfw: True to mark the server as NSFW.
|
:param nsfw: True to mark the server as NSFW.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -588,8 +551,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
return await self._set_config_value("/api/admin/config/nsfw", nsfw, "NSFW flag")
|
return await self._set_config_value("/api/admin/config/nsfw", nsfw, "NSFW flag")
|
||||||
|
|
||||||
async def set_directory_enabled(self, enabled: bool) -> str:
|
async def set_directory_enabled(self, enabled: bool) -> str:
|
||||||
"""
|
"""Enable or disable listing in the Owncast directory.
|
||||||
Enable or disable listing in the Owncast directory.
|
|
||||||
|
|
||||||
:param enabled: True to enable directory listing.
|
:param enabled: True to enable directory listing.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -599,8 +561,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_hide_viewer_count(self, hide: bool) -> str:
|
async def set_hide_viewer_count(self, hide: bool) -> str:
|
||||||
"""
|
"""Show or hide the viewer count.
|
||||||
Show or hide the viewer count.
|
|
||||||
|
|
||||||
:param hide: True to hide the viewer count.
|
:param hide: True to hide the viewer count.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -610,8 +571,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_disable_search_indexing(self, disabled: bool) -> str:
|
async def set_disable_search_indexing(self, disabled: bool) -> str:
|
||||||
"""
|
"""Enable or disable search engine indexing.
|
||||||
Enable or disable search engine indexing.
|
|
||||||
|
|
||||||
:param disabled: True to disable search indexing.
|
:param disabled: True to disable search indexing.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -623,8 +583,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_forbidden_usernames(self, names: list[str]) -> str:
|
async def set_forbidden_usernames(self, names: list[str]) -> str:
|
||||||
"""
|
"""Set the list of forbidden usernames.
|
||||||
Set the list of forbidden usernames.
|
|
||||||
|
|
||||||
:param names: List of forbidden username strings.
|
:param names: List of forbidden username strings.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -634,8 +593,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_suggested_usernames(self, names: list[str]) -> str:
|
async def set_suggested_usernames(self, names: list[str]) -> str:
|
||||||
"""
|
"""Set the list of suggested usernames for new viewers.
|
||||||
Set the list of suggested usernames for new viewers.
|
|
||||||
|
|
||||||
:param names: List of suggested username strings.
|
:param names: List of suggested username strings.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -645,8 +603,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_tags(self, tags: list[str]) -> str:
|
async def set_tags(self, tags: list[str]) -> str:
|
||||||
"""
|
"""Set the server tags.
|
||||||
Set the server tags.
|
|
||||||
|
|
||||||
:param tags: List of tag strings.
|
:param tags: List of tag strings.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -654,8 +611,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
return await self._set_config_value("/api/admin/config/tags", tags, "tags")
|
return await self._set_config_value("/api/admin/config/tags", tags, "tags")
|
||||||
|
|
||||||
async def set_federation_blocked_domains(self, domains: list[str]) -> str:
|
async def set_federation_blocked_domains(self, domains: list[str]) -> str:
|
||||||
"""
|
"""Set the list of blocked federation domains.
|
||||||
Set the list of blocked federation domains.
|
|
||||||
|
|
||||||
:param domains: List of domain strings to block.
|
:param domains: List of domain strings to block.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -667,8 +623,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_stream_keys(self, keys: list[StreamKey]) -> str:
|
async def set_stream_keys(self, keys: list[StreamKey]) -> str:
|
||||||
"""
|
"""Set the stream keys.
|
||||||
Set the stream keys.
|
|
||||||
|
|
||||||
:param keys: List of :class:`StreamKey` objects.
|
:param keys: List of :class:`StreamKey` objects.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -680,8 +635,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_video_variants(self, variants: list[VideoVariant]) -> str:
|
async def set_video_variants(self, variants: list[VideoVariant]) -> str:
|
||||||
"""
|
"""Set the video output variants (quality levels).
|
||||||
Set the video output variants (quality levels).
|
|
||||||
|
|
||||||
:param variants: List of :class:`VideoVariant` objects.
|
:param variants: List of :class:`VideoVariant` objects.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -693,8 +647,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_social_handles(self, handles: list[SocialHandle]) -> str:
|
async def set_social_handles(self, handles: list[SocialHandle]) -> str:
|
||||||
"""
|
"""Set the social media handles displayed on the page.
|
||||||
Set the social media handles displayed on the page.
|
|
||||||
|
|
||||||
:param handles: List of :class:`SocialHandle` objects.
|
:param handles: List of :class:`SocialHandle` objects.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -706,8 +659,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_external_actions(self, actions: list[ExternalAction]) -> str:
|
async def set_external_actions(self, actions: list[ExternalAction]) -> str:
|
||||||
"""
|
"""Set the external actions (buttons/links in the player).
|
||||||
Set the external actions (buttons/links in the player).
|
|
||||||
|
|
||||||
:param actions: List of :class:`ExternalAction` objects.
|
:param actions: List of :class:`ExternalAction` objects.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -727,8 +679,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
bucket: str,
|
bucket: str,
|
||||||
region: str,
|
region: str,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""Set the S3 storage configuration.
|
||||||
Set the S3 storage configuration.
|
|
||||||
|
|
||||||
:param enabled: Whether S3 storage is enabled.
|
:param enabled: Whether S3 storage is enabled.
|
||||||
:param endpoint: The S3 endpoint URL.
|
:param endpoint: The S3 endpoint URL.
|
||||||
@@ -757,8 +708,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
webhook: str,
|
webhook: str,
|
||||||
go_live_message: str,
|
go_live_message: str,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""Set the Discord notification configuration.
|
||||||
Set the Discord notification configuration.
|
|
||||||
|
|
||||||
:param enabled: Whether Discord notifications are enabled.
|
:param enabled: Whether Discord notifications are enabled.
|
||||||
:param webhook: The Discord webhook URL.
|
:param webhook: The Discord webhook URL.
|
||||||
@@ -780,8 +730,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
enabled: bool,
|
enabled: bool,
|
||||||
go_live_message: str,
|
go_live_message: str,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""Set the browser notification configuration.
|
||||||
Set the browser notification configuration.
|
|
||||||
|
|
||||||
:param enabled: Whether browser notifications are enabled.
|
:param enabled: Whether browser notifications are enabled.
|
||||||
:param go_live_message: The message shown when going live.
|
:param go_live_message: The message shown when going live.
|
||||||
@@ -797,8 +746,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_color_variables(self, variables: dict[str, Any]) -> str:
|
async def set_color_variables(self, variables: dict[str, Any]) -> str:
|
||||||
"""
|
"""Set the custom color variables for the web interface.
|
||||||
Set the custom color variables for the web interface.
|
|
||||||
|
|
||||||
:param variables: Color variables dict.
|
:param variables: Color variables dict.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -808,8 +756,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_stream_latency(self, level: int) -> str:
|
async def set_stream_latency(self, level: int) -> str:
|
||||||
"""
|
"""Set the stream latency level.
|
||||||
Set the stream latency level.
|
|
||||||
|
|
||||||
:param level: Latency level value.
|
:param level: Latency level value.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -819,8 +766,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_rtmp_port(self, port: int) -> str:
|
async def set_rtmp_port(self, port: int) -> str:
|
||||||
"""
|
"""Set the RTMP server port.
|
||||||
Set the RTMP server port.
|
|
||||||
|
|
||||||
:param port: The RTMP port number.
|
:param port: The RTMP port number.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -830,8 +776,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_web_server_port(self, port: int) -> str:
|
async def set_web_server_port(self, port: int) -> str:
|
||||||
"""
|
"""Set the web server port.
|
||||||
Set the web server port.
|
|
||||||
|
|
||||||
:param port: The web server port number.
|
:param port: The web server port number.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -841,8 +786,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_web_server_ip(self, ip: str) -> str:
|
async def set_web_server_ip(self, ip: str) -> str:
|
||||||
"""
|
"""Set the web server bind IP address.
|
||||||
Set the web server bind IP address.
|
|
||||||
|
|
||||||
:param ip: The IP address to bind to.
|
:param ip: The IP address to bind to.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -852,8 +796,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_federation_enabled(self, enabled: bool) -> str:
|
async def set_federation_enabled(self, enabled: bool) -> str:
|
||||||
"""
|
"""Enable or disable federation (ActivityPub).
|
||||||
Enable or disable federation (ActivityPub).
|
|
||||||
|
|
||||||
:param enabled: True to enable federation.
|
:param enabled: True to enable federation.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -863,8 +806,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_federation_username(self, name: str) -> str:
|
async def set_federation_username(self, name: str) -> str:
|
||||||
"""
|
"""Set the federation (ActivityPub) username.
|
||||||
Set the federation (ActivityPub) username.
|
|
||||||
|
|
||||||
:param name: The federation username.
|
:param name: The federation username.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -874,8 +816,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_federation_go_live_message(self, message: str) -> str:
|
async def set_federation_go_live_message(self, message: str) -> str:
|
||||||
"""
|
"""Set the message sent to followers when going live.
|
||||||
Set the message sent to followers when going live.
|
|
||||||
|
|
||||||
:param message: The go-live notification message.
|
:param message: The go-live notification message.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -887,8 +828,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def set_logo(self, base64_data_url: str) -> str:
|
async def set_logo(self, base64_data_url: str) -> str:
|
||||||
"""
|
"""Set the server logo from a base64 data URL.
|
||||||
Set the server logo from a base64 data URL.
|
|
||||||
|
|
||||||
:param base64_data_url: Data URL string
|
:param base64_data_url: Data URL string
|
||||||
(e.g., ``data:image/png;base64,iVBOR...``).
|
(e.g., ``data:image/png;base64,iVBOR...``).
|
||||||
@@ -899,8 +839,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def upload_emoji(self, name: str, data_base64: str) -> str:
|
async def upload_emoji(self, name: str, data_base64: str) -> str:
|
||||||
"""
|
"""Upload a custom emoji.
|
||||||
Upload a custom emoji.
|
|
||||||
|
|
||||||
:param name: The emoji name.
|
:param name: The emoji name.
|
||||||
:param data_base64: Base64-encoded image data for the emoji.
|
:param data_base64: Base64-encoded image data for the emoji.
|
||||||
@@ -912,8 +851,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def delete_emoji(self, name: str) -> str:
|
async def delete_emoji(self, name: str) -> str:
|
||||||
"""
|
"""Delete a custom emoji.
|
||||||
Delete a custom emoji.
|
|
||||||
|
|
||||||
:param name: The emoji name to delete.
|
:param name: The emoji name to delete.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -922,16 +860,14 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
return await self._post("/api/admin/emoji/delete", {"name": name})
|
return await self._post("/api/admin/emoji/delete", {"name": name})
|
||||||
|
|
||||||
async def get_webhooks(self) -> list[dict[str, Any]]:
|
async def get_webhooks(self) -> list[dict[str, Any]]:
|
||||||
"""
|
"""Get all registered webhooks.
|
||||||
Get all registered webhooks.
|
|
||||||
|
|
||||||
:return: Webhook list as returned by the Owncast API.
|
:return: Webhook list as returned by the Owncast API.
|
||||||
"""
|
"""
|
||||||
return list(await self._get("/api/admin/webhooks"))
|
return list(await self._get("/api/admin/webhooks"))
|
||||||
|
|
||||||
async def create_webhook(self, url: str, events: list[str]) -> str:
|
async def create_webhook(self, url: str, events: list[str]) -> str:
|
||||||
"""
|
"""Create a new webhook.
|
||||||
Create a new webhook.
|
|
||||||
|
|
||||||
:param url: The URL to send webhook events to.
|
:param url: The URL to send webhook events to.
|
||||||
:param events: List of event type strings to subscribe to.
|
:param events: List of event type strings to subscribe to.
|
||||||
@@ -943,8 +879,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def delete_webhook(self, webhook_id: int) -> str:
|
async def delete_webhook(self, webhook_id: int) -> str:
|
||||||
"""
|
"""Delete a webhook.
|
||||||
Delete a webhook.
|
|
||||||
|
|
||||||
:param webhook_id: The ID of the webhook to delete.
|
:param webhook_id: The ID of the webhook to delete.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -953,16 +888,14 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
return await self._post("/api/admin/webhooks/delete", {"id": webhook_id})
|
return await self._post("/api/admin/webhooks/delete", {"id": webhook_id})
|
||||||
|
|
||||||
async def get_access_tokens(self) -> list[dict[str, Any]]:
|
async def get_access_tokens(self) -> list[dict[str, Any]]:
|
||||||
"""
|
"""Get all access tokens.
|
||||||
Get all access tokens.
|
|
||||||
|
|
||||||
:return: Token list as returned by the Owncast API.
|
:return: Token list as returned by the Owncast API.
|
||||||
"""
|
"""
|
||||||
return list(await self._get("/api/admin/accesstokens"))
|
return list(await self._get("/api/admin/accesstokens"))
|
||||||
|
|
||||||
async def create_access_token(self, name: str, scopes: list[str]) -> str:
|
async def create_access_token(self, name: str, scopes: list[str]) -> str:
|
||||||
"""
|
"""Create a new access token.
|
||||||
Create a new access token.
|
|
||||||
|
|
||||||
:param name: Display name for the token.
|
:param name: Display name for the token.
|
||||||
:param scopes: List of permission scope strings.
|
:param scopes: List of permission scope strings.
|
||||||
@@ -974,8 +907,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def delete_access_token(self, token: str) -> str:
|
async def delete_access_token(self, token: str) -> str:
|
||||||
"""
|
"""Delete an access token.
|
||||||
Delete an access token.
|
|
||||||
|
|
||||||
:param token: The token string to delete.
|
:param token: The token string to delete.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -985,8 +917,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
return await self._post("/api/admin/accesstokens/delete", {"token": token})
|
return await self._post("/api/admin/accesstokens/delete", {"token": token})
|
||||||
|
|
||||||
async def get_followers(self, offset: int = 0, limit: int = 25) -> dict[str, Any]:
|
async def get_followers(self, offset: int = 0, limit: int = 25) -> dict[str, Any]:
|
||||||
"""
|
"""Get a paginated list of followers.
|
||||||
Get a paginated list of followers.
|
|
||||||
|
|
||||||
:param offset: Number of followers to skip.
|
:param offset: Number of followers to skip.
|
||||||
:param limit: Maximum number of followers to return.
|
:param limit: Maximum number of followers to return.
|
||||||
@@ -999,24 +930,21 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def get_pending_follow_requests(self) -> list[dict[str, Any]]:
|
async def get_pending_follow_requests(self) -> list[dict[str, Any]]:
|
||||||
"""
|
"""Get pending follow requests.
|
||||||
Get pending follow requests.
|
|
||||||
|
|
||||||
:return: Follower request list as returned by the Owncast API.
|
:return: Follower request list as returned by the Owncast API.
|
||||||
"""
|
"""
|
||||||
return list(await self._get("/api/admin/followers/pending"))
|
return list(await self._get("/api/admin/followers/pending"))
|
||||||
|
|
||||||
async def get_blocked_followers(self) -> list[dict[str, Any]]:
|
async def get_blocked_followers(self) -> list[dict[str, Any]]:
|
||||||
"""
|
"""Get blocked and rejected followers.
|
||||||
Get blocked and rejected followers.
|
|
||||||
|
|
||||||
:return: Follower list as returned by the Owncast API.
|
:return: Follower list as returned by the Owncast API.
|
||||||
"""
|
"""
|
||||||
return list(await self._get("/api/admin/followers/blocked"))
|
return list(await self._get("/api/admin/followers/blocked"))
|
||||||
|
|
||||||
async def approve_follower(self, actor_iri: str, approved: bool) -> str:
|
async def approve_follower(self, actor_iri: str, approved: bool) -> str:
|
||||||
"""
|
"""Approve or reject a follow request.
|
||||||
Approve or reject a follow request.
|
|
||||||
|
|
||||||
:param actor_iri: The ActivityPub actor IRI of the follower.
|
:param actor_iri: The ActivityPub actor IRI of the follower.
|
||||||
:param approved: True to approve, False to reject.
|
:param approved: True to approve, False to reject.
|
||||||
@@ -1030,8 +958,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def send_federated_message(self, message: str) -> str:
|
async def send_federated_message(self, message: str) -> str:
|
||||||
"""
|
"""Send a message to all followers via federation.
|
||||||
Send a message to all followers via federation.
|
|
||||||
|
|
||||||
:param message: The message text to send.
|
:param message: The message text to send.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -1040,24 +967,21 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
return await self._post("/api/admin/federation/send", {"value": message})
|
return await self._post("/api/admin/federation/send", {"value": message})
|
||||||
|
|
||||||
async def get_logs(self) -> list[dict[str, Any]]:
|
async def get_logs(self) -> list[dict[str, Any]]:
|
||||||
"""
|
"""Get server logs.
|
||||||
Get server logs.
|
|
||||||
|
|
||||||
:return: Log entry list as returned by the Owncast API.
|
:return: Log entry list as returned by the Owncast API.
|
||||||
"""
|
"""
|
||||||
return list(await self._get("/api/admin/logs"))
|
return list(await self._get("/api/admin/logs"))
|
||||||
|
|
||||||
async def get_warnings(self) -> list[dict[str, Any]]:
|
async def get_warnings(self) -> list[dict[str, Any]]:
|
||||||
"""
|
"""Get server warning and error logs.
|
||||||
Get server warning and error logs.
|
|
||||||
|
|
||||||
:return: Log entry list as returned by the Owncast API.
|
:return: Log entry list as returned by the Owncast API.
|
||||||
"""
|
"""
|
||||||
return list(await self._get("/api/admin/logs/warnings"))
|
return list(await self._get("/api/admin/logs/warnings"))
|
||||||
|
|
||||||
async def get_playback_metrics(self) -> dict[str, Any]:
|
async def get_playback_metrics(self) -> dict[str, Any]:
|
||||||
"""
|
"""Get playback quality metrics.
|
||||||
Get playback quality metrics.
|
|
||||||
|
|
||||||
:return: Playback metrics as returned by the Owncast API.
|
:return: Playback metrics as returned by the Owncast API.
|
||||||
"""
|
"""
|
||||||
@@ -1066,8 +990,7 @@ class OwncastAdminClient(OwncastClient):
|
|||||||
async def _set_config_value(
|
async def _set_config_value(
|
||||||
self, endpoint: str, value: Any, description: str
|
self, endpoint: str, value: Any, description: str
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""Set a configuration value via POST with a ``{"value": ...}`` body.
|
||||||
Set a configuration value via POST with a ``{"value": ...}`` body.
|
|
||||||
|
|
||||||
:param endpoint: The config API endpoint path.
|
:param endpoint: The config API endpoint path.
|
||||||
:param value: The value to set.
|
:param value: The value to set.
|
||||||
|
|||||||
@@ -52,8 +52,7 @@ class OwncastError(Exception):
|
|||||||
"""Raised when an Owncast API request fails."""
|
"""Raised when an Owncast API request fails."""
|
||||||
|
|
||||||
def __init__(self, status: int, message: str):
|
def __init__(self, status: int, message: str):
|
||||||
"""
|
"""Initialize the error.
|
||||||
Initialize the error.
|
|
||||||
|
|
||||||
:param status: HTTP status code from the failed request, or 0 if the
|
:param status: HTTP status code from the failed request, or 0 if the
|
||||||
request failed due to a connection error before receiving a response.
|
request failed due to a connection error before receiving a response.
|
||||||
@@ -72,8 +71,7 @@ class OwncastClient:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, base_url: str, access_token: str, http_client: HttpClient):
|
def __init__(self, base_url: str, access_token: str, http_client: HttpClient):
|
||||||
"""
|
"""Initialize the Owncast client.
|
||||||
Initialize the Owncast client.
|
|
||||||
|
|
||||||
:param base_url: The Owncast server URL (e.g., "https://stream.logal.dev").
|
:param base_url: The Owncast server URL (e.g., "https://stream.logal.dev").
|
||||||
:param access_token: API access token from Owncast admin settings.
|
:param access_token: API access token from Owncast admin settings.
|
||||||
@@ -94,8 +92,7 @@ class OwncastClient:
|
|||||||
return self._base_url
|
return self._base_url
|
||||||
|
|
||||||
async def get_status(self) -> dict[str, Any]:
|
async def get_status(self) -> dict[str, Any]:
|
||||||
"""
|
"""Get the public server status.
|
||||||
Get the public server status.
|
|
||||||
|
|
||||||
This is a public endpoint that does not require authentication.
|
This is a public endpoint that does not require authentication.
|
||||||
Returns server info including version, online status, and viewer count.
|
Returns server info including version, online status, and viewer count.
|
||||||
@@ -106,8 +103,7 @@ class OwncastClient:
|
|||||||
return dict(await self._get("/api/status"))
|
return dict(await self._get("/api/status"))
|
||||||
|
|
||||||
async def send_message(self, body: str) -> str:
|
async def send_message(self, body: str) -> str:
|
||||||
"""
|
"""Send a chat message visible to all viewers.
|
||||||
Send a chat message visible to all viewers.
|
|
||||||
|
|
||||||
:param body: The message text (supports markdown).
|
:param body: The message text (supports markdown).
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -116,8 +112,7 @@ class OwncastClient:
|
|||||||
return await self._post("/api/integrations/chat/send", {"body": body})
|
return await self._post("/api/integrations/chat/send", {"body": body})
|
||||||
|
|
||||||
async def send_system_message(self, body: str) -> str:
|
async def send_system_message(self, body: str) -> str:
|
||||||
"""
|
"""Send a system message visible to all viewers.
|
||||||
Send a system message visible to all viewers.
|
|
||||||
|
|
||||||
System messages are styled differently from regular chat (typically
|
System messages are styled differently from regular chat (typically
|
||||||
italicized or dimmed) and are used for announcements or notifications.
|
italicized or dimmed) and are used for announcements or notifications.
|
||||||
@@ -129,8 +124,7 @@ class OwncastClient:
|
|||||||
return await self._post("/api/integrations/chat/system", {"body": body})
|
return await self._post("/api/integrations/chat/system", {"body": body})
|
||||||
|
|
||||||
async def send_action(self, body: str) -> str:
|
async def send_action(self, body: str) -> str:
|
||||||
"""
|
"""Send an action message (like IRC /me).
|
||||||
Send an action message (like IRC /me).
|
|
||||||
|
|
||||||
Action messages display as "*BotName does something*" and are used
|
Action messages display as "*BotName does something*" and are used
|
||||||
for describing actions rather than speech.
|
for describing actions rather than speech.
|
||||||
@@ -142,8 +136,7 @@ class OwncastClient:
|
|||||||
return await self._post("/api/integrations/chat/action", {"body": body})
|
return await self._post("/api/integrations/chat/action", {"body": body})
|
||||||
|
|
||||||
async def send_system_message_to_client(self, client_id: int, body: str) -> str:
|
async def send_system_message_to_client(self, client_id: int, body: str) -> str:
|
||||||
"""
|
"""Send a private system message to a specific viewer.
|
||||||
Send a private system message to a specific viewer.
|
|
||||||
|
|
||||||
The message is only visible to the targeted client, useful for
|
The message is only visible to the targeted client, useful for
|
||||||
welcome messages or private notifications.
|
welcome messages or private notifications.
|
||||||
@@ -160,8 +153,7 @@ class OwncastClient:
|
|||||||
async def set_message_visibility(
|
async def set_message_visibility(
|
||||||
self, message_ids: list[str], visible: bool
|
self, message_ids: list[str], visible: bool
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""Hide or show chat messages (moderation).
|
||||||
Hide or show chat messages (moderation).
|
|
||||||
|
|
||||||
Hidden messages are removed from the chat display for all viewers.
|
Hidden messages are removed from the chat display for all viewers.
|
||||||
This is typically used for moderation purposes.
|
This is typically used for moderation purposes.
|
||||||
@@ -179,8 +171,7 @@ class OwncastClient:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def get_chat_history(self) -> list[dict[str, Any]]:
|
async def get_chat_history(self) -> list[dict[str, Any]]:
|
||||||
"""
|
"""Fetch recent chat messages.
|
||||||
Fetch recent chat messages.
|
|
||||||
|
|
||||||
:return: List of recent chat message objects with user info and content.
|
:return: List of recent chat message objects with user info and content.
|
||||||
"""
|
"""
|
||||||
@@ -188,8 +179,7 @@ class OwncastClient:
|
|||||||
return list(await self._get("/api/integrations/chat"))
|
return list(await self._get("/api/integrations/chat"))
|
||||||
|
|
||||||
async def get_connected_clients(self) -> list[dict[str, Any]]:
|
async def get_connected_clients(self) -> list[dict[str, Any]]:
|
||||||
"""
|
"""Get list of currently connected viewers.
|
||||||
Get list of currently connected viewers.
|
|
||||||
|
|
||||||
:return: List of connected client objects with user info and connection details.
|
:return: List of connected client objects with user info and connection details.
|
||||||
"""
|
"""
|
||||||
@@ -197,8 +187,7 @@ class OwncastClient:
|
|||||||
return list(await self._get("/api/integrations/clients"))
|
return list(await self._get("/api/integrations/clients"))
|
||||||
|
|
||||||
async def set_stream_title(self, title: str) -> str:
|
async def set_stream_title(self, title: str) -> str:
|
||||||
"""
|
"""Update the stream title.
|
||||||
Update the stream title.
|
|
||||||
|
|
||||||
:param title: The new stream title.
|
:param title: The new stream title.
|
||||||
:return: Success message from the server.
|
:return: Success message from the server.
|
||||||
@@ -207,8 +196,7 @@ class OwncastClient:
|
|||||||
return await self._post("/api/integrations/streamtitle", {"value": title})
|
return await self._post("/api/integrations/streamtitle", {"value": title})
|
||||||
|
|
||||||
async def _post(self, endpoint: str, data: dict[str, Any] | None = None) -> str:
|
async def _post(self, endpoint: str, data: dict[str, Any] | None = None) -> str:
|
||||||
"""
|
"""Send a POST request to the Owncast API.
|
||||||
Send a POST request to the Owncast API.
|
|
||||||
|
|
||||||
Owncast POST endpoints return ``{"success": true, "message": "..."}``.
|
Owncast POST endpoints return ``{"success": true, "message": "..."}``.
|
||||||
This method validates the response and returns just the message string.
|
This method validates the response and returns just the message string.
|
||||||
@@ -270,8 +258,7 @@ class OwncastClient:
|
|||||||
raise OwncastError(0, str(e)) from e
|
raise OwncastError(0, str(e)) from e
|
||||||
|
|
||||||
async def _get(self, endpoint: str, params: dict[str, Any] | None = None) -> Any:
|
async def _get(self, endpoint: str, params: dict[str, Any] | None = None) -> Any:
|
||||||
"""
|
"""Send a GET request to the Owncast API.
|
||||||
Send a GET request to the Owncast API.
|
|
||||||
|
|
||||||
:param endpoint: The API endpoint path.
|
:param endpoint: The API endpoint path.
|
||||||
:param params: Optional query parameters.
|
:param params: Optional query parameters.
|
||||||
|
|||||||
@@ -62,8 +62,7 @@ def on_route(
|
|||||||
*,
|
*,
|
||||||
methods: list[str] | None = None,
|
methods: list[str] | None = None,
|
||||||
) -> Callable[[RouteHandler], RouteHandler]:
|
) -> Callable[[RouteHandler], RouteHandler]:
|
||||||
"""
|
"""Register an HTTP route handler.
|
||||||
Decorator to register an HTTP route handler.
|
|
||||||
|
|
||||||
Routes are namespaced under /owlbot/<module_name>/<path>.
|
Routes are namespaced under /owlbot/<module_name>/<path>.
|
||||||
|
|
||||||
|
|||||||
+13
-24
@@ -34,8 +34,7 @@ class StorageError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class ModuleStorage:
|
class ModuleStorage:
|
||||||
"""
|
"""Module-scoped async SQLite storage backed by a lazy connection pool.
|
||||||
Module-scoped async SQLite storage backed by a lazy connection pool.
|
|
||||||
|
|
||||||
Each module gets its own isolated database file. Connections are created
|
Each module gets its own isolated database file. Connections are created
|
||||||
lazily and pooled up to ``pool_size``. WAL mode is enabled so that
|
lazily and pooled up to ``pool_size``. WAL mode is enabled so that
|
||||||
@@ -54,8 +53,7 @@ class ModuleStorage:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, storage_dir: Path, module_name: str, pool_size: int = 4):
|
def __init__(self, storage_dir: Path, module_name: str, pool_size: int = 4):
|
||||||
"""
|
"""Initialize the storage API.
|
||||||
Initialize the storage API.
|
|
||||||
|
|
||||||
:param storage_dir: Directory where module databases are stored.
|
:param storage_dir: Directory where module databases are stored.
|
||||||
:param module_name: Name of the module this storage belongs to.
|
:param module_name: Name of the module this storage belongs to.
|
||||||
@@ -92,8 +90,7 @@ class ModuleStorage:
|
|||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def transaction(self) -> AsyncIterator[ModuleStorage]:
|
async def transaction(self) -> AsyncIterator[ModuleStorage]:
|
||||||
"""
|
"""Context manager for explicit transaction control within a handler.
|
||||||
Context manager for explicit transaction control within a handler.
|
|
||||||
|
|
||||||
Use this when you need multiple operations to succeed or fail together
|
Use this when you need multiple operations to succeed or fail together
|
||||||
within a single handler. Commits on success, rolls back on exception.
|
within a single handler. Commits on success, rolls back on exception.
|
||||||
@@ -105,6 +102,7 @@ class ModuleStorage:
|
|||||||
# Both committed together, or both rolled back on error
|
# Both committed together, or both rolled back on error
|
||||||
|
|
||||||
:return: This ModuleStorage instance.
|
:return: This ModuleStorage instance.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self._logger.debug("Explicit transaction started.")
|
self._logger.debug("Explicit transaction started.")
|
||||||
try:
|
try:
|
||||||
@@ -119,8 +117,7 @@ class ModuleStorage:
|
|||||||
sql: str,
|
sql: str,
|
||||||
parameters: tuple[Any, ...] | dict[str, Any] = (),
|
parameters: tuple[Any, ...] | dict[str, Any] = (),
|
||||||
) -> aiosqlite.Cursor:
|
) -> aiosqlite.Cursor:
|
||||||
"""
|
"""Execute a SQL statement.
|
||||||
Execute a SQL statement.
|
|
||||||
|
|
||||||
:param sql: SQL statement (use ? or :name for parameters).
|
:param sql: SQL statement (use ? or :name for parameters).
|
||||||
:param parameters: Query parameters (tuple for ?, dict for :name).
|
:param parameters: Query parameters (tuple for ?, dict for :name).
|
||||||
@@ -140,8 +137,7 @@ class ModuleStorage:
|
|||||||
sql: str,
|
sql: str,
|
||||||
parameters: list[tuple[Any, ...]] | list[dict[str, Any]],
|
parameters: list[tuple[Any, ...]] | list[dict[str, Any]],
|
||||||
) -> aiosqlite.Cursor:
|
) -> aiosqlite.Cursor:
|
||||||
"""
|
"""Execute a SQL statement with multiple parameter sets.
|
||||||
Execute a SQL statement with multiple parameter sets.
|
|
||||||
|
|
||||||
Useful for batch inserts/updates.
|
Useful for batch inserts/updates.
|
||||||
|
|
||||||
@@ -166,8 +162,7 @@ class ModuleStorage:
|
|||||||
sql: str,
|
sql: str,
|
||||||
parameters: tuple[Any, ...] | dict[str, Any] = (),
|
parameters: tuple[Any, ...] | dict[str, Any] = (),
|
||||||
) -> aiosqlite.Row | None:
|
) -> aiosqlite.Row | None:
|
||||||
"""
|
"""Execute a query and fetch one row.
|
||||||
Execute a query and fetch one row.
|
|
||||||
|
|
||||||
:param sql: SELECT statement.
|
:param sql: SELECT statement.
|
||||||
:param parameters: Query parameters.
|
:param parameters: Query parameters.
|
||||||
@@ -189,8 +184,7 @@ class ModuleStorage:
|
|||||||
sql: str,
|
sql: str,
|
||||||
parameters: tuple[Any, ...] | dict[str, Any] = (),
|
parameters: tuple[Any, ...] | dict[str, Any] = (),
|
||||||
) -> list[aiosqlite.Row]:
|
) -> list[aiosqlite.Row]:
|
||||||
"""
|
"""Execute a query and fetch all rows.
|
||||||
Execute a query and fetch all rows.
|
|
||||||
|
|
||||||
:param sql: SELECT statement.
|
:param sql: SELECT statement.
|
||||||
:param parameters: Query parameters.
|
:param parameters: Query parameters.
|
||||||
@@ -212,8 +206,7 @@ class ModuleStorage:
|
|||||||
sql: str,
|
sql: str,
|
||||||
parameters: tuple[Any, ...] | dict[str, Any] = (),
|
parameters: tuple[Any, ...] | dict[str, Any] = (),
|
||||||
) -> Any | None:
|
) -> Any | None:
|
||||||
"""
|
"""Execute a query and fetch a single value.
|
||||||
Execute a query and fetch a single value.
|
|
||||||
|
|
||||||
:param sql: SELECT statement returning one column.
|
:param sql: SELECT statement returning one column.
|
||||||
:param parameters: Query parameters.
|
:param parameters: Query parameters.
|
||||||
@@ -260,8 +253,7 @@ class ModuleStorage:
|
|||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def _connection(self) -> AsyncIterator[aiosqlite.Connection]:
|
async def _connection(self) -> AsyncIterator[aiosqlite.Connection]:
|
||||||
"""
|
"""Async context manager that provides a connection.
|
||||||
Async context manager that provides a connection.
|
|
||||||
|
|
||||||
If already inside a ``_checkout``, yields the checked-out connection
|
If already inside a ``_checkout``, yields the checked-out connection
|
||||||
without releasing it. Otherwise acquires a standalone connection from
|
without releasing it. Otherwise acquires a standalone connection from
|
||||||
@@ -285,8 +277,7 @@ class ModuleStorage:
|
|||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def _checkout(self) -> AsyncIterator[None]:
|
async def _checkout(self) -> AsyncIterator[None]:
|
||||||
"""
|
"""Check out a connection from the pool for the duration of a handler.
|
||||||
Check out a connection from the pool for the duration of a handler.
|
|
||||||
|
|
||||||
Sets a ContextVar so that all storage operations within the handler
|
Sets a ContextVar so that all storage operations within the handler
|
||||||
reuse the same connection.
|
reuse the same connection.
|
||||||
@@ -300,8 +291,7 @@ class ModuleStorage:
|
|||||||
self._release(conn)
|
self._release(conn)
|
||||||
|
|
||||||
async def _commit(self) -> None:
|
async def _commit(self) -> None:
|
||||||
"""
|
"""Commit the current transaction (internal use by bot).
|
||||||
Commit the current transaction (internal use by bot).
|
|
||||||
|
|
||||||
Called automatically after each handler completes successfully.
|
Called automatically after each handler completes successfully.
|
||||||
"""
|
"""
|
||||||
@@ -311,8 +301,7 @@ class ModuleStorage:
|
|||||||
self._logger.debug("Transaction committed.")
|
self._logger.debug("Transaction committed.")
|
||||||
|
|
||||||
async def _rollback(self) -> None:
|
async def _rollback(self) -> None:
|
||||||
"""
|
"""Rollback the current transaction (internal use by bot).
|
||||||
Rollback the current transaction (internal use by bot).
|
|
||||||
|
|
||||||
Called automatically if a handler throws an exception.
|
Called automatically if a handler throws an exception.
|
||||||
"""
|
"""
|
||||||
|
|||||||
+2
-4
@@ -47,8 +47,7 @@ class Owlbot:
|
|||||||
overrides: dict[str, Any] | None = None,
|
overrides: dict[str, Any] | None = None,
|
||||||
skip_api_check: bool = False,
|
skip_api_check: bool = False,
|
||||||
):
|
):
|
||||||
"""
|
"""Initialize Owlbot.
|
||||||
Initialize Owlbot.
|
|
||||||
|
|
||||||
:param config_path: Path to the YAML config file.
|
:param config_path: Path to the YAML config file.
|
||||||
:param overrides: CLI overrides passed to the config manager.
|
:param overrides: CLI overrides passed to the config manager.
|
||||||
@@ -161,8 +160,7 @@ class Owlbot:
|
|||||||
logger.info("Owlbot shutdown complete.")
|
logger.info("Owlbot shutdown complete.")
|
||||||
|
|
||||||
async def _check_api_accessibility(self) -> None:
|
async def _check_api_accessibility(self) -> None:
|
||||||
"""
|
"""Verify that the Owncast APIs are reachable before proceeding with startup.
|
||||||
Verify that the Owncast APIs are reachable before proceeding with startup.
|
|
||||||
|
|
||||||
Calls a non-transformative endpoint on each configured client to confirm
|
Calls a non-transformative endpoint on each configured client to confirm
|
||||||
the server is accessible and credentials are valid. Raises
|
the server is accessible and credentials are valid. Raises
|
||||||
|
|||||||
@@ -56,8 +56,7 @@ __all__ = [
|
|||||||
|
|
||||||
@on_setup
|
@on_setup
|
||||||
async def setup(ctx: ModuleContext) -> None:
|
async def setup(ctx: ModuleContext) -> None:
|
||||||
"""
|
"""Initialize the custom_commands module.
|
||||||
Initialize the custom_commands module.
|
|
||||||
|
|
||||||
Creates the database schema and loads existing commands from the database.
|
Creates the database schema and loads existing commands from the database.
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ from .placeholders import DEFAULT_MAX_DEPTH, process_placeholders
|
|||||||
async def get_aliases_for_command(
|
async def get_aliases_for_command(
|
||||||
storage: ModuleStorage, command_name: str
|
storage: ModuleStorage, command_name: str
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""
|
"""Fetch all aliases for a command from the database.
|
||||||
Fetch all aliases for a command from the database.
|
|
||||||
|
|
||||||
:param storage: The module storage instance.
|
:param storage: The module storage instance.
|
||||||
:param command_name: The canonical command name.
|
:param command_name: The canonical command name.
|
||||||
@@ -47,8 +46,7 @@ async def get_aliases_for_command(
|
|||||||
async def resolve_command_name(
|
async def resolve_command_name(
|
||||||
storage: ModuleStorage, name: str
|
storage: ModuleStorage, name: str
|
||||||
) -> aiosqlite.Row | None:
|
) -> aiosqlite.Row | None:
|
||||||
"""
|
"""Resolve a command name or alias to the full command row.
|
||||||
Resolve a command name or alias to the full command row.
|
|
||||||
|
|
||||||
Checks the commands table first, then falls back to the aliases table.
|
Checks the commands table first, then falls back to the aliases table.
|
||||||
Returns the command's name, requires_moderator, and cooldown columns.
|
Returns the command's name, requires_moderator, and cooldown columns.
|
||||||
@@ -70,8 +68,7 @@ async def resolve_command_name(
|
|||||||
|
|
||||||
|
|
||||||
async def custom_command_handler(ctx: CommandContext) -> None:
|
async def custom_command_handler(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Shared handler for all custom commands.
|
||||||
Shared handler for all custom commands.
|
|
||||||
|
|
||||||
Looks up the command in the database, increments use count,
|
Looks up the command in the database, increments use count,
|
||||||
processes placeholders, and sends the response.
|
processes placeholders, and sends the response.
|
||||||
@@ -112,8 +109,7 @@ def reregister_command(
|
|||||||
requires_moderator: bool,
|
requires_moderator: bool,
|
||||||
cooldown: int,
|
cooldown: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Unregister and re-register a custom command with updated settings.
|
||||||
Unregister and re-register a custom command with updated settings.
|
|
||||||
|
|
||||||
Both registry operations are synchronous, so no other coroutine can observe
|
Both registry operations are synchronous, so no other coroutine can observe
|
||||||
the intermediate unregistered state.
|
the intermediate unregistered state.
|
||||||
|
|||||||
@@ -67,8 +67,7 @@ async def _resolve_or_error(ctx: CommandContext, raw_name: str) -> aiosqlite.Row
|
|||||||
|
|
||||||
@on_command("addcommand", aliases=["addcmd"], requires_moderator=True)
|
@on_command("addcommand", aliases=["addcmd"], requires_moderator=True)
|
||||||
async def addcommand(ctx: CommandContext) -> None:
|
async def addcommand(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Create a new custom command.
|
||||||
Create a new custom command.
|
|
||||||
|
|
||||||
Usage: !addcommand !name response text
|
Usage: !addcommand !name response text
|
||||||
|
|
||||||
@@ -134,8 +133,7 @@ async def addcommand(ctx: CommandContext) -> None:
|
|||||||
|
|
||||||
@on_command("editcommand", aliases=["editcmd"], requires_moderator=True)
|
@on_command("editcommand", aliases=["editcmd"], requires_moderator=True)
|
||||||
async def editcommand(ctx: CommandContext) -> None:
|
async def editcommand(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Edit an existing custom command's response.
|
||||||
Edit an existing custom command's response.
|
|
||||||
|
|
||||||
Usage: !editcommand !name new response
|
Usage: !editcommand !name new response
|
||||||
|
|
||||||
@@ -175,8 +173,7 @@ async def editcommand(ctx: CommandContext) -> None:
|
|||||||
|
|
||||||
@on_command("deletecommand", aliases=["delcmd"], requires_moderator=True)
|
@on_command("deletecommand", aliases=["delcmd"], requires_moderator=True)
|
||||||
async def deletecommand(ctx: CommandContext) -> None:
|
async def deletecommand(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Delete a custom command.
|
||||||
Delete a custom command.
|
|
||||||
|
|
||||||
Usage: !deletecommand !name
|
Usage: !deletecommand !name
|
||||||
|
|
||||||
@@ -215,8 +212,7 @@ async def deletecommand(ctx: CommandContext) -> None:
|
|||||||
|
|
||||||
@on_command("commandmodonly", aliases=["cmdmodonly"], requires_moderator=True)
|
@on_command("commandmodonly", aliases=["cmdmodonly"], requires_moderator=True)
|
||||||
async def commandmodonly(ctx: CommandContext) -> None:
|
async def commandmodonly(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Toggle moderator-only access for a custom command.
|
||||||
Toggle moderator-only access for a custom command.
|
|
||||||
|
|
||||||
Usage: !commandmodonly !name <on|off>
|
Usage: !commandmodonly !name <on|off>
|
||||||
|
|
||||||
@@ -255,8 +251,7 @@ async def commandmodonly(ctx: CommandContext) -> None:
|
|||||||
|
|
||||||
@on_command("resetcommand", aliases=["resetcmd"], requires_moderator=True)
|
@on_command("resetcommand", aliases=["resetcmd"], requires_moderator=True)
|
||||||
async def resetcommand(ctx: CommandContext) -> None:
|
async def resetcommand(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Reset a custom command's use counter to 0.
|
||||||
Reset a custom command's use counter to 0.
|
|
||||||
|
|
||||||
Usage: !resetcommand !name
|
Usage: !resetcommand !name
|
||||||
|
|
||||||
@@ -288,8 +283,7 @@ async def resetcommand(ctx: CommandContext) -> None:
|
|||||||
|
|
||||||
@on_command("editcounter", aliases=["editcount"], requires_moderator=True)
|
@on_command("editcounter", aliases=["editcount"], requires_moderator=True)
|
||||||
async def editcounter(ctx: CommandContext) -> None:
|
async def editcounter(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Set, increment, or decrement a named counter.
|
||||||
Set, increment, or decrement a named counter.
|
|
||||||
|
|
||||||
Usage: !editcounter <name> <value>
|
Usage: !editcounter <name> <value>
|
||||||
|
|
||||||
@@ -351,8 +345,7 @@ async def editcounter(ctx: CommandContext) -> None:
|
|||||||
|
|
||||||
@on_command("commandcooldown", aliases=["cmdcooldown"], requires_moderator=True)
|
@on_command("commandcooldown", aliases=["cmdcooldown"], requires_moderator=True)
|
||||||
async def commandcooldown(ctx: CommandContext) -> None:
|
async def commandcooldown(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Set or disable a custom command's cooldown.
|
||||||
Set or disable a custom command's cooldown.
|
|
||||||
|
|
||||||
Usage: !commandcooldown !name <seconds>
|
Usage: !commandcooldown !name <seconds>
|
||||||
|
|
||||||
@@ -404,8 +397,7 @@ async def commandcooldown(ctx: CommandContext) -> None:
|
|||||||
|
|
||||||
@on_command("addalias", requires_moderator=True)
|
@on_command("addalias", requires_moderator=True)
|
||||||
async def addalias(ctx: CommandContext) -> None:
|
async def addalias(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Add an alias to an existing custom command.
|
||||||
Add an alias to an existing custom command.
|
|
||||||
|
|
||||||
Usage: !addalias !command !alias
|
Usage: !addalias !command !alias
|
||||||
|
|
||||||
@@ -485,8 +477,7 @@ async def addalias(ctx: CommandContext) -> None:
|
|||||||
|
|
||||||
@on_command("removealias", requires_moderator=True)
|
@on_command("removealias", requires_moderator=True)
|
||||||
async def removealias(ctx: CommandContext) -> None:
|
async def removealias(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Remove an alias from a custom command.
|
||||||
Remove an alias from a custom command.
|
|
||||||
|
|
||||||
Usage: !removealias !alias
|
Usage: !removealias !alias
|
||||||
|
|
||||||
@@ -535,8 +526,7 @@ async def removealias(ctx: CommandContext) -> None:
|
|||||||
|
|
||||||
@on_command("listcommands", aliases=["listcmds"], cooldown=15)
|
@on_command("listcommands", aliases=["listcmds"], cooldown=15)
|
||||||
async def listcommands(ctx: CommandContext) -> None:
|
async def listcommands(ctx: CommandContext) -> None:
|
||||||
"""
|
"""List all custom commands.
|
||||||
List all custom commands.
|
|
||||||
|
|
||||||
Sends a URL to the command list web page.
|
Sends a URL to the command list web page.
|
||||||
Has a 15-second cooldown to prevent spam.
|
Has a 15-second cooldown to prevent spam.
|
||||||
|
|||||||
@@ -105,10 +105,10 @@ class PlaceholderContext:
|
|||||||
|
|
||||||
|
|
||||||
def _find_matching_close(template: str, pos: int) -> int:
|
def _find_matching_close(template: str, pos: int) -> int:
|
||||||
"""Find the position of the ``)`` that closes an escaped ``\\$(...)`` group.
|
r"""Find the position of the ``)`` that closes an escaped ``\$(...)`` group.
|
||||||
|
|
||||||
Tracks nested ``$(`` / ``)`` pairs so that escaped groups containing inner
|
Tracks nested ``$(`` / ``)`` pairs so that escaped groups containing inner
|
||||||
placeholders (e.g. ``\\$(rand $(1) $(2))``) are consumed in their entirety.
|
placeholders (e.g. ``\$(rand $(1) $(2))``) are consumed in their entirety.
|
||||||
|
|
||||||
:param template: The full template string.
|
:param template: The full template string.
|
||||||
:param pos: The position immediately after the opening ``$(`` of the
|
:param pos: The position immediately after the opening ``$(`` of the
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ _jinja_env = jinja2.Environment(
|
|||||||
|
|
||||||
@on_route("/list", methods=["GET"])
|
@on_route("/list", methods=["GET"])
|
||||||
async def command_list_page(ctx: RouteContext) -> web.Response:
|
async def command_list_page(ctx: RouteContext) -> web.Response:
|
||||||
"""
|
"""Serve an HTML page listing all custom commands in a table.
|
||||||
Serve an HTML page listing all custom commands in a table.
|
|
||||||
|
|
||||||
Columns: Command, Aliases, Response, Cooldown, Permissions.
|
Columns: Command, Aliases, Response, Cooldown, Permissions.
|
||||||
Accessible at /owlbot/custom_commands/list.
|
Accessible at /owlbot/custom_commands/list.
|
||||||
|
|||||||
@@ -41,8 +41,7 @@ _jinja_env = jinja2.Environment(
|
|||||||
|
|
||||||
@on_setup
|
@on_setup
|
||||||
async def setup(ctx: ModuleContext) -> None:
|
async def setup(ctx: ModuleContext) -> None:
|
||||||
"""
|
"""Initialize the quotes module.
|
||||||
Initialize the quotes module.
|
|
||||||
|
|
||||||
Creates the database schema.
|
Creates the database schema.
|
||||||
|
|
||||||
@@ -63,8 +62,7 @@ async def setup(ctx: ModuleContext) -> None:
|
|||||||
|
|
||||||
@on_route("/list", methods=["GET"])
|
@on_route("/list", methods=["GET"])
|
||||||
async def quotes_list_page(ctx: RouteContext) -> web.Response:
|
async def quotes_list_page(ctx: RouteContext) -> web.Response:
|
||||||
"""
|
"""Serve an HTML page listing all quotes in a table.
|
||||||
Serve an HTML page listing all quotes in a table.
|
|
||||||
|
|
||||||
Columns: #, Quote, Added By, Date Added.
|
Columns: #, Quote, Added By, Date Added.
|
||||||
Accessible at /owlbot/quotes/list.
|
Accessible at /owlbot/quotes/list.
|
||||||
@@ -94,8 +92,7 @@ async def quotes_list_page(ctx: RouteContext) -> web.Response:
|
|||||||
|
|
||||||
@on_command("quote", aliases=["q"])
|
@on_command("quote", aliases=["q"])
|
||||||
async def quote_command(ctx: CommandContext) -> None:
|
async def quote_command(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Display a quote. Random if no argument, specific if an ID is given.
|
||||||
Display a quote. Random if no argument, specific if an ID is given.
|
|
||||||
|
|
||||||
:param ctx: The command context.
|
:param ctx: The command context.
|
||||||
"""
|
"""
|
||||||
@@ -137,8 +134,7 @@ async def quote_command(ctx: CommandContext) -> None:
|
|||||||
|
|
||||||
@on_command("addquote", requires_moderator=True)
|
@on_command("addquote", requires_moderator=True)
|
||||||
async def addquote_command(ctx: CommandContext) -> None:
|
async def addquote_command(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Add a new quote to the database. Moderator only.
|
||||||
Add a new quote to the database. Moderator only.
|
|
||||||
|
|
||||||
:param ctx: The command context.
|
:param ctx: The command context.
|
||||||
"""
|
"""
|
||||||
@@ -159,8 +155,7 @@ async def addquote_command(ctx: CommandContext) -> None:
|
|||||||
|
|
||||||
@on_command("deletequote", aliases=["delquote"], requires_moderator=True)
|
@on_command("deletequote", aliases=["delquote"], requires_moderator=True)
|
||||||
async def deletequote_command(ctx: CommandContext) -> None:
|
async def deletequote_command(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Delete a quote by ID. Moderator only.
|
||||||
Delete a quote by ID. Moderator only.
|
|
||||||
|
|
||||||
:param ctx: The command context.
|
:param ctx: The command context.
|
||||||
"""
|
"""
|
||||||
@@ -190,8 +185,7 @@ async def deletequote_command(ctx: CommandContext) -> None:
|
|||||||
|
|
||||||
@on_command("listquotes", cooldown=15)
|
@on_command("listquotes", cooldown=15)
|
||||||
async def listquotes_command(ctx: CommandContext) -> None:
|
async def listquotes_command(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Send the URL to the quotes list web page.
|
||||||
Send the URL to the quotes list web page.
|
|
||||||
|
|
||||||
:param ctx: The command context.
|
:param ctx: The command context.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -56,8 +56,7 @@ __all__ = [
|
|||||||
|
|
||||||
@on_setup
|
@on_setup
|
||||||
async def setup(ctx: ModuleContext) -> None:
|
async def setup(ctx: ModuleContext) -> None:
|
||||||
"""
|
"""Initialize the timers module.
|
||||||
Initialize the timers module.
|
|
||||||
|
|
||||||
Creates the database schema, initializes chat counters for enabled timers,
|
Creates the database schema, initializes chat counters for enabled timers,
|
||||||
and starts the background scheduler.
|
and starts the background scheduler.
|
||||||
@@ -97,8 +96,7 @@ async def setup(ctx: ModuleContext) -> None:
|
|||||||
|
|
||||||
@on_teardown
|
@on_teardown
|
||||||
async def teardown(ctx: ModuleContext) -> None:
|
async def teardown(ctx: ModuleContext) -> None:
|
||||||
"""
|
"""Clean up the timers module.
|
||||||
Clean up the timers module.
|
|
||||||
|
|
||||||
Stops the background scheduler task.
|
Stops the background scheduler task.
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ from .scheduler import get_scheduler
|
|||||||
|
|
||||||
@on_event(EventType.CHAT, priority=Priority.LOWEST)
|
@on_event(EventType.CHAT, priority=Priority.LOWEST)
|
||||||
async def count_chat_message(ctx: EventContext[ChatEvent]) -> None:
|
async def count_chat_message(ctx: EventContext[ChatEvent]) -> None:
|
||||||
"""
|
"""Count a chat message for all tracked timers.
|
||||||
Count a chat message for all tracked timers.
|
|
||||||
|
|
||||||
Runs at lowest priority so all other CHAT handlers (moderation, etc.)
|
Runs at lowest priority so all other CHAT handlers (moderation, etc.)
|
||||||
execute first. Skips bot messages and hidden messages.
|
execute first. Skips bot messages and hidden messages.
|
||||||
@@ -60,8 +59,7 @@ async def count_chat_message(ctx: EventContext[ChatEvent]) -> None:
|
|||||||
|
|
||||||
@on_event(EventType.VISIBILITY_UPDATE, priority=Priority.LOWEST)
|
@on_event(EventType.VISIBILITY_UPDATE, priority=Priority.LOWEST)
|
||||||
async def handle_visibility_update(ctx: EventContext[VisibilityUpdateEvent]) -> None:
|
async def handle_visibility_update(ctx: EventContext[VisibilityUpdateEvent]) -> None:
|
||||||
"""
|
"""Remove hidden messages from chat counts.
|
||||||
Remove hidden messages from chat counts.
|
|
||||||
|
|
||||||
Only handles the hide case. Un-hiding does not re-add messages because
|
Only handles the hide case. Un-hiding does not re-add messages because
|
||||||
we cannot distinguish previously counted user messages from bot messages
|
we cannot distinguish previously counted user messages from bot messages
|
||||||
|
|||||||
@@ -33,8 +33,7 @@ _NAME_PATTERN = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]{0,31}$")
|
|||||||
|
|
||||||
|
|
||||||
async def _resolve_timer(ctx: CommandContext, identifier: str) -> aiosqlite.Row | None:
|
async def _resolve_timer(ctx: CommandContext, identifier: str) -> aiosqlite.Row | None:
|
||||||
"""
|
"""Resolve a timer by numeric ID or name.
|
||||||
Resolve a timer by numeric ID or name.
|
|
||||||
|
|
||||||
Tries parsing as an integer first, then falls back to a name lookup.
|
Tries parsing as an integer first, then falls back to a name lookup.
|
||||||
|
|
||||||
@@ -66,8 +65,7 @@ async def _resolve_timer(ctx: CommandContext, identifier: str) -> aiosqlite.Row
|
|||||||
|
|
||||||
|
|
||||||
def _timer_display(row: aiosqlite.Row) -> str:
|
def _timer_display(row: aiosqlite.Row) -> str:
|
||||||
"""
|
"""Format a timer's display identifier.
|
||||||
Format a timer's display identifier.
|
|
||||||
|
|
||||||
:param row: Timer database row.
|
:param row: Timer database row.
|
||||||
:return: Display string like "timer_name (#3)" or "timer #3".
|
:return: Display string like "timer_name (#3)" or "timer #3".
|
||||||
@@ -79,8 +77,7 @@ def _timer_display(row: aiosqlite.Row) -> str:
|
|||||||
|
|
||||||
@on_command("addtimer", requires_moderator=True)
|
@on_command("addtimer", requires_moderator=True)
|
||||||
async def addtimer(ctx: CommandContext) -> None:
|
async def addtimer(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Create a new empty timer with an optional name.
|
||||||
Create a new empty timer with an optional name.
|
|
||||||
|
|
||||||
Usage: !addtimer [name]
|
Usage: !addtimer [name]
|
||||||
|
|
||||||
@@ -123,8 +120,7 @@ async def addtimer(ctx: CommandContext) -> None:
|
|||||||
|
|
||||||
@on_command("settimermessage", requires_moderator=True)
|
@on_command("settimermessage", requires_moderator=True)
|
||||||
async def settimermessage(ctx: CommandContext) -> None:
|
async def settimermessage(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Set the message text for a timer.
|
||||||
Set the message text for a timer.
|
|
||||||
|
|
||||||
Usage: !settimermessage <id|name> <message>
|
Usage: !settimermessage <id|name> <message>
|
||||||
|
|
||||||
@@ -164,8 +160,7 @@ async def settimermessage(ctx: CommandContext) -> None:
|
|||||||
|
|
||||||
@on_command("settimerinterval", requires_moderator=True)
|
@on_command("settimerinterval", requires_moderator=True)
|
||||||
async def settimerinterval(ctx: CommandContext) -> None:
|
async def settimerinterval(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Set the interval for a timer.
|
||||||
Set the interval for a timer.
|
|
||||||
|
|
||||||
Accepts simple durations (15m, 1h30m) or cron expressions (*/15 * * * *).
|
Accepts simple durations (15m, 1h30m) or cron expressions (*/15 * * * *).
|
||||||
|
|
||||||
@@ -212,8 +207,7 @@ async def settimerinterval(ctx: CommandContext) -> None:
|
|||||||
|
|
||||||
@on_command("settimerlines", requires_moderator=True)
|
@on_command("settimerlines", requires_moderator=True)
|
||||||
async def settimerlines(ctx: CommandContext) -> None:
|
async def settimerlines(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Set the minimum chat lines between timer firings.
|
||||||
Set the minimum chat lines between timer firings.
|
|
||||||
|
|
||||||
Usage: !settimerlines <id|name> <count>
|
Usage: !settimerlines <id|name> <count>
|
||||||
|
|
||||||
@@ -260,8 +254,7 @@ async def settimerlines(ctx: CommandContext) -> None:
|
|||||||
|
|
||||||
@on_command("enabletimer", requires_moderator=True)
|
@on_command("enabletimer", requires_moderator=True)
|
||||||
async def enabletimer(ctx: CommandContext) -> None:
|
async def enabletimer(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Enable a timer.
|
||||||
Enable a timer.
|
|
||||||
|
|
||||||
Won't enable a timer that has no message set.
|
Won't enable a timer that has no message set.
|
||||||
|
|
||||||
@@ -310,8 +303,7 @@ async def enabletimer(ctx: CommandContext) -> None:
|
|||||||
|
|
||||||
@on_command("disabletimer", requires_moderator=True)
|
@on_command("disabletimer", requires_moderator=True)
|
||||||
async def disabletimer(ctx: CommandContext) -> None:
|
async def disabletimer(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Disable a timer.
|
||||||
Disable a timer.
|
|
||||||
|
|
||||||
Usage: !disabletimer <id|name>
|
Usage: !disabletimer <id|name>
|
||||||
|
|
||||||
@@ -351,8 +343,7 @@ async def disabletimer(ctx: CommandContext) -> None:
|
|||||||
|
|
||||||
@on_command("deletetimer", requires_moderator=True)
|
@on_command("deletetimer", requires_moderator=True)
|
||||||
async def deletetimer(ctx: CommandContext) -> None:
|
async def deletetimer(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Permanently delete a timer.
|
||||||
Permanently delete a timer.
|
|
||||||
|
|
||||||
Usage: !deletetimer <id|name>
|
Usage: !deletetimer <id|name>
|
||||||
|
|
||||||
@@ -382,8 +373,7 @@ async def deletetimer(ctx: CommandContext) -> None:
|
|||||||
|
|
||||||
@on_command("listtimers", requires_moderator=True, cooldown=15)
|
@on_command("listtimers", requires_moderator=True, cooldown=15)
|
||||||
async def listtimers(ctx: CommandContext) -> None:
|
async def listtimers(ctx: CommandContext) -> None:
|
||||||
"""
|
"""Send the URL to the timer list web page.
|
||||||
Send the URL to the timer list web page.
|
|
||||||
|
|
||||||
Usage: !listtimers
|
Usage: !listtimers
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ _jinja_env = jinja2.Environment(
|
|||||||
|
|
||||||
@on_route("/list", methods=["GET"])
|
@on_route("/list", methods=["GET"])
|
||||||
async def timer_list_page(ctx: RouteContext) -> web.Response:
|
async def timer_list_page(ctx: RouteContext) -> web.Response:
|
||||||
"""
|
"""Serve an HTML page listing all timers in a table.
|
||||||
Serve an HTML page listing all timers in a table.
|
|
||||||
|
|
||||||
Columns: #, Name, Message, Interval, Min Lines, Status, Last Fired.
|
Columns: #, Name, Message, Interval, Min Lines, Status, Last Fired.
|
||||||
Accessible at /owlbot/timers/list.
|
Accessible at /owlbot/timers/list.
|
||||||
|
|||||||
@@ -51,8 +51,7 @@ _MIN_CRON_MINUTES = 1
|
|||||||
|
|
||||||
|
|
||||||
def parse_interval(value: str) -> tuple[IntervalType, str]:
|
def parse_interval(value: str) -> tuple[IntervalType, str]:
|
||||||
"""
|
"""Parse and validate an interval string.
|
||||||
Parse and validate an interval string.
|
|
||||||
|
|
||||||
Simple durations (no spaces) are parsed with a regex. Cron expressions
|
Simple durations (no spaces) are parsed with a regex. Cron expressions
|
||||||
(contain spaces) are validated with cronsim. Returns the detected type
|
(contain spaces) are validated with cronsim. Returns the detected type
|
||||||
@@ -72,8 +71,7 @@ def parse_interval(value: str) -> tuple[IntervalType, str]:
|
|||||||
|
|
||||||
|
|
||||||
def _parse_simple(value: str) -> tuple[IntervalType, str]:
|
def _parse_simple(value: str) -> tuple[IntervalType, str]:
|
||||||
"""
|
"""Parse and validate a simple duration string.
|
||||||
Parse and validate a simple duration string.
|
|
||||||
|
|
||||||
:param value: Duration string like "30s", "5m", "1h30m".
|
:param value: Duration string like "30s", "5m", "1h30m".
|
||||||
:return: Tuple of (IntervalType.SIMPLE, value).
|
:return: Tuple of (IntervalType.SIMPLE, value).
|
||||||
@@ -99,8 +97,7 @@ def _parse_simple(value: str) -> tuple[IntervalType, str]:
|
|||||||
|
|
||||||
|
|
||||||
def _parse_cron(value: str) -> tuple[IntervalType, str]:
|
def _parse_cron(value: str) -> tuple[IntervalType, str]:
|
||||||
"""
|
"""Validate a cron expression.
|
||||||
Validate a cron expression.
|
|
||||||
|
|
||||||
:param value: Cron expression string (5 fields).
|
:param value: Cron expression string (5 fields).
|
||||||
:return: Tuple of (IntervalType.CRON, value).
|
:return: Tuple of (IntervalType.CRON, value).
|
||||||
@@ -125,8 +122,7 @@ def _parse_cron(value: str) -> tuple[IntervalType, str]:
|
|||||||
|
|
||||||
|
|
||||||
def _duration_to_seconds(value: str) -> int:
|
def _duration_to_seconds(value: str) -> int:
|
||||||
"""
|
"""Convert a validated simple duration string to total seconds.
|
||||||
Convert a validated simple duration string to total seconds.
|
|
||||||
|
|
||||||
:param value: A previously validated duration string.
|
:param value: A previously validated duration string.
|
||||||
:return: Total seconds.
|
:return: Total seconds.
|
||||||
@@ -141,8 +137,7 @@ def _duration_to_seconds(value: str) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def _next_fire_time(row: aiosqlite.Row, now: datetime) -> datetime | None:
|
def _next_fire_time(row: aiosqlite.Row, now: datetime) -> datetime | None:
|
||||||
"""
|
"""Compute when a timer will next be time-due.
|
||||||
Compute when a timer will next be time-due.
|
|
||||||
|
|
||||||
:param row: Database row with interval_type, interval_value, last_fired_at.
|
:param row: Database row with interval_type, interval_value, last_fired_at.
|
||||||
:param now: Current UTC time.
|
:param now: Current UTC time.
|
||||||
@@ -167,8 +162,7 @@ def _next_fire_time(row: aiosqlite.Row, now: datetime) -> datetime | None:
|
|||||||
|
|
||||||
|
|
||||||
def _is_timer_due(row: aiosqlite.Row, now: datetime) -> bool:
|
def _is_timer_due(row: aiosqlite.Row, now: datetime) -> bool:
|
||||||
"""
|
"""Check whether a timer should fire based on its interval and last fire time.
|
||||||
Check whether a timer should fire based on its interval and last fire time.
|
|
||||||
|
|
||||||
:param row: Database row with interval_type, interval_value, last_fired_at.
|
:param row: Database row with interval_type, interval_value, last_fired_at.
|
||||||
:param now: Current UTC time.
|
:param now: Current UTC time.
|
||||||
@@ -187,6 +181,7 @@ class TimerScheduler:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
"""Initialize an empty scheduler with no running task."""
|
||||||
self._task: asyncio.Task[None] | None = None
|
self._task: asyncio.Task[None] | None = None
|
||||||
self._counted_ids: dict[int, set[str]] = {}
|
self._counted_ids: dict[int, set[str]] = {}
|
||||||
self._wake_event: asyncio.Event | None = None
|
self._wake_event: asyncio.Event | None = None
|
||||||
@@ -197,8 +192,7 @@ class TimerScheduler:
|
|||||||
return self._counted_ids
|
return self._counted_ids
|
||||||
|
|
||||||
def init_counted_ids(self, timer_ids: list[int]) -> None:
|
def init_counted_ids(self, timer_ids: list[int]) -> None:
|
||||||
"""
|
"""Initialize empty counter sets for the given timer IDs.
|
||||||
Initialize empty counter sets for the given timer IDs.
|
|
||||||
|
|
||||||
Called during module setup to prepare tracking for enabled timers.
|
Called during module setup to prepare tracking for enabled timers.
|
||||||
|
|
||||||
@@ -207,8 +201,7 @@ class TimerScheduler:
|
|||||||
self._counted_ids = {tid: set() for tid in timer_ids}
|
self._counted_ids = {tid: set() for tid in timer_ids}
|
||||||
|
|
||||||
def start(self, ctx: ModuleContext) -> None:
|
def start(self, ctx: ModuleContext) -> None:
|
||||||
"""
|
"""Start the background scheduler task.
|
||||||
Start the background scheduler task.
|
|
||||||
|
|
||||||
:param ctx: The module context.
|
:param ctx: The module context.
|
||||||
"""
|
"""
|
||||||
@@ -219,8 +212,7 @@ class TimerScheduler:
|
|||||||
ctx.logger.debug("Timer scheduler started.")
|
ctx.logger.debug("Timer scheduler started.")
|
||||||
|
|
||||||
async def stop(self, ctx: ModuleContext) -> None:
|
async def stop(self, ctx: ModuleContext) -> None:
|
||||||
"""
|
"""Cancel the background scheduler task and wait for it to exit.
|
||||||
Cancel the background scheduler task and wait for it to exit.
|
|
||||||
|
|
||||||
:param ctx: The module context.
|
:param ctx: The module context.
|
||||||
"""
|
"""
|
||||||
@@ -233,8 +225,7 @@ class TimerScheduler:
|
|||||||
ctx.logger.info("Timer scheduler stopped.")
|
ctx.logger.info("Timer scheduler stopped.")
|
||||||
|
|
||||||
def reschedule(self) -> None:
|
def reschedule(self) -> None:
|
||||||
"""
|
"""Wake the scheduler so it recalculates the next fire time.
|
||||||
Wake the scheduler so it recalculates the next fire time.
|
|
||||||
|
|
||||||
Called by timer management commands when timer state changes.
|
Called by timer management commands when timer state changes.
|
||||||
"""
|
"""
|
||||||
@@ -242,8 +233,7 @@ class TimerScheduler:
|
|||||||
self._wake_event.set()
|
self._wake_event.set()
|
||||||
|
|
||||||
async def _scheduler_loop(self, ctx: ModuleContext, wake: asyncio.Event) -> None:
|
async def _scheduler_loop(self, ctx: ModuleContext, wake: asyncio.Event) -> None:
|
||||||
"""
|
"""Background loop that sleeps until the next timer is due and fires it.
|
||||||
Background loop that sleeps until the next timer is due and fires it.
|
|
||||||
|
|
||||||
Uses an asyncio.Event to allow early wake-ups when timer state changes.
|
Uses an asyncio.Event to allow early wake-ups when timer state changes.
|
||||||
|
|
||||||
@@ -283,8 +273,7 @@ class TimerScheduler:
|
|||||||
ctx.logger.exception("Scheduler tick failed.")
|
ctx.logger.exception("Scheduler tick failed.")
|
||||||
|
|
||||||
async def _tick(self, ctx: ModuleContext) -> None:
|
async def _tick(self, ctx: ModuleContext) -> None:
|
||||||
"""
|
"""Single scheduler tick: query enabled timers and fire any that are due.
|
||||||
Single scheduler tick: query enabled timers and fire any that are due.
|
|
||||||
|
|
||||||
:param ctx: The module context.
|
:param ctx: The module context.
|
||||||
"""
|
"""
|
||||||
@@ -342,8 +331,7 @@ class TimerScheduler:
|
|||||||
|
|
||||||
|
|
||||||
async def _compute_next_delay(ctx: ModuleContext) -> tuple[float | None, str | None]:
|
async def _compute_next_delay(ctx: ModuleContext) -> tuple[float | None, str | None]:
|
||||||
"""
|
"""Query enabled timers and return seconds until the soonest one is time-due.
|
||||||
Query enabled timers and return seconds until the soonest one is time-due.
|
|
||||||
|
|
||||||
:param ctx: The module context.
|
:param ctx: The module context.
|
||||||
:return: Tuple of (seconds until next timer, display name of that timer),
|
:return: Tuple of (seconds until next timer, display name of that timer),
|
||||||
@@ -394,8 +382,7 @@ _scheduler: TimerScheduler | None = None
|
|||||||
|
|
||||||
|
|
||||||
def get_scheduler() -> TimerScheduler:
|
def get_scheduler() -> TimerScheduler:
|
||||||
"""
|
"""Return the active scheduler instance.
|
||||||
Return the active scheduler instance.
|
|
||||||
|
|
||||||
:return: The active TimerScheduler.
|
:return: The active TimerScheduler.
|
||||||
:raises RuntimeError: If the scheduler has not been initialized.
|
:raises RuntimeError: If the scheduler has not been initialized.
|
||||||
@@ -406,8 +393,7 @@ def get_scheduler() -> TimerScheduler:
|
|||||||
|
|
||||||
|
|
||||||
def set_scheduler(scheduler: TimerScheduler) -> None:
|
def set_scheduler(scheduler: TimerScheduler) -> None:
|
||||||
"""
|
"""Set the active scheduler instance.
|
||||||
Set the active scheduler instance.
|
|
||||||
|
|
||||||
:param scheduler: The TimerScheduler to install.
|
:param scheduler: The TimerScheduler to install.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -48,8 +48,7 @@ type WebhookCallback = Callable[[EventType, Event], Coroutine[Any, Any, None]]
|
|||||||
|
|
||||||
|
|
||||||
class HttpServer:
|
class HttpServer:
|
||||||
"""
|
"""HTTP server for Owlbot.
|
||||||
HTTP server for Owlbot.
|
|
||||||
|
|
||||||
Manages the aiohttp Application, built-in routes (webhook),
|
Manages the aiohttp Application, built-in routes (webhook),
|
||||||
and module-registered routes via the RouteDispatcher.
|
and module-registered routes via the RouteDispatcher.
|
||||||
@@ -61,8 +60,7 @@ class HttpServer:
|
|||||||
event_dispatch: WebhookCallback,
|
event_dispatch: WebhookCallback,
|
||||||
route_dispatcher: RouteDispatcher,
|
route_dispatcher: RouteDispatcher,
|
||||||
):
|
):
|
||||||
"""
|
"""Initialize the web server.
|
||||||
Initialize the web server.
|
|
||||||
|
|
||||||
:param config: Bot configuration.
|
:param config: Bot configuration.
|
||||||
:param event_dispatch: Async callback to dispatch webhook events.
|
:param event_dispatch: Async callback to dispatch webhook events.
|
||||||
@@ -94,8 +92,7 @@ class HttpServer:
|
|||||||
logger.debug("HttpServer initialized.")
|
logger.debug("HttpServer initialized.")
|
||||||
|
|
||||||
async def start(self, host: str, port: int) -> None:
|
async def start(self, host: str, port: int) -> None:
|
||||||
"""
|
"""Start the HTTP server.
|
||||||
Start the HTTP server.
|
|
||||||
|
|
||||||
:param host: Address to bind to.
|
:param host: Address to bind to.
|
||||||
:param port: Port to bind to.
|
:param port: Port to bind to.
|
||||||
|
|||||||
+14
-28
@@ -54,8 +54,7 @@ class ModuleLoader:
|
|||||||
config: Config,
|
config: Config,
|
||||||
http_client: HttpClient,
|
http_client: HttpClient,
|
||||||
):
|
):
|
||||||
"""
|
"""Initialize the module loader.
|
||||||
Initialize the module loader.
|
|
||||||
|
|
||||||
:param modules_dir: Path to the user modules directory.
|
:param modules_dir: Path to the user modules directory.
|
||||||
:param config: Configuration object for checking module enable/disable state.
|
:param config: Configuration object for checking module enable/disable state.
|
||||||
@@ -105,8 +104,7 @@ class ModuleLoader:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_module_context(self, module_name: str) -> ModuleContext | None:
|
def get_module_context(self, module_name: str) -> ModuleContext | None:
|
||||||
"""
|
"""Look up a module's context by name.
|
||||||
Look up a module's context by name.
|
|
||||||
|
|
||||||
:param module_name: The module name.
|
:param module_name: The module name.
|
||||||
:return: The ModuleContext if the module is loaded, None otherwise.
|
:return: The ModuleContext if the module is loaded, None otherwise.
|
||||||
@@ -128,8 +126,7 @@ class ModuleLoader:
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def discover_module_names(self) -> list[str]:
|
def discover_module_names(self) -> list[str]:
|
||||||
"""
|
"""Discover all loadable modules from both built-in and user directories.
|
||||||
Discover all loadable modules from both built-in and user directories.
|
|
||||||
|
|
||||||
Built-in modules ship with the package. User modules are discovered
|
Built-in modules ship with the package. User modules are discovered
|
||||||
from the configured modules directory. If a user module has the same
|
from the configured modules directory. If a user module has the same
|
||||||
@@ -160,8 +157,7 @@ class ModuleLoader:
|
|||||||
return modules
|
return modules
|
||||||
|
|
||||||
def _discover_user_module_names(self) -> set[str]:
|
def _discover_user_module_names(self) -> set[str]:
|
||||||
"""
|
"""Scan the user modules directory for loadable modules.
|
||||||
Scan the user modules directory for loadable modules.
|
|
||||||
|
|
||||||
Supports both single-file modules (``name.py``) and package modules
|
Supports both single-file modules (``name.py``) and package modules
|
||||||
(``name/__init__.py``). Files and directories starting with underscore
|
(``name/__init__.py``). Files and directories starting with underscore
|
||||||
@@ -197,8 +193,7 @@ class ModuleLoader:
|
|||||||
return found
|
return found
|
||||||
|
|
||||||
async def load_all_modules(self) -> list[str]:
|
async def load_all_modules(self) -> list[str]:
|
||||||
"""
|
"""Discover and load all enabled modules using two-phase loading.
|
||||||
Discover and load all enabled modules using two-phase loading.
|
|
||||||
|
|
||||||
**Phase 1:** Import every module, create contexts, and register all
|
**Phase 1:** Import every module, create contexts, and register all
|
||||||
decorated handlers (``@on_event``, ``@on_command``, ``@on_route``).
|
decorated handlers (``@on_event``, ``@on_command``, ``@on_route``).
|
||||||
@@ -255,8 +250,7 @@ class ModuleLoader:
|
|||||||
return loaded
|
return loaded
|
||||||
|
|
||||||
async def load_module(self, module_name: str, *, _run_setup: bool = True) -> None:
|
async def load_module(self, module_name: str, *, _run_setup: bool = True) -> None:
|
||||||
"""
|
"""Load a single module by name.
|
||||||
Load a single module by name.
|
|
||||||
|
|
||||||
If both a package and single-file form exist for the same name,
|
If both a package and single-file form exist for the same name,
|
||||||
the package form is used. Checks if the module is enabled in
|
the package form is used. Checks if the module is enabled in
|
||||||
@@ -354,8 +348,7 @@ class ModuleLoader:
|
|||||||
raise ModuleLoadError(f"Failed to load module '{module_name}': {e}") from e
|
raise ModuleLoadError(f"Failed to load module '{module_name}': {e}") from e
|
||||||
|
|
||||||
async def unload_module(self, module_name: str) -> bool:
|
async def unload_module(self, module_name: str) -> bool:
|
||||||
"""
|
"""Unload a module, calling its teardown and cleaning up all state.
|
||||||
Unload a module, calling its teardown and cleaning up all state.
|
|
||||||
|
|
||||||
:param module_name: The module to unload.
|
:param module_name: The module to unload.
|
||||||
:return: True if module was unloaded, False if not found.
|
:return: True if module was unloaded, False if not found.
|
||||||
@@ -397,8 +390,7 @@ class ModuleLoader:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
async def unload_all_modules(self) -> None:
|
async def unload_all_modules(self) -> None:
|
||||||
"""
|
"""Unload all modules, calling teardown and cleaning up all state.
|
||||||
Unload all modules, calling teardown and cleaning up all state.
|
|
||||||
|
|
||||||
Called during bot shutdown to allow modules to clean up resources.
|
Called during bot shutdown to allow modules to clean up resources.
|
||||||
"""
|
"""
|
||||||
@@ -411,8 +403,7 @@ class ModuleLoader:
|
|||||||
logger.info("All module unload complete.")
|
logger.info("All module unload complete.")
|
||||||
|
|
||||||
async def _run_module_setup(self, module_name: str) -> None:
|
async def _run_module_setup(self, module_name: str) -> None:
|
||||||
"""
|
"""Run a module's ``@on_setup`` hooks if any are defined.
|
||||||
Run a module's ``@on_setup`` hooks if any are defined.
|
|
||||||
|
|
||||||
All setup handlers run inside a single storage transaction that is
|
All setup handlers run inside a single storage transaction that is
|
||||||
committed on success. If any handler fails, the transaction is
|
committed on success. If any handler fails, the transaction is
|
||||||
@@ -449,8 +440,7 @@ class ModuleLoader:
|
|||||||
) from e
|
) from e
|
||||||
|
|
||||||
def _resolve_module_path(self, module_name: str) -> Path:
|
def _resolve_module_path(self, module_name: str) -> Path:
|
||||||
"""
|
"""Resolve the filesystem path for a module by name.
|
||||||
Resolve the filesystem path for a module by name.
|
|
||||||
|
|
||||||
User modules are checked first (package form, then single-file).
|
User modules are checked first (package form, then single-file).
|
||||||
If no user module is found and the name is a built-in, the built-in
|
If no user module is found and the name is a built-in, the built-in
|
||||||
@@ -478,8 +468,7 @@ class ModuleLoader:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _resolve_builtin_module_path(module_name: str) -> Path:
|
def _resolve_builtin_module_path(module_name: str) -> Path:
|
||||||
"""
|
"""Locate a built-in module's ``__init__.py`` inside the package.
|
||||||
Locate a built-in module's ``__init__.py`` inside the package.
|
|
||||||
|
|
||||||
:param module_name: Name of the built-in module.
|
:param module_name: Name of the built-in module.
|
||||||
:return: Path to the module's ``__init__.py``.
|
:return: Path to the module's ``__init__.py``.
|
||||||
@@ -493,8 +482,7 @@ class ModuleLoader:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _register_module_handlers(self, module: ModuleType, module_name: str) -> None:
|
def _register_module_handlers(self, module: ModuleType, module_name: str) -> None:
|
||||||
"""
|
"""Scan a module for decorated handlers and register them.
|
||||||
Scan a module for decorated handlers and register them.
|
|
||||||
|
|
||||||
Delegates to each registry's ``register_from_module()`` method,
|
Delegates to each registry's ``register_from_module()`` method,
|
||||||
which knows how to find its own decorator markers.
|
which knows how to find its own decorator markers.
|
||||||
@@ -510,8 +498,7 @@ class ModuleLoader:
|
|||||||
def _collect_lifecycle_handlers(
|
def _collect_lifecycle_handlers(
|
||||||
module: ModuleType, marker: str
|
module: ModuleType, marker: str
|
||||||
) -> list[LifecycleHandler]:
|
) -> list[LifecycleHandler]:
|
||||||
"""
|
"""Collect callables from a module that have a given marker attribute.
|
||||||
Collect callables from a module that have a given marker attribute.
|
|
||||||
|
|
||||||
Scans ``vars(module)`` for callable objects where ``getattr(obj, marker)``
|
Scans ``vars(module)`` for callable objects where ``getattr(obj, marker)``
|
||||||
is truthy. Used to find ``@on_setup`` (marker ``"_owlbot_setup"``) and
|
is truthy. Used to find ``@on_setup`` (marker ``"_owlbot_setup"``) and
|
||||||
@@ -528,8 +515,7 @@ class ModuleLoader:
|
|||||||
]
|
]
|
||||||
|
|
||||||
def _cleanup_module(self, module_name: str) -> None:
|
def _cleanup_module(self, module_name: str) -> None:
|
||||||
"""
|
"""Remove all state associated with a module.
|
||||||
Remove all state associated with a module.
|
|
||||||
|
|
||||||
Removes the top-level module entry and any submodule entries
|
Removes the top-level module entry and any submodule entries
|
||||||
(for package-style modules) from ``sys.modules``.
|
(for package-style modules) from ``sys.modules``.
|
||||||
|
|||||||
@@ -41,16 +41,14 @@ logger = logging.getLogger("owlbot.commands")
|
|||||||
|
|
||||||
|
|
||||||
class CommandRegistry:
|
class CommandRegistry:
|
||||||
"""
|
"""Holds all registered commands for a bot instance.
|
||||||
Holds all registered commands for a bot instance.
|
|
||||||
|
|
||||||
Instance-scoped to enable proper dependency injection and allow multiple
|
Instance-scoped to enable proper dependency injection and allow multiple
|
||||||
bot instances to coexist without sharing state.
|
bot instances to coexist without sharing state.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, prefix: str = "!") -> None:
|
def __init__(self, prefix: str = "!") -> None:
|
||||||
"""
|
"""Initialize the command registry.
|
||||||
Initialize the command registry.
|
|
||||||
|
|
||||||
:param prefix: Command prefix character (e.g., "!" for "!ping").
|
:param prefix: Command prefix character (e.g., "!" for "!ping").
|
||||||
"""
|
"""
|
||||||
@@ -76,8 +74,7 @@ class CommandRegistry:
|
|||||||
cooldown: int | float = 0,
|
cooldown: int | float = 0,
|
||||||
module_name: str,
|
module_name: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Register a command handler.
|
||||||
Register a command handler.
|
|
||||||
|
|
||||||
:param name: Primary command name (case-insensitive).
|
:param name: Primary command name (case-insensitive).
|
||||||
:param handler: Async function to handle the command.
|
:param handler: Async function to handle the command.
|
||||||
@@ -123,8 +120,7 @@ class CommandRegistry:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def unregister(self, name: str) -> bool:
|
def unregister(self, name: str) -> bool:
|
||||||
"""
|
"""Unregister a command and all its aliases.
|
||||||
Unregister a command and all its aliases.
|
|
||||||
|
|
||||||
:param name: The primary command name or any alias.
|
:param name: The primary command name or any alias.
|
||||||
:return: True if command was found and removed, False otherwise.
|
:return: True if command was found and removed, False otherwise.
|
||||||
@@ -149,8 +145,7 @@ class CommandRegistry:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def get(self, trigger: str) -> CommandInfo | None:
|
def get(self, trigger: str) -> CommandInfo | None:
|
||||||
"""
|
"""Look up a command by name or alias.
|
||||||
Look up a command by name or alias.
|
|
||||||
|
|
||||||
:param trigger: Command name or alias (case-insensitive).
|
:param trigger: Command name or alias (case-insensitive).
|
||||||
:return: CommandInfo if found, None otherwise.
|
:return: CommandInfo if found, None otherwise.
|
||||||
@@ -162,8 +157,7 @@ class CommandRegistry:
|
|||||||
return self._commands.get(primary)
|
return self._commands.get(primary)
|
||||||
|
|
||||||
def exists(self, trigger: str) -> bool:
|
def exists(self, trigger: str) -> bool:
|
||||||
"""
|
"""Check if a command is registered.
|
||||||
Check if a command is registered.
|
|
||||||
|
|
||||||
:param trigger: Command name or alias (case-insensitive).
|
:param trigger: Command name or alias (case-insensitive).
|
||||||
:return: True if the command exists, False otherwise.
|
:return: True if the command exists, False otherwise.
|
||||||
@@ -171,16 +165,14 @@ class CommandRegistry:
|
|||||||
return trigger.lower() in self._aliases
|
return trigger.lower() in self._aliases
|
||||||
|
|
||||||
def get_all(self) -> dict[str, CommandInfo]:
|
def get_all(self) -> dict[str, CommandInfo]:
|
||||||
"""
|
"""Get all registered commands.
|
||||||
Get all registered commands.
|
|
||||||
|
|
||||||
:return: Dict mapping primary command names to CommandInfo.
|
:return: Dict mapping primary command names to CommandInfo.
|
||||||
"""
|
"""
|
||||||
return self._commands.copy()
|
return self._commands.copy()
|
||||||
|
|
||||||
def unregister_by_module(self, module_name: str) -> int:
|
def unregister_by_module(self, module_name: str) -> int:
|
||||||
"""
|
"""Remove all commands registered by a specific module.
|
||||||
Remove all commands registered by a specific module.
|
|
||||||
|
|
||||||
:param module_name: The module whose commands should be removed.
|
:param module_name: The module whose commands should be removed.
|
||||||
:return: Number of commands removed.
|
:return: Number of commands removed.
|
||||||
@@ -197,8 +189,7 @@ class CommandRegistry:
|
|||||||
return len(to_remove)
|
return len(to_remove)
|
||||||
|
|
||||||
def register_from_module(self, module: ModuleType, module_name: str) -> None:
|
def register_from_module(self, module: ModuleType, module_name: str) -> None:
|
||||||
"""
|
"""Scan a Python module for @on_command-decorated functions and register them.
|
||||||
Scan a Python module for @on_command-decorated functions and register them.
|
|
||||||
|
|
||||||
Looks for functions with the ``_owlbot_command`` attribute set by
|
Looks for functions with the ``_owlbot_command`` attribute set by
|
||||||
the ``@on_command`` decorator and registers each one.
|
the ``@on_command`` decorator and registers each one.
|
||||||
@@ -222,8 +213,7 @@ class CommandRegistry:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def parse(self, message: str) -> tuple[str, str] | None:
|
def parse(self, message: str) -> tuple[str, str] | None:
|
||||||
"""
|
"""Parse a message to extract command and arguments.
|
||||||
Parse a message to extract command and arguments.
|
|
||||||
|
|
||||||
:param message: The chat message body.
|
:param message: The chat message body.
|
||||||
:return: Tuple of (command_name, args_string), or None if not a command.
|
:return: Tuple of (command_name, args_string), or None if not a command.
|
||||||
@@ -245,8 +235,7 @@ class CommandRegistry:
|
|||||||
|
|
||||||
|
|
||||||
class CommandDispatcher:
|
class CommandDispatcher:
|
||||||
"""
|
"""Dispatches chat events to registered command handlers.
|
||||||
Dispatches chat events to registered command handlers.
|
|
||||||
|
|
||||||
Parses messages, checks authentication/moderator requirements,
|
Parses messages, checks authentication/moderator requirements,
|
||||||
and calls the appropriate command handler.
|
and calls the appropriate command handler.
|
||||||
@@ -260,8 +249,7 @@ class CommandDispatcher:
|
|||||||
loaded_modules: set[str],
|
loaded_modules: set[str],
|
||||||
command_prefix: str = "!",
|
command_prefix: str = "!",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Initialize the command dispatcher.
|
||||||
Initialize the command dispatcher.
|
|
||||||
|
|
||||||
Creates and owns a :class:`CommandRegistry` internally.
|
Creates and owns a :class:`CommandRegistry` internally.
|
||||||
|
|
||||||
@@ -296,8 +284,7 @@ class CommandDispatcher:
|
|||||||
*,
|
*,
|
||||||
aliases: list[str] | tuple[str, ...] | None = None,
|
aliases: list[str] | tuple[str, ...] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Register a built-in command handler.
|
||||||
Register a built-in command handler.
|
|
||||||
|
|
||||||
Built-in commands use a simpler handler signature (event, owncast_client)
|
Built-in commands use a simpler handler signature (event, owncast_client)
|
||||||
and don't require module infrastructure.
|
and don't require module infrastructure.
|
||||||
@@ -328,8 +315,7 @@ class CommandDispatcher:
|
|||||||
cooldown: int | float = 0,
|
cooldown: int | float = 0,
|
||||||
module_name: str,
|
module_name: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Register a command handler.
|
||||||
Register a command handler.
|
|
||||||
|
|
||||||
Delegates to the internal CommandRegistry.
|
Delegates to the internal CommandRegistry.
|
||||||
|
|
||||||
@@ -355,8 +341,7 @@ class CommandDispatcher:
|
|||||||
self._cooldown_tracker.pop(name.lower(), None)
|
self._cooldown_tracker.pop(name.lower(), None)
|
||||||
|
|
||||||
def unregister(self, name: str) -> bool:
|
def unregister(self, name: str) -> bool:
|
||||||
"""
|
"""Unregister a command and all its aliases.
|
||||||
Unregister a command and all its aliases.
|
|
||||||
|
|
||||||
Delegates to the internal CommandRegistry.
|
Delegates to the internal CommandRegistry.
|
||||||
|
|
||||||
@@ -371,8 +356,7 @@ class CommandDispatcher:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def get(self, trigger: str) -> CommandInfo | None:
|
def get(self, trigger: str) -> CommandInfo | None:
|
||||||
"""
|
"""Look up a command by name or alias.
|
||||||
Look up a command by name or alias.
|
|
||||||
|
|
||||||
Delegates to the internal CommandRegistry.
|
Delegates to the internal CommandRegistry.
|
||||||
|
|
||||||
@@ -382,8 +366,7 @@ class CommandDispatcher:
|
|||||||
return self._command_registry.get(trigger)
|
return self._command_registry.get(trigger)
|
||||||
|
|
||||||
def exists(self, trigger: str) -> bool:
|
def exists(self, trigger: str) -> bool:
|
||||||
"""
|
"""Check if a command is registered.
|
||||||
Check if a command is registered.
|
|
||||||
|
|
||||||
Delegates to the internal CommandRegistry.
|
Delegates to the internal CommandRegistry.
|
||||||
|
|
||||||
@@ -393,8 +376,7 @@ class CommandDispatcher:
|
|||||||
return self._command_registry.exists(trigger)
|
return self._command_registry.exists(trigger)
|
||||||
|
|
||||||
def get_by_module(self, module_name: str) -> dict[str, CommandInfo]:
|
def get_by_module(self, module_name: str) -> dict[str, CommandInfo]:
|
||||||
"""
|
"""Get all commands registered by a specific module.
|
||||||
Get all commands registered by a specific module.
|
|
||||||
|
|
||||||
:param module_name: The module whose commands to return.
|
:param module_name: The module whose commands to return.
|
||||||
:return: Dict mapping primary command names to CommandInfo for that module.
|
:return: Dict mapping primary command names to CommandInfo for that module.
|
||||||
@@ -406,8 +388,7 @@ class CommandDispatcher:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def register_from_module(self, module: ModuleType, module_name: str) -> None:
|
def register_from_module(self, module: ModuleType, module_name: str) -> None:
|
||||||
"""
|
"""Scan a Python module for @on_command-decorated functions and register them.
|
||||||
Scan a Python module for @on_command-decorated functions and register them.
|
|
||||||
|
|
||||||
Delegates to the internal CommandRegistry.
|
Delegates to the internal CommandRegistry.
|
||||||
|
|
||||||
@@ -417,8 +398,7 @@ class CommandDispatcher:
|
|||||||
self._command_registry.register_from_module(module, module_name)
|
self._command_registry.register_from_module(module, module_name)
|
||||||
|
|
||||||
def unregister_by_module(self, module_name: str) -> int:
|
def unregister_by_module(self, module_name: str) -> int:
|
||||||
"""
|
"""Remove all commands registered by a specific module.
|
||||||
Remove all commands registered by a specific module.
|
|
||||||
|
|
||||||
Delegates to the internal CommandRegistry.
|
Delegates to the internal CommandRegistry.
|
||||||
|
|
||||||
@@ -432,8 +412,7 @@ class CommandDispatcher:
|
|||||||
return self._command_registry.unregister_by_module(module_name)
|
return self._command_registry.unregister_by_module(module_name)
|
||||||
|
|
||||||
async def dispatch(self, event: ChatEvent) -> None:
|
async def dispatch(self, event: ChatEvent) -> None:
|
||||||
"""
|
"""Dispatch a chat event to the appropriate command handler if it's a command.
|
||||||
Dispatch a chat event to the appropriate command handler if it's a command.
|
|
||||||
|
|
||||||
:param event: The chat event to check for commands.
|
:param event: The chat event to check for commands.
|
||||||
"""
|
"""
|
||||||
@@ -605,8 +584,7 @@ class CommandDispatcher:
|
|||||||
|
|
||||||
|
|
||||||
class ModuleCommands:
|
class ModuleCommands:
|
||||||
"""
|
"""Module-scoped wrapper around CommandDispatcher.
|
||||||
Module-scoped wrapper around CommandDispatcher.
|
|
||||||
|
|
||||||
This wrapper auto-supplies the module name for registration operations,
|
This wrapper auto-supplies the module name for registration operations,
|
||||||
so modules don't need to pass their own name back into the API.
|
so modules don't need to pass their own name back into the API.
|
||||||
@@ -614,8 +592,7 @@ class ModuleCommands:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, dispatcher: CommandDispatcher, module_name: str) -> None:
|
def __init__(self, dispatcher: CommandDispatcher, module_name: str) -> None:
|
||||||
"""
|
"""Initialize the module-scoped command wrapper.
|
||||||
Initialize the module-scoped command wrapper.
|
|
||||||
|
|
||||||
:param dispatcher: The CommandDispatcher that owns the command registry.
|
:param dispatcher: The CommandDispatcher that owns the command registry.
|
||||||
:param module_name: The name of the module using this wrapper.
|
:param module_name: The name of the module using this wrapper.
|
||||||
@@ -643,8 +620,7 @@ class ModuleCommands:
|
|||||||
requires_moderator: bool = False,
|
requires_moderator: bool = False,
|
||||||
cooldown: int | float = 0,
|
cooldown: int | float = 0,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Register a command handler for this module.
|
||||||
Register a command handler for this module.
|
|
||||||
|
|
||||||
The module name is automatically supplied.
|
The module name is automatically supplied.
|
||||||
|
|
||||||
@@ -667,8 +643,7 @@ class ModuleCommands:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def unregister(self, name: str) -> bool:
|
def unregister(self, name: str) -> bool:
|
||||||
"""
|
"""Unregister a command and all its aliases.
|
||||||
Unregister a command and all its aliases.
|
|
||||||
|
|
||||||
Only commands registered by this module can be unregistered.
|
Only commands registered by this module can be unregistered.
|
||||||
|
|
||||||
@@ -682,8 +657,7 @@ class ModuleCommands:
|
|||||||
return self._dispatcher.unregister(name)
|
return self._dispatcher.unregister(name)
|
||||||
|
|
||||||
def get(self, trigger: str) -> CommandInfo | None:
|
def get(self, trigger: str) -> CommandInfo | None:
|
||||||
"""
|
"""Look up a command by name or alias within this module's registrations.
|
||||||
Look up a command by name or alias within this module's registrations.
|
|
||||||
|
|
||||||
:param trigger: Command name or alias (case-insensitive).
|
:param trigger: Command name or alias (case-insensitive).
|
||||||
:return: CommandInfo if found and owned by this module, None otherwise.
|
:return: CommandInfo if found and owned by this module, None otherwise.
|
||||||
@@ -694,8 +668,7 @@ class ModuleCommands:
|
|||||||
return info
|
return info
|
||||||
|
|
||||||
def exists(self, trigger: str) -> bool:
|
def exists(self, trigger: str) -> bool:
|
||||||
"""
|
"""Check if a command is registered across all modules.
|
||||||
Check if a command is registered across all modules.
|
|
||||||
|
|
||||||
:param trigger: Command name or alias (case-insensitive).
|
:param trigger: Command name or alias (case-insensitive).
|
||||||
:return: True if the command exists, False otherwise.
|
:return: True if the command exists, False otherwise.
|
||||||
|
|||||||
+23
-46
@@ -49,8 +49,7 @@ logger = logging.getLogger("owlbot.events")
|
|||||||
|
|
||||||
|
|
||||||
class EventRegistry:
|
class EventRegistry:
|
||||||
"""
|
"""Holds all registered event handlers for a bot instance.
|
||||||
Holds all registered event handlers for a bot instance.
|
|
||||||
|
|
||||||
Instance-scoped to enable proper dependency injection and allow multiple
|
Instance-scoped to enable proper dependency injection and allow multiple
|
||||||
bot instances to coexist without sharing state.
|
bot instances to coexist without sharing state.
|
||||||
@@ -69,8 +68,7 @@ class EventRegistry:
|
|||||||
module_name: str,
|
module_name: str,
|
||||||
priority: int = Priority.NORMAL,
|
priority: int = Priority.NORMAL,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Register a handler for the given event types.
|
||||||
Register a handler for the given event types.
|
|
||||||
|
|
||||||
Called by the module loader after scanning for decorated functions.
|
Called by the module loader after scanning for decorated functions.
|
||||||
|
|
||||||
@@ -97,8 +95,7 @@ class EventRegistry:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def unregister(self, handler: EventHandler) -> bool:
|
def unregister(self, handler: EventHandler) -> bool:
|
||||||
"""
|
"""Unregister a handler from all event types it is registered for.
|
||||||
Unregister a handler from all event types it is registered for.
|
|
||||||
|
|
||||||
:param handler: The handler function to unregister.
|
:param handler: The handler function to unregister.
|
||||||
:return: True if handler was found and removed, False otherwise.
|
:return: True if handler was found and removed, False otherwise.
|
||||||
@@ -126,8 +123,7 @@ class EventRegistry:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def get(self, event_type: EventType) -> list[HandlerEntry]:
|
def get(self, event_type: EventType) -> list[HandlerEntry]:
|
||||||
"""
|
"""Get all handlers registered for a specific event type.
|
||||||
Get all handlers registered for a specific event type.
|
|
||||||
|
|
||||||
:param event_type: The event type to look up.
|
:param event_type: The event type to look up.
|
||||||
:return: List of (handler, module_name, priority) tuples.
|
:return: List of (handler, module_name, priority) tuples.
|
||||||
@@ -135,8 +131,7 @@ class EventRegistry:
|
|||||||
return self._handlers.get(event_type.value, [])
|
return self._handlers.get(event_type.value, [])
|
||||||
|
|
||||||
def get_all(self) -> EventHandlerMap:
|
def get_all(self) -> EventHandlerMap:
|
||||||
"""
|
"""Get a copy of the entire handler registry.
|
||||||
Get a copy of the entire handler registry.
|
|
||||||
|
|
||||||
:return: Dict mapping event type values to handler lists.
|
:return: Dict mapping event type values to handler lists.
|
||||||
"""
|
"""
|
||||||
@@ -145,8 +140,7 @@ class EventRegistry:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def get_handler_module(self, handler: EventHandler) -> str | None:
|
def get_handler_module(self, handler: EventHandler) -> str | None:
|
||||||
"""
|
"""Look up which module registered a given handler.
|
||||||
Look up which module registered a given handler.
|
|
||||||
|
|
||||||
:param handler: The handler function to look up.
|
:param handler: The handler function to look up.
|
||||||
:return: The module name if found, None otherwise.
|
:return: The module name if found, None otherwise.
|
||||||
@@ -158,8 +152,7 @@ class EventRegistry:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def unregister_by_module(self, module_name: str) -> int:
|
def unregister_by_module(self, module_name: str) -> int:
|
||||||
"""
|
"""Remove all handlers registered by a specific module.
|
||||||
Remove all handlers registered by a specific module.
|
|
||||||
|
|
||||||
:param module_name: The module whose handlers should be removed.
|
:param module_name: The module whose handlers should be removed.
|
||||||
:return: Number of handlers removed.
|
:return: Number of handlers removed.
|
||||||
@@ -181,8 +174,7 @@ class EventRegistry:
|
|||||||
return len(seen)
|
return len(seen)
|
||||||
|
|
||||||
def register_from_module(self, module: ModuleType, module_name: str) -> None:
|
def register_from_module(self, module: ModuleType, module_name: str) -> None:
|
||||||
"""
|
"""Scan a Python module for @on_event-decorated functions and register them.
|
||||||
Scan a Python module for @on_event-decorated functions and register them.
|
|
||||||
|
|
||||||
Looks for functions with the ``_owlbot_event`` attribute set by
|
Looks for functions with the ``_owlbot_event`` attribute set by
|
||||||
the ``@on_event`` decorator and registers each one.
|
the ``@on_event`` decorator and registers each one.
|
||||||
@@ -201,8 +193,7 @@ class EventRegistry:
|
|||||||
|
|
||||||
|
|
||||||
class EventDispatcher:
|
class EventDispatcher:
|
||||||
"""
|
"""Dispatches events to registered handlers sequentially by priority.
|
||||||
Dispatches events to registered handlers sequentially by priority.
|
|
||||||
|
|
||||||
After all event handlers complete, command dispatch is triggered for
|
After all event handlers complete, command dispatch is triggered for
|
||||||
CHAT events (via the injected command_dispatch callback).
|
CHAT events (via the injected command_dispatch callback).
|
||||||
@@ -214,8 +205,7 @@ class EventDispatcher:
|
|||||||
get_module_context: Callable[[str], ModuleContext],
|
get_module_context: Callable[[str], ModuleContext],
|
||||||
handler_timeout: float,
|
handler_timeout: float,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Initialize the event dispatcher.
|
||||||
Initialize the event dispatcher.
|
|
||||||
|
|
||||||
Creates and owns a :class:`EventRegistry` internally.
|
Creates and owns a :class:`EventRegistry` internally.
|
||||||
|
|
||||||
@@ -237,8 +227,7 @@ class EventDispatcher:
|
|||||||
module_name: str,
|
module_name: str,
|
||||||
priority: int = Priority.NORMAL,
|
priority: int = Priority.NORMAL,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Register a handler for the given event types.
|
||||||
Register a handler for the given event types.
|
|
||||||
|
|
||||||
A single handler can respond to multiple event types; the registry
|
A single handler can respond to multiple event types; the registry
|
||||||
stores a separate entry per event type. This is the shared entry
|
stores a separate entry per event type. This is the shared entry
|
||||||
@@ -255,8 +244,7 @@ class EventDispatcher:
|
|||||||
self._handler_registry.register(handler, event_types, module_name, priority)
|
self._handler_registry.register(handler, event_types, module_name, priority)
|
||||||
|
|
||||||
def unregister(self, handler: EventHandler) -> bool:
|
def unregister(self, handler: EventHandler) -> bool:
|
||||||
"""
|
"""Unregister a handler from all event types it is registered for.
|
||||||
Unregister a handler from all event types it is registered for.
|
|
||||||
|
|
||||||
Unlike commands (looked up by name string), event handlers are
|
Unlike commands (looked up by name string), event handlers are
|
||||||
identified by object identity. A handler registered for multiple
|
identified by object identity. A handler registered for multiple
|
||||||
@@ -268,8 +256,7 @@ class EventDispatcher:
|
|||||||
return self._handler_registry.unregister(handler)
|
return self._handler_registry.unregister(handler)
|
||||||
|
|
||||||
def get_by_module(self, module_name: str) -> EventHandlerMap:
|
def get_by_module(self, module_name: str) -> EventHandlerMap:
|
||||||
"""
|
"""Get all handlers registered by a specific module, grouped by event type.
|
||||||
Get all handlers registered by a specific module, grouped by event type.
|
|
||||||
|
|
||||||
The registry stores handlers grouped by event type, not by module,
|
The registry stores handlers grouped by event type, not by module,
|
||||||
so this filters across all event types to collect a single module's
|
so this filters across all event types to collect a single module's
|
||||||
@@ -288,8 +275,7 @@ class EventDispatcher:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def get_handler_module(self, handler: EventHandler) -> str | None:
|
def get_handler_module(self, handler: EventHandler) -> str | None:
|
||||||
"""
|
"""Reverse-lookup which module registered a given handler.
|
||||||
Reverse-lookup which module registered a given handler.
|
|
||||||
|
|
||||||
Scans all event types since handlers are stored by event type,
|
Scans all event types since handlers are stored by event type,
|
||||||
not by module.
|
not by module.
|
||||||
@@ -300,8 +286,7 @@ class EventDispatcher:
|
|||||||
return self._handler_registry.get_handler_module(handler)
|
return self._handler_registry.get_handler_module(handler)
|
||||||
|
|
||||||
def register_from_module(self, module: ModuleType, module_name: str) -> None:
|
def register_from_module(self, module: ModuleType, module_name: str) -> None:
|
||||||
"""
|
"""Scan a Python module for ``@on_event``-decorated functions and register them.
|
||||||
Scan a Python module for ``@on_event``-decorated functions and register them.
|
|
||||||
|
|
||||||
This is the import-phase entry point: the module loader calls it once
|
This is the import-phase entry point: the module loader calls it once
|
||||||
per module. Decorator attributes are read here and passed as explicit
|
per module. Decorator attributes are read here and passed as explicit
|
||||||
@@ -314,8 +299,7 @@ class EventDispatcher:
|
|||||||
self._handler_registry.register_from_module(module, module_name)
|
self._handler_registry.register_from_module(module, module_name)
|
||||||
|
|
||||||
def unregister_by_module(self, module_name: str) -> int:
|
def unregister_by_module(self, module_name: str) -> int:
|
||||||
"""
|
"""Remove all handlers registered by a specific module.
|
||||||
Remove all handlers registered by a specific module.
|
|
||||||
|
|
||||||
Used during module teardown to clean up all of a module's handlers
|
Used during module teardown to clean up all of a module's handlers
|
||||||
in one call, regardless of which event types they were registered for.
|
in one call, regardless of which event types they were registered for.
|
||||||
@@ -326,8 +310,7 @@ class EventDispatcher:
|
|||||||
return self._handler_registry.unregister_by_module(module_name)
|
return self._handler_registry.unregister_by_module(module_name)
|
||||||
|
|
||||||
async def dispatch(self, event_type: EventType, event: Event) -> None:
|
async def dispatch(self, event_type: EventType, event: Event) -> None:
|
||||||
"""
|
"""Dispatch an event to handlers sequentially by priority, then to commands.
|
||||||
Dispatch an event to handlers sequentially by priority, then to commands.
|
|
||||||
|
|
||||||
:param event_type: The type of event to dispatch.
|
:param event_type: The type of event to dispatch.
|
||||||
:param event: The parsed event instance.
|
:param event: The parsed event instance.
|
||||||
@@ -391,8 +374,7 @@ class EventDispatcher:
|
|||||||
module_name: str,
|
module_name: str,
|
||||||
propagation: PropagationState,
|
propagation: PropagationState,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Call a single handler with timeout enforcement and transaction management.
|
||||||
Call a single handler with timeout enforcement and transaction management.
|
|
||||||
|
|
||||||
:param handler: The handler function to call.
|
:param handler: The handler function to call.
|
||||||
:param event: The event to pass to the handler.
|
:param event: The event to pass to the handler.
|
||||||
@@ -444,8 +426,7 @@ class EventDispatcher:
|
|||||||
|
|
||||||
|
|
||||||
class ModuleEvents:
|
class ModuleEvents:
|
||||||
"""
|
"""Module-scoped wrapper around EventDispatcher.
|
||||||
Module-scoped wrapper around EventDispatcher.
|
|
||||||
|
|
||||||
This wrapper auto-supplies the module name for registration operations,
|
This wrapper auto-supplies the module name for registration operations,
|
||||||
so modules don't need to pass their own name back into the API.
|
so modules don't need to pass their own name back into the API.
|
||||||
@@ -453,8 +434,7 @@ class ModuleEvents:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, dispatcher: EventDispatcher, module_name: str) -> None:
|
def __init__(self, dispatcher: EventDispatcher, module_name: str) -> None:
|
||||||
"""
|
"""Initialize the module-scoped handler wrapper.
|
||||||
Initialize the module-scoped handler wrapper.
|
|
||||||
|
|
||||||
:param dispatcher: The EventDispatcher that owns the handler registry.
|
:param dispatcher: The EventDispatcher that owns the handler registry.
|
||||||
:param module_name: The name of the module using this wrapper.
|
:param module_name: The name of the module using this wrapper.
|
||||||
@@ -473,8 +453,7 @@ class ModuleEvents:
|
|||||||
event_types: tuple[EventType, ...],
|
event_types: tuple[EventType, ...],
|
||||||
priority: int = Priority.NORMAL,
|
priority: int = Priority.NORMAL,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Register an event handler for this module.
|
||||||
Register an event handler for this module.
|
|
||||||
|
|
||||||
The module name is automatically supplied.
|
The module name is automatically supplied.
|
||||||
|
|
||||||
@@ -491,8 +470,7 @@ class ModuleEvents:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def unregister(self, handler: EventHandler) -> bool:
|
def unregister(self, handler: EventHandler) -> bool:
|
||||||
"""
|
"""Unregister a handler from all event types it is registered for.
|
||||||
Unregister a handler from all event types it is registered for.
|
|
||||||
|
|
||||||
Only handlers registered by this module can be unregistered.
|
Only handlers registered by this module can be unregistered.
|
||||||
|
|
||||||
@@ -505,8 +483,7 @@ class ModuleEvents:
|
|||||||
return self._dispatcher.unregister(handler)
|
return self._dispatcher.unregister(handler)
|
||||||
|
|
||||||
def get(self, event_type: EventType) -> list[HandlerEntry]:
|
def get(self, event_type: EventType) -> list[HandlerEntry]:
|
||||||
"""
|
"""Get handlers registered by this module for a specific event type.
|
||||||
Get handlers registered by this module for a specific event type.
|
|
||||||
|
|
||||||
:param event_type: The event type to look up.
|
:param event_type: The event type to look up.
|
||||||
:return: List of (handler, module_name, priority) tuples for this module only.
|
:return: List of (handler, module_name, priority) tuples for this module only.
|
||||||
|
|||||||
+27
-54
@@ -39,8 +39,7 @@ logger = logging.getLogger("owlbot.web")
|
|||||||
|
|
||||||
|
|
||||||
class RouteRegistry:
|
class RouteRegistry:
|
||||||
"""
|
"""Holds all registered routes for a bot instance.
|
||||||
Holds all registered routes for a bot instance.
|
|
||||||
|
|
||||||
Routes are namespaced by module to prevent conflicts. Supports path
|
Routes are namespaced by module to prevent conflicts. Supports path
|
||||||
patterns using aiohttp's ``{name}`` and ``{name:regex}`` syntax via
|
patterns using aiohttp's ``{name}`` and ``{name:regex}`` syntax via
|
||||||
@@ -64,8 +63,7 @@ class RouteRegistry:
|
|||||||
methods: list[str] | None = None,
|
methods: list[str] | None = None,
|
||||||
module_name: str,
|
module_name: str,
|
||||||
) -> RouteInfo:
|
) -> RouteInfo:
|
||||||
"""
|
"""Register a route handler.
|
||||||
Register a route handler.
|
|
||||||
|
|
||||||
:param path: URL path relative to module namespace. Supports
|
:param path: URL path relative to module namespace. Supports
|
||||||
``{name}`` and ``{name:regex}`` patterns.
|
``{name}`` and ``{name:regex}`` patterns.
|
||||||
@@ -108,8 +106,7 @@ class RouteRegistry:
|
|||||||
return info
|
return info
|
||||||
|
|
||||||
def unregister(self, full_path: str) -> bool:
|
def unregister(self, full_path: str) -> bool:
|
||||||
"""
|
"""Unregister a route by its full path.
|
||||||
Unregister a route by its full path.
|
|
||||||
|
|
||||||
:param full_path: The full route path including namespace.
|
:param full_path: The full route path including namespace.
|
||||||
:return: True if route was found and removed, False otherwise.
|
:return: True if route was found and removed, False otherwise.
|
||||||
@@ -134,8 +131,7 @@ class RouteRegistry:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def get(self, full_path: str) -> RouteInfo | None:
|
def get(self, full_path: str) -> RouteInfo | None:
|
||||||
"""
|
"""Look up a route by its full path (exact match on registered pattern).
|
||||||
Look up a route by its full path (exact match on registered pattern).
|
|
||||||
|
|
||||||
:param full_path: The full route path including namespace.
|
:param full_path: The full route path including namespace.
|
||||||
:return: RouteInfo if found, None otherwise.
|
:return: RouteInfo if found, None otherwise.
|
||||||
@@ -146,8 +142,7 @@ class RouteRegistry:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def match(self, full_path: str) -> tuple[RouteInfo, dict[str, str]] | None:
|
def match(self, full_path: str) -> tuple[RouteInfo, dict[str, str]] | None:
|
||||||
"""
|
"""Match a request path against registered routes.
|
||||||
Match a request path against registered routes.
|
|
||||||
|
|
||||||
Scans routes in registration order (first match wins). Uses
|
Scans routes in registration order (first match wins). Uses
|
||||||
``DynamicResource._match()`` for both plain and parameterized paths.
|
``DynamicResource._match()`` for both plain and parameterized paths.
|
||||||
@@ -162,16 +157,14 @@ class RouteRegistry:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def get_all(self) -> dict[str, RouteInfo]:
|
def get_all(self) -> dict[str, RouteInfo]:
|
||||||
"""
|
"""Get all registered routes.
|
||||||
Get all registered routes.
|
|
||||||
|
|
||||||
:return: Dict mapping full paths to RouteInfo.
|
:return: Dict mapping full paths to RouteInfo.
|
||||||
"""
|
"""
|
||||||
return {info.full_path: info for _, info in self._routes}
|
return {info.full_path: info for _, info in self._routes}
|
||||||
|
|
||||||
def get_by_module(self, module_name: str) -> list[RouteInfo]:
|
def get_by_module(self, module_name: str) -> list[RouteInfo]:
|
||||||
"""
|
"""Get all routes registered by a specific module.
|
||||||
Get all routes registered by a specific module.
|
|
||||||
|
|
||||||
:param module_name: The module name.
|
:param module_name: The module name.
|
||||||
:return: List of RouteInfo for that module.
|
:return: List of RouteInfo for that module.
|
||||||
@@ -180,8 +173,7 @@ class RouteRegistry:
|
|||||||
return [info for _, info in self._routes if info.full_path in paths]
|
return [info for _, info in self._routes if info.full_path in paths]
|
||||||
|
|
||||||
def unregister_by_module(self, module_name: str) -> int:
|
def unregister_by_module(self, module_name: str) -> int:
|
||||||
"""
|
"""Remove all routes registered by a specific module.
|
||||||
Remove all routes registered by a specific module.
|
|
||||||
|
|
||||||
:param module_name: The module whose routes should be removed.
|
:param module_name: The module whose routes should be removed.
|
||||||
:return: Number of routes removed.
|
:return: Number of routes removed.
|
||||||
@@ -202,8 +194,7 @@ class RouteRegistry:
|
|||||||
return count
|
return count
|
||||||
|
|
||||||
def register_from_module(self, module: ModuleType, module_name: str) -> None:
|
def register_from_module(self, module: ModuleType, module_name: str) -> None:
|
||||||
"""
|
"""Scan a Python module for @on_route-decorated functions and register them.
|
||||||
Scan a Python module for @on_route-decorated functions and register them.
|
|
||||||
|
|
||||||
Looks for functions with the ``_owlbot_route`` attribute set by
|
Looks for functions with the ``_owlbot_route`` attribute set by
|
||||||
the ``@on_route`` decorator and registers each one.
|
the ``@on_route`` decorator and registers each one.
|
||||||
@@ -225,8 +216,7 @@ class RouteRegistry:
|
|||||||
|
|
||||||
|
|
||||||
class RouteDispatcher:
|
class RouteDispatcher:
|
||||||
"""
|
"""Dispatches HTTP requests to registered module route handlers.
|
||||||
Dispatches HTTP requests to registered module route handlers.
|
|
||||||
|
|
||||||
Looks up routes in the RouteRegistry, validates methods, creates
|
Looks up routes in the RouteRegistry, validates methods, creates
|
||||||
RouteContext, and calls the handler with timeout and transaction management.
|
RouteContext, and calls the handler with timeout and transaction management.
|
||||||
@@ -237,8 +227,7 @@ class RouteDispatcher:
|
|||||||
get_module_context: Callable[[str], ModuleContext],
|
get_module_context: Callable[[str], ModuleContext],
|
||||||
handler_timeout: float,
|
handler_timeout: float,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Initialize the route dispatcher.
|
||||||
Initialize the route dispatcher.
|
|
||||||
|
|
||||||
Creates and owns a :class:`RouteRegistry` internally.
|
Creates and owns a :class:`RouteRegistry` internally.
|
||||||
|
|
||||||
@@ -258,8 +247,7 @@ class RouteDispatcher:
|
|||||||
methods: list[str] | None = None,
|
methods: list[str] | None = None,
|
||||||
module_name: str,
|
module_name: str,
|
||||||
) -> RouteInfo:
|
) -> RouteInfo:
|
||||||
"""
|
"""Register a route handler.
|
||||||
Register a route handler.
|
|
||||||
|
|
||||||
Delegates to the internal RouteRegistry.
|
Delegates to the internal RouteRegistry.
|
||||||
|
|
||||||
@@ -278,8 +266,7 @@ class RouteDispatcher:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def unregister(self, full_path: str) -> bool:
|
def unregister(self, full_path: str) -> bool:
|
||||||
"""
|
"""Unregister a route by its full path.
|
||||||
Unregister a route by its full path.
|
|
||||||
|
|
||||||
Delegates to the internal RouteRegistry.
|
Delegates to the internal RouteRegistry.
|
||||||
|
|
||||||
@@ -289,8 +276,7 @@ class RouteDispatcher:
|
|||||||
return self._route_registry.unregister(full_path)
|
return self._route_registry.unregister(full_path)
|
||||||
|
|
||||||
def get(self, full_path: str) -> RouteInfo | None:
|
def get(self, full_path: str) -> RouteInfo | None:
|
||||||
"""
|
"""Look up a route by its full path.
|
||||||
Look up a route by its full path.
|
|
||||||
|
|
||||||
Delegates to the internal RouteRegistry.
|
Delegates to the internal RouteRegistry.
|
||||||
|
|
||||||
@@ -300,8 +286,7 @@ class RouteDispatcher:
|
|||||||
return self._route_registry.get(full_path)
|
return self._route_registry.get(full_path)
|
||||||
|
|
||||||
def get_by_module(self, module_name: str) -> list[RouteInfo]:
|
def get_by_module(self, module_name: str) -> list[RouteInfo]:
|
||||||
"""
|
"""Get all routes registered by a specific module.
|
||||||
Get all routes registered by a specific module.
|
|
||||||
|
|
||||||
Delegates to the internal RouteRegistry.
|
Delegates to the internal RouteRegistry.
|
||||||
|
|
||||||
@@ -311,8 +296,7 @@ class RouteDispatcher:
|
|||||||
return self._route_registry.get_by_module(module_name)
|
return self._route_registry.get_by_module(module_name)
|
||||||
|
|
||||||
def register_from_module(self, module: ModuleType, module_name: str) -> None:
|
def register_from_module(self, module: ModuleType, module_name: str) -> None:
|
||||||
"""
|
"""Scan a Python module for @on_route-decorated functions and register them.
|
||||||
Scan a Python module for @on_route-decorated functions and register them.
|
|
||||||
|
|
||||||
Delegates to the internal RouteRegistry.
|
Delegates to the internal RouteRegistry.
|
||||||
|
|
||||||
@@ -322,8 +306,7 @@ class RouteDispatcher:
|
|||||||
self._route_registry.register_from_module(module, module_name)
|
self._route_registry.register_from_module(module, module_name)
|
||||||
|
|
||||||
def unregister_by_module(self, module_name: str) -> int:
|
def unregister_by_module(self, module_name: str) -> int:
|
||||||
"""
|
"""Remove all routes registered by a specific module.
|
||||||
Remove all routes registered by a specific module.
|
|
||||||
|
|
||||||
Delegates to the internal RouteRegistry.
|
Delegates to the internal RouteRegistry.
|
||||||
|
|
||||||
@@ -333,8 +316,7 @@ class RouteDispatcher:
|
|||||||
return self._route_registry.unregister_by_module(module_name)
|
return self._route_registry.unregister_by_module(module_name)
|
||||||
|
|
||||||
async def dispatch(self, request: web.Request) -> web.StreamResponse:
|
async def dispatch(self, request: web.Request) -> web.StreamResponse:
|
||||||
"""
|
"""Dispatch an HTTP request to the appropriate module route handler.
|
||||||
Dispatch an HTTP request to the appropriate module route handler.
|
|
||||||
|
|
||||||
Extracts module_name and path from the URL, matches it against
|
Extracts module_name and path from the URL, matches it against
|
||||||
registered routes (supporting path patterns), validates the HTTP
|
registered routes (supporting path patterns), validates the HTTP
|
||||||
@@ -374,8 +356,7 @@ class RouteDispatcher:
|
|||||||
route_info: RouteInfo,
|
route_info: RouteInfo,
|
||||||
match_info: dict[str, str] | None = None,
|
match_info: dict[str, str] | None = None,
|
||||||
) -> web.StreamResponse:
|
) -> web.StreamResponse:
|
||||||
"""
|
"""Handle an HTTP request to a module-registered route.
|
||||||
Handle an HTTP request to a module-registered route.
|
|
||||||
|
|
||||||
:param request: The aiohttp request object.
|
:param request: The aiohttp request object.
|
||||||
:param route_info: Information about the registered route.
|
:param route_info: Information about the registered route.
|
||||||
@@ -447,8 +428,7 @@ class RouteDispatcher:
|
|||||||
|
|
||||||
|
|
||||||
class ModuleRoutes:
|
class ModuleRoutes:
|
||||||
"""
|
"""Module-scoped wrapper around RouteDispatcher.
|
||||||
Module-scoped wrapper around RouteDispatcher.
|
|
||||||
|
|
||||||
This wrapper auto-supplies the module name for route operations,
|
This wrapper auto-supplies the module name for route operations,
|
||||||
so modules don't need to know the internal routing namespace.
|
so modules don't need to know the internal routing namespace.
|
||||||
@@ -458,8 +438,7 @@ class ModuleRoutes:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self, dispatcher: RouteDispatcher, module_name: str, public_base_url: str
|
self, dispatcher: RouteDispatcher, module_name: str, public_base_url: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Initialize the module-scoped routes wrapper.
|
||||||
Initialize the module-scoped routes wrapper.
|
|
||||||
|
|
||||||
:param dispatcher: The RouteDispatcher that owns the route registry.
|
:param dispatcher: The RouteDispatcher that owns the route registry.
|
||||||
:param module_name: The name of the module using this wrapper.
|
:param module_name: The name of the module using this wrapper.
|
||||||
@@ -475,8 +454,7 @@ class ModuleRoutes:
|
|||||||
return self._dispatcher.get_by_module(self._module_name)
|
return self._dispatcher.get_by_module(self._module_name)
|
||||||
|
|
||||||
def url_for(self, path: str) -> str:
|
def url_for(self, path: str) -> str:
|
||||||
"""
|
"""Build a public URL for a route registered by this module.
|
||||||
Build a public URL for a route registered by this module.
|
|
||||||
|
|
||||||
:param path: The route path (e.g., "/list").
|
:param path: The route path (e.g., "/list").
|
||||||
:return: Full public URL (e.g., "http://host/owlbot/quotes/list").
|
:return: Full public URL (e.g., "http://host/owlbot/quotes/list").
|
||||||
@@ -490,8 +468,7 @@ class ModuleRoutes:
|
|||||||
*,
|
*,
|
||||||
methods: list[str] | None = None,
|
methods: list[str] | None = None,
|
||||||
) -> RouteInfo:
|
) -> RouteInfo:
|
||||||
"""
|
"""Register a route handler for this module.
|
||||||
Register a route handler for this module.
|
|
||||||
|
|
||||||
The module name is automatically supplied.
|
The module name is automatically supplied.
|
||||||
|
|
||||||
@@ -509,8 +486,7 @@ class ModuleRoutes:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def unregister(self, path: str) -> bool:
|
def unregister(self, path: str) -> bool:
|
||||||
"""
|
"""Unregister a route by its relative path.
|
||||||
Unregister a route by its relative path.
|
|
||||||
|
|
||||||
:param path: Relative route path (e.g., "/stats").
|
:param path: Relative route path (e.g., "/stats").
|
||||||
:return: True if route was found and removed, False otherwise.
|
:return: True if route was found and removed, False otherwise.
|
||||||
@@ -518,8 +494,7 @@ class ModuleRoutes:
|
|||||||
return self._dispatcher.unregister(self._full_path(path))
|
return self._dispatcher.unregister(self._full_path(path))
|
||||||
|
|
||||||
def get(self, path: str) -> RouteInfo | None:
|
def get(self, path: str) -> RouteInfo | None:
|
||||||
"""
|
"""Look up a route by its relative path.
|
||||||
Look up a route by its relative path.
|
|
||||||
|
|
||||||
:param path: Relative route path (e.g., "/stats").
|
:param path: Relative route path (e.g., "/stats").
|
||||||
:return: RouteInfo if found, None otherwise.
|
:return: RouteInfo if found, None otherwise.
|
||||||
@@ -527,8 +502,7 @@ class ModuleRoutes:
|
|||||||
return self._dispatcher.get(self._full_path(path))
|
return self._dispatcher.get(self._full_path(path))
|
||||||
|
|
||||||
def exists(self, path: str) -> bool:
|
def exists(self, path: str) -> bool:
|
||||||
"""
|
"""Check if a route is registered at the given relative path.
|
||||||
Check if a route is registered at the given relative path.
|
|
||||||
|
|
||||||
:param path: Relative route path (e.g., "/stats").
|
:param path: Relative route path (e.g., "/stats").
|
||||||
:return: True if the route exists, False otherwise.
|
:return: True if the route exists, False otherwise.
|
||||||
@@ -536,8 +510,7 @@ class ModuleRoutes:
|
|||||||
return self.get(path) is not None
|
return self.get(path) is not None
|
||||||
|
|
||||||
def _full_path(self, path: str) -> str:
|
def _full_path(self, path: str) -> str:
|
||||||
"""
|
"""Normalize a relative path into the full namespaced path.
|
||||||
Normalize a relative path into the full namespaced path.
|
|
||||||
|
|
||||||
:param path: Relative route path (e.g., "/list" or "list").
|
:param path: Relative route path (e.g., "/list" or "list").
|
||||||
:return: Full path (e.g., "/owlbot/quotes/list").
|
:return: Full path (e.g., "/owlbot/quotes/list").
|
||||||
|
|||||||
@@ -77,6 +77,11 @@ select = [
|
|||||||
"SIM", # flake8-simplify
|
"SIM", # flake8-simplify
|
||||||
"TCH", # flake8-type-checking
|
"TCH", # flake8-type-checking
|
||||||
"RUF", # Ruff-specific rules
|
"RUF", # Ruff-specific rules
|
||||||
|
"D", # pydocstyle
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
"D203", # incompatible with D211 (no blank line before class docstring)
|
||||||
|
"D213", # incompatible with D212 (summary on first line)
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Shared test fixtures for the Owlbot test suite."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
@@ -27,5 +29,6 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
async def storage(tmp_path: Path) -> AsyncIterator[ModuleStorage]:
|
async def storage(tmp_path: Path) -> AsyncIterator[ModuleStorage]:
|
||||||
|
"""Yield an open ModuleStorage backed by a temporary directory."""
|
||||||
async with ModuleStorage(tmp_path, "test_module") as s:
|
async with ModuleStorage(tmp_path, "test_module") as s:
|
||||||
yield s
|
yield s
|
||||||
|
|||||||
Reference in New Issue
Block a user