Module trappedbot.commands.command

Built-in commands

See the implemented sample bot commands of echo, date, dir, help, and whoami? Have a close look at them and style your commands after these example commands.

Expand source code
"""Built-in commands

See the implemented sample bot commands of `echo`, `date`, `dir`, `help`,
and `whoami`? Have a close look at them and style your commands after these
example commands.
"""

import shlex
import traceback
from typing import List, Optional

from nio import AsyncClient
from nio.events.room_events import RoomMessageText
from nio.rooms import MatrixRoom

from trappedbot import appconfig
from trappedbot.applogger import LOGGER
from trappedbot.mxutil import MessageFormat, Mxid
from trappedbot.chat_functions import send_text_to_room
from trappedbot.tasks.task import Task, TaskMessageContext


class Command(object):
    """A command that a bot can perform

    name: The word that activates this command
    task: The task to perform
    help: Help text for the command
    allow_untrusted: Allow any user to run this command?
    allow_homeservers: Allow any user from these homeservers to run this command?
    allow_users: Allow any user in this list to run this command?
    """

    def __init__(
        self,
        name: str,
        task: Task,
        # This isn't a NamedTuple because I want to be able to pass help=None
        # and have it use fallback text. Sigh.
        help: Optional[str] = None,
        allow_untrusted: bool = False,
        allow_homeservers: Optional[List[str]] = None,
        allow_users: Optional[List[str]] = None,
    ):
        self.name = name
        self.task = task
        self.help = help or "No help available for this command"
        self.allow_untrusted = allow_untrusted
        self.allow_homeservers = allow_homeservers or []
        self.allow_users = allow_users or []


async def process_command(
    client: AsyncClient,
    input: str,
    room: MatrixRoom,
    event: RoomMessageText,
) -> None:
    """Process the command."""

    config = appconfig.get()

    # Split the command into arguments, and always user lower case for the command name
    # (No reason to allow commands like 'host' and 'Host' to be different, right?)
    cmdsplit = shlex.split(input)
    cmdsplit[0] = cmdsplit[0].lower()
    cmdname = cmdsplit[0]

    LOGGER.debug(f"commands :: Command.process: {input} {room}")

    command = config.commands.get(cmdname)
    if not command:
        await send_text_to_room(
            client,
            room.room_id,
            f"Unknown command `{input}`. Try the `help` command for more information.",
            format=MessageFormat.MARKDOWN,
        )
        return

    sender = Mxid.fromstr(event.sender)
    if sender.mxid in config.trusted_users:
        LOGGER.debug(
            f"Processing command {input} from sender {sender.mxid} because sender is in the list of trusted users"
        )
    elif command.allow_untrusted:
        LOGGER.debug(
            f"Processing command {input} from sender {sender.mxid} because the invoked command {command.name} allows untrusted invocation"
        )
    elif command.allow_homeservers and sender.homeserver in command.allow_homeservers:
        LOGGER.debug(
            f"Processing command {input} from sender {sender.mxid} because the invoked command {command.name} allows users from homeserver {sender.homeserver}"
        )
    elif command.allow_users and sender.mxid in command.allow_users:
        LOGGER.debug(
            f"Processing command {input} from sender {sender.mxid} because the invoked command {command.name} allows that user explicitly"
        )
    else:
        LOGGER.critical(
            f"Refusing to process command {input} from sender {sender.mxid} because the invoked command {command.name} does not permit execution by this user"
        )
        await send_text_to_room(
            client,
            room.room_id,
            f"Not authorized: User {sender.mxid} is not authorized to run the command {command.name}",
            format=MessageFormat.NATURAL,
            split=None,
        )
        return

    taskctx = TaskMessageContext(event.sender, room.room_id)
    try:
        result = command.task.taskfunc(cmdsplit[1:], taskctx)
        message = result.output
        format = result.format
        split = result.split
        LOGGER.debug(
            f"Task {command.task.name} completed successfully; replying with output:\n{message}"
        )
    except BaseException as exc:
        message = f"Error:\n{exc}\n{traceback.format_exc()}"
        # Always format errors in a code block
        format = MessageFormat.CODE
        split = None
        LOGGER.debug(
            f"Task {command.task.name} encountered an error; replying with error:\n{message}"
        )

    await send_text_to_room(
        client,
        room.room_id,
        message,
        format=format,
        split=split,
    )

