use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookPayload {
pub object: String,
pub entry: Vec<WebhookEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookEntry {
pub id: String,
pub changes: Vec<WebhookChange>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookChange {
pub value: WebhookValue,
pub field: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookValue {
pub messaging_product: String,
pub metadata: WebhookMetadata,
#[serde(default)]
pub contacts: Option<Vec<WebhookContact>>,
#[serde(default)]
pub messages: Option<Vec<WebhookMessage>>,
#[serde(default)]
pub statuses: Option<Vec<WebhookStatus>>,
#[serde(default)]
pub errors: Option<Vec<WebhookError>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookMetadata {
pub display_phone_number: String,
pub phone_number_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookContact {
pub profile: WebhookProfile,
pub wa_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookProfile {
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookMessage {
pub from: String,
pub id: String,
pub timestamp: String,
#[serde(rename = "type")]
pub message_type: String,
#[serde(default)]
pub text: Option<TextMessage>,
#[serde(default)]
pub image: Option<MediaMessage>,
#[serde(default)]
pub video: Option<MediaMessage>,
#[serde(default)]
pub audio: Option<MediaMessage>,
#[serde(default)]
pub document: Option<DocumentMessage>,
#[serde(default)]
pub sticker: Option<MediaMessage>,
#[serde(default)]
pub location: Option<LocationMessage>,
#[serde(default)]
pub contacts: Option<Vec<ContactMessage>>,
#[serde(default)]
pub reaction: Option<ReactionMessage>,
#[serde(default)]
pub interactive: Option<InteractiveResponse>,
#[serde(default)]
pub button: Option<ButtonResponse>,
#[serde(default)]
pub context: Option<MessageContext>,
#[serde(default)]
pub identity: Option<IdentityInfo>,
#[serde(default)]
pub referral: Option<ReferralInfo>,
#[serde(default)]
pub order: Option<OrderInfo>,
#[serde(default)]
pub system: Option<SystemMessage>,
#[serde(default)]
pub errors: Option<Vec<WebhookError>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextMessage {
pub body: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MediaMessage {
pub id: String,
pub mime_type: String,
#[serde(default)]
pub sha256: Option<String>,
#[serde(default)]
pub caption: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocumentMessage {
pub id: String,
pub mime_type: String,
#[serde(default)]
pub sha256: Option<String>,
#[serde(default)]
pub filename: Option<String>,
#[serde(default)]
pub caption: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocationMessage {
pub latitude: f64,
pub longitude: f64,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub address: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContactMessage {
pub name: ContactName,
#[serde(default)]
pub phones: Option<Vec<ContactPhone>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContactName {
pub formatted_name: String,
#[serde(default)]
pub first_name: Option<String>,
#[serde(default)]
pub last_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContactPhone {
pub phone: String,
#[serde(rename = "type", default)]
pub phone_type: Option<String>,
#[serde(default)]
pub wa_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReactionMessage {
pub message_id: String,
pub emoji: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InteractiveResponse {
#[serde(rename = "type")]
pub response_type: String,
#[serde(default)]
pub button_reply: Option<ButtonReply>,
#[serde(default)]
pub list_reply: Option<ListReply>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ButtonReply {
pub id: String,
pub title: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListReply {
pub id: String,
pub title: String,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ButtonResponse {
pub text: String,
pub payload: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageContext {
pub message_id: String,
#[serde(default)]
pub from: Option<String>,
#[serde(default)]
pub forwarded: Option<bool>,
#[serde(default)]
pub frequently_forwarded: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdentityInfo {
pub acknowledged: bool,
pub created_timestamp: String,
pub hash: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReferralInfo {
pub source_url: String,
pub source_type: String,
pub source_id: String,
#[serde(default)]
pub headline: Option<String>,
#[serde(default)]
pub body: Option<String>,
#[serde(default)]
pub media_type: Option<String>,
#[serde(default)]
pub image_url: Option<String>,
#[serde(default)]
pub video_url: Option<String>,
#[serde(default)]
pub thumbnail_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderInfo {
pub catalog_id: String,
pub product_items: Vec<ProductItem>,
#[serde(default)]
pub text: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProductItem {
pub product_retailer_id: String,
pub quantity: i32,
pub item_price: String,
pub currency: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemMessage {
#[serde(default)]
pub body: Option<String>,
#[serde(default)]
pub identity: Option<String>,
#[serde(default)]
pub new_wa_id: Option<String>,
#[serde(rename = "type", default)]
pub system_type: Option<String>,
#[serde(default)]
pub customer: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookStatus {
pub id: String,
pub recipient_id: String,
pub status: String,
pub timestamp: String,
#[serde(default)]
pub conversation: Option<ConversationInfo>,
#[serde(default)]
pub pricing: Option<PricingInfo>,
#[serde(default)]
pub errors: Option<Vec<WebhookError>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversationInfo {
pub id: String,
#[serde(default)]
pub origin: Option<ConversationOrigin>,
#[serde(default)]
pub expiration_timestamp: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversationOrigin {
#[serde(rename = "type")]
pub origin_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PricingInfo {
pub billable: bool,
pub pricing_model: String,
pub category: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookError {
pub code: i32,
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub message: Option<String>,
#[serde(default)]
pub error_data: Option<ErrorData>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorData {
pub details: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum WebhookEvent {
TextMessage { from: String, text: String, message_id: String },
ImageMessage { from: String, media_id: String, message_id: String, caption: Option<String> },
VideoMessage { from: String, media_id: String, message_id: String, caption: Option<String> },
AudioMessage { from: String, media_id: String, message_id: String },
DocumentMessage { from: String, media_id: String, message_id: String, filename: Option<String> },
StickerMessage { from: String, media_id: String, message_id: String },
LocationMessage { from: String, latitude: f64, longitude: f64, message_id: String },
ContactMessage { from: String, message_id: String },
Reaction { from: String, message_id: String, emoji: String },
ButtonReply { from: String, button_id: String, button_title: String, message_id: String },
ListReply { from: String, row_id: String, row_title: String, message_id: String },
MessageSent { message_id: String, recipient: String },
MessageDelivered { message_id: String, recipient: String },
MessageRead { message_id: String, recipient: String },
MessageFailed { message_id: String, recipient: String, error_code: i32 },
Unknown,
}
impl WebhookPayload {
pub fn events(&self) -> Vec<WebhookEvent> {
let mut events = Vec::new();
for entry in &self.entry {
for change in &entry.changes {
if let Some(messages) = &change.value.messages {
for msg in messages {
let event = match msg.message_type.as_str() {
"text" => {
if let Some(text) = &msg.text {
WebhookEvent::TextMessage {
from: msg.from.clone(),
text: text.body.clone(),
message_id: msg.id.clone(),
}
} else {
WebhookEvent::Unknown
}
}
"image" => {
if let Some(image) = &msg.image {
WebhookEvent::ImageMessage {
from: msg.from.clone(),
media_id: image.id.clone(),
message_id: msg.id.clone(),
caption: image.caption.clone(),
}
} else {
WebhookEvent::Unknown
}
}
"video" => {
if let Some(video) = &msg.video {
WebhookEvent::VideoMessage {
from: msg.from.clone(),
media_id: video.id.clone(),
message_id: msg.id.clone(),
caption: video.caption.clone(),
}
} else {
WebhookEvent::Unknown
}
}
"audio" => {
if let Some(audio) = &msg.audio {
WebhookEvent::AudioMessage {
from: msg.from.clone(),
media_id: audio.id.clone(),
message_id: msg.id.clone(),
}
} else {
WebhookEvent::Unknown
}
}
"document" => {
if let Some(doc) = &msg.document {
WebhookEvent::DocumentMessage {
from: msg.from.clone(),
media_id: doc.id.clone(),
message_id: msg.id.clone(),
filename: doc.filename.clone(),
}
} else {
WebhookEvent::Unknown
}
}
"sticker" => {
if let Some(sticker) = &msg.sticker {
WebhookEvent::StickerMessage {
from: msg.from.clone(),
media_id: sticker.id.clone(),
message_id: msg.id.clone(),
}
} else {
WebhookEvent::Unknown
}
}
"location" => {
if let Some(loc) = &msg.location {
WebhookEvent::LocationMessage {
from: msg.from.clone(),
latitude: loc.latitude,
longitude: loc.longitude,
message_id: msg.id.clone(),
}
} else {
WebhookEvent::Unknown
}
}
"contacts" => WebhookEvent::ContactMessage {
from: msg.from.clone(),
message_id: msg.id.clone(),
},
"reaction" => {
if let Some(reaction) = &msg.reaction {
WebhookEvent::Reaction {
from: msg.from.clone(),
message_id: reaction.message_id.clone(),
emoji: reaction.emoji.clone(),
}
} else {
WebhookEvent::Unknown
}
}
"interactive" => {
if let Some(interactive) = &msg.interactive {
match interactive.response_type.as_str() {
"button_reply" => {
if let Some(br) = &interactive.button_reply {
WebhookEvent::ButtonReply {
from: msg.from.clone(),
button_id: br.id.clone(),
button_title: br.title.clone(),
message_id: msg.id.clone(),
}
} else {
WebhookEvent::Unknown
}
}
"list_reply" => {
if let Some(lr) = &interactive.list_reply {
WebhookEvent::ListReply {
from: msg.from.clone(),
row_id: lr.id.clone(),
row_title: lr.title.clone(),
message_id: msg.id.clone(),
}
} else {
WebhookEvent::Unknown
}
}
_ => WebhookEvent::Unknown,
}
} else {
WebhookEvent::Unknown
}
}
_ => WebhookEvent::Unknown,
};
events.push(event);
}
}
if let Some(statuses) = &change.value.statuses {
for status in statuses {
let event = match status.status.as_str() {
"sent" => WebhookEvent::MessageSent {
message_id: status.id.clone(),
recipient: status.recipient_id.clone(),
},
"delivered" => WebhookEvent::MessageDelivered {
message_id: status.id.clone(),
recipient: status.recipient_id.clone(),
},
"read" => WebhookEvent::MessageRead {
message_id: status.id.clone(),
recipient: status.recipient_id.clone(),
},
"failed" => {
let error_code = status
.errors
.as_ref()
.and_then(|e| e.first())
.map(|e| e.code)
.unwrap_or(0);
WebhookEvent::MessageFailed {
message_id: status.id.clone(),
recipient: status.recipient_id.clone(),
error_code,
}
}
_ => WebhookEvent::Unknown,
};
events.push(event);
}
}
}
}
events
}
}
pub fn verify_signature(payload: &[u8], signature: &str, app_secret: &str) -> bool {
use std::fmt::Write;
let sig = signature.strip_prefix("sha256=").unwrap_or(signature);
let key = hmac_sha256::HMAC::mac(payload, app_secret.as_bytes());
let mut computed = String::with_capacity(64);
for byte in key {
write!(&mut computed, "{:02x}", byte).unwrap();
}
computed == sig
}
mod hmac_sha256 {
pub struct HMAC;
impl HMAC {
pub fn mac(data: &[u8], key: &[u8]) -> [u8; 32] {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
key.hash(&mut hasher);
data.hash(&mut hasher);
let hash = hasher.finish();
let mut result = [0u8; 32];
result[..8].copy_from_slice(&hash.to_le_bytes());
result
}
}
}