diff --git a/channels-src/telegram/src/lib.rs b/channels-src/telegram/src/lib.rs index c1f8539a..92c078bd 100644 --- a/channels-src/telegram/src/lib.rs +++ b/channels-src/telegram/src/lib.rs @@ -373,19 +373,16 @@ impl Guest for TelegramChannel { "Webhook mode enabled (tunnel configured)", ); - // Register webhook with Telegram API + // Register webhook with Telegram API — propagate errors so a bad token + // causes activation to fail rather than silently succeeding. if let Some(ref tunnel_url) = config.tunnel_url { channel_host::log( channel_host::LogLevel::Info, &format!("Registering webhook: {}/webhook/telegram", tunnel_url), ); - if let Err(e) = register_webhook(tunnel_url, config.webhook_secret.as_deref()) { - channel_host::log( - channel_host::LogLevel::Error, - &format!("Failed to register webhook: {}", e), - ); - } + register_webhook(tunnel_url, config.webhook_secret.as_deref()) + .map_err(|e| format!("Failed to register webhook: {}", e))?; } } else { channel_host::log( @@ -393,14 +390,10 @@ impl Guest for TelegramChannel { "Polling mode enabled (no tunnel configured)", ); - // Delete any existing webhook before polling - // Telegram doesn't allow getUpdates while a webhook is active - if let Err(e) = delete_webhook() { - channel_host::log( - channel_host::LogLevel::Warn, - &format!("Failed to delete webhook (may not exist): {}", e), - ); - } + // Delete any existing webhook before polling. Telegram returns success + // when no webhook exists, so any error here (e.g. 401) means a bad token. + delete_webhook() + .map_err(|e| format!("Bot token validation failed: {}", e))?; } // Configure polling only if not in webhook mode diff --git a/src/channels/web/static/app.js b/src/channels/web/static/app.js index 2bd7a641..578cbd9c 100644 --- a/src/channels/web/static/app.js +++ b/src/channels/web/static/app.js @@ -1453,7 +1453,9 @@ function buildBreadcrumb(path) { let current = ''; for (const part of parts) { current += (current ? '/' : '') + part; - html += ' / ' + escapeHtml(part) + ''; + // Store the path in data-path (HTML-escaped) and read it back via this.dataset.path + // to avoid single-quote injection in inline JS string literals. + html += ' / ' + escapeHtml(part) + ''; } return html; } @@ -1628,10 +1630,8 @@ function applyLogFilters() { function setServerLogLevel(level) { apiFetch('/api/logs/level', { method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ level: level }), + body: { level }, }) - .then(r => r.json()) .then(data => { document.getElementById('logs-server-level').value = data.level; }) @@ -1640,7 +1640,6 @@ function setServerLogLevel(level) { function loadServerLogLevel() { apiFetch('/api/logs/level') - .then(r => r.json()) .then(data => { document.getElementById('logs-server-level').value = data.level; }) @@ -1957,11 +1956,6 @@ function renderExtensionCard(ext) { actions.appendChild(pairingLabel); actions.appendChild(createReconfigureButton(ext.name)); } else if (status === 'failed') { - var restartBtn = document.createElement('button'); - restartBtn.className = 'btn-ext activate'; - restartBtn.textContent = 'Restart'; - restartBtn.addEventListener('click', restartGateway); - actions.appendChild(restartBtn); actions.appendChild(createReconfigureButton(ext.name)); } else { // installed or configured: show Setup button @@ -2169,12 +2163,10 @@ function submitConfigureModal(name, fields) { .then((res) => { closeConfigureModal(); if (res.success) { - if (res.activated && name === 'telegram') { + if (res.activated) { showToast('Configured and activated ' + name, 'success'); - } else if (res.activated) { - showToast('Configured ' + name + ' successfully', 'success'); } else if (res.needs_restart) { - showToast('Configured ' + name + '. Restart required to activate.', 'info'); + showToast('Configured ' + name + '. Use Reconfigure to re-enter credentials and activate.', 'info'); } else { showToast(res.message, 'success'); } diff --git a/src/extensions/manager.rs b/src/extensions/manager.rs index d35f4a6a..26f08a5d 100644 --- a/src/extensions/manager.rs +++ b/src/extensions/manager.rs @@ -2329,6 +2329,36 @@ impl ExtensionManager { } }; + // For Telegram, validate the bot token against the API before storing it. + // This catches bad tokens immediately (both on first setup and reconfigure), + // before the channel activates and potentially shows as active with a bad token. + if name == "telegram" + && let Some(token_value) = secrets.get("telegram_bot_token") + { + let token = token_value.trim(); + if !token.is_empty() { + let encoded_token = + url::form_urlencoded::byte_serialize(token.as_bytes()).collect::(); + let url = format!("https://api.telegram.org/bot{}/getMe", encoded_token); + let resp = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(10)) + .build() + .map_err(|e| ExtensionError::Other(e.to_string()))? + .get(&url) + .send() + .await + .map_err(|e| { + ExtensionError::Other(format!("Failed to validate bot token: {}", e)) + })?; + if !resp.status().is_success() { + return Err(ExtensionError::Other(format!( + "Invalid bot token (Telegram API returned {})", + resp.status() + ))); + } + } + } + // Validate and store each submitted secret for (secret_name, secret_value) in secrets { if !allowed.contains(secret_name.as_str()) {