pub mod signature;
#[cfg(any(feature = "axum", feature = "actix"))]
pub mod server;
#[cfg(feature = "actix")]
pub mod actix;
#[cfg(feature = "axum")]
pub mod axum;
pub use signature::{DEFAULT_TOLERANCE_SECS, VerifyError, verify, verify_at, verify_default};
#[cfg(any(feature = "axum", feature = "actix"))]
pub use server::{DEFAULT_SIGNATURE_HEADER, VerifiedWebhook, WebhookRejection, WebhookVerifier};
use crate::error::{Error, Result};
pub use crate::types::WebhookEventPayload;
#[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum MessageEventKind {
Received,
Sent,
Delivered,
Failed,
Read,
Other(String),
}
impl MessageEventKind {
pub fn from_event(event: &str) -> Self {
let eq = |name: &str| event.eq_ignore_ascii_case(name);
if eq("message.received") {
MessageEventKind::Received
} else if eq("message.delivered") {
MessageEventKind::Delivered
} else if eq("message.failed") {
MessageEventKind::Failed
} else if eq("message.read") {
MessageEventKind::Read
} else if eq("message.sent") {
MessageEventKind::Sent
} else {
MessageEventKind::Other(event.to_owned())
}
}
}
#[derive(Debug, Clone)]
pub struct WebhookEvent {
pub payload: WebhookEventPayload,
}
impl WebhookEvent {
pub fn parse(raw_body: &[u8]) -> Result<Self> {
let payload: WebhookEventPayload =
serde_json::from_slice(raw_body).map_err(Error::decode)?;
Ok(WebhookEvent { payload })
}
pub fn kind(&self) -> Option<MessageEventKind> {
self.payload
.event
.as_deref()
.map(MessageEventKind::from_event)
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::print_stdout,
clippy::unreadable_literal
)]
mod tests {
use super::*;
#[test]
fn parses_and_classifies() {
let raw = br#"{"event":"message.received","message_id":"m1","text":"hi"}"#;
let ev = WebhookEvent::parse(raw).unwrap();
assert_eq!(ev.kind(), Some(MessageEventKind::Received));
assert_eq!(ev.payload.message_id.as_deref(), Some("m1"));
}
#[test]
fn unknown_event_falls_back() {
assert_eq!(
MessageEventKind::from_event("message.reacted"),
MessageEventKind::Other("message.reacted".into())
);
}
#[test]
fn classifies_each_known_event() {
assert_eq!(
MessageEventKind::from_event("message.sent"),
MessageEventKind::Sent
);
assert_eq!(
MessageEventKind::from_event("message.delivered"),
MessageEventKind::Delivered
);
assert_eq!(
MessageEventKind::from_event("message.failed"),
MessageEventKind::Failed
);
assert_eq!(
MessageEventKind::from_event("message.read"),
MessageEventKind::Read
);
}
#[test]
fn classification_is_case_insensitive() {
assert_eq!(
MessageEventKind::from_event("MESSAGE.DELIVERED"),
MessageEventKind::Delivered
);
}
#[test]
fn parses_delivered_payload_fields() {
let raw = br#"{"event":"message.delivered","message_id":"m2","status":"delivered","delivered_at":1700000000}"#;
let ev = WebhookEvent::parse(raw).unwrap();
assert_eq!(ev.kind(), Some(MessageEventKind::Delivered));
assert_eq!(ev.payload.delivered_at, Some(1700000000));
assert_eq!(ev.payload.status.as_deref(), Some("delivered"));
}
#[test]
fn parses_failed_payload_with_error_fields() {
let raw = br#"{"event":"message.failed","message_id":"m3","error_code":"unreachable","error_message":"no service"}"#;
let ev = WebhookEvent::parse(raw).unwrap();
assert_eq!(ev.kind(), Some(MessageEventKind::Failed));
assert_eq!(ev.payload.error_code.as_deref(), Some("unreachable"));
assert_eq!(ev.payload.error_message.as_deref(), Some("no service"));
}
#[test]
fn parses_group_received_payload() {
let raw = br#"{"event":"message.received","is_group":true,"group_id":"g1","group_name":"Team","sender":"+15550001111","text":"hi all"}"#;
let ev = WebhookEvent::parse(raw).unwrap();
assert_eq!(ev.kind(), Some(MessageEventKind::Received));
assert_eq!(ev.payload.is_group, Some(true));
assert_eq!(ev.payload.group_id.as_deref(), Some("g1"));
}
#[test]
fn kind_is_none_when_event_absent() {
let raw = br#"{"message_id":"m4","text":"no event field"}"#;
let ev = WebhookEvent::parse(raw).unwrap();
assert_eq!(ev.kind(), None);
assert_eq!(ev.payload.message_id.as_deref(), Some("m4"));
}
#[test]
fn parse_rejects_malformed_json() {
let err = WebhookEvent::parse(b"not json").unwrap_err();
assert!(matches!(err, Error::Decode(_)));
assert_eq!(err.code(), None);
assert_eq!(err.status(), None);
}
#[test]
fn parse_accepts_unknown_extra_fields() {
let raw = br#"{"event":"message.received","brand_new_field":{"x":1},"message_id":"m5"}"#;
let ev = WebhookEvent::parse(raw).unwrap();
assert_eq!(ev.payload.message_id.as_deref(), Some("m5"));
}
}