struct MyChannel;
impl Guest for MyChannel {
/// Called once when the channel starts.
fn on_start(config_json: String) -> Result<ChannelConfig, String> {
let config: MyConfig = serde_json::from_str(&config_json)
.unwrap_or_default();
Ok(ChannelConfig {
display_name: "My Channel".to_string(),
http_endpoints: vec![
HttpEndpointConfig {
path: "/webhook/my-channel".to_string(),
methods: vec!["POST".to_string()],
require_secret: true,
},
],
poll: None, // Or Some(PollConfig { interval_ms, enabled })
})
}
/// Handle incoming HTTP requests (webhooks).
fn on_http_request(req: IncomingHttpRequest) -> OutgoingHttpResponse {
// Validate webhook secret (host validates, but defense in depth)
if !req.secret_validated {
channel_host::log(channel_host::LogLevel::Warn, "Invalid secret");
return OutgoingHttpResponse {
status_code: 401,
headers: vec![],
body: b"Unauthorized".to_vec(),
};
}
// Parse webhook payload
let payload: WebhookPayload = match serde_json::from_slice(&req.body) {
Ok(p) => p,
Err(e) => {
return OutgoingHttpResponse {
status_code: 400,
headers: vec![],
body: format!("Bad request: {}", e).into_bytes(),
};
}
};
// Process messages
for message in payload.messages {
// Store routing info in metadata
let metadata = MessageMetadata {
chat_id: message.chat_id.clone(),
sender_id: message.from.clone(),
message_id: message.id.clone(),
};
// Emit to agent
channel_host::emit_message(&EmittedMessage {
user_id: message.from,
user_name: Some(message.sender_name),
content: message.text,
thread_id: None,
metadata_json: serde_json::to_string(&metadata).unwrap_or_default(),
});
}
OutgoingHttpResponse {
status_code: 200,
headers: vec![],
body: b"OK".to_vec(),
}
}
/// Called periodically if polling is enabled.
fn on_poll() {
// Read last offset
let offset = channel_host::workspace_read("state/offset")
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(0);
// Fetch updates
let url = format!(
"https://api.example.com/updates?offset={}&token={{MY_CHANNEL_TOKEN}}",
offset
);
let response = match channel_host::http_request("GET", &url, "{}", None) {
Ok(r) => r,
Err(e) => {
channel_host::log(channel_host::LogLevel::Error, &e);
return;
}
};
// Parse and emit messages
let updates: Updates = match serde_json::from_str(&response) {
Ok(u) => u,
Err(_) => return,
};
let mut new_offset = offset;
for update in updates.messages {
if update.id >= new_offset {
new_offset = update.id + 1;
}
emit_message(update);
}
// Save new offset
if new_offset != offset {
let _ = channel_host::workspace_write(
"state/offset",
&new_offset.to_string(),
);
}
}
/// Send a response back to the messaging platform.
fn on_respond(response: AgentResponse) -> Result<(), String> {
// Parse metadata from ORIGINAL message
let metadata: MessageMetadata = serde_json::from_str(&response.metadata_json)
.map_err(|e| format!("Invalid metadata: {}", e))?;
// Build API request
let body = serde_json::json!({
"chat_id": metadata.chat_id,
"text": response.content,
});
// Send (credentials auto-injected)
let url = "https://api.example.com/bot{MY_CHANNEL_TOKEN}/sendMessage";
channel_host::http_request(
"POST",
url,
r#"{"Content-Type": "application/json"}"#,
Some(&body.to_string()),
)?;
Ok(())
}
/// Called when channel is shutting down.
fn on_shutdown() {
channel_host::log(channel_host::LogLevel::Info, "Shutting down");
}
}
export!(MyChannel);