Functions

async def process_command(client: nio.client.async_client.AsyncClient, input: str, room: nio.rooms.MatrixRoom, event: nio.events.room_events.RoomMessageText) ‑> None

Process the command.

Expand source code
async def process_command(
    client: AsyncClient,
    input: str,
    room: MatrixRoom,
    event: RoomMessageText,
) -> None:
    """Process the command."""

    config = appconfig.get()

    # Split the command into arguments, and always user lower case for the command name
    # (No reason to allow commands like 'host' and 'Host' to be different, right?)
    cmdsplit = shlex.split(input)
    cmdsplit[0] = cmdsplit[0].lower()
    cmdname = cmdsplit[0]

    LOGGER.debug(f"commands :: Command.process: {input} {room}")

    command = config.commands.get(cmdname)
    if not command:
        await send_text_to_room(
            client,
            room.room_id,
            f"Unknown command `{input}`. Try the `help` command for more information.",
            format=MessageFormat.MARKDOWN,
        )
        return

    sender = Mxid.fromstr(event.sender)
    if sender.mxid in config.trusted_users:
        LOGGER.debug(
            f"Processing command {input} from sender {sender.mxid} because sender is in the list of trusted users"
        )
    elif command.allow_untrusted:
        LOGGER.debug(
            f"Processing command {input} from sender {sender.mxid} because the invoked command {command.name} allows untrusted invocation"
        )
    elif command.allow_homeservers and sender.homeserver in command.allow_homeservers:
        LOGGER.debug(
            f"Processing command {input} from sender {sender.mxid} because the invoked command {command.name} allows users from homeserver {sender.homeserver}"
        )
    elif command.allow_users and sender.mxid in command.allow_users:
        LOGGER.debug(
            f"Processing command {input} from sender {sender.mxid} because the invoked command {command.name} allows that user explicitly"
        )
    else:
        LOGGER.critical(
            f"Refusing to process command {input} from sender {sender.mxid} because the invoked command {command.name} does not permit execution by this user"
        )
        await send_text_to_room(
            client,
            room.room_id,
            f"Not authorized: User {sender.mxid} is not authorized to run the command {command.name}",
            format=MessageFormat.NATURAL,
            split=None,
        )
        return

    taskctx = TaskMessageContext(event.sender, room.room_id)
    try:
        result = command.task.taskfunc(cmdsplit[1:], taskctx)
        message = result.output
        format = result.format
        split = result.split
        LOGGER.debug(
            f"Task {command.task.name} completed successfully; replying with output:\n{message}"
        )
    except BaseException as exc:
        message = f"Error:\n{exc}\n{traceback.format_exc()}"
        # Always format errors in a code block
        format = MessageFormat.CODE
        split = None
        LOGGER.debug(
            f"Task {command.task.name} encountered an error; replying with error:\n{message}"
        )

    await send_text_to_room(
        client,
        room.room_id,
        message,
        format=format,
        split=split,
    )

Classes

class Command (name: str, task: Task, help: Optional[str] = None, allow_untrusted: bool = False, allow_homeservers: Optional[List[str]] = None, allow_users: Optional[List[str]] = None)

A command that a bot can perform

name: The word that activates this command task: The task to perform help: Help text for the command allow_untrusted: Allow any user to run this command? allow_homeservers: Allow any user from these homeservers to run this command? allow_users: Allow any user in this list to run this command?

Expand source code
class Command(object):
    """A command that a bot can perform

    name: The word that activates this command
    task: The task to perform
    help: Help text for the command
    allow_untrusted: Allow any user to run this command?
    allow_homeservers: Allow any user from these homeservers to run this command?
    allow_users: Allow any user in this list to run this command?
    """

    def __init__(
        self,
        name: str,
        task: Task,
        # This isn't a NamedTuple because I want to be able to pass help=None
        # and have it use fallback text. Sigh.
        help: Optional[str] = None,
        allow_untrusted: bool = False,
        allow_homeservers: Optional[List[str]] = None,
        allow_users: Optional[List[str]] = None,
    ):
        self.name = name
        self.task = task
        self.help = help or "No help available for this command"
        self.allow_untrusted = allow_untrusted
        self.allow_homeservers = allow_homeservers or []
        self.allow_users = allow_users or []