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()) {