mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 23:50:17 +00:00
Add WhatsApp channel WASM module
Implements the sandboxed-channel WIT interface for WhatsApp Cloud API: - Webhook verification (GET with hub.mode=subscribe) - Incoming message handling (POST webhooks) - Outgoing responses via Graph API - Parses WhatsApp webhook payload format Built and tested in Docker sandbox with wasm32-wasip2 target. Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
9d3f993b4e
commit
c14911009f
Generated
+401
@@ -0,0 +1,401 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "leb128"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "spdx"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.220.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e913f9242315ca39eff82aee0e19ee7a372155717ff0eb082c741e435ce25ed1"
|
||||
dependencies = [
|
||||
"leb128",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.220.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "185dfcd27fa5db2e6a23906b54c28199935f71d9a27a1a27b3a88d6fee2afae7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"spdx",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.220.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d07b6a3b550fefa1a914b6d54fc175dd11c3392da11eee604e6ffc759805d25"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bitflags",
|
||||
"hashbrown 0.14.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whatsapp-channel"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a2b3e15cd6068f233926e7d8c7c588b2ec4fb7cc7bf3824115e7c7e2a8485a3"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b632a5a0fa2409489bd49c9e6d99fcc61bb3d4ce9d1907d44662e75a28c71172"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7947d0131c7c9da3f01dfde0ab8bd4c4cf3c5bd49b6dba0ae640f1fa752572ea"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4329de4186ee30e2ef30a0533f9b3c123c019a237a7c82d692807bf1b3ee2697"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "177fb7ee1484d113b4792cc480b1ba57664bbc951b42a4beebe573502135b1fc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.220.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b505603761ed400c90ed30261f44a768317348e49f1864e82ecdc3b2744e5627"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.220.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae2a7999ed18efe59be8de2db9cb2b7f84d88b27818c79353dfc53131840fe1a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "whatsapp-channel"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "WhatsApp channel for near-agent using the Cloud API"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
wit-bindgen = "0.36"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
lto = true
|
||||
strip = true
|
||||
@@ -0,0 +1,266 @@
|
||||
//! WhatsApp Channel for near-agent
|
||||
//!
|
||||
//! Implements the channel interface for WhatsApp Cloud API.
|
||||
//! Handles incoming webhooks and sends responses via the API.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// Generate bindings from the WIT file
|
||||
wit_bindgen::generate!({
|
||||
world: "sandboxed-channel",
|
||||
path: "../../wit/channel.wit",
|
||||
});
|
||||
|
||||
use exports::near::agent::channel::*;
|
||||
use near::agent::channel_host::*;
|
||||
|
||||
struct WhatsAppChannel;
|
||||
|
||||
impl Guest for WhatsAppChannel {
|
||||
fn on_start(config_json: String) -> Result<ChannelConfig, String> {
|
||||
log(LogLevel::Info, &format!("WhatsApp channel starting with config: {}", config_json));
|
||||
|
||||
Ok(ChannelConfig {
|
||||
display_name: "WhatsApp".to_string(),
|
||||
http_endpoints: vec![
|
||||
HttpEndpointConfig {
|
||||
path: "/webhook/whatsapp".to_string(),
|
||||
methods: vec!["GET".to_string(), "POST".to_string()],
|
||||
require_secret: true,
|
||||
},
|
||||
],
|
||||
poll: None, // WhatsApp uses webhooks, not polling
|
||||
})
|
||||
}
|
||||
|
||||
fn on_http_request(req: IncomingHttpRequest) -> OutgoingHttpResponse {
|
||||
log(LogLevel::Debug, &format!("Received {} request to {}", req.method, req.path));
|
||||
|
||||
// Handle webhook verification (GET request)
|
||||
if req.method == "GET" {
|
||||
return handle_verification(&req);
|
||||
}
|
||||
|
||||
// Handle incoming messages (POST request)
|
||||
if req.method == "POST" {
|
||||
return handle_incoming_message(&req);
|
||||
}
|
||||
|
||||
// Method not allowed
|
||||
OutgoingHttpResponse {
|
||||
status: 405,
|
||||
headers_json: r#"{"Content-Type": "text/plain"}"#.to_string(),
|
||||
body: b"Method not allowed".to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_poll() {
|
||||
// WhatsApp uses webhooks, no polling needed
|
||||
}
|
||||
|
||||
fn on_respond(response: AgentResponse) -> Result<(), String> {
|
||||
log(LogLevel::Info, &format!("Sending response to WhatsApp: {}", response.message_id));
|
||||
|
||||
// Parse metadata to get phone number
|
||||
let metadata: ResponseMetadata = serde_json::from_str(&response.metadata_json)
|
||||
.map_err(|e| format!("Failed to parse metadata: {}", e))?;
|
||||
|
||||
// Build WhatsApp API request
|
||||
let phone_number_id = metadata.phone_number_id.ok_or("Missing phone_number_id")?;
|
||||
let recipient = metadata.recipient.ok_or("Missing recipient")?;
|
||||
|
||||
let api_url = format!(
|
||||
"https://graph.facebook.com/v18.0/{}/messages",
|
||||
phone_number_id
|
||||
);
|
||||
|
||||
let request_body = serde_json::json!({
|
||||
"messaging_product": "whatsapp",
|
||||
"to": recipient,
|
||||
"type": "text",
|
||||
"text": {
|
||||
"body": response.content
|
||||
}
|
||||
});
|
||||
|
||||
let body_bytes = serde_json::to_vec(&request_body)
|
||||
.map_err(|e| format!("Failed to serialize request: {}", e))?;
|
||||
|
||||
// Make API request (host will inject the access token)
|
||||
let result = http_request(
|
||||
"POST",
|
||||
&api_url,
|
||||
r#"{"Content-Type": "application/json"}"#,
|
||||
Some(&body_bytes),
|
||||
);
|
||||
|
||||
match result {
|
||||
Ok(resp) if resp.status >= 200 && resp.status < 300 => {
|
||||
log(LogLevel::Info, "Message sent successfully");
|
||||
Ok(())
|
||||
}
|
||||
Ok(resp) => {
|
||||
let body_str = String::from_utf8_lossy(&resp.body);
|
||||
Err(format!("WhatsApp API error {}: {}", resp.status, body_str))
|
||||
}
|
||||
Err(e) => Err(format!("HTTP request failed: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_shutdown() {
|
||||
log(LogLevel::Info, "WhatsApp channel shutting down");
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle WhatsApp webhook verification request
|
||||
fn handle_verification(req: &IncomingHttpRequest) -> OutgoingHttpResponse {
|
||||
// Parse query parameters
|
||||
let query: serde_json::Value = serde_json::from_str(&req.query_json)
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
|
||||
let mode = query.get("hub.mode").and_then(|v| v.as_str());
|
||||
let challenge = query.get("hub.challenge").and_then(|v| v.as_str());
|
||||
|
||||
// WhatsApp sends hub.mode=subscribe for verification
|
||||
if mode == Some("subscribe") {
|
||||
if let Some(challenge) = challenge {
|
||||
log(LogLevel::Info, "Webhook verification successful");
|
||||
return OutgoingHttpResponse {
|
||||
status: 200,
|
||||
headers_json: r#"{"Content-Type": "text/plain"}"#.to_string(),
|
||||
body: challenge.as_bytes().to_vec(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
OutgoingHttpResponse {
|
||||
status: 403,
|
||||
headers_json: r#"{"Content-Type": "text/plain"}"#.to_string(),
|
||||
body: b"Verification failed".to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle incoming WhatsApp message
|
||||
fn handle_incoming_message(req: &IncomingHttpRequest) -> OutgoingHttpResponse {
|
||||
// Parse webhook payload
|
||||
let payload: WebhookPayload = match serde_json::from_slice(&req.body) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
log(LogLevel::Warn, &format!("Failed to parse webhook payload: {}", e));
|
||||
return OutgoingHttpResponse {
|
||||
status: 400,
|
||||
headers_json: r#"{"Content-Type": "text/plain"}"#.to_string(),
|
||||
body: b"Invalid payload".to_vec(),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Process each entry
|
||||
for entry in payload.entry.iter() {
|
||||
for change in entry.changes.iter() {
|
||||
if change.field != "messages" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = &change.value;
|
||||
let phone_number_id = value.metadata.phone_number_id.clone();
|
||||
|
||||
// Process messages
|
||||
for message in value.messages.iter() {
|
||||
// Only handle text messages for now
|
||||
if message.r#type != "text" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let text = message.text.as_ref().map(|t| t.body.clone()).unwrap_or_default();
|
||||
let from = message.from.clone();
|
||||
let msg_id = message.id.clone();
|
||||
|
||||
// Build metadata for response routing
|
||||
let metadata = MessageMetadata {
|
||||
phone_number_id: phone_number_id.clone(),
|
||||
message_id: msg_id.clone(),
|
||||
timestamp: message.timestamp.clone(),
|
||||
};
|
||||
|
||||
// Emit message to the agent
|
||||
emit_message(&EmittedMessage {
|
||||
user_id: from.clone(),
|
||||
user_name: None, // Could look up contact name
|
||||
content: text,
|
||||
thread_id: None,
|
||||
metadata_json: serde_json::to_string(&metadata).unwrap_or_default(),
|
||||
});
|
||||
|
||||
log(LogLevel::Info, &format!("Emitted message from {}", from));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Acknowledge receipt
|
||||
OutgoingHttpResponse {
|
||||
status: 200,
|
||||
headers_json: r#"{"Content-Type": "text/plain"}"#.to_string(),
|
||||
body: b"OK".to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== WhatsApp API Types ====================
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WebhookPayload {
|
||||
entry: Vec<WebhookEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WebhookEntry {
|
||||
changes: Vec<WebhookChange>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WebhookChange {
|
||||
field: String,
|
||||
value: WebhookValue,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WebhookValue {
|
||||
metadata: WhatsAppMetadata,
|
||||
#[serde(default)]
|
||||
messages: Vec<WhatsAppMessage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WhatsAppMetadata {
|
||||
phone_number_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WhatsAppMessage {
|
||||
id: String,
|
||||
from: String,
|
||||
timestamp: String,
|
||||
r#type: String,
|
||||
text: Option<WhatsAppText>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WhatsAppText {
|
||||
body: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct MessageMetadata {
|
||||
phone_number_id: String,
|
||||
message_id: String,
|
||||
timestamp: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ResponseMetadata {
|
||||
phone_number_id: Option<String>,
|
||||
recipient: Option<String>,
|
||||
}
|
||||
|
||||
// Export the channel implementation
|
||||
export!(WhatsAppChannel);
|
||||
Reference in New Issue
Block a user