use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct WebhookEvent(pub String);
impl WebhookEvent {
pub const SMS_RECEIVED: &'static str = "sms:received";
pub const SMS_DATA_RECEIVED: &'static str = "sms:data-received";
pub const SMS_SENT: &'static str = "sms:sent";
pub const SMS_DELIVERED: &'static str = "sms:delivered";
pub const SMS_FAILED: &'static str = "sms:failed";
pub const SMS_CANCELLED: &'static str = "sms:cancelled";
pub const SYSTEM_PING: &'static str = "system:ping";
pub const MMS_RECEIVED: &'static str = "mms:received";
pub const MMS_DOWNLOADED: &'static str = "mms:downloaded";
pub const APP_STARTED: &'static str = "app:started";
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
pub const WEBHOOK_EVENT_TYPES: &[&str] = &[
WebhookEvent::SMS_RECEIVED,
WebhookEvent::SMS_DATA_RECEIVED,
WebhookEvent::SMS_SENT,
WebhookEvent::SMS_DELIVERED,
WebhookEvent::SMS_FAILED,
WebhookEvent::SMS_CANCELLED,
WebhookEvent::SYSTEM_PING,
WebhookEvent::MMS_RECEIVED,
WebhookEvent::MMS_DOWNLOADED,
WebhookEvent::APP_STARTED,
];
pub fn is_valid_webhook_event(e: &str) -> bool {
WEBHOOK_EVENT_TYPES.contains(&e)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Webhook {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub device_id: Option<String>,
pub url: String,
pub event: WebhookEvent,
}
impl Webhook {
pub fn validate(&self) -> Result<(), crate::Error> {
if !is_valid_webhook_event(self.event.as_str()) {
return Err(crate::Error::Validation("invalid event type".to_string()));
}
if !self.url.to_lowercase().starts_with("https://") {
return Err(crate::Error::Validation(
"url must start with https://".to_string(),
));
}
let parsed = url::Url::parse(&self.url)
.map_err(|_| crate::Error::Validation("invalid url".to_string()))?;
if parsed.host_str().is_none_or(|h| h.is_empty()) {
return Err(crate::Error::Validation(
"url must have a valid host".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SmsEventPayload {
pub message_id: String,
pub phone_number: String,
pub sender: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recipient: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sim_number: Option<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SmsReceivedPayload {
#[serde(flatten)]
pub base: SmsEventPayload,
pub message: String,
pub received_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SmsSentPayload {
#[serde(flatten)]
pub base: SmsEventPayload,
pub sent_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SmsDeliveredPayload {
#[serde(flatten)]
pub base: SmsEventPayload,
pub delivered_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SmsCancelledPayload {
#[serde(flatten)]
pub base: SmsEventPayload,
pub cancelled_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SmsFailedPayload {
#[serde(flatten)]
pub base: SmsEventPayload,
pub failed_at: DateTime<Utc>,
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SmsDataReceivedPayload {
#[serde(flatten)]
pub base: SmsEventPayload,
pub data: String,
pub received_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MmsReceivedPayload {
#[serde(flatten)]
pub base: SmsEventPayload,
pub transaction_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
pub content_class: String,
pub size: i64,
pub received_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MmsDownloadedAttachment {
pub part_id: i32,
pub content_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MmsDownloadedPayload {
#[serde(flatten)]
pub base: SmsEventPayload,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
pub attachments: Vec<MmsDownloadedAttachment>,
pub received_at: DateTime<Utc>,
}