2025-11-22 22:55:35 -03:00
|
|
|
use async_trait::async_trait;
|
2025-11-26 22:54:22 -03:00
|
|
|
use log::{error, info};
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
2025-11-27 13:53:00 -03:00
|
|
|
use uuid::Uuid;
|
2025-11-26 22:54:22 -03:00
|
|
|
|
|
|
|
|
use crate::core::bot::channels::ChannelAdapter;
|
2025-11-27 13:53:00 -03:00
|
|
|
use crate::core::config::ConfigManager;
|
2025-11-26 22:54:22 -03:00
|
|
|
use crate::shared::models::BotResponse;
|
2025-11-27 13:53:00 -03:00
|
|
|
use crate::shared::utils::DbPool;
|
2025-11-22 22:55:35 -03:00
|
|
|
|
2025-11-27 09:38:50 -03:00
|
|
|
#[derive(Debug)]
|
2025-11-22 22:55:35 -03:00
|
|
|
pub struct WhatsAppAdapter {
|
2025-11-26 22:54:22 -03:00
|
|
|
api_key: String,
|
|
|
|
|
phone_number_id: String,
|
|
|
|
|
webhook_verify_token: String,
|
|
|
|
|
business_account_id: String,
|
|
|
|
|
api_version: String,
|
2025-11-22 22:55:35 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl WhatsAppAdapter {
|
2025-11-27 13:53:00 -03:00
|
|
|
pub fn new(pool: DbPool, bot_id: Uuid) -> Self {
|
|
|
|
|
let config_manager = ConfigManager::new(pool);
|
|
|
|
|
|
|
|
|
|
// Load from bot_configuration table with fallback to environment variables
|
|
|
|
|
let api_key = config_manager
|
|
|
|
|
.get_config(&bot_id, "whatsapp-api-key", None)
|
|
|
|
|
.unwrap_or_else(|_| std::env::var("WHATSAPP_API_KEY").unwrap_or_default());
|
|
|
|
|
|
|
|
|
|
let phone_number_id = config_manager
|
|
|
|
|
.get_config(&bot_id, "whatsapp-phone-number-id", None)
|
|
|
|
|
.unwrap_or_else(|_| std::env::var("WHATSAPP_PHONE_NUMBER_ID").unwrap_or_default());
|
|
|
|
|
|
|
|
|
|
let webhook_verify_token = config_manager
|
|
|
|
|
.get_config(&bot_id, "whatsapp-verify-token", Some("webhook_verify"))
|
|
|
|
|
.unwrap_or_else(|_| {
|
|
|
|
|
std::env::var("WHATSAPP_VERIFY_TOKEN")
|
|
|
|
|
.unwrap_or_else(|_| "webhook_verify".to_string())
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let business_account_id = config_manager
|
|
|
|
|
.get_config(&bot_id, "whatsapp-business-account-id", None)
|
|
|
|
|
.unwrap_or_else(|_| std::env::var("WHATSAPP_BUSINESS_ACCOUNT_ID").unwrap_or_default());
|
|
|
|
|
|
|
|
|
|
let api_version = config_manager
|
|
|
|
|
.get_config(&bot_id, "whatsapp-api-version", Some("v17.0"))
|
|
|
|
|
.unwrap_or_else(|_| "v17.0".to_string());
|
2025-11-26 22:54:22 -03:00
|
|
|
|
|
|
|
|
Self {
|
|
|
|
|
api_key,
|
|
|
|
|
phone_number_id,
|
|
|
|
|
webhook_verify_token,
|
|
|
|
|
business_account_id,
|
|
|
|
|
api_version,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn send_whatsapp_message(
|
|
|
|
|
&self,
|
|
|
|
|
to: &str,
|
|
|
|
|
message: &str,
|
|
|
|
|
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
|
|
|
|
let client = reqwest::Client::new();
|
|
|
|
|
|
|
|
|
|
let url = format!(
|
|
|
|
|
"https://graph.facebook.com/{}/{}/messages",
|
|
|
|
|
self.api_version, self.phone_number_id
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let payload = serde_json::json!({
|
|
|
|
|
"messaging_product": "whatsapp",
|
|
|
|
|
"to": to,
|
|
|
|
|
"type": "text",
|
|
|
|
|
"text": {
|
|
|
|
|
"body": message
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let response = client
|
|
|
|
|
.post(&url)
|
|
|
|
|
.header("Authorization", format!("Bearer {}", self.api_key))
|
|
|
|
|
.header("Content-Type", "application/json")
|
|
|
|
|
.json(&payload)
|
|
|
|
|
.send()
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
if response.status().is_success() {
|
|
|
|
|
let result: serde_json::Value = response.json().await?;
|
|
|
|
|
Ok(result["messages"][0]["id"]
|
|
|
|
|
.as_str()
|
|
|
|
|
.unwrap_or("")
|
|
|
|
|
.to_string())
|
|
|
|
|
} else {
|
|
|
|
|
let error_text = response.text().await?;
|
|
|
|
|
Err(format!("WhatsApp API error: {}", error_text).into())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn send_template_message(
|
|
|
|
|
&self,
|
|
|
|
|
to: &str,
|
|
|
|
|
template_name: &str,
|
|
|
|
|
language_code: &str,
|
2025-11-27 13:53:00 -03:00
|
|
|
components: Vec<serde_json::Value>,
|
2025-11-26 22:54:22 -03:00
|
|
|
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
|
|
|
|
let client = reqwest::Client::new();
|
|
|
|
|
|
|
|
|
|
let url = format!(
|
|
|
|
|
"https://graph.facebook.com/{}/{}/messages",
|
|
|
|
|
self.api_version, self.phone_number_id
|
|
|
|
|
);
|
|
|
|
|
|
2025-11-27 13:53:00 -03:00
|
|
|
let mut payload = serde_json::json!({
|
2025-11-26 22:54:22 -03:00
|
|
|
"messaging_product": "whatsapp",
|
|
|
|
|
"to": to,
|
|
|
|
|
"type": "template",
|
|
|
|
|
"template": {
|
|
|
|
|
"name": template_name,
|
|
|
|
|
"language": {
|
|
|
|
|
"code": language_code
|
2025-11-27 13:53:00 -03:00
|
|
|
}
|
2025-11-26 22:54:22 -03:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-27 13:53:00 -03:00
|
|
|
if !components.is_empty() {
|
|
|
|
|
payload["template"]["components"] = serde_json::Value::Array(components);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-26 22:54:22 -03:00
|
|
|
let response = client
|
|
|
|
|
.post(&url)
|
|
|
|
|
.header("Authorization", format!("Bearer {}", self.api_key))
|
|
|
|
|
.header("Content-Type", "application/json")
|
|
|
|
|
.json(&payload)
|
|
|
|
|
.send()
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
if response.status().is_success() {
|
|
|
|
|
let result: serde_json::Value = response.json().await?;
|
|
|
|
|
Ok(result["messages"][0]["id"]
|
|
|
|
|
.as_str()
|
|
|
|
|
.unwrap_or("")
|
|
|
|
|
.to_string())
|
|
|
|
|
} else {
|
|
|
|
|
let error_text = response.text().await?;
|
|
|
|
|
Err(format!("WhatsApp API error: {}", error_text).into())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn send_media_message(
|
|
|
|
|
&self,
|
|
|
|
|
to: &str,
|
|
|
|
|
media_url: &str,
|
2025-11-27 13:53:00 -03:00
|
|
|
media_type: &str,
|
2025-11-26 22:54:22 -03:00
|
|
|
caption: Option<&str>,
|
|
|
|
|
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
|
|
|
|
let client = reqwest::Client::new();
|
|
|
|
|
|
|
|
|
|
let url = format!(
|
|
|
|
|
"https://graph.facebook.com/{}/{}/messages",
|
|
|
|
|
self.api_version, self.phone_number_id
|
|
|
|
|
);
|
|
|
|
|
|
2025-11-27 13:53:00 -03:00
|
|
|
let media_object = match media_type {
|
|
|
|
|
"image" => {
|
|
|
|
|
let mut obj = serde_json::json!({
|
|
|
|
|
"link": media_url
|
|
|
|
|
});
|
|
|
|
|
if let Some(cap) = caption {
|
|
|
|
|
obj["caption"] = serde_json::Value::String(cap.to_string());
|
|
|
|
|
}
|
|
|
|
|
obj
|
|
|
|
|
}
|
|
|
|
|
"video" => {
|
|
|
|
|
let mut obj = serde_json::json!({
|
|
|
|
|
"link": media_url
|
|
|
|
|
});
|
|
|
|
|
if let Some(cap) = caption {
|
|
|
|
|
obj["caption"] = serde_json::Value::String(cap.to_string());
|
|
|
|
|
}
|
|
|
|
|
obj
|
|
|
|
|
}
|
|
|
|
|
"audio" => serde_json::json!({
|
|
|
|
|
"link": media_url
|
|
|
|
|
}),
|
|
|
|
|
"document" => {
|
|
|
|
|
let mut obj = serde_json::json!({
|
|
|
|
|
"link": media_url
|
|
|
|
|
});
|
|
|
|
|
if let Some(cap) = caption {
|
|
|
|
|
obj["filename"] = serde_json::Value::String(cap.to_string());
|
|
|
|
|
}
|
|
|
|
|
obj
|
|
|
|
|
}
|
|
|
|
|
_ => serde_json::json!({
|
|
|
|
|
"link": media_url
|
|
|
|
|
}),
|
|
|
|
|
};
|
2025-11-26 22:54:22 -03:00
|
|
|
|
|
|
|
|
let payload = serde_json::json!({
|
|
|
|
|
"messaging_product": "whatsapp",
|
|
|
|
|
"to": to,
|
|
|
|
|
"type": media_type,
|
|
|
|
|
media_type: media_object
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let response = client
|
|
|
|
|
.post(&url)
|
|
|
|
|
.header("Authorization", format!("Bearer {}", self.api_key))
|
|
|
|
|
.header("Content-Type", "application/json")
|
|
|
|
|
.json(&payload)
|
|
|
|
|
.send()
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
if response.status().is_success() {
|
|
|
|
|
let result: serde_json::Value = response.json().await?;
|
|
|
|
|
Ok(result["messages"][0]["id"]
|
|
|
|
|
.as_str()
|
|
|
|
|
.unwrap_or("")
|
|
|
|
|
.to_string())
|
|
|
|
|
} else {
|
|
|
|
|
let error_text = response.text().await?;
|
|
|
|
|
Err(format!("WhatsApp API error: {}", error_text).into())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 13:53:00 -03:00
|
|
|
pub async fn send_location_message(
|
2025-11-27 09:38:50 -03:00
|
|
|
&self,
|
2025-11-27 13:53:00 -03:00
|
|
|
to: &str,
|
|
|
|
|
latitude: f64,
|
|
|
|
|
longitude: f64,
|
|
|
|
|
name: Option<&str>,
|
|
|
|
|
address: Option<&str>,
|
2025-11-27 09:38:50 -03:00
|
|
|
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
|
|
|
|
let client = reqwest::Client::new();
|
|
|
|
|
|
|
|
|
|
let url = format!(
|
2025-11-27 13:53:00 -03:00
|
|
|
"https://graph.facebook.com/{}/{}/messages",
|
|
|
|
|
self.api_version, self.phone_number_id
|
2025-11-27 09:38:50 -03:00
|
|
|
);
|
|
|
|
|
|
2025-11-27 13:53:00 -03:00
|
|
|
let mut location = serde_json::json!({
|
|
|
|
|
"latitude": latitude,
|
|
|
|
|
"longitude": longitude
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if let Some(n) = name {
|
|
|
|
|
location["name"] = serde_json::Value::String(n.to_string());
|
|
|
|
|
}
|
|
|
|
|
if let Some(a) = address {
|
|
|
|
|
location["address"] = serde_json::Value::String(a.to_string());
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 09:38:50 -03:00
|
|
|
let payload = serde_json::json!({
|
2025-11-27 13:53:00 -03:00
|
|
|
"messaging_product": "whatsapp",
|
|
|
|
|
"to": to,
|
|
|
|
|
"type": "location",
|
|
|
|
|
"location": location
|
2025-11-27 09:38:50 -03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let response = client
|
|
|
|
|
.post(&url)
|
|
|
|
|
.header("Authorization", format!("Bearer {}", self.api_key))
|
2025-11-27 13:53:00 -03:00
|
|
|
.header("Content-Type", "application/json")
|
2025-11-27 09:38:50 -03:00
|
|
|
.json(&payload)
|
|
|
|
|
.send()
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
if response.status().is_success() {
|
|
|
|
|
let result: serde_json::Value = response.json().await?;
|
2025-11-27 13:53:00 -03:00
|
|
|
Ok(result["messages"][0]["id"]
|
|
|
|
|
.as_str()
|
|
|
|
|
.unwrap_or("")
|
|
|
|
|
.to_string())
|
2025-11-27 09:38:50 -03:00
|
|
|
} else {
|
|
|
|
|
let error_text = response.text().await?;
|
2025-11-27 13:53:00 -03:00
|
|
|
Err(format!("WhatsApp API error: {}", error_text).into())
|
2025-11-27 09:38:50 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 13:53:00 -03:00
|
|
|
pub async fn mark_message_as_read(
|
2025-11-27 09:38:50 -03:00
|
|
|
&self,
|
2025-11-27 13:53:00 -03:00
|
|
|
message_id: &str,
|
|
|
|
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
2025-11-27 09:38:50 -03:00
|
|
|
let client = reqwest::Client::new();
|
|
|
|
|
|
|
|
|
|
let url = format!(
|
2025-11-27 13:53:00 -03:00
|
|
|
"https://graph.facebook.com/{}/{}/messages",
|
|
|
|
|
self.api_version, self.phone_number_id
|
2025-11-27 09:38:50 -03:00
|
|
|
);
|
|
|
|
|
|
2025-11-27 13:53:00 -03:00
|
|
|
let payload = serde_json::json!({
|
|
|
|
|
"messaging_product": "whatsapp",
|
|
|
|
|
"status": "read",
|
|
|
|
|
"message_id": message_id
|
|
|
|
|
});
|
2025-11-27 09:38:50 -03:00
|
|
|
|
|
|
|
|
let response = client
|
|
|
|
|
.post(&url)
|
|
|
|
|
.header("Authorization", format!("Bearer {}", self.api_key))
|
2025-11-27 13:53:00 -03:00
|
|
|
.header("Content-Type", "application/json")
|
|
|
|
|
.json(&payload)
|
2025-11-27 09:38:50 -03:00
|
|
|
.send()
|
|
|
|
|
.await?;
|
|
|
|
|
|
2025-11-27 13:53:00 -03:00
|
|
|
if !response.status().is_success() {
|
2025-11-27 09:38:50 -03:00
|
|
|
let error_text = response.text().await?;
|
2025-11-27 13:53:00 -03:00
|
|
|
return Err(format!("Failed to mark message as read: {}", error_text).into());
|
2025-11-27 09:38:50 -03:00
|
|
|
}
|
2025-11-27 13:53:00 -03:00
|
|
|
|
|
|
|
|
Ok(())
|
2025-11-27 09:38:50 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-27 13:53:00 -03:00
|
|
|
pub async fn get_business_profile(&self) -> Result<WhatsAppBusinessProfile, Box<dyn std::error::Error + Send + Sync>> {
|
2025-11-27 09:38:50 -03:00
|
|
|
let client = reqwest::Client::new();
|
|
|
|
|
|
|
|
|
|
let url = format!(
|
|
|
|
|
"https://graph.facebook.com/{}/{}/whatsapp_business_profile",
|
2025-11-27 13:53:00 -03:00
|
|
|
self.api_version, self.phone_number_id
|
2025-11-27 09:38:50 -03:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let response = client
|
|
|
|
|
.get(&url)
|
|
|
|
|
.header("Authorization", format!("Bearer {}", self.api_key))
|
2025-11-27 13:53:00 -03:00
|
|
|
.query(&[("fields", "about,address,description,email,profile_picture_url,websites,vertical")])
|
2025-11-27 09:38:50 -03:00
|
|
|
.send()
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
if response.status().is_success() {
|
2025-11-27 13:53:00 -03:00
|
|
|
let profiles: serde_json::Value = response.json().await?;
|
|
|
|
|
if let Some(data) = profiles["data"].as_array() {
|
|
|
|
|
if let Some(first_profile) = data.first() {
|
|
|
|
|
let profile: WhatsAppBusinessProfile = serde_json::from_value(first_profile.clone())?;
|
|
|
|
|
return Ok(profile);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err("No business profile found".into())
|
2025-11-27 09:38:50 -03:00
|
|
|
} else {
|
|
|
|
|
let error_text = response.text().await?;
|
|
|
|
|
Err(format!("Failed to get business profile: {}", error_text).into())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 13:53:00 -03:00
|
|
|
pub async fn upload_media(
|
2025-11-27 09:38:50 -03:00
|
|
|
&self,
|
2025-11-27 13:53:00 -03:00
|
|
|
file_path: &str,
|
|
|
|
|
mime_type: &str,
|
|
|
|
|
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
2025-11-27 09:38:50 -03:00
|
|
|
let client = reqwest::Client::new();
|
|
|
|
|
|
|
|
|
|
let url = format!(
|
2025-11-27 13:53:00 -03:00
|
|
|
"https://graph.facebook.com/{}/{}/media",
|
|
|
|
|
self.api_version, self.phone_number_id
|
2025-11-27 09:38:50 -03:00
|
|
|
);
|
|
|
|
|
|
2025-11-27 13:53:00 -03:00
|
|
|
let file_data = tokio::fs::read(file_path).await?;
|
|
|
|
|
|
|
|
|
|
let part = reqwest::multipart::Part::bytes(file_data)
|
|
|
|
|
.mime_str(mime_type)?
|
|
|
|
|
.file_name(file_path.to_string());
|
|
|
|
|
|
|
|
|
|
let form = reqwest::multipart::Form::new()
|
|
|
|
|
.part("file", part)
|
|
|
|
|
.text("messaging_product", "whatsapp");
|
|
|
|
|
|
2025-11-27 09:38:50 -03:00
|
|
|
let response = client
|
|
|
|
|
.post(&url)
|
|
|
|
|
.header("Authorization", format!("Bearer {}", self.api_key))
|
2025-11-27 13:53:00 -03:00
|
|
|
.multipart(form)
|
2025-11-27 09:38:50 -03:00
|
|
|
.send()
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
if response.status().is_success() {
|
2025-11-27 13:53:00 -03:00
|
|
|
let result: serde_json::Value = response.json().await?;
|
|
|
|
|
Ok(result["id"].as_str().unwrap_or("").to_string())
|
2025-11-27 09:38:50 -03:00
|
|
|
} else {
|
|
|
|
|
let error_text = response.text().await?;
|
2025-11-27 13:53:00 -03:00
|
|
|
Err(format!("Failed to upload media: {}", error_text).into())
|
2025-11-27 09:38:50 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 13:53:00 -03:00
|
|
|
pub fn verify_webhook(&self, token: &str) -> bool {
|
|
|
|
|
token == self.webhook_verify_token
|
|
|
|
|
}
|
2025-11-27 09:38:50 -03:00
|
|
|
|
2025-11-27 13:53:00 -03:00
|
|
|
pub async fn handle_webhook_verification(
|
|
|
|
|
&self,
|
|
|
|
|
mode: &str,
|
|
|
|
|
token: &str,
|
|
|
|
|
challenge: &str,
|
|
|
|
|
) -> Option<String> {
|
|
|
|
|
if mode == "subscribe" && self.verify_webhook(token) {
|
|
|
|
|
Some(challenge.to_string())
|
2025-11-27 09:38:50 -03:00
|
|
|
} else {
|
2025-11-27 13:53:00 -03:00
|
|
|
None
|
2025-11-27 09:38:50 -03:00
|
|
|
}
|
|
|
|
|
}
|
2025-11-22 22:55:35 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[async_trait]
|
2025-11-26 22:54:22 -03:00
|
|
|
impl ChannelAdapter for WhatsAppAdapter {
|
|
|
|
|
fn name(&self) -> &str {
|
|
|
|
|
"WhatsApp"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn is_configured(&self) -> bool {
|
|
|
|
|
!self.api_key.is_empty() && !self.phone_number_id.is_empty()
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-22 22:55:35 -03:00
|
|
|
async fn send_message(
|
|
|
|
|
&self,
|
|
|
|
|
response: BotResponse,
|
|
|
|
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
2025-11-26 22:54:22 -03:00
|
|
|
if !self.is_configured() {
|
|
|
|
|
error!("WhatsApp adapter not configured. Please set whatsapp-api-key and whatsapp-phone-number-id in config.csv");
|
|
|
|
|
return Err("WhatsApp not configured".into());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let message_id = self
|
|
|
|
|
.send_whatsapp_message(&response.user_id, &response.content)
|
|
|
|
|
.await?;
|
|
|
|
|
|
2025-11-22 22:55:35 -03:00
|
|
|
info!(
|
2025-11-26 22:54:22 -03:00
|
|
|
"WhatsApp message sent to {}: {} (message_id: {})",
|
|
|
|
|
response.user_id, response.content, message_id
|
2025-11-22 22:55:35 -03:00
|
|
|
);
|
2025-11-26 22:54:22 -03:00
|
|
|
|
2025-11-22 22:55:35 -03:00
|
|
|
Ok(())
|
|
|
|
|
}
|
2025-11-26 22:54:22 -03:00
|
|
|
|
|
|
|
|
async fn receive_message(
|
|
|
|
|
&self,
|
|
|
|
|
payload: serde_json::Value,
|
|
|
|
|
) -> Result<Option<String>, Box<dyn std::error::Error + Send + Sync>> {
|
|
|
|
|
// Parse WhatsApp webhook payload
|
|
|
|
|
if let Some(entry) = payload["entry"].as_array() {
|
|
|
|
|
if let Some(first_entry) = entry.first() {
|
|
|
|
|
if let Some(changes) = first_entry["changes"].as_array() {
|
|
|
|
|
if let Some(first_change) = changes.first() {
|
|
|
|
|
if let Some(messages) = first_change["value"]["messages"].as_array() {
|
|
|
|
|
if let Some(first_message) = messages.first() {
|
2025-11-27 13:53:00 -03:00
|
|
|
// Mark message as read
|
|
|
|
|
if let Some(message_id) = first_message["id"].as_str() {
|
|
|
|
|
let _ = self.mark_message_as_read(message_id).await;
|
|
|
|
|
}
|
2025-11-26 22:54:22 -03:00
|
|
|
|
2025-11-27 13:53:00 -03:00
|
|
|
// Extract message content based on type
|
|
|
|
|
let message_type = first_message["type"].as_str().unwrap_or("unknown");
|
|
|
|
|
|
|
|
|
|
return match message_type {
|
2025-11-26 22:54:22 -03:00
|
|
|
"text" => {
|
2025-11-27 13:53:00 -03:00
|
|
|
Ok(first_message["text"]["body"].as_str().map(|s| s.to_string()))
|
|
|
|
|
}
|
|
|
|
|
"image" | "video" | "audio" | "document" => {
|
|
|
|
|
let caption = first_message[message_type]["caption"]
|
2025-11-26 22:54:22 -03:00
|
|
|
.as_str()
|
2025-11-27 13:53:00 -03:00
|
|
|
.unwrap_or("");
|
|
|
|
|
Ok(Some(format!("Received {} with caption: {}", message_type, caption)))
|
2025-11-26 22:54:22 -03:00
|
|
|
}
|
2025-11-27 13:53:00 -03:00
|
|
|
"location" => {
|
|
|
|
|
let lat = first_message["location"]["latitude"].as_f64().unwrap_or(0.0);
|
|
|
|
|
let lon = first_message["location"]["longitude"].as_f64().unwrap_or(0.0);
|
|
|
|
|
Ok(Some(format!("Location: {}, {}", lat, lon)))
|
2025-11-26 22:54:22 -03:00
|
|
|
}
|
2025-11-27 13:53:00 -03:00
|
|
|
"button" => {
|
|
|
|
|
Ok(first_message["button"]["text"].as_str().map(|s| s.to_string()))
|
2025-11-26 22:54:22 -03:00
|
|
|
}
|
2025-11-27 13:53:00 -03:00
|
|
|
"interactive" => {
|
|
|
|
|
if let Some(button_reply) = first_message["interactive"]["button_reply"].as_object() {
|
|
|
|
|
Ok(button_reply["id"].as_str().map(|s| s.to_string()))
|
|
|
|
|
} else if let Some(list_reply) = first_message["interactive"]["list_reply"].as_object() {
|
|
|
|
|
Ok(list_reply["id"].as_str().map(|s| s.to_string()))
|
|
|
|
|
} else {
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => Ok(None)
|
|
|
|
|
};
|
2025-11-26 22:54:22 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn get_user_info(
|
|
|
|
|
&self,
|
|
|
|
|
user_id: &str,
|
|
|
|
|
) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
|
2025-11-27 13:53:00 -03:00
|
|
|
// WhatsApp doesn't provide user profile info via API for privacy reasons
|
|
|
|
|
// Return basic info
|
|
|
|
|
Ok(serde_json::json!({
|
|
|
|
|
"id": user_id,
|
|
|
|
|
"platform": "whatsapp"
|
|
|
|
|
}))
|
2025-11-26 22:54:22 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 13:53:00 -03:00
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct WhatsAppBusinessProfile {
|
|
|
|
|
pub about: Option<String>,
|
|
|
|
|
pub address: Option<String>,
|
|
|
|
|
pub description: Option<String>,
|
|
|
|
|
pub email: Option<String>,
|
|
|
|
|
pub profile_picture_url: Option<String>,
|
|
|
|
|
pub websites: Option<Vec<String>>,
|
|
|
|
|
pub vertical: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-26 22:54:22 -03:00
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct WhatsAppWebhookPayload {
|
2025-11-27 13:53:00 -03:00
|
|
|
pub object: String,
|
2025-11-26 22:54:22 -03:00
|
|
|
pub entry: Vec<WhatsAppEntry>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct WhatsAppEntry {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub changes: Vec<WhatsAppChange>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct WhatsAppChange {
|
|
|
|
|
pub field: String,
|
|
|
|
|
pub value: WhatsAppValue,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct WhatsAppValue {
|
|
|
|
|
pub messaging_product: String,
|
|
|
|
|
pub metadata: WhatsAppMetadata,
|
2025-11-27 13:53:00 -03:00
|
|
|
pub contacts: Option<Vec<WhatsAppContact>>,
|
2025-11-26 22:54:22 -03:00
|
|
|
pub messages: Option<Vec<WhatsAppMessage>>,
|
|
|
|
|
pub statuses: Option<Vec<WhatsAppStatus>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct WhatsAppMetadata {
|
|
|
|
|
pub display_phone_number: String,
|
|
|
|
|
pub phone_number_id: String,
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 13:53:00 -03:00
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct WhatsAppContact {
|
|
|
|
|
pub profile: WhatsAppProfile,
|
|
|
|
|
pub wa_id: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct WhatsAppProfile {
|
|
|
|
|
pub name: String,
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-26 22:54:22 -03:00
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct WhatsAppMessage {
|
|
|
|
|
pub from: String,
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub timestamp: String,
|
|
|
|
|
#[serde(rename = "type")]
|
|
|
|
|
pub message_type: String,
|
|
|
|
|
pub text: Option<WhatsAppText>,
|
2025-11-27 13:53:00 -03:00
|
|
|
pub image: Option<WhatsAppMedia>,
|
|
|
|
|
pub video: Option<WhatsAppMedia>,
|
|
|
|
|
pub audio: Option<WhatsAppMedia>,
|
|
|
|
|
pub document: Option<WhatsAppMedia>,
|
|
|
|
|
pub location: Option<WhatsAppLocation>,
|
|
|
|
|
pub button: Option<WhatsAppButton>,
|
|
|
|
|
pub interactive: Option<WhatsAppInteractive>,
|
2025-11-26 22:54:22 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct WhatsAppText {
|
|
|
|
|
pub body: String,
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 13:53:00 -03:00
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct WhatsAppMedia {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub mime_type: Option<String>,
|
|
|
|
|
pub sha256: Option<String>,
|
|
|
|
|
pub caption: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct WhatsAppLocation {
|
|
|
|
|
pub latitude: f64,
|
|
|
|
|
pub longitude: f64,
|
|
|
|
|
pub name: Option<String>,
|
|
|
|
|
pub address: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct WhatsAppButton {
|
|
|
|
|
pub text: String,
|
|
|
|
|
pub payload: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct WhatsAppInteractive {
|
|
|
|
|
#[serde(rename = "type")]
|
|
|
|
|
pub interactive_type: String,
|
|
|
|
|
pub button_reply: Option<WhatsAppButtonReply>,
|
|
|
|
|
pub list_reply: Option<WhatsAppListReply>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct WhatsAppButtonReply {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub title: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct WhatsAppListReply {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub title: String,
|
|
|
|
|
pub description: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-26 22:54:22 -03:00
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct WhatsAppStatus {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub status: String,
|
|
|
|
|
pub timestamp: String,
|
|
|
|
|
pub recipient_id: String,
|
2025-11-22 22:55:35 -03:00
|
|
|
}
|
2025-11-27 13:53:00 -03:00
|
|
|
|
|
|
|
|
// Helper functions for WhatsApp-specific features
|
|
|
|
|
|
|
|
|
|
pub fn create_interactive_buttons(text: &str, buttons: Vec<(&str, &str)>) -> serde_json::Value {
|
|
|
|
|
let button_list: Vec<serde_json::Value> = buttons
|
|
|
|
|
.into_iter()
|
|
|
|
|
.take(3) // WhatsApp limits to 3 buttons
|
|
|
|
|
.map(|(id, title)| {
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"type": "reply",
|
|
|
|
|
"reply": {
|
|
|
|
|
"id": id,
|
|
|
|
|
"title": title
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"type": "button",
|
|
|
|
|
"body": {
|
|
|
|
|
"text": text
|
|
|
|
|
},
|
|
|
|
|
"action": {
|
|
|
|
|
"buttons": button_list
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn create_interactive_list(
|
|
|
|
|
text: &str,
|
|
|
|
|
button_text: &str,
|
|
|
|
|
sections: Vec<(String, Vec<(String, String, Option<String>)>)>,
|
|
|
|
|
) -> serde_json::Value {
|
|
|
|
|
let section_list: Vec<serde_json::Value> = sections
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|(title, rows)| {
|
|
|
|
|
let row_list: Vec<serde_json::Value> = rows
|
|
|
|
|
.into_iter()
|
|
|
|
|
.take(10) // WhatsApp limits to 10 rows per section
|
|
|
|
|
.map(|(id, title, description)| {
|
|
|
|
|
let mut row = serde_json::json!({
|
|
|
|
|
"id": id,
|
|
|
|
|
"title": title
|
|
|
|
|
});
|
|
|
|
|
if let Some(desc) = description {
|
|
|
|
|
row["description"] = serde_json::Value::String(desc);
|
|
|
|
|
}
|
|
|
|
|
row
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"title": title,
|
|
|
|
|
"rows": row_list
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"type": "list",
|
|
|
|
|
"body": {
|
|
|
|
|
"text": text
|
|
|
|
|
},
|
|
|
|
|
"action": {
|
|
|
|
|
"button": button_text,
|
|
|
|
|
"sections": section_list
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|