mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
Simplify Telegram channel config with host-injected tunnel/webhook settings
The WASM channel no longer needs its own polling_enabled/tunnel_url settings. Instead, the host injects tunnel_url and webhook_secret into the channel config at runtime before start() is called. Changes: - Add update_config() method to WasmChannel for runtime config injection - Simplify TelegramConfig to only have bot_username, respond_to_all_group_messages - Host injects tunnel_url (from Settings) and webhook_secret (from secrets store) - Channel checks if tunnel_url is present to determine webhook vs polling mode - Add delete_webhook() for clean transition to polling mode when no tunnel Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
e6946172f7
commit
9edc666ee3
@@ -177,7 +177,11 @@ struct TelegramMessageMetadata {
|
|||||||
is_private: bool,
|
is_private: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Channel configuration from capabilities file.
|
/// Channel configuration injected by host.
|
||||||
|
///
|
||||||
|
/// The host injects runtime values like tunnel_url and webhook_secret.
|
||||||
|
/// The channel doesn't need to know about polling vs webhook mode - it just
|
||||||
|
/// checks if tunnel_url is set to determine behavior.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct TelegramConfig {
|
struct TelegramConfig {
|
||||||
/// Bot username (without @) for mention detection in groups.
|
/// Bot username (without @) for mention detection in groups.
|
||||||
@@ -188,30 +192,17 @@ struct TelegramConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
respond_to_all_group_messages: bool,
|
respond_to_all_group_messages: bool,
|
||||||
|
|
||||||
/// Whether to use polling instead of webhooks.
|
/// Public tunnel URL for webhook mode (injected by host from global settings).
|
||||||
/// Automatically disabled if tunnel_url is set.
|
|
||||||
#[serde(default)]
|
|
||||||
polling_enabled: bool,
|
|
||||||
|
|
||||||
/// Polling interval in milliseconds (if polling enabled).
|
|
||||||
#[serde(default = "default_poll_interval")]
|
|
||||||
poll_interval_ms: u32,
|
|
||||||
|
|
||||||
/// Public tunnel URL for webhook mode (e.g., "https://abc123.ngrok.io").
|
|
||||||
/// When set, webhook mode is enabled and polling is disabled.
|
/// When set, webhook mode is enabled and polling is disabled.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
tunnel_url: Option<String>,
|
tunnel_url: Option<String>,
|
||||||
|
|
||||||
/// Secret token for webhook validation.
|
/// Secret token for webhook validation (injected by host from secrets store).
|
||||||
/// Telegram will include this in the X-Telegram-Bot-Api-Secret-Token header.
|
/// Telegram will include this in the X-Telegram-Bot-Api-Secret-Token header.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
webhook_secret: Option<String>,
|
webhook_secret: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_poll_interval() -> u32 {
|
|
||||||
30000 // 30 seconds (minimum allowed)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Channel Implementation
|
// Channel Implementation
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -220,6 +211,11 @@ struct TelegramChannel;
|
|||||||
|
|
||||||
impl Guest for TelegramChannel {
|
impl Guest for TelegramChannel {
|
||||||
fn on_start(config_json: String) -> Result<ChannelConfig, String> {
|
fn on_start(config_json: String) -> Result<ChannelConfig, String> {
|
||||||
|
channel_host::log(
|
||||||
|
channel_host::LogLevel::Debug,
|
||||||
|
&format!("Telegram channel config: {}", config_json),
|
||||||
|
);
|
||||||
|
|
||||||
let config: TelegramConfig = serde_json::from_str(&config_json)
|
let config: TelegramConfig = serde_json::from_str(&config_json)
|
||||||
.map_err(|e| format!("Failed to parse config: {}", e))?;
|
.map_err(|e| format!("Failed to parse config: {}", e))?;
|
||||||
|
|
||||||
@@ -232,21 +228,21 @@ impl Guest for TelegramChannel {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine mode: webhook or polling
|
// Mode is determined by whether the host injected a tunnel_url
|
||||||
// Webhook mode is enabled if tunnel_url is set, which disables polling
|
// If tunnel is configured, use webhooks. Otherwise, use polling.
|
||||||
let webhook_mode = config.tunnel_url.is_some();
|
let webhook_mode = config.tunnel_url.is_some();
|
||||||
|
|
||||||
if webhook_mode {
|
if webhook_mode {
|
||||||
channel_host::log(
|
channel_host::log(
|
||||||
channel_host::LogLevel::Info,
|
channel_host::LogLevel::Info,
|
||||||
"Webhook mode enabled (polling disabled)",
|
"Webhook mode enabled (tunnel configured)",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Register webhook with Telegram API
|
// Register webhook with Telegram API
|
||||||
if let Some(ref tunnel_url) = config.tunnel_url {
|
if let Some(ref tunnel_url) = config.tunnel_url {
|
||||||
channel_host::log(
|
channel_host::log(
|
||||||
channel_host::LogLevel::Info,
|
channel_host::LogLevel::Info,
|
||||||
&format!("Registering webhook with Telegram API: {}", tunnel_url),
|
&format!("Registering webhook: {}/webhook/telegram", tunnel_url),
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Err(e) = register_webhook(tunnel_url, config.webhook_secret.as_deref()) {
|
if let Err(e) = register_webhook(tunnel_url, config.webhook_secret.as_deref()) {
|
||||||
@@ -254,28 +250,35 @@ impl Guest for TelegramChannel {
|
|||||||
channel_host::LogLevel::Error,
|
channel_host::LogLevel::Error,
|
||||||
&format!("Failed to register webhook: {}", e),
|
&format!("Failed to register webhook: {}", e),
|
||||||
);
|
);
|
||||||
// Continue anyway, the host will start the server
|
|
||||||
// and Telegram will eventually receive updates
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
channel_host::log(
|
||||||
|
channel_host::LogLevel::Info,
|
||||||
|
"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),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure polling only if not in webhook mode and polling is enabled
|
// Configure polling only if not in webhook mode
|
||||||
let poll = if !webhook_mode && config.polling_enabled {
|
let poll = if !webhook_mode {
|
||||||
channel_host::log(
|
|
||||||
channel_host::LogLevel::Info,
|
|
||||||
&format!("Polling enabled (interval: {}ms)", config.poll_interval_ms.max(30000)),
|
|
||||||
);
|
|
||||||
Some(PollConfig {
|
Some(PollConfig {
|
||||||
interval_ms: config.poll_interval_ms.max(30000), // Enforce minimum
|
interval_ms: 30000, // 30 seconds minimum
|
||||||
enabled: true,
|
enabled: true,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// Webhook secret validation is handled by the host (X-Telegram-Bot-Api-Secret-Token header)
|
// Webhook secret validation is handled by the host
|
||||||
// The require_secret flag tells the host to validate the secret_validated field
|
|
||||||
let require_secret = config.webhook_secret.is_some();
|
let require_secret = config.webhook_secret.is_some();
|
||||||
|
|
||||||
Ok(ChannelConfig {
|
Ok(ChannelConfig {
|
||||||
@@ -507,9 +510,54 @@ impl Guest for TelegramChannel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Webhook Registration
|
// Webhook Management
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Delete any existing webhook with Telegram API.
|
||||||
|
///
|
||||||
|
/// Called during on_start() when switching to polling mode.
|
||||||
|
/// Telegram doesn't allow getUpdates while a webhook is active.
|
||||||
|
fn delete_webhook() -> Result<(), String> {
|
||||||
|
let headers = serde_json::json!({
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = channel_host::http_request(
|
||||||
|
"POST",
|
||||||
|
"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/deleteWebhook",
|
||||||
|
&headers.to_string(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(response) => {
|
||||||
|
if response.status != 200 {
|
||||||
|
let body_str = String::from_utf8_lossy(&response.body);
|
||||||
|
return Err(format!("HTTP {}: {}", response.status, body_str));
|
||||||
|
}
|
||||||
|
|
||||||
|
let api_response: TelegramApiResponse<bool> =
|
||||||
|
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())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
channel_host::log(
|
||||||
|
channel_host::LogLevel::Info,
|
||||||
|
"Webhook deleted successfully (switching to polling mode)",
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err(format!("HTTP request failed: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Register webhook URL with Telegram API.
|
/// Register webhook URL with Telegram API.
|
||||||
///
|
///
|
||||||
/// Called during on_start() when tunnel_url is configured.
|
/// Called during on_start() when tunnel_url is configured.
|
||||||
|
|||||||
@@ -373,7 +373,8 @@ pub struct WasmChannel {
|
|||||||
capabilities: ChannelCapabilities,
|
capabilities: ChannelCapabilities,
|
||||||
|
|
||||||
/// Channel configuration JSON (passed to on_start).
|
/// Channel configuration JSON (passed to on_start).
|
||||||
config_json: String,
|
/// Wrapped in RwLock to allow updating before start.
|
||||||
|
config_json: RwLock<String>,
|
||||||
|
|
||||||
/// Channel configuration returned by on_start.
|
/// Channel configuration returned by on_start.
|
||||||
channel_config: RwLock<Option<ChannelConfig>>,
|
channel_config: RwLock<Option<ChannelConfig>>,
|
||||||
@@ -420,7 +421,7 @@ impl WasmChannel {
|
|||||||
runtime,
|
runtime,
|
||||||
prepared,
|
prepared,
|
||||||
capabilities,
|
capabilities,
|
||||||
config_json,
|
config_json: RwLock::new(config_json),
|
||||||
channel_config: RwLock::new(None),
|
channel_config: RwLock::new(None),
|
||||||
message_tx: Arc::new(RwLock::new(None)),
|
message_tx: Arc::new(RwLock::new(None)),
|
||||||
pending_responses: RwLock::new(HashMap::new()),
|
pending_responses: RwLock::new(HashMap::new()),
|
||||||
@@ -432,6 +433,32 @@ impl WasmChannel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the channel config before starting.
|
||||||
|
///
|
||||||
|
/// Merges the provided values into the existing config JSON.
|
||||||
|
/// Call this before `start()` to inject runtime values like tunnel_url.
|
||||||
|
pub async fn update_config(&self, updates: HashMap<String, serde_json::Value>) {
|
||||||
|
let mut config_guard = self.config_json.write().await;
|
||||||
|
|
||||||
|
// Parse existing config
|
||||||
|
let mut config: HashMap<String, serde_json::Value> =
|
||||||
|
serde_json::from_str(&config_guard).unwrap_or_default();
|
||||||
|
|
||||||
|
// Merge updates
|
||||||
|
for (key, value) in updates {
|
||||||
|
config.insert(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize back
|
||||||
|
*config_guard = serde_json::to_string(&config).unwrap_or_else(|_| "{}".to_string());
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
channel = %self.name,
|
||||||
|
config = %*config_guard,
|
||||||
|
"Updated channel config"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Set a credential for URL injection.
|
/// Set a credential for URL injection.
|
||||||
pub async fn set_credential(&self, name: &str, value: String) {
|
pub async fn set_credential(&self, name: &str, value: String) {
|
||||||
self.credentials
|
self.credentials
|
||||||
@@ -590,7 +617,7 @@ impl WasmChannel {
|
|||||||
let runtime = Arc::clone(&self.runtime);
|
let runtime = Arc::clone(&self.runtime);
|
||||||
let prepared = Arc::clone(&self.prepared);
|
let prepared = Arc::clone(&self.prepared);
|
||||||
let capabilities = self.capabilities.clone();
|
let capabilities = self.capabilities.clone();
|
||||||
let config_json = self.config_json.clone();
|
let config_json = self.config_json.read().await.clone();
|
||||||
let timeout = self.runtime.config().callback_timeout;
|
let timeout = self.runtime.config().callback_timeout;
|
||||||
let channel_name = self.name.clone();
|
let channel_name = self.name.clone();
|
||||||
let credentials = self.get_credentials().await;
|
let credentials = self.get_credentials().await;
|
||||||
|
|||||||
+31
-1
@@ -399,6 +399,36 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let channel_arc = Arc::new(loaded.channel);
|
let channel_arc = Arc::new(loaded.channel);
|
||||||
|
|
||||||
|
// Inject runtime config into the channel (tunnel_url, webhook_secret)
|
||||||
|
// This must be done before start() is called
|
||||||
|
{
|
||||||
|
let mut config_updates = std::collections::HashMap::new();
|
||||||
|
|
||||||
|
if let Some(ref tunnel_url) = config.tunnel.public_url {
|
||||||
|
config_updates.insert(
|
||||||
|
"tunnel_url".to_string(),
|
||||||
|
serde_json::Value::String(tunnel_url.clone()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref secret) = webhook_secret {
|
||||||
|
config_updates.insert(
|
||||||
|
"webhook_secret".to_string(),
|
||||||
|
serde_json::Value::String(secret.clone()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config_updates.is_empty() {
|
||||||
|
channel_arc.update_config(config_updates).await;
|
||||||
|
tracing::info!(
|
||||||
|
channel = %channel_name,
|
||||||
|
has_tunnel = config.tunnel.public_url.is_some(),
|
||||||
|
has_webhook_secret = webhook_secret.is_some(),
|
||||||
|
"Injected runtime config into channel"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
channel = %channel_name,
|
channel = %channel_name,
|
||||||
has_webhook_secret = webhook_secret.is_some(),
|
has_webhook_secret = webhook_secret.is_some(),
|
||||||
@@ -410,7 +440,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.register(
|
.register(
|
||||||
Arc::clone(&channel_arc),
|
Arc::clone(&channel_arc),
|
||||||
endpoints,
|
endpoints,
|
||||||
webhook_secret,
|
webhook_secret.clone(),
|
||||||
secret_header,
|
secret_header,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|||||||
Reference in New Issue
Block a user