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.
Start the Telegram gateway
Section titled “Start the Telegram gateway”-
Configure
.envSet these in
.envat the workspace root:BUB_TELEGRAM_TOKEN=123456789:your-telegram-bot-token BUB_TELEGRAM_ALLOW_USERS=123456789Set
BUB_TELEGRAM_ALLOW_USERSto your numeric ID; you can look it up with @userinfobot. -
Start the gateway
Run it in the workspace that contains the
.envfile:bub gatewayExample log from a successful startup:
telegram.start allow_users_count=1 allow_chats_count=0 proxy_enabled=False telegram.start polling -
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.
Configure Telegram
Section titled “Configure Telegram”Restrict access
Section titled “Restrict access”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.
Configure group chats
Section titled “Configure group chats”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.
Configure replies
Section titled “Configure replies”| 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.
Merge consecutive messages
Section titled “Merge consecutive messages”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
Connect other platforms
Section titled “Connect other platforms”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 |
bub-wechat |
|
| WeCom | wecom |
bub-wecom |
| Feishu | feishu |
bub-feishu |
| DingTalk | dingtalk |
bub-dingtalk |
qq |
bub-qq |
See each plugin’s documentation for channel-specific configuration.
Custom channels
Section titled “Custom channels”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_receiveevent handler for every inbound message. - Use the
<channel>:<chat_id>format forsession_idso sessions from different platforms do not collide. - Return
Falsefromenabledby default when the configuration is incomplete; the gateway skips that channel. - Return
Truefromneeds_debouncewhen 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.
Run as a daemon
Section titled “Run as a daemon”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.
Troubleshooting
Section titled “Troubleshooting”| 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 |