# 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'
echo 'hello' | ./rho-telegram send <name>
```

`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.
