Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ allowed_channels = ["1234567890"] # ↑ omitted + non-empty list → auto-
# once another bot has posted in the thread
# max_bot_turns = 20 # soft cap on consecutive bot turns per thread (human msg resets)

# [gateway]
# url = "ws://openab-gateway:8080/ws" # WebSocket URL of the custom gateway
# platform = "line" # "telegram" (default) | "line"
# token = "${GATEWAY_TOKEN}" # shared token for WebSocket auth (optional but recommended)
# bot_username = "my_bot" # for @mention gating in groups
# allow_all_channels = true # true = allow all channels; false = only allowed_channels
# # omitted = auto-detect from list (non-empty → false, empty → true)
# allowed_channels = ["C1234"] # only checked when allow_all_channels = false
# allow_all_users = true # true = any user; false = only allowed_users
# # omitted = auto-detect from list (non-empty → false, empty → true)
# allowed_users = ["U5678"] # only checked when allow_all_users = false

[agent]
command = "kiro-cli"
args = ["acp", "--trust-all-tools"]
Expand Down
17 changes: 17 additions & 0 deletions docs/config-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,23 @@ Slack adapter using Socket Mode. Requires both a Bot User OAuth Token and an App

---

## `[gateway]`

Custom Gateway adapter for platforms like Telegram and LINE. Connects to the gateway via WebSocket.

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `url` | string | *required* | WebSocket URL of the gateway (e.g. `ws://openab-gateway:8080/ws`). |
| `platform` | string | `"telegram"` | Platform name for session key namespacing (e.g. `"telegram"`, `"line"`). |
| `token` | string | — | Shared token for WebSocket authentication (optional but recommended). |
| `bot_username` | string | — | Bot username for @mention gating in groups. |
| `allow_all_channels` | bool \| omit | auto-detect | `true` = all channels; `false` = only `allowed_channels`. Omitted = inferred from list (non-empty → false, empty → true). |
| `allowed_channels` | string[] | `[]` | Chat/group IDs to allow. Only checked when `allow_all_channels` resolves to false. |
| `allow_all_users` | bool \| omit | auto-detect | `true` = any user; `false` = only `allowed_users`. Omitted = inferred from list. |
| `allowed_users` | string[] | `[]` | User IDs to allow. Only checked when `allow_all_users` resolves to false. |

---

## `[agent]`

The AI agent subprocess that OpenAB spawns to handle messages via ACP.
Expand Down
4 changes: 4 additions & 0 deletions docs/line.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,17 @@ In the LINE Developers Console → **Messaging API** tab:
[gateway]
url = "ws://openab-gateway:8080/ws"
platform = "line"
# allowed_users = ["U1234567890abcdef"] # restrict to specific LINE user IDs
# allowed_channels = ["C1234567890abcdef"] # restrict to specific chat/group IDs

[agent]
command = "kiro-cli"
args = ["acp", "--trust-all-tools"]
working_dir = "/home/agent"
```

> **Tip:** To find a LINE user ID, check the gateway logs — the sender ID is logged for each incoming message. By default all users and channels are allowed. Setting `allowed_users` or `allowed_channels` automatically restricts access to only those listed.

## 6. Add the Bot as Friend

In the LINE Developers Console → **Messaging API** tab → scan the QR code with your LINE app, or search for the bot by its LINE ID.
Expand Down
4 changes: 4 additions & 0 deletions docs/telegram.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ url = "ws://openab-gateway:8080/ws"
platform = "telegram"
token = "${GATEWAY_WS_TOKEN}"
bot_username = "your_bot_username"
# allowed_users = ["123456789"] # restrict to specific Telegram user IDs
# allowed_channels = ["-1001234567890"] # restrict to specific chat/group IDs

[agent]
command = "kiro-cli"
Expand All @@ -109,6 +111,8 @@ working_dir = "/home/agent"
| `platform` | No | Session key namespace (default: `telegram`) |
| `token` | No | Shared WS auth token (recommended) |
| `bot_username` | No | Bot username for @mention gating in groups |
| `allowed_users` | No | Restrict to listed user IDs (empty = allow all) |
| `allowed_channels` | No | Restrict to listed chat IDs (empty = allow all) |

## 4. Set the Telegram Webhook

Expand Down
71 changes: 71 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,16 @@ pub struct GatewayConfig {
pub token: Option<String>,
/// Bot username for @mention gating in groups (e.g. "my_bot")
pub bot_username: Option<String>,
/// Explicit flag: true = allow all channels, false = check allowed_channels list.
/// When not set, auto-detected: non-empty list → false, empty list → true.
pub allow_all_channels: Option<bool>,
/// Explicit flag: true = allow all users, false = check allowed_users list.
/// When not set, auto-detected: non-empty list → false, empty list → true.
pub allow_all_users: Option<bool>,
#[serde(default)]
pub allowed_channels: Vec<String>,
#[serde(default)]
pub allowed_users: Vec<String>,
}

fn default_gateway_platform() -> String {
Expand Down Expand Up @@ -487,4 +497,65 @@ command = "echo"
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("failed to fetch remote config"));
}

#[test]
fn parse_gateway_config_defaults() {
let toml = r#"
[gateway]
url = "ws://gw:8080/ws"

[agent]
command = "echo"
"#;
let cfg = parse_config(toml, "test").unwrap();
let gw = cfg.gateway.unwrap();
assert_eq!(gw.url, "ws://gw:8080/ws");
assert_eq!(gw.platform, "telegram");
assert!(gw.allowed_users.is_empty());
assert!(gw.allowed_channels.is_empty());
assert!(gw.allow_all_users.is_none());
assert!(gw.allow_all_channels.is_none());
// resolve_allow_all: empty lists → allow all
assert!(resolve_allow_all(gw.allow_all_users, &gw.allowed_users));
assert!(resolve_allow_all(gw.allow_all_channels, &gw.allowed_channels));
}

#[test]
fn parse_gateway_config_with_allowlists() {
let toml = r#"
[gateway]
url = "ws://gw:8080/ws"
platform = "line"
allowed_users = ["U1", "U2"]
allowed_channels = ["C1"]

[agent]
command = "echo"
"#;
let cfg = parse_config(toml, "test").unwrap();
let gw = cfg.gateway.unwrap();
assert_eq!(gw.platform, "line");
assert_eq!(gw.allowed_users, vec!["U1", "U2"]);
assert_eq!(gw.allowed_channels, vec!["C1"]);
// resolve_allow_all: non-empty lists → restricted
assert!(!resolve_allow_all(gw.allow_all_users, &gw.allowed_users));
assert!(!resolve_allow_all(gw.allow_all_channels, &gw.allowed_channels));
}

#[test]
fn parse_gateway_config_explicit_allow_all_overrides_list() {
let toml = r#"
[gateway]
url = "ws://gw:8080/ws"
allow_all_users = true
allowed_users = ["U1"]

[agent]
command = "echo"
"#;
let cfg = parse_config(toml, "test").unwrap();
let gw = cfg.gateway.unwrap();
// explicit flag overrides non-empty list
assert!(resolve_allow_all(gw.allow_all_users, &gw.allowed_users));
}
}
41 changes: 34 additions & 7 deletions src/gateway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,19 +258,34 @@ impl ChatAdapter for GatewayAdapter {

// --- Run the gateway adapter (connects to gateway WS, routes events to AdapterRouter) ---

/// Resolved gateway configuration passed to the adapter at startup.
pub struct GatewayParams {
pub url: String,
pub platform: String,
pub token: Option<String>,
pub bot_username: Option<String>,
pub allow_all_channels: bool,
pub allowed_channels: Vec<String>,
pub allow_all_users: bool,
pub allowed_users: Vec<String>,
}

pub async fn run_gateway_adapter(
gateway_url: String,
platform_name: String,
ws_token: Option<String>,
bot_username: Option<String>,
params: GatewayParams,
router: Arc<AdapterRouter>,
mut shutdown_rx: tokio::sync::watch::Receiver<bool>,
) -> Result<()> {
// Leak the platform name for 'static lifetime — one allocation per adapter lifetime
let platform: &'static str = Box::leak(platform_name.into_boxed_str());
let platform: &'static str = Box::leak(params.platform.into_boxed_str());

// Append auth token as query param if configured
let connect_url = match &ws_token {
let gateway_url = params.url;
let bot_username = params.bot_username;
let allow_all_channels = params.allow_all_channels;
let allowed_channels = params.allowed_channels;
let allow_all_users = params.allow_all_users;
let allowed_users = params.allowed_users;

let connect_url = match &params.token {
Some(token) => {
let sep = if gateway_url.contains('?') { "&" } else { "?" };
format!("{gateway_url}{sep}token={token}")
Expand Down Expand Up @@ -338,6 +353,18 @@ pub async fn run_gateway_adapter(
continue; // skip bot messages
}

// Channel allowlist gate
if !allow_all_channels && !allowed_channels.contains(&event.channel.id) {
info!(channel = %event.channel.id, "gateway: channel not in allowed_channels, skipping");
continue;
}

// User allowlist gate
if !allow_all_users && !allowed_users.contains(&event.sender.id) {
info!(sender = %event.sender.id, "gateway: user not in allowed_users, skipping");
continue;
}

// @mention gating: in groups, only respond if bot is mentioned
// DMs (private) and thread replies always pass through
let is_group = event.channel.channel_type == "group"
Expand Down
12 changes: 11 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,18 @@ async fn main() -> anyhow::Result<()> {
let router = router.clone();
let shutdown_rx = shutdown_rx.clone();
info!(url = %gw_cfg.url, "starting gateway adapter");
let params = gateway::GatewayParams {
url: gw_cfg.url,
platform: gw_cfg.platform,
token: gw_cfg.token,
bot_username: gw_cfg.bot_username,
allow_all_channels: config::resolve_allow_all(gw_cfg.allow_all_channels, &gw_cfg.allowed_channels),
allowed_channels: gw_cfg.allowed_channels,
allow_all_users: config::resolve_allow_all(gw_cfg.allow_all_users, &gw_cfg.allowed_users),
allowed_users: gw_cfg.allowed_users,
};
Some(tokio::spawn(async move {
if let Err(e) = gateway::run_gateway_adapter(gw_cfg.url, gw_cfg.platform, gw_cfg.token, gw_cfg.bot_username, router, shutdown_rx).await {
if let Err(e) = gateway::run_gateway_adapter(params, router, shutdown_rx).await {
error!("gateway adapter error: {e}");
}
}))
Expand Down
Loading