diff --git a/channels-src/telegram/src/lib.rs b/channels-src/telegram/src/lib.rs index c6d5d312..984385bc 100644 --- a/channels-src/telegram/src/lib.rs +++ b/channels-src/telegram/src/lib.rs @@ -157,6 +157,9 @@ struct SentMessage { /// Workspace path for storing polling state. const POLLING_STATE_PATH: &str = "state/last_update_id"; +/// Workspace path for persisting owner_id across WASM callbacks. +const OWNER_ID_PATH: &str = "state/owner_id"; + // ============================================================================ // Channel Metadata // ============================================================================ @@ -188,6 +191,11 @@ struct TelegramConfig { #[serde(default)] bot_username: Option, + /// Telegram user ID of the bot owner. When set, only messages from this + /// user are processed. All others are silently dropped. + #[serde(default)] + owner_id: Option, + /// Whether to respond to all group messages (not just mentions). #[serde(default)] respond_to_all_group_messages: bool, @@ -228,6 +236,27 @@ impl Guest for TelegramChannel { ); } + // Persist owner_id so subsequent callbacks (on_http_request, on_poll) can read it + if let Some(owner_id) = config.owner_id { + if let Err(e) = channel_host::workspace_write(OWNER_ID_PATH, &owner_id.to_string()) { + channel_host::log( + channel_host::LogLevel::Error, + &format!("Failed to persist owner_id: {}", e), + ); + } + channel_host::log( + channel_host::LogLevel::Info, + &format!("Owner restriction enabled: user {}", owner_id), + ); + } else { + // Clear any stale owner_id from a previous config + let _ = channel_host::workspace_write(OWNER_ID_PATH, ""); + channel_host::log( + channel_host::LogLevel::Warn, + "No owner_id configured, bot is open to all users", + ); + } + // Mode is determined by whether the host injected a tunnel_url // If tunnel is configured, use webhooks. Otherwise, use polling. let webhook_mode = config.tunnel_url.is_some(); @@ -584,14 +613,15 @@ fn delete_webhook() -> Result<(), String> { return Err(format!("HTTP {}: {}", response.status, body_str)); } - let api_response: TelegramApiResponse = - serde_json::from_slice(&response.body) - .map_err(|e| format!("Failed to parse response: {}", e))?; + let api_response: TelegramApiResponse = serde_json::from_slice(&response.body) + .map_err(|e| format!("Failed to parse response: {}", e))?; if !api_response.ok { return Err(format!( "Telegram API error: {}", - api_response.description.unwrap_or_else(|| "unknown".to_string()) + api_response + .description + .unwrap_or_else(|| "unknown".to_string()) )); } @@ -622,7 +652,8 @@ fn register_webhook(tunnel_url: &str, webhook_secret: Option<&str>) -> Result<() body["secret_token"] = serde_json::Value::String(secret.to_string()); } - let body_bytes = serde_json::to_vec(&body).map_err(|e| format!("Failed to serialize body: {}", e))?; + let body_bytes = + serde_json::to_vec(&body).map_err(|e| format!("Failed to serialize body: {}", e))?; let headers = serde_json::json!({ "Content-Type": "application/json" @@ -705,6 +736,24 @@ fn handle_message(message: TelegramMessage) { return; } + // Owner validation: silently drop messages from non-owner users + if let Some(owner_id_str) = channel_host::workspace_read(OWNER_ID_PATH) { + if !owner_id_str.is_empty() { + if let Ok(owner_id) = owner_id_str.parse::() { + if from.id != owner_id { + channel_host::log( + channel_host::LogLevel::Debug, + &format!( + "Dropping message from non-owner user {} (owner: {})", + from.id, owner_id + ), + ); + return; + } + } + } + } + let is_private = message.chat.chat_type == "private"; // For group chats, check if the bot was mentioned @@ -831,6 +880,40 @@ mod tests { assert_eq!(clean_message_text(" spaced "), "spaced"); } + #[test] + fn test_config_with_owner_id() { + let json = r#"{"owner_id": 123456789}"#; + let config: TelegramConfig = serde_json::from_str(json).unwrap(); + assert_eq!(config.owner_id, Some(123456789)); + } + + #[test] + fn test_config_without_owner_id() { + let json = r#"{}"#; + let config: TelegramConfig = serde_json::from_str(json).unwrap(); + assert_eq!(config.owner_id, None); + } + + #[test] + fn test_config_with_null_owner_id() { + let json = r#"{"owner_id": null}"#; + let config: TelegramConfig = serde_json::from_str(json).unwrap(); + assert_eq!(config.owner_id, None); + } + + #[test] + fn test_config_full() { + let json = r#"{ + "bot_username": "my_bot", + "owner_id": 42, + "respond_to_all_group_messages": true + }"#; + let config: TelegramConfig = serde_json::from_str(json).unwrap(); + assert_eq!(config.bot_username, Some("my_bot".to_string())); + assert_eq!(config.owner_id, Some(42)); + assert!(config.respond_to_all_group_messages); + } + #[test] fn test_parse_update() { let json = r#"{ diff --git a/channels-src/telegram/telegram.capabilities.json b/channels-src/telegram/telegram.capabilities.json index 23a4af65..70f56e01 100644 --- a/channels-src/telegram/telegram.capabilities.json +++ b/channels-src/telegram/telegram.capabilities.json @@ -35,6 +35,7 @@ }, "config": { "bot_username": null, + "owner_id": null, "respond_to_all_group_messages": false, "polling_enabled": false, "poll_interval_ms": 30000