Skip to main content

dravr_canot/
models.rs

1// ABOUTME: Standalone messaging data models decoupled from pierre-core
2// ABOUTME: Channel types, message content, delivery receipts, and configuration structs
3//
4// SPDX-License-Identifier: MIT OR Apache-2.0
5// Copyright (c) 2026 dravr.ai
6
7use std::fmt;
8use std::str::FromStr;
9
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use uuid::Uuid;
14
15// ============================================================================
16// Constants
17// ============================================================================
18
19/// Retry delay schedule in seconds (exponential backoff: 1s, 5s, 30s)
20pub const RETRY_DELAYS_SECS: [u64; 3] = [1, 5, 30];
21
22/// Maximum number of delivery retry attempts before dead-lettering
23pub const MAX_RETRY_ATTEMPTS: i32 = 3;
24
25/// Duration in minutes before a link verification code expires
26pub const LINK_CODE_TTL_MINUTES: i64 = 10;
27
28/// OTP code expires after 10 minutes
29pub const OTP_TTL_MINUTES: i64 = 10;
30
31/// Maximum OTP verification attempts before flow is invalidated
32pub const MAX_OTP_ATTEMPTS: i32 = 3;
33
34/// Maximum OTP flows per hour per `channel_user_id` (rate limiting)
35pub const MAX_OTP_FLOWS_PER_HOUR: i64 = 5;
36
37// ============================================================================
38// Channel Type
39// ============================================================================
40
41/// Supported messaging channel platforms
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
43#[serde(rename_all = "lowercase")]
44pub enum ChannelType {
45    /// Whatsapp Business Cloud API
46    WhatsApp,
47    /// Meta Messenger Platform
48    Messenger,
49    /// Discord Bot API
50    Discord,
51    /// Slack Events API
52    Slack,
53    /// Telegram Bot API
54    Telegram,
55}
56
57impl ChannelType {
58    /// Determine the linking method for this channel type
59    #[must_use]
60    pub const fn linking_method(self) -> LinkingMethod {
61        match self {
62            Self::Telegram | Self::WhatsApp => LinkingMethod::DeepLink,
63            Self::Slack | Self::Discord | Self::Messenger => LinkingMethod::OAuth,
64        }
65    }
66}
67
68impl fmt::Display for ChannelType {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        match self {
71            Self::WhatsApp => write!(f, "whatsapp"),
72            Self::Messenger => write!(f, "messenger"),
73            Self::Discord => write!(f, "discord"),
74            Self::Slack => write!(f, "slack"),
75            Self::Telegram => write!(f, "telegram"),
76        }
77    }
78}
79
80impl FromStr for ChannelType {
81    type Err = String;
82
83    fn from_str(s: &str) -> Result<Self, Self::Err> {
84        match s.to_lowercase().as_str() {
85            "whatsapp" => Ok(Self::WhatsApp),
86            "messenger" => Ok(Self::Messenger),
87            "discord" => Ok(Self::Discord),
88            "slack" => Ok(Self::Slack),
89            "telegram" => Ok(Self::Telegram),
90            other => Err(format!("unknown channel type: {other}")),
91        }
92    }
93}
94
95// ============================================================================
96// Linking
97// ============================================================================
98
99/// Channel linking method: deep link (Telegram, Whatsapp) or oauth (Slack, Discord, Messenger)
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(rename_all = "snake_case")]
102pub enum LinkingMethod {
103    /// Deep link with embedded verification code (Telegram `/start`, Whatsapp `LINK`)
104    DeepLink,
105    /// Standard oauth2 authorization code flow
106    OAuth,
107}
108
109impl fmt::Display for LinkingMethod {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        match self {
112            Self::DeepLink => write!(f, "deep_link"),
113            Self::OAuth => write!(f, "oauth"),
114        }
115    }
116}
117
118impl FromStr for LinkingMethod {
119    type Err = String;
120
121    fn from_str(s: &str) -> Result<Self, Self::Err> {
122        match s {
123            "deep_link" => Ok(Self::DeepLink),
124            "oauth" => Ok(Self::OAuth),
125            other => Err(format!("unknown linking method: {other}")),
126        }
127    }
128}
129
130// ============================================================================
131// Message Content
132// ============================================================================
133
134/// Content variants for inbound and outbound messages
135#[derive(Debug, Clone, Serialize, Deserialize)]
136#[serde(tag = "type", rename_all = "snake_case")]
137pub enum MessageContent {
138    /// Plain text message
139    Text {
140        /// Message body text
141        body: String,
142    },
143    /// Media attachment (image, video, audio, document)
144    Media {
145        /// Media URL or file identifier
146        url: String,
147        /// MIME type (e.g., "image/jpeg")
148        mime_type: String,
149        /// Optional caption
150        caption: Option<String>,
151    },
152    /// Geographic location
153    Location {
154        /// Latitude coordinate
155        latitude: f64,
156        /// Longitude coordinate
157        longitude: f64,
158    },
159    /// Rich card with title, body, and action buttons
160    Card {
161        /// Card title
162        title: String,
163        /// Card body text
164        body: String,
165        /// Interactive action buttons
166        actions: Vec<CardAction>,
167    },
168}
169
170/// An interactive button action within a Card message
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct CardAction {
173    /// Button label displayed to the user
174    pub label: String,
175    /// Action type: "url" for links, "postback" for callback actions
176    pub action_type: String,
177    /// Action value: URL or callback data
178    pub value: String,
179}
180
181// ============================================================================
182// Messages
183// ============================================================================
184
185/// A normalized inbound message received from a webhook
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct IncomingMessage {
188    /// Source channel platform
189    pub channel_type: ChannelType,
190    /// Platform-specific sender identifier
191    pub sender_id: String,
192    /// Human-readable sender name (if available from the platform)
193    pub sender_name: Option<String>,
194    /// Parsed message content
195    pub content: MessageContent,
196    /// Platform-specific conversation/thread identifier
197    pub conversation_id: Option<String>,
198    /// Platform-specific message identifier
199    pub channel_message_id: String,
200    /// Timestamp when the message was received
201    pub timestamp: DateTime<Utc>,
202    /// Raw webhook payload for debugging and audit
203    pub raw_payload: Value,
204    /// Correlation ID for distributed tracing
205    pub correlation_id: Uuid,
206    /// Additional platform-specific metadata
207    pub metadata: Value,
208}
209
210/// An outbound message to be sent through a channel
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct OutgoingMessage {
213    /// Target channel platform
214    pub channel_type: ChannelType,
215    /// Platform-specific recipient identifier
216    pub recipient_id: String,
217    /// Message content to send
218    pub content: MessageContent,
219    /// Correlation ID for distributed tracing
220    pub correlation_id: Uuid,
221    /// Message ID to reply to (platform-specific threading)
222    pub reply_to: Option<String>,
223}
224
225// ============================================================================
226// Delivery
227// ============================================================================
228
229/// Status of an outbound message delivery attempt
230#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
231#[serde(rename_all = "lowercase")]
232pub enum DeliveryStatus {
233    /// Queued for delivery
234    Pending,
235    /// Accepted by the channel API
236    Sent,
237    /// Confirmed delivered to the recipient
238    Delivered,
239    /// Read by the recipient
240    Read,
241    /// Delivery failed
242    Failed,
243    /// Moved to dead-letter queue after exhausting retries
244    Dlq,
245}
246
247/// Receipt returned after a message delivery attempt
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct DeliveryReceipt {
250    /// Internal message identifier
251    pub message_id: String,
252    /// Platform-specific message identifier (if available)
253    pub channel_message_id: Option<String>,
254    /// Delivery outcome status
255    pub status: DeliveryStatus,
256    /// Timestamp of the delivery attempt
257    pub timestamp: DateTime<Utc>,
258}
259
260// ============================================================================
261// Outbound Queue
262// ============================================================================
263
264/// Outbound queue entry for retry tracking
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct OutboundQueueEntry {
267    /// Queue entry identifier
268    pub id: String,
269    /// Reference to the original message
270    pub message_id: String,
271    /// Tenant identifier for isolation
272    pub tenant_id: String,
273    /// Target channel
274    pub channel_type: ChannelType,
275    /// Serialized outbound payload
276    pub payload: Value,
277    /// Current queue status (pending, retrying:N, sent, dlq)
278    pub status: String,
279    /// Number of delivery attempts made
280    pub attempt_count: i32,
281    /// Scheduled time for next retry attempt
282    pub next_retry_at: Option<DateTime<Utc>>,
283}
284
285// ============================================================================
286// Configuration
287// ============================================================================
288
289/// Per-tenant channel configuration for API access and webhook verification
290#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct ChannelConfig {
292    /// Configuration identifier
293    pub id: String,
294    /// Tenant identifier for isolation
295    pub tenant_id: String,
296    /// Channel platform type
297    pub channel_type: ChannelType,
298    /// API key or access token for outbound API calls
299    pub api_key: Option<String>,
300    /// API secret for signing outbound requests
301    pub api_secret: Option<String>,
302    /// Webhook secret for inbound signature verification
303    pub webhook_secret: Option<String>,
304    /// Meta webhook verify token (distinct from `webhook_secret` to avoid leaking HMAC key)
305    pub verify_token: Option<String>,
306    /// Platform account identifier (e.g., Discord application ID)
307    pub account_id: Option<String>,
308    /// Phone number identifier (Whatsapp/SMS)
309    pub phone_number: Option<String>,
310    /// Bot token for platforms that use separate bot credentials
311    pub bot_token: Option<String>,
312    /// Whether this channel configuration is active
313    pub is_active: bool,
314}
315
316/// Policy for webhook timestamp validation to prevent replay attacks
317#[derive(Debug, Clone)]
318pub struct WebhookTimestampPolicy {
319    /// Maximum allowed age of a webhook timestamp in seconds
320    pub max_age_secs: u64,
321}
322
323impl Default for WebhookTimestampPolicy {
324    fn default() -> Self {
325        Self { max_age_secs: 300 }
326    }
327}
328
329// ============================================================================
330// Sessions
331// ============================================================================
332
333/// Active messaging session linking a channel user to a conversation
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct MessagingSession {
336    /// Session identifier
337    pub id: String,
338    /// User identifier
339    pub user_id: String,
340    /// Tenant identifier
341    pub tenant_id: String,
342    /// Channel platform
343    pub channel_type: ChannelType,
344    /// Channel-specific user identifier
345    pub channel_user_id: String,
346    /// Channel-specific conversation or thread ID
347    pub channel_conversation_id: Option<String>,
348    /// Upstream conversation identifier
349    pub conversation_id: Option<String>,
350    /// Timestamp of last message activity
351    pub last_message_at: DateTime<Utc>,
352}
353
354// ============================================================================
355// Channel Linking
356// ============================================================================
357
358/// Ephemeral link state for a pending channel linking request
359#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct MessagingLinkState {
361    /// State identifier
362    pub id: String,
363    /// Tenant identifier
364    pub tenant_id: String,
365    /// User requesting the link
366    pub user_id: String,
367    /// Target channel platform
368    pub channel_type: ChannelType,
369    /// Cryptographically random verification code
370    pub code: String,
371    /// Linking method (`deep_link` or `oauth`)
372    pub method: LinkingMethod,
373    /// Whether this code has been consumed
374    pub used: bool,
375    /// Expiration timestamp (10 minutes from creation)
376    pub expires_at: DateTime<Utc>,
377    /// Creation timestamp
378    pub created_at: DateTime<Utc>,
379}
380
381/// Permanent mapping between a user and a messaging channel identity
382#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct MessagingChannelLink {
384    /// Link identifier
385    pub id: String,
386    /// Tenant identifier
387    pub tenant_id: String,
388    /// User identifier
389    pub user_id: String,
390    /// Channel platform type
391    pub channel_type: ChannelType,
392    /// Channel-specific user identifier (phone number, platform user ID, etc.)
393    pub channel_user_id: String,
394    /// Human-readable display name from the platform
395    pub display_name: Option<String>,
396    /// Timestamp when the link was established
397    pub linked_at: DateTime<Utc>,
398}