Skip to content

Gateway

Bub was originally inspired by always-on agents such as OpenClaw and nanobot. In addition to terminal interaction, Bub provides a gateway.

Running the bub gateway subcommand starts Bub as a gateway and runs all enabled channels.

Bub currently ships with only the Telegram channel. Other platforms require community plugins, so this document focuses on configuring and using the Telegram channel.

  1. Configure .env

    Set these in .env at the workspace root:

    BUB_TELEGRAM_TOKEN=123456789:your-telegram-bot-token
    BUB_TELEGRAM_ALLOW_USERS=123456789

    Set BUB_TELEGRAM_ALLOW_USERS to your numeric ID; you can look it up with @userinfobot.

  2. Start the gateway

    Run it in the workspace that contains the .env file:

    bub gateway

    Example log from a successful startup:

    telegram.start allow_users_count=1 allow_chats_count=0 proxy_enabled=False
    telegram.start polling
  3. Verify a private chat

    Send the bot a private message and wait a moment to see whether it replies. If there is no reply, see Configure replies and Troubleshooting.

Bub requires at least one enabled channel to run as a gateway. The Telegram channel is enabled whenever BUB_TELEGRAM_TOKEN is non-empty.

Set BUB_TELEGRAM_ALLOW_USERS to restrict which users can access the bot, and BUB_TELEGRAM_ALLOW_CHATS to restrict which chats can access it.

Separate multiple values with commas; usernames are case-sensitive:

BUB_TELEGRAM_ALLOW_USERS=123456789,your_username
BUB_TELEGRAM_ALLOW_CHATS=-1001234567890,-1009876543210
Allowlist configuration Pass condition When it fails
BUB_TELEGRAM_ALLOW_USERS Numeric ID or username matches The user receives Access denied.
BUB_TELEGRAM_ALLOW_CHATS Chat ID matches The message is dropped silently

When both are set, both must pass. When both allowlists are empty, anyone can use the bot; set at least one before deploying it publicly.

Turn off Privacy Mode in BotFather. Otherwise the bot only sees commands and cannot read ordinary group messages.

Message source or type Received when
Non-command messages in a private chat Always
Text messages in a group chat Mentions @<bot_username>, contains bub, or replies to the bot
Images and files with text captions in a group chat Same as text messages
Non-text messages without captions in a group chat Only when replying to the bot
/bub <text> The /bub prefix is removed and the rest becomes the prompt

Group messages that do not meet these conditions are ignored. If the session is still within BUB_ACTIVE_TIME_WINDOW, those messages may also join the current batch. bub uses substring matching, so bubble also matches.

Input output_channel How it is sent
Ordinary message "null" The built-in telegram skill sends replies, progress, and edits
Comma command "telegram" Tool output goes to the originating chat_id

The model output for an ordinary message is not sent to Telegram automatically; the model must call the telegram skill, or the user receives no reply. Typing is renewed every 4 seconds and only means the Turn has not finished.

When a comma command’s output is JSON with a message field, only that field is sent; other non-empty output is sent as-is, and empty output is dropped.

Telegram merges consecutive messages sent within a short period into one Turn. You can adjust the merging strategy with the following environment variables:

Variable Default Description
BUB_DEBOUNCE_SECONDS 1.0 Short wait that is reset by each active message
BUB_MAX_WAIT_SECONDS 10.0 Upper bound on waiting for inactive follow-up messages
BUB_ACTIVE_TIME_WINDOW 60.0 Time window in which inactive messages can join the batch

Messages explicitly addressed to the bot in private and group chats are active; subsequent unmentioned group messages in the same session are inactive. Both are staged per session and merged with newlines once the timer ends. Input that starts with the command prefix (, by default) does not participate in merging and is dispatched immediately.

You can configure a proxy for the Telegram channel:

BUB_TELEGRAM_PROXY=http://127.0.0.1:7890
# socks5://<user>:<YOUR_PROXY_PASSWORD>@host:port is also supported

Although Bub ships with only the Telegram channel, the community provides a variety of plugins for connecting other platforms.

After installing a plugin for another platform, restart the gateway and use bub hooks to confirm the corresponding name appears under provide_channels. At startup, channels without configured credentials are skipped.

Platform Channel name Plugin
Discord discord bub-discord
Slack slack bub-slack
WeChat wechat bub-wechat
WeCom wecom bub-wecom
Feishu feishu bub-feishu
DingTalk dingtalk bub-dingtalk
QQ qq bub-qq

See each plugin’s documentation for channel-specific configuration.

If no existing plugin is available, you can implement your own channel in Python: subclass Channel, set the name attribute, and implement the start, stop, and send methods. Here is a minimal example:

import asyncio

from bub.channels.base import Channel
from bub.channels.message import ChannelMessage


class MyChannel(Channel):
    name = "myapp"  # Channel name

    def __init__(self, on_receive, token: str) -> None:
        self._on_receive = on_receive
        self._token = token
        self._listener: asyncio.Task | None = None

    @property
    def enabled(self) -> bool:
        return bool(self._token)

    async def start(self, stop_event: asyncio.Event) -> None:
        # start must not block the main loop, or the gateway cannot receive subsequent messages.
        self._listener = asyncio.create_task(self._listen(stop_event))

    async def stop(self) -> None:
        if self._listener is not None:
            self._listener.cancel()

    async def _listen(self, stop_event: asyncio.Event) -> None:
        while not stop_event.is_set():
            chat_id, text = await self._next_message()  # Platform message
            await self._on_receive(
                ChannelMessage(
                    session_id=f"myapp:{chat_id}",
                    channel=self.name,
                    chat_id=chat_id,
                    content=text,
                )
            )

    async def send(self, message: ChannelMessage) -> None:
        await self._post(message.chat_id, message.content)

Note:

  • Call the framework-provided on_receive event handler for every inbound message.
  • Use the <channel>:<chat_id> format for session_id so sessions from different platforms do not collide.
  • Return False from enabled by default when the configuration is incomplete; the gateway skips that channel.
  • Return True from needs_debounce when consecutive short messages need to be merged.

When finished, expose the channel through the plugin’s entry-point module with provide_channels:

import os

from bub import hookimpl

# From the previous snippet
# class MyChannel(Channel):
#     ...


@hookimpl
def provide_channels(message_handler):
    return [MyChannel(on_receive=message_handler, token=os.environ.get("MYAPP_TOKEN", ""))]

After installing the plugin, run the corresponding channel with bub gateway --enable-channel myapp.

To keep Bub running as a system service, configure it with the following commands:

bub gateway --install

# Uninstall
# bub gateway --uninstall

Behavior varies slightly by system:

System Behavior
Linux systemd user unit bub-gateway.service; to keep it running after logout, run loginctl enable-linger $USER
Windows Scheduled task for the current user; starts at login and restarts on failure
macOS --install is not supported; use launchd, tmux, or container deployment

Installation records the current Python interpreter, workspace, and explicit --enable-channel values. Reinstall after changing the environment or workspace.

Symptom Possible cause Fix
No channels are enabled No usable channel Check the token and the .env location; inspect plugins with bub hooks
No telegram.start in the log Telegram token is empty Set the token and confirm it reaches the bub process
Private chat receives Access denied. The user allowlist rejected the user Add the numeric ID or username to BUB_TELEGRAM_ALLOW_USERS
No response in a private chat The chat allowlist rejected the chat Add the chat ID to BUB_TELEGRAM_ALLOW_CHATS
No response in a group chat Privacy Mode is on, or receive conditions are not met Turn off Privacy Mode; for testing, @-mention the bot directly
Typing forever with no reply The model did not call the telegram skill See reply rules
409 Conflict Multiple long-polling processes use the same token Stop the duplicate instances
No offline messages after a restart Backlog updates are discarded at startup Expected behavior