async-zulip-bot-sdk

BaseBot API

BaseBot is the base class for all Zulip bots. It handles message dispatch, command parsing, and replies.

Class: BaseBot

from bot_sdk import BaseBot

Inherit and implement

from bot_sdk import BaseBot, Message

class MyBot(BaseBot):
    async def on_message(self, message: Message) -> None:
        await self.send_reply(message, "Received!")

Configuration (breaking change)

Built-ins registered by default (still enabled unless you disable auto_help_command in YAML):

Permission enforcement: if a CommandSpec has min_level, BaseBot checks it before dispatch. Early permission denial is returned before argument parsing, so users see “Permission denied” rather than “Missing argument”.

Built-in commands use i18n, so descriptions and success/error messages are localized based on the bot’s configured language.

Instance attributes

Lifecycle hooks

Event handling

Commands

Internationalization (i18n)

BaseBot automatically initializes an i18n system during post_init():

Example:

class MyBot(BaseBot):
    def register_commands(self):
        self.command_parser.register_spec(
            CommandSpec(
                name="greet",
                description=self.tr("Greet the user"),  # Translatable at registration time
                handler=self.handle_greet,
            )
        )
    
    async def handle_greet(self, inv, message, bot):
        # User-facing strings are translated
        await self.send_reply(message, self.tr("Hello, {name}!", name=message.sender_full_name))

See docs/i18n.md (if available) for detailed i18n setup and per-bot translation files.

Reply helper

Examples

Simple bot

from bot_sdk import BaseBot, Message, run_bot

class SimpleBot(BaseBot):
    async def on_message(self, message: Message) -> None:
        await self.send_reply(message, f"Echo: {message.content}")

if __name__ == "__main__":
    run_bot(SimpleBot)

Command bot

from bot_sdk import BaseBot, Message, CommandSpec, CommandArgument

class TodoBot(BaseBot):
    def __init__(self, client):
        super().__init__(client)
        self.todos = []
        self.command_parser.register_spec(
            CommandSpec(
                name="add",
                description="Add todo",
                args=[CommandArgument(name="task", type=str, multiple=True)],
                handler=self.handle_add,
            )
        )
        self.command_parser.register_spec(
            CommandSpec(name="list", description="List todos", handler=self.handle_list)
        )
        self.command_parser.register_spec(
            CommandSpec(
                name="done",
                description="Mark done",
                args=[CommandArgument(name="index", type=int, required=True)],
                handler=self.handle_done,
            )
        )

    async def handle_add(self, invocation, message, bot):
        task = " ".join(invocation.args["task"])
        self.todos.append(task)
        await self.send_reply(message, f"✅ Added: {task}")

    async def handle_list(self, invocation, message, bot):
        if not self.todos:
            await self.send_reply(message, "No todos")
            return
        lines = [f"{i+1}. {task}" for i, task in enumerate(self.todos)]
        await self.send_reply(message, "\n".join(lines))

    async def handle_done(self, invocation, message, bot):
        idx = invocation.args["index"] - 1
        if 0 <= idx < len(self.todos):
            task = self.todos.pop(idx)
            await self.send_reply(message, f"✅ Done: {task}")
        else:
            await self.send_reply(message, "❌ Invalid index")

    async def on_message(self, message: Message) -> None:
        await self.send_reply(message, "Use /help to see commands")

Best practices