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}