diff --git a/src/channels/web/handlers/extensions.rs b/src/channels/web/handlers/extensions.rs index 71a3db12..888ffa99 100644 --- a/src/channels/web/handlers/extensions.rs +++ b/src/channels/web/handlers/extensions.rs @@ -33,7 +33,7 @@ pub async fn extensions_list_handler( "failed".to_string() } else if !ext.authenticated { "installed".to_string() - } else if ext.active && ext.name == "telegram" { + } else if ext.active { let has_paired = pairing_store .read_allow_from(&ext.name) .map(|list| !list.is_empty()) diff --git a/src/channels/web/mod.rs b/src/channels/web/mod.rs index 8970651e..68165924 100644 --- a/src/channels/web/mod.rs +++ b/src/channels/web/mod.rs @@ -94,7 +94,6 @@ impl GatewayChannel { registry_entries: Vec::new(), cost_guard: None, startup_time: std::time::Instant::now(), - restart_requested: std::sync::atomic::AtomicBool::new(false), }); Self { @@ -128,7 +127,6 @@ impl GatewayChannel { registry_entries: self.state.registry_entries.clone(), cost_guard: self.state.cost_guard.clone(), startup_time: self.state.startup_time, - restart_requested: std::sync::atomic::AtomicBool::new(false), }; mutate(&mut new_state); self.state = Arc::new(new_state); diff --git a/src/channels/web/server.rs b/src/channels/web/server.rs index 7bcb30eb..bbc15052 100644 --- a/src/channels/web/server.rs +++ b/src/channels/web/server.rs @@ -165,8 +165,6 @@ pub struct GatewayState { pub cost_guard: Option>, /// Server startup time for uptime calculation. pub startup_time: std::time::Instant, - /// Flag set when a restart has been requested via the API. - pub restart_requested: std::sync::atomic::AtomicBool, } /// Start the gateway HTTP server. @@ -247,8 +245,6 @@ pub async fn start_server( "/api/extensions/{name}/setup", get(extensions_setup_handler).post(extensions_setup_submit_handler), ) - // Gateway management - .route("/api/gateway/restart", post(gateway_restart_handler)) // Pairing .route("/api/pairing/{channel}", get(pairing_list_handler)) .route( @@ -1218,8 +1214,8 @@ async fn extensions_list_handler( } else if !ext.authenticated { // No credentials configured yet. "installed".to_string() - } else if ext.active && ext.name == "telegram" { - // Telegram: check pairing status (end-to-end setup via web UI). + } else if ext.active { + // Check pairing status for active channels. let has_paired = pairing_store .read_allow_from(&ext.name) .map(|list| !list.is_empty()) @@ -1230,7 +1226,7 @@ async fn extensions_list_handler( "pairing".to_string() } } else { - // Authenticated but not fully active (or non-Telegram). + // Authenticated but not yet active. "configured".to_string() }) } else { @@ -1552,41 +1548,12 @@ async fn extensions_setup_submit_handler( Ok(result) => { let mut resp = ActionResponse::ok(result.message); resp.activated = Some(result.activated); - if !result.activated { - resp.needs_restart = Some(true); - } Ok(Json(resp)) } Err(e) => Ok(Json(ActionResponse::fail(e.to_string()))), } } -// --- Gateway management handlers --- - -async fn gateway_restart_handler(State(state): State>) -> Json { - // Idempotency guard: only allow one restart at a time. - if state - .restart_requested - .compare_exchange( - false, - true, - std::sync::atomic::Ordering::SeqCst, - std::sync::atomic::Ordering::SeqCst, - ) - .is_err() - { - return Json(ActionResponse::ok("Restart already in progress")); - } - - // Take the shutdown sender and trigger graceful shutdown. - if let Some(tx) = state.shutdown_tx.write().await.take() { - let _ = tx.send(()); - tracing::info!("Gateway restart requested via API"); - } - - Json(ActionResponse::ok("Restarting...")) -} - // --- Pairing handlers --- async fn pairing_list_handler( diff --git a/src/channels/web/static/app.js b/src/channels/web/static/app.js index aa18dc53..4fddb20f 100644 --- a/src/channels/web/static/app.js +++ b/src/channels/web/static/app.js @@ -1931,14 +1931,6 @@ function renderExtensionCard(ext) { card.appendChild(errorDiv); } - // Show "coming soon" note for non-Telegram channels that are configured but not fully supported yet - if (ext.kind === 'wasm_channel' && ext.name !== 'telegram' - && (ext.activation_status === 'configured' || ext.active)) { - const noteDiv = document.createElement('div'); - noteDiv.className = 'ext-note'; - noteDiv.textContent = 'Full integration coming soon. Use the CLI to complete setup.'; - card.appendChild(noteDiv); - } const actions = document.createElement('div'); actions.className = 'ext-actions'; @@ -2168,10 +2160,8 @@ function submitConfigureModal(name, fields) { if (res.success) { if (res.activated) { showToast('Configured and activated ' + name, 'success'); - } else if (res.needs_restart) { - showToast('Configured ' + name + '. Use Reconfigure to re-enter credentials and activate.', 'info'); } else { - showToast(res.message, 'success'); + showToast(res.message || 'Configuration saved but activation failed', 'warning'); } } else { showToast(res.message || 'Configuration failed', 'error'); @@ -2235,7 +2225,7 @@ function approvePairing(channel, code, container) { }).then(res => { if (res.success) { showToast('Pairing approved', 'success'); - loadPairingRequests(channel, container); + loadExtensions(); } else { showToast(res.message || 'Approve failed', 'error'); } @@ -2258,53 +2248,6 @@ function stopPairingPoll() { } } -// --- Gateway restart --- - -function restartGateway() { - if (!confirm('Restart IronClaw gateway? Active connections will be dropped.')) return; - - apiFetch('/api/gateway/restart', { method: 'POST' }) - .then(function() { - showRestartOverlay(); - }) - .catch(function() { - showRestartOverlay(); - }); -} - -function showRestartOverlay() { - var overlay = document.createElement('div'); - overlay.className = 'restart-overlay'; - overlay.innerHTML = '
' - + '
' - + '

Restarting IronClaw...

' - + '

Waiting for server to come back online

' - + '
'; - document.body.appendChild(overlay); - - var pollCount = 0; - var pollTimer = setInterval(function() { - pollCount++; - if (pollCount > 30) { // 60 seconds - clearInterval(pollTimer); - overlay.querySelector('h2').textContent = 'Restart timed out'; - overlay.querySelector('p').textContent = 'Server did not come back within 60 seconds. Check logs.'; - overlay.querySelector('.restart-spinner').style.display = 'none'; - return; - } - fetch('/api/gateway/status', { - headers: { 'Authorization': 'Bearer ' + token }, - }) - .then(function(r) { - if (r.ok) { - clearInterval(pollTimer); - window.location.reload(); - } - }) - .catch(function() { /* still restarting */ }); - }, 2000); -} - // --- WASM channel stepper --- function renderWasmChannelStepper(ext) { @@ -2312,23 +2255,17 @@ function renderWasmChannelStepper(ext) { stepper.className = 'ext-stepper'; var status = ext.activation_status || 'installed'; - var isTelegram = ext.name === 'telegram'; - // Telegram gets a 3-step stepper (Installed → Configured → Active/Pairing). - // Other channels only get 2 steps (Installed → Configured) since full - // integration isn't available in the web UI yet. var steps = [ { label: 'Installed', key: 'installed' }, { label: 'Configured', key: 'configured' }, + { label: status === 'pairing' ? 'Awaiting Pairing' : 'Active', key: 'active' }, ]; - if (isTelegram) { - steps.push({ label: status === 'pairing' ? 'Awaiting Pairing' : 'Active', key: 'active' }); - } var reachedIdx; - if (status === 'active') reachedIdx = isTelegram ? 2 : 1; + if (status === 'active') reachedIdx = 2; else if (status === 'pairing') reachedIdx = 2; - else if (status === 'failed') reachedIdx = isTelegram ? 2 : 1; + else if (status === 'failed') reachedIdx = 2; else if (status === 'configured') reachedIdx = 1; else reachedIdx = 0; diff --git a/src/channels/web/static/style.css b/src/channels/web/static/style.css index b887b031..7f24843d 100644 --- a/src/channels/web/static/style.css +++ b/src/channels/web/static/style.css @@ -2312,43 +2312,6 @@ body { margin-top: 6px; } -/* Restart overlay */ -.restart-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.8); - z-index: 2000; - display: flex; - align-items: center; - justify-content: center; -} - -.restart-message { - text-align: center; - color: var(--text); -} - -.restart-message h2 { - margin: 16px 0 8px; -} - -.restart-message p { - color: var(--text-secondary); -} - -.restart-spinner { - width: 40px; - height: 40px; - border: 3px solid var(--border); - border-top-color: var(--accent); - border-radius: 50%; - animation: spin 0.8s linear infinite; - margin: 0 auto; -} - @keyframes spin { to { transform: rotate(360deg); } } diff --git a/src/channels/web/types.rs b/src/channels/web/types.rs index c96e3d4b..564e9107 100644 --- a/src/channels/web/types.rs +++ b/src/channels/web/types.rs @@ -451,9 +451,6 @@ pub struct ActionResponse { /// Whether the channel was successfully activated after setup. #[serde(skip_serializing_if = "Option::is_none")] pub activated: Option, - /// Whether a gateway restart is needed (activation failed). - #[serde(skip_serializing_if = "Option::is_none")] - pub needs_restart: Option, } impl ActionResponse { @@ -465,7 +462,6 @@ impl ActionResponse { awaiting_token: None, instructions: None, activated: None, - needs_restart: None, } } @@ -477,7 +473,6 @@ impl ActionResponse { awaiting_token: None, instructions: None, activated: None, - needs_restart: None, } } } diff --git a/src/channels/web/ws.rs b/src/channels/web/ws.rs index e0b7eb35..96c6f783 100644 --- a/src/channels/web/ws.rs +++ b/src/channels/web/ws.rs @@ -493,7 +493,6 @@ mod tests { registry_entries: Vec::new(), cost_guard: None, startup_time: std::time::Instant::now(), - restart_requested: std::sync::atomic::AtomicBool::new(false), } } } diff --git a/src/config/channels.rs b/src/config/channels.rs index 5cc35da1..fb0caf30 100644 --- a/src/config/channels.rs +++ b/src/config/channels.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::path::PathBuf; use secrecy::SecretString; @@ -18,8 +19,9 @@ pub struct ChannelsConfig { pub wasm_channels_dir: std::path::PathBuf, /// Whether WASM channels are enabled. pub wasm_channels_enabled: bool, - /// Telegram owner user ID. When set, the bot only responds to this user. - pub telegram_owner_id: Option, + /// Per-channel owner user IDs. When set, the channel only responds to this user. + /// Key: channel name (e.g., "telegram"), Value: owner user ID. + pub wasm_channel_owner_ids: HashMap, } #[derive(Debug, Clone)] @@ -180,14 +182,20 @@ impl ChannelsConfig { .map(PathBuf::from) .unwrap_or_else(default_channels_dir), wasm_channels_enabled: parse_bool_env("WASM_CHANNELS_ENABLED", true)?, - telegram_owner_id: optional_env("TELEGRAM_OWNER_ID")? - .map(|s| s.parse()) - .transpose() - .map_err(|e: std::num::ParseIntError| ConfigError::InvalidValue { - key: "TELEGRAM_OWNER_ID".to_string(), - message: format!("must be an integer: {e}"), - })? - .or(settings.channels.telegram_owner_id), + wasm_channel_owner_ids: { + let mut ids = settings.channels.wasm_channel_owner_ids.clone(); + // Backwards compat: TELEGRAM_OWNER_ID env var + if let Some(id_str) = optional_env("TELEGRAM_OWNER_ID")? { + let id: i64 = id_str.parse().map_err(|e: std::num::ParseIntError| { + ConfigError::InvalidValue { + key: "TELEGRAM_OWNER_ID".to_string(), + message: format!("must be an integer: {e}"), + } + })?; + ids.insert("telegram".to_string(), id); + } + ids + }, }) } } diff --git a/src/extensions/manager.rs b/src/extensions/manager.rs index 9c29d5e8..50e4d9ac 100644 --- a/src/extensions/manager.rs +++ b/src/extensions/manager.rs @@ -49,7 +49,7 @@ struct ChannelRuntimeState { wasm_channel_runtime: Arc, pairing_store: Arc, wasm_channel_router: Arc, - telegram_owner_id: Option, + wasm_channel_owner_ids: std::collections::HashMap, } /// Result of saving setup secrets and attempting activation. @@ -150,14 +150,14 @@ impl ExtensionManager { wasm_channel_runtime: Arc, pairing_store: Arc, wasm_channel_router: Arc, - telegram_owner_id: Option, + wasm_channel_owner_ids: std::collections::HashMap, ) { *self.channel_runtime.write().await = Some(ChannelRuntimeState { channel_manager, wasm_channel_runtime, pairing_store, wasm_channel_router, - telegram_owner_id, + wasm_channel_owner_ids, }); } @@ -1872,21 +1872,18 @@ impl ExtensionManager { channel_manager, pairing_store, wasm_channel_router, - telegram_owner_id, + wasm_channel_owner_ids, ) = { let rt_guard = self.channel_runtime.read().await; let rt = rt_guard.as_ref().ok_or_else(|| { - ExtensionError::ActivationFailed( - "WASM channel runtime not configured. Restart IronClaw to activate." - .to_string(), - ) + ExtensionError::ActivationFailed("WASM channel runtime not configured".to_string()) })?; ( Arc::clone(&rt.wasm_channel_runtime), Arc::clone(&rt.channel_manager), Arc::clone(&rt.pairing_store), Arc::clone(&rt.wasm_channel_router), - rt.telegram_owner_id, + rt.wasm_channel_owner_ids.clone(), ) }; @@ -1956,9 +1953,7 @@ impl ExtensionManager { ); } - if channel_name == "telegram" - && let Some(owner_id) = telegram_owner_id - { + if let Some(&owner_id) = wasm_channel_owner_ids.get(channel_name.as_str()) { config_updates.insert("owner_id".to_string(), serde_json::json!(owner_id)); } @@ -2527,7 +2522,7 @@ impl ExtensionManager { tracing::warn!( channel = name, error = %e, - "Saved configuration but hot-activation failed, restart may be needed" + "Saved configuration but hot-activation failed" ); self.activation_errors .write() @@ -2537,8 +2532,7 @@ impl ExtensionManager { .await; Ok(SetupResult { message: format!( - "Configuration saved for '{}'. \ - Automatic activation failed ({}), restart IronClaw to activate.", + "Configuration saved for '{}'. Activation failed: {}", name, e ), activated: false, diff --git a/src/main.rs b/src/main.rs index 71118e35..0cc24a3b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -484,8 +484,6 @@ async fn async_main() -> anyhow::Result<()> { let mut sse_sender: Option< tokio::sync::broadcast::Sender, > = None; - let mut gateway_state: Option> = - None; if let Some(ref gw_config) = config.channels.gateway { let mut gw = GatewayChannel::new(gw_config.clone()).with_llm_provider(Arc::clone(&components.llm)); @@ -542,7 +540,6 @@ async fn async_main() -> anyhow::Result<()> { // IMPORTANT: This must come after all `with_*` calls since `rebuild_state` // creates a new SseManager, which would orphan this sender. sse_sender = Some(gw.state().sse.sender()); - gateway_state = Some(Arc::clone(gw.state())); channel_names.push("gateway".to_string()); channels.add(Box::new(gw)).await; @@ -618,7 +615,7 @@ async fn async_main() -> anyhow::Result<()> { rt, ps, router, - config.channels.telegram_owner_id, + config.channels.wasm_channel_owner_ids.clone(), ) .await; tracing::info!("Channel runtime wired into extension manager for hot-activation"); @@ -700,16 +697,6 @@ async fn async_main() -> anyhow::Result<()> { tracing::info!("Agent shutdown complete"); - // Check if a restart was requested via the gateway API. - if let Some(ref gw_state) = gateway_state - && gw_state - .restart_requested - .load(std::sync::atomic::Ordering::Relaxed) - { - eprintln!("Restarting IronClaw (exit code 75)..."); - std::process::exit(75); - } - Ok(()) } @@ -982,9 +969,11 @@ async fn setup_wasm_channels( ); } - // Inject owner_id for Telegram so the bot only responds to the bound user. - if channel_name == "telegram" - && let Some(owner_id) = config.channels.telegram_owner_id + // Inject owner_id if configured for this channel. + if let Some(&owner_id) = config + .channels + .wasm_channel_owner_ids + .get(channel_name.as_str()) { config_updates.insert("owner_id".to_string(), serde_json::json!(owner_id)); } diff --git a/src/settings.rs b/src/settings.rs index 0e4b1fd9..5ae6c7e8 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -249,10 +249,10 @@ pub struct ChannelSettings { #[serde(default)] pub signal_group_allow_from: Option, - /// Telegram owner user ID. When set, the bot only responds to this user. - /// Captured during setup by having the user message the bot. + /// Per-channel owner user IDs. When set, the channel only responds to this user. + /// Key: channel name (e.g., "telegram"), Value: owner user ID. #[serde(default)] - pub telegram_owner_id: Option, + pub wasm_channel_owner_ids: std::collections::HashMap, /// Enabled WASM channels by name. /// Channels not in this list but present in the channels directory will still load. @@ -1049,28 +1049,37 @@ mod tests { } #[test] - fn test_telegram_owner_id_db_round_trip() { + fn test_wasm_channel_owner_ids_db_round_trip() { let mut settings = Settings::default(); - settings.channels.telegram_owner_id = Some(123456789); + settings + .channels + .wasm_channel_owner_ids + .insert("telegram".to_string(), 123456789); let map = settings.to_db_map(); let restored = Settings::from_db_map(&map); - assert_eq!(restored.channels.telegram_owner_id, Some(123456789)); + assert_eq!( + restored.channels.wasm_channel_owner_ids.get("telegram"), + Some(&123456789) + ); } #[test] - fn test_telegram_owner_id_default_none() { + fn test_wasm_channel_owner_ids_default_empty() { let settings = Settings::default(); - assert_eq!(settings.channels.telegram_owner_id, None); + assert!(settings.channels.wasm_channel_owner_ids.is_empty()); } #[test] - fn test_telegram_owner_id_via_set() { + fn test_wasm_channel_owner_ids_via_set() { let mut settings = Settings::default(); settings - .set("channels.telegram_owner_id", "987654321") + .set("channels.wasm_channel_owner_ids.telegram", "987654321") .unwrap(); - assert_eq!(settings.channels.telegram_owner_id, Some(987654321)); + assert_eq!( + settings.channels.wasm_channel_owner_ids.get("telegram"), + Some(&987654321) + ); } #[test] @@ -1406,7 +1415,11 @@ mod tests { channels: ChannelSettings { http_enabled: true, http_port: Some(9090), - telegram_owner_id: Some(12345), + wasm_channel_owner_ids: { + let mut m = std::collections::HashMap::new(); + m.insert("telegram".to_string(), 12345); + m + }, ..Default::default() }, heartbeat: HeartbeatSettings { @@ -1473,9 +1486,9 @@ mod tests { assert!(restored.channels.http_enabled, "http_enabled lost"); assert_eq!(restored.channels.http_port, Some(9090), "http_port lost"); assert_eq!( - restored.channels.telegram_owner_id, - Some(12345), - "telegram_owner_id lost" + restored.channels.wasm_channel_owner_ids.get("telegram"), + Some(&12345), + "wasm_channel_owner_ids lost" ); assert!(restored.heartbeat.enabled, "heartbeat.enabled lost"); assert_eq!( diff --git a/src/setup/channels.rs b/src/setup/channels.rs index 58edf5d9..bb55b835 100644 --- a/src/setup/channels.rs +++ b/src/setup/channels.rs @@ -1,6 +1,6 @@ -//! Channel-specific setup flows. +//! Channel setup flows. //! -//! Each channel (Telegram, HTTP, etc.) has its own setup function that: +//! Each channel (HTTP, Signal, WASM, etc.) has its own setup function that: //! 1. Displays setup instructions //! 2. Collects configuration (tokens, ports, etc.) //! 3. Validates the configuration @@ -9,9 +9,7 @@ use std::sync::Arc; use base64::Engine; -use reqwest::Client; use secrecy::{ExposeSecret, SecretString}; -use serde::Deserialize; use url::Url; use uuid::Uuid; @@ -105,261 +103,6 @@ impl SecretsContext { } } -/// Result of Telegram setup. -#[derive(Debug, Clone)] -pub struct TelegramSetupResult { - pub enabled: bool, - pub bot_username: Option, - pub webhook_secret: Option, - pub owner_id: Option, -} - -/// Telegram Bot API response for getMe. -#[derive(Debug, Deserialize)] -struct TelegramGetMeResponse { - ok: bool, - result: Option, -} - -#[derive(Debug, Deserialize)] -struct TelegramUser { - username: Option, - #[allow(dead_code)] - first_name: String, -} - -/// Telegram Bot API response for getUpdates. -#[derive(Debug, Deserialize)] -struct TelegramGetUpdatesResponse { - ok: bool, - result: Vec, -} - -#[derive(Debug, Deserialize)] -struct TelegramUpdate { - update_id: i64, - message: Option, -} - -#[derive(Debug, Deserialize)] -struct TelegramUpdateMessage { - from: Option, -} - -#[derive(Debug, Deserialize)] -struct TelegramUpdateUser { - id: i64, - first_name: String, - username: Option, -} - -/// Set up Telegram bot channel. -/// -/// Guides the user through: -/// 1. Creating a bot with @BotFather -/// 2. Entering the bot token -/// 3. Validating the token -/// 4. Saving the token to the database -pub async fn setup_telegram( - secrets: &SecretsContext, - settings: &Settings, -) -> Result { - println!("Telegram Setup:"); - println!(); - print_info("To create a Telegram bot:"); - print_info("1. Open Telegram and message @BotFather"); - print_info("2. Send /newbot and follow the prompts"); - print_info("3. Copy the bot token (looks like 123456:ABC-DEF...)"); - println!(); - - // Check if token already exists - if secrets.secret_exists("telegram_bot_token").await { - print_info("Existing Telegram token found in database."); - if !confirm("Replace existing token?", false)? { - // Still offer to configure webhook secret and owner binding - let webhook_secret = setup_telegram_webhook_secret(secrets, &settings.tunnel).await?; - let owner_id = bind_telegram_owner_flow(secrets, settings).await?; - return Ok(TelegramSetupResult { - enabled: true, - bot_username: None, - webhook_secret, - owner_id, - }); - } - } - - loop { - let token = secret_input("Bot token (from @BotFather)")?; - - // Validate the token - print_info("Validating bot token..."); - - match validate_telegram_token(&token).await { - Ok(username) => { - print_success(&format!( - "Bot validated: @{}", - username.as_deref().unwrap_or("unknown") - )); - - // Save to database - secrets.save_secret("telegram_bot_token", &token).await?; - print_success("Token saved to database"); - - // Bind bot to owner's Telegram account - let owner_id = bind_telegram_owner(&token).await?; - - // Offer webhook secret configuration - let webhook_secret = - setup_telegram_webhook_secret(secrets, &settings.tunnel).await?; - - return Ok(TelegramSetupResult { - enabled: true, - bot_username: username, - webhook_secret, - owner_id, - }); - } - Err(e) => { - print_error(&format!("Token validation failed: {}", e)); - - if !confirm("Try again?", true)? { - return Ok(TelegramSetupResult { - enabled: false, - bot_username: None, - webhook_secret: None, - owner_id: None, - }); - } - } - } - } -} - -/// Bind the bot to the owner's Telegram account by having them send a message. -/// -/// Polls `getUpdates` until a message arrives, then captures the sender's user ID. -/// Returns `None` if the user declines or the flow times out. -async fn bind_telegram_owner(token: &SecretString) -> Result, ChannelSetupError> { - println!(); - print_info("Account Binding (recommended):"); - print_info("Binding restricts the bot so only YOU can use it."); - print_info("Without this, anyone who finds your bot can send it messages."); - println!(); - - if !confirm("Bind bot to your Telegram account?", true)? { - print_info("Skipping account binding. Bot will accept messages from all users."); - return Ok(None); - } - - print_info("Send any message (e.g. /start) to your bot in Telegram."); - print_info("Waiting for your message (up to 120 seconds)..."); - - let client = Client::builder() - .timeout(std::time::Duration::from_secs(35)) - .build() - .map_err(|e| ChannelSetupError::Network(format!("Failed to create HTTP client: {}", e)))?; - - // Clear any existing webhook so getUpdates works - let delete_url = format!( - "https://api.telegram.org/bot{}/deleteWebhook", - token.expose_secret() - ); - if let Err(e) = client.post(&delete_url).send().await { - tracing::warn!("Failed to delete webhook (getUpdates may not work): {e}"); - } - - let updates_url = format!( - "https://api.telegram.org/bot{}/getUpdates", - token.expose_secret() - ); - - let deadline = std::time::Instant::now() + std::time::Duration::from_secs(120); - - while std::time::Instant::now() < deadline { - let response = client - .get(&updates_url) - .query(&[("timeout", "30"), ("allowed_updates", "[\"message\"]")]) - .send() - .await - .map_err(|e| ChannelSetupError::Network(format!("getUpdates request failed: {}", e)))?; - - if !response.status().is_success() { - return Err(ChannelSetupError::Network(format!( - "getUpdates returned status {}", - response.status() - ))); - } - - let body: TelegramGetUpdatesResponse = response.json().await.map_err(|e| { - ChannelSetupError::Network(format!("Failed to parse getUpdates response: {}", e)) - })?; - - if !body.ok { - return Err(ChannelSetupError::Network( - "Telegram API returned error for getUpdates".to_string(), - )); - } - - // Find the first message with a sender - for update in &body.result { - if let Some(ref msg) = update.message - && let Some(ref from) = msg.from - { - let display_name = from - .username - .as_ref() - .map(|u| format!("@{}", u)) - .unwrap_or_else(|| from.first_name.clone()); - - print_success(&format!( - "Received message from {} (ID: {})", - display_name, from.id - )); - - // Acknowledge the update so it doesn't pile up - let ack_url = format!( - "https://api.telegram.org/bot{}/getUpdates", - token.expose_secret() - ); - if let Err(e) = client - .get(&ack_url) - .query(&[("offset", &(update.update_id + 1).to_string())]) - .send() - .await - { - tracing::warn!("Failed to acknowledge Telegram update: {e}"); - } - - return Ok(Some(from.id)); - } - } - } - - print_error("Timed out waiting for a message. You can re-run setup to try again."); - print_info("Bot will accept messages from all users until owner is bound."); - Ok(None) -} - -/// Bind flow when the token already exists (reads from secrets store). -/// -/// Retrieves the saved bot token and delegates to `bind_telegram_owner`. -async fn bind_telegram_owner_flow( - secrets: &SecretsContext, - settings: &Settings, -) -> Result, ChannelSetupError> { - if settings.channels.telegram_owner_id.is_some() { - print_info("Bot is already bound to a Telegram account."); - if !confirm("Re-bind to a different account?", false)? { - return Ok(settings.channels.telegram_owner_id); - } - } - - // We need the token to poll getUpdates - let token = secrets.get_secret("telegram_bot_token").await?; - - bind_telegram_owner(&token).await -} - /// Set up a tunnel for exposing the agent to the internet. /// /// This is shared across all channels that need webhook endpoints. @@ -725,84 +468,6 @@ fn setup_tunnel_static() -> Result { }) } -/// Set up Telegram webhook secret for signature validation. -/// -/// Returns the webhook secret if configured. -async fn setup_telegram_webhook_secret( - secrets: &SecretsContext, - tunnel: &TunnelSettings, -) -> Result, ChannelSetupError> { - if tunnel.public_url.is_none() { - print_info(""); - print_info("No tunnel configured. Telegram will use polling mode (30s+ delay)."); - print_info("Run setup again to configure a tunnel for instant delivery."); - return Ok(None); - } - - println!(); - print_info("Telegram Webhook Security:"); - print_info("A webhook secret adds an extra layer of security by validating"); - print_info("that requests actually come from Telegram's servers."); - - if !confirm("Generate a webhook secret?", true)? { - return Ok(None); - } - - let secret = generate_webhook_secret(); - secrets - .save_secret( - "telegram_webhook_secret", - &SecretString::from(secret.clone()), - ) - .await?; - print_success("Webhook secret generated and saved"); - - Ok(Some(secret)) -} - -/// Validate a Telegram bot token by calling the getMe API. -/// -/// Returns the bot's username if valid. -pub async fn validate_telegram_token( - token: &SecretString, -) -> Result, ChannelSetupError> { - let client = Client::builder() - .timeout(std::time::Duration::from_secs(10)) - .build() - .map_err(|e| ChannelSetupError::Network(format!("Failed to create HTTP client: {}", e)))?; - - let url = format!( - "https://api.telegram.org/bot{}/getMe", - token.expose_secret() - ); - - let response = client - .get(&url) - .send() - .await - .map_err(|e| ChannelSetupError::Network(format!("Request failed: {}", e)))?; - - if !response.status().is_success() { - return Err(ChannelSetupError::Network(format!( - "API returned status {}", - response.status() - ))); - } - - let body: TelegramGetMeResponse = response - .json() - .await - .map_err(|e| ChannelSetupError::Network(format!("Failed to parse response: {}", e)))?; - - if body.ok { - Ok(body.result.and_then(|u| u.username)) - } else { - Err(ChannelSetupError::Network( - "Telegram API returned error".to_string(), - )) - } -} - /// Result of HTTP webhook setup. #[derive(Debug, Clone)] pub struct HttpSetupResult { diff --git a/src/setup/mod.rs b/src/setup/mod.rs index b556ba92..a0ea82ce 100644 --- a/src/setup/mod.rs +++ b/src/setup/mod.rs @@ -24,10 +24,7 @@ mod prompts; #[cfg(any(feature = "postgres", feature = "libsql"))] mod wizard; -pub use channels::{ - ChannelSetupError, SecretsContext, setup_http, setup_telegram, setup_tunnel, - validate_telegram_token, -}; +pub use channels::{ChannelSetupError, SecretsContext, setup_http, setup_tunnel}; pub use prompts::{ confirm, input, optional_input, print_error, print_header, print_info, print_step, print_success, secret_input, select_many, select_one, diff --git a/src/setup/wizard.rs b/src/setup/wizard.rs index b31a94f5..2874ca89 100644 --- a/src/setup/wizard.rs +++ b/src/setup/wizard.rs @@ -26,7 +26,7 @@ use crate::llm::{SessionConfig, SessionManager}; use crate::secrets::{SecretsCrypto, SecretsStore}; use crate::settings::{KeySource, Settings}; use crate::setup::channels::{ - SecretsContext, setup_http, setup_signal, setup_telegram, setup_tunnel, setup_wasm_channel, + SecretsContext, setup_http, setup_signal, setup_tunnel, setup_wasm_channel, }; use crate::setup::prompts::{ confirm, input, optional_input, print_error, print_header, print_info, print_step, @@ -1670,15 +1670,6 @@ impl SetupWizard { let result = if let Some(cap_file) = discovered_by_name.get(&channel_name) { if !cap_file.setup.required_secrets.is_empty() { setup_wasm_channel(ctx, &channel_name, &cap_file.setup).await? - } else if channel_name == "telegram" { - let telegram_result = setup_telegram(ctx, &self.settings).await?; - if let Some(owner_id) = telegram_result.owner_id { - self.settings.channels.telegram_owner_id = Some(owner_id); - } - crate::setup::channels::WasmChannelSetupResult { - enabled: telegram_result.enabled, - channel_name: "telegram".to_string(), - } } else { print_info(&format!( "No setup configuration found for {}", diff --git a/tests/openai_compat_integration.rs b/tests/openai_compat_integration.rs index d788a93d..f8b8631a 100644 --- a/tests/openai_compat_integration.rs +++ b/tests/openai_compat_integration.rs @@ -201,7 +201,6 @@ async fn start_test_server_with_provider( registry_entries: Vec::new(), cost_guard: None, startup_time: std::time::Instant::now(), - restart_requested: std::sync::atomic::AtomicBool::new(false), }); let addr: SocketAddr = "127.0.0.1:0".parse().unwrap(); @@ -690,7 +689,6 @@ async fn test_no_llm_provider_returns_503() { registry_entries: Vec::new(), cost_guard: None, startup_time: std::time::Instant::now(), - restart_requested: std::sync::atomic::AtomicBool::new(false), }); let addr: SocketAddr = "127.0.0.1:0".parse().unwrap(); diff --git a/tests/ws_gateway_integration.rs b/tests/ws_gateway_integration.rs index 7a4eb440..beb01859 100644 --- a/tests/ws_gateway_integration.rs +++ b/tests/ws_gateway_integration.rs @@ -59,7 +59,6 @@ async fn start_test_server() -> ( registry_entries: Vec::new(), cost_guard: None, startup_time: std::time::Instant::now(), - restart_requested: std::sync::atomic::AtomicBool::new(false), }); let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();