use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::common::WebhookId;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Webhook {
pub id: WebhookId,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub trigger_types: Vec<WebhookTrigger>,
pub webhook_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub webhook_secret: Option<String>,
#[serde(default)]
pub notification_email_addresses: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub privacy_mode: Option<PrivacyMode>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct PrivacyMode {
pub enabled: bool,
#[serde(default)]
pub redact_emails: bool,
#[serde(default)]
pub redact_bodies: bool,
#[serde(default)]
pub redact_names: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum WebhookTrigger {
MessageCreated,
MessageUpdated,
MessageDeleted,
ThreadCreated,
ThreadUpdated,
DraftCreated,
DraftUpdated,
DraftDeleted,
EventCreated,
EventUpdated,
EventDeleted,
CalendarCreated,
CalendarUpdated,
CalendarDeleted,
ContactCreated,
ContactUpdated,
ContactDeleted,
FolderCreated,
FolderUpdated,
FolderDeleted,
GrantCreated,
GrantUpdated,
GrantDeleted,
GrantExpired,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WebhookNotification {
pub id: String,
pub grant_id: String,
pub application_id: String,
pub trigger: String,
pub timestamp: i64,
pub data: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub calendar_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub master_event_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreateWebhookRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub webhook_url: String,
pub trigger_types: Vec<WebhookTrigger>,
#[serde(skip_serializing_if = "Option::is_none")]
pub webhook_secret: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notification_email_addresses: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub privacy_mode: Option<PrivacyMode>,
}
impl CreateWebhookRequest {
pub fn builder() -> CreateWebhookRequestBuilder {
CreateWebhookRequestBuilder::default()
}
}
#[derive(Debug, Clone, Default)]
pub struct CreateWebhookRequestBuilder {
description: Option<String>,
webhook_url: Option<String>,
trigger_types: Option<Vec<WebhookTrigger>>,
webhook_secret: Option<String>,
notification_email_addresses: Option<Vec<String>>,
privacy_mode: Option<PrivacyMode>,
}
impl CreateWebhookRequestBuilder {
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn webhook_url(mut self, url: impl Into<String>) -> Self {
self.webhook_url = Some(url.into());
self
}
pub fn trigger_types(mut self, triggers: Vec<WebhookTrigger>) -> Self {
self.trigger_types = Some(triggers);
self
}
pub fn webhook_secret(mut self, secret: impl Into<String>) -> Self {
self.webhook_secret = Some(secret.into());
self
}
pub fn notification_email_addresses(mut self, emails: Vec<String>) -> Self {
self.notification_email_addresses = Some(emails);
self
}
pub fn privacy_mode(mut self, mode: PrivacyMode) -> Self {
self.privacy_mode = Some(mode);
self
}
pub fn build(self) -> CreateWebhookRequest {
CreateWebhookRequest {
description: self.description,
webhook_url: self.webhook_url.expect("webhook_url is required"),
trigger_types: self.trigger_types.expect("trigger_types is required"),
webhook_secret: self.webhook_secret,
notification_email_addresses: self.notification_email_addresses,
privacy_mode: self.privacy_mode,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct UpdateWebhookRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub webhook_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_types: Option<Vec<WebhookTrigger>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub webhook_secret: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notification_email_addresses: Option<Vec<String>>,
}
impl UpdateWebhookRequest {
pub fn builder() -> UpdateWebhookRequestBuilder {
UpdateWebhookRequestBuilder::default()
}
}
#[derive(Debug, Clone, Default)]
pub struct UpdateWebhookRequestBuilder {
description: Option<String>,
webhook_url: Option<String>,
trigger_types: Option<Vec<WebhookTrigger>>,
webhook_secret: Option<String>,
notification_email_addresses: Option<Vec<String>>,
}
impl UpdateWebhookRequestBuilder {
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn webhook_url(mut self, url: impl Into<String>) -> Self {
self.webhook_url = Some(url.into());
self
}
pub fn trigger_types(mut self, triggers: Vec<WebhookTrigger>) -> Self {
self.trigger_types = Some(triggers);
self
}
pub fn webhook_secret(mut self, secret: impl Into<String>) -> Self {
self.webhook_secret = Some(secret.into());
self
}
pub fn notification_email_addresses(mut self, emails: Vec<String>) -> Self {
self.notification_email_addresses = Some(emails);
self
}
pub fn build(self) -> UpdateWebhookRequest {
UpdateWebhookRequest {
description: self.description,
webhook_url: self.webhook_url,
trigger_types: self.trigger_types,
webhook_secret: self.webhook_secret,
notification_email_addresses: self.notification_email_addresses,
}
}
}
pub fn verify_webhook_signature(payload: &[u8], signature: &str, secret: &str) -> bool {
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let mut mac = match HmacSha256::new_from_slice(secret.as_bytes()) {
Ok(m) => m,
Err(_) => return false,
};
mac.update(payload);
let result = mac.finalize();
let expected = hex::encode(result.into_bytes());
signature == expected
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_webhook_serialization() {
let webhook = Webhook {
id: WebhookId::new("webhook_123"),
description: Some("Test webhook".to_string()),
trigger_types: vec![WebhookTrigger::MessageCreated],
webhook_url: "https://example.com/webhook".to_string(),
webhook_secret: Some("secret".to_string()),
notification_email_addresses: vec!["admin@example.com".to_string()],
privacy_mode: None,
};
let json = serde_json::to_string(&webhook).unwrap();
assert!(json.contains("webhook_123"));
assert!(json.contains("https://example.com/webhook"));
}
#[test]
fn test_webhook_notification_deserialization() {
let json = r#"{
"id": "notif_123",
"grant_id": "grant_456",
"application_id": "app_789",
"trigger": "message.created",
"timestamp": 1609459200,
"data": {"id": "msg_abc", "subject": "Test"}
}"#;
let notification: WebhookNotification = serde_json::from_str(json).unwrap();
assert_eq!(notification.id, "notif_123");
assert_eq!(notification.trigger, "message.created");
assert_eq!(notification.timestamp, 1609459200);
}
#[test]
fn test_webhook_trigger_serialization() {
let trigger = WebhookTrigger::MessageCreated;
let json = serde_json::to_string(&trigger).unwrap();
assert_eq!(json, "\"message-created\"");
let trigger = WebhookTrigger::EventUpdated;
let json = serde_json::to_string(&trigger).unwrap();
assert_eq!(json, "\"event-updated\"");
}
#[test]
fn test_create_webhook_request_builder() {
let request = CreateWebhookRequest::builder()
.description("My webhook")
.webhook_url("https://api.example.com/webhooks")
.trigger_types(vec![WebhookTrigger::MessageCreated])
.webhook_secret("secret_key")
.notification_email_addresses(vec!["admin@example.com".to_string()])
.build();
assert_eq!(request.description, Some("My webhook".to_string()));
assert_eq!(request.webhook_url, "https://api.example.com/webhooks");
assert_eq!(request.trigger_types.len(), 1);
assert_eq!(request.webhook_secret, Some("secret_key".to_string()));
}
#[test]
fn test_update_webhook_request_builder() {
let update = UpdateWebhookRequest::builder()
.description("Updated")
.trigger_types(vec![
WebhookTrigger::MessageCreated,
WebhookTrigger::MessageUpdated,
])
.build();
assert_eq!(update.description, Some("Updated".to_string()));
assert_eq!(update.trigger_types.as_ref().unwrap().len(), 2);
assert!(update.webhook_url.is_none());
}
#[test]
fn test_verify_webhook_signature() {
let payload = b"test payload";
let secret = "test_secret";
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
mac.update(payload);
let result = mac.finalize();
let signature = hex::encode(result.into_bytes());
assert!(verify_webhook_signature(payload, &signature, secret));
assert!(!verify_webhook_signature(
payload,
"wrong_signature",
secret
));
assert!(!verify_webhook_signature(
payload,
&signature,
"wrong_secret"
));
}
#[test]
fn test_privacy_mode_default() {
let mode = PrivacyMode::default();
assert!(!mode.enabled);
assert!(!mode.redact_emails);
}
#[test]
fn test_privacy_mode_enabled() {
let mode = PrivacyMode {
enabled: true,
redact_emails: true,
redact_bodies: true,
redact_names: true,
};
let json = serde_json::to_string(&mode).unwrap();
assert!(json.contains("\"enabled\":true"));
assert!(json.contains("\"redact_emails\":true"));
}
#[test]
fn test_webhook_with_privacy_mode() {
let privacy = PrivacyMode {
enabled: true,
redact_emails: true,
redact_bodies: false,
redact_names: false,
};
let request = CreateWebhookRequest::builder()
.webhook_url("https://example.com")
.trigger_types(vec![WebhookTrigger::MessageCreated])
.privacy_mode(privacy.clone())
.build();
assert!(request.privacy_mode.is_some());
let mode = request.privacy_mode.unwrap();
assert!(mode.enabled);
assert!(mode.redact_emails);
}
}