# rho-telegram
`rho-telegram` is a standalone Rust CLI in this repo for connecting a Telegram bot to your terminal. It stores one config file per named bot profile, can send one-off messages, can listen for inbound bot messages, can run an interactive two-way terminal chat, and can act as an approval middleware adapter on top of the core `rho` approval CLI.
See also: [approval-middleware.md](/Users/madhavajay/dev/rho/main/docs/approval-middleware.md)
## Files
- Wrapper script: [rho-telegram](/Users/madhavajay/dev/rho/main/rho-telegram)
- Rust crate: [telegram-bridge/Cargo.toml](/Users/madhavajay/dev/rho/main/telegram-bridge/Cargo.toml)
- Main implementation: [telegram-bridge/src/main.rs](/Users/madhavajay/dev/rho/main/telegram-bridge/src/main.rs)
- Per-user profile config: `users/<user>/telegram/<profile>.json`
The config file is gitignored because it contains the bot token.
## How Telegram affects the design
Telegram bots cannot start a private conversation with a user on their own. A user must message the bot first. That is why `rho-telegram` has a setup flow where the bot must be contacted once before inbound message handling and exact chat binding can be completed.
## Configuration flow
1. Create a Telegram bot with BotFather and copy the bot token.
2. Initialize a named profile:
```bash
./rho-telegram init <name> --token '<telegram-bot-token>'
```
Example:
```bash
./rho-telegram init madhava_syft_test --token '123456:ABCDEF...'
```
This writes, by default:
```text
users/<profile>/telegram/<profile>.json
```
If you pass `--user`, it writes:
```text
users/<user>/telegram/<profile>.json
```
Initial config shape:
```json
{
"name": "madhava_syft_test",
"token": "123456:ABCDEF...",
"chat_id": null,
"allowed_user_id": null,
"allowed_username": null,
"last_seen_chat_id": null,
"last_seen_user_id": null,
"last_seen_username": null,
"last_update_id": null
}
```
## Binding a bot to one Telegram user
The tool can be bound to a specific chat and user identity so that inbound messages are only accepted from that sender.
There are two ways to bind:
1. Manual bind:
```bash
./rho-telegram bind <name> --chat-id <chat_id> --user-id <telegram_user_id>
```
or:
```bash
./rho-telegram bind <name> --chat-id <chat_id> --username <telegram_username>
```
2. Discover first, then bind:
```bash
./rho-telegram listen <name>
```
After the user messages the bot, `listen` prints a line like:
```text
[update:469198813 msg:496 chat:42232674 user:42232674 at:1775109906] @madhavajay> wazzup?
```
That gives you the `chat` and `user` values for an exact bind.
After binding, the config stores:
- `chat_id`: the allowed Telegram chat
- `allowed_user_id`: the allowed Telegram user id, if bound numerically
- `allowed_username`: the allowed Telegram username, if bound by username
If a message arrives from some other user or chat, the tool ignores it for terminal output.
## Commands
### `init`
Creates or overwrites the profile config.
```bash
./rho-telegram init <name> --token '<telegram-bot-token>'
./rho-telegram init <name> --user <user> --token '<telegram-bot-token>'
./rho-telegram init <name> --token '<telegram-bot-token>' --chat-id <chat_id>
```
Use `--chat-id` only if you already know the destination chat id.
### `bind`
Stores the allowed chat/user identity in the config.
```bash
./rho-telegram bind <name> --chat-id <chat_id> --user-id <telegram_user_id>
./rho-telegram bind <name> --user <user> --chat-id <chat_id> --username <telegram_username>
```
You can also run `bind` after `listen` if the config has recently seen a sender and chat, but passing the values explicitly is more reliable.
### `send`
Sends one outbound message to the configured chat.
```bash
./rho-telegram send <name> --text 'hello'
./rho-telegram send <name> --user <user> --text 'hello'
`send` needs a configured `chat_id`. Once that is saved, `send` works without `listen`.
### `listen`
Long-polls Telegram for new messages and prints inbound text messages that match the configured binding.
```bash
./rho-telegram listen <name>
./rho-telegram listen <name> --user <user> --once
./rho-telegram listen <name> --timeout-seconds 30
```
What `listen` does:
- Fetches bot updates from Telegram
- Saves `last_update_id` so the same update is not processed repeatedly
- Saves the most recently seen sender/chat in `last_seen_*`
- Prints matching inbound messages to stdout
Use `listen` when you want inbound visibility, or when you are discovering the chat/user details needed for binding.
### `chat`
Runs a simple two-way terminal session.
```bash
./rho-telegram chat <name>
./rho-telegram chat <name> --user <user>
```
What `chat` does:
- Reads lines from your terminal and sends them to the configured chat
- Simultaneously polls Telegram for inbound messages
- Prints inbound messages that match the binding
Use `chat` when you want one terminal command for both directions.
### `show`
Prints the current config with the token redacted.
```bash
./rho-telegram show <name>
./rho-telegram show <name> --user <user>
```
### `send-approval-request`
Sends a rich approval request summary for a request id.
```bash
./rho-telegram send-approval-request <name> \
--user test-agent1 \
--shared-root ./sandbox/two-console-demo/shared \
--request-id req-001
```
What it does:
- loads request details through `./rho request show ...`
- sends a detailed summary message
- attaches inline `Approve`, `Deny`, and `Show Details` buttons
### `approval-listen`
Listens for approval actions and maps them back into the local `rho` CLI.
```bash
./rho-telegram approval-listen <name> \
--user test-agent1 \
--shared-root ./sandbox/two-console-demo/shared \
--once \
--timeout-seconds 90
```
Supported inputs:
- `/approve <request-id>`
- `/deny <request-id>`
- `/show <request-id>`
- `/approve_last`
- `/deny_last`
- inline button presses for `Approve`, `Deny`, and `Show Details`
## Typical usage patterns
### Outbound only
If `chat_id` is already configured:
```bash
./rho-telegram send <name> --text 'hello'
```
You do not need `listen` for that.
### Inbound only
```bash
./rho-telegram listen <name>
```
### Two-way session
```bash
./rho-telegram chat <name>
```
### First-time setup with user binding
```bash
./rho-telegram init <name> --token '<telegram-bot-token>'
./rho-telegram listen <name>
```
Then message the bot from Telegram, note the printed `chat` and `user`, stop the listener, and bind:
```bash
./rho-telegram bind <name> --chat-id <chat_id> --user-id <telegram_user_id>
```
Or:
```bash
./rho-telegram bind <name> --chat-id <chat_id> --username <telegram_username>
```
After that:
```bash
./rho-telegram send <name> --text 'hello'
./rho-telegram chat <name>
```
### Approval middleware flow
With a bound bot profile:
```bash
./rho-telegram send-approval-request <name> \
--user test-agent1 \
--shared-root ./sandbox/two-console-demo/shared \
--request-id req-001
./rho-telegram approval-listen <name> \
--user test-agent1 \
--shared-root ./sandbox/two-console-demo/shared \
--once \
--timeout-seconds 90
```
The bridge delegates the final state change to the core `rho` approval commands. Telegram is only one transport adapter.
## Security notes
- The bot token is stored in plaintext in `users/<user>/telegram/<profile>.json`.
- When `RHO_TELEGRAM_ROOT` is set, the config is written under that root instead. The live sandbox test uses this so bot state stays inside the sandbox.
- The file is gitignored, but it is still a local secret and should be treated as such.
- If the token is pasted into terminal history or chat history, rotate it in BotFather if needed.