Skip to main content

sms_types/
sms.rs

1//! Generic types that apply to both HTTP and Websocket interfaces.
2
3use serde::{Deserialize, Serialize};
4
5/// Represents a stored SMS message from the database.
6#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
7#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
8pub struct SmsMessage {
9    /// Unique identifier for the message.
10    pub message_id: Option<i64>,
11
12    /// The phone number associated with this message.
13    pub phone_number: String,
14
15    /// The actual text content of the message.
16    pub message_content: String,
17
18    /// Optional reference number for message tracking.
19    /// This is assigned by the modem and is only present for outgoing messages.
20    pub message_reference: Option<u8>,
21
22    /// Whether this message was sent (true) or received (false).
23    pub is_outgoing: bool,
24
25    /// Unix timestamp when the message was created.
26    pub created_at: Option<u32>,
27
28    /// Optional Unix timestamp when the message was completed/delivered.
29    pub completed_at: Option<u32>,
30
31    /// Service message center delivery status.
32    pub status: Option<u8>,
33}
34impl SmsMessage {
35    /// Returns a clone of the message with the `message_id` option replaced.
36    #[must_use]
37    pub fn with_message_id(&self, id: Option<i64>) -> Self {
38        Self {
39            message_id: id,
40            ..self.clone()
41        }
42    }
43
44    /// Get the message created_at time as SystemTime.
45    #[must_use]
46    pub fn created_at(&self) -> Option<std::time::SystemTime> {
47        self.created_at
48            .map(|ts| std::time::UNIX_EPOCH + std::time::Duration::from_secs(u64::from(ts)))
49    }
50}
51
52/// The outgoing SMS message to be sent to a target number.
53#[derive(Serialize, PartialEq, Default, Debug, Clone)]
54pub struct SmsOutgoingMessage {
55    /// The target phone number, this should be in international format.
56    pub to: String,
57
58    /// The full message content. This will be split into multiple messages
59    /// by the server if required. This also supports Unicode emojis etc.
60    pub content: String,
61
62    /// The relative validity period to use for message sending. This determines
63    /// how long the message should remain waiting while undelivered.
64    /// By default, this is determined by the server (24 hours).
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub validity_period: Option<u8>,
67
68    /// Should the SMS message be sent as a Silent class? This makes a popup
69    /// show on the users device with the message content if they're logged in.
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub flash: Option<bool>,
72
73    /// A timeout that should be applied to the entire request.
74    /// If one is not set, the default timeout is used.
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub timeout: Option<u32>,
77}
78impl SmsOutgoingMessage {
79    /// Create a new outgoing message with a default validity period and no flash.
80    /// The default validity period is applied by SMS-API, so usually 24 hours.
81    pub fn simple_message(to: impl Into<String>, content: impl Into<String>) -> Self {
82        Self {
83            to: to.into(),
84            content: content.into(),
85            ..Default::default()
86        }
87    }
88
89    /// Set the message flash state. This will show a popup if the recipient is
90    /// logged-in to their phone, otherwise as a normal text message.
91    #[must_use]
92    pub fn with_flash(mut self, flash: bool) -> Self {
93        self.flash = Some(flash);
94        self
95    }
96
97    /// Set a relative validity period value.
98    #[must_use]
99    pub fn with_validity_period(mut self, period: u8) -> Self {
100        self.validity_period = Some(period);
101        self
102    }
103
104    /// Set a request timeout value.
105    #[must_use]
106    pub fn with_timeout(mut self, timeout: u32) -> Self {
107        self.timeout = Some(timeout);
108        self
109    }
110
111    /// Get the message sending validity period, either as set or default.
112    /// Returns class 0 for a flash message.
113    #[must_use]
114    pub fn get_validity_period(&self) -> u8 {
115        if self.flash.unwrap_or(false) {
116            return 0;
117        }
118        self.validity_period.unwrap_or(167) // 24hr
119    }
120}
121impl From<&SmsOutgoingMessage> for SmsMessage {
122    fn from(outgoing: &SmsOutgoingMessage) -> Self {
123        SmsMessage {
124            message_id: None,
125            phone_number: outgoing.to.clone(),
126            message_content: outgoing.content.clone(),
127            message_reference: None,
128            is_outgoing: true,
129            status: None,
130            created_at: None,
131            completed_at: None,
132        }
133    }
134}
135
136/// An incoming message from the Modem.
137#[derive(Debug, Clone)]
138pub struct SmsIncomingMessage {
139    /// The incoming sender address. This could also be an alphanumeric sender name.
140    /// This is usually for registered businesses or carrier messages.
141    pub phone_number: String,
142
143    /// The decoded multipart header.
144    pub user_data_header: Option<SmsMultipartHeader>,
145
146    /// The raw message content.
147    pub content: String,
148}
149impl From<&SmsIncomingMessage> for SmsMessage {
150    fn from(incoming: &SmsIncomingMessage) -> Self {
151        SmsMessage {
152            message_id: None,
153            phone_number: incoming.phone_number.clone(),
154            message_content: incoming.content.clone(),
155            message_reference: None,
156            is_outgoing: false,
157            status: None,
158            created_at: None,
159            completed_at: None,
160        }
161    }
162}
163
164/// A received or stored delivery report.
165#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Copy)]
166#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
167#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
168pub struct SmsDeliveryReport {
169    /// Unique identifier for this delivery report.
170    pub report_id: Option<i64>,
171
172    /// Delivery status code from the network.
173    pub status: u8,
174
175    /// Whether this is the final delivery report for the message.
176    pub is_final: bool,
177
178    /// Unix timestamp when this report was created.
179    pub created_at: Option<u32>,
180}
181
182/// A partial message delivery report, as it comes from the modem.
183#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
184pub struct SmsPartialDeliveryReport {
185    /// The target phone number that received the message (and has now sent back a delivery report).
186    pub phone_number: String,
187    /// The modem assigned message reference, this is basically useless outside short-term tracking
188    /// the `message_id` is unique should always be used instead for identification.
189    pub reference_id: u8,
190
191    /// The SMS TP-Status: <https://www.etsi.org/deliver/etsi_ts/123000_123099/123040/16.00.00_60/ts_123040v160000p.pdf#page=71>
192    pub status: u8,
193}
194
195/// A general category of status message delivery status reports.
196#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
197pub enum SmsDeliveryReportStatusCategory {
198    /// The message has been sent, however not yet delivered.
199    Sent,
200
201    /// The message has been delivered.
202    Received,
203
204    /// The message has a temporary error, and sending will be retried by the carrier.
205    Retrying,
206
207    /// The message has a permanent error, the message will not be retried.
208    Failed,
209}
210impl From<u8> for SmsDeliveryReportStatusCategory {
211    fn from(value: u8) -> Self {
212        match value {
213            0x00 => SmsDeliveryReportStatusCategory::Received, // Received by SME
214            0x01..=0x02 => SmsDeliveryReportStatusCategory::Sent, // Forwarded/Replaced
215            0x03..=0x1F => SmsDeliveryReportStatusCategory::Sent, // Reserved/SC-specific success
216            0x20..=0x3F => SmsDeliveryReportStatusCategory::Retrying,
217            0x40..=0x6F => SmsDeliveryReportStatusCategory::Failed,
218            _ => SmsDeliveryReportStatusCategory::Failed,
219        }
220    }
221}
222impl std::fmt::Display for SmsDeliveryReportStatusCategory {
223    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224        f.write_str(match self {
225            SmsDeliveryReportStatusCategory::Sent => "Sent",
226            SmsDeliveryReportStatusCategory::Received => "Received",
227            SmsDeliveryReportStatusCategory::Retrying => "Retrying",
228            SmsDeliveryReportStatusCategory::Failed => "Failed",
229        })
230    }
231}
232impl From<&SmsDeliveryReport> for SmsDeliveryReportStatusCategory {
233    fn from(value: &SmsDeliveryReport) -> Self {
234        SmsDeliveryReportStatusCategory::from(value.status)
235    }
236}
237impl From<&SmsPartialDeliveryReport> for SmsDeliveryReportStatusCategory {
238    fn from(value: &SmsPartialDeliveryReport) -> Self {
239        SmsDeliveryReportStatusCategory::from(value.status)
240    }
241}
242
243/// The sms message multipart header.
244#[derive(Debug, Clone, Copy)]
245pub struct SmsMultipartHeader {
246    /// Modem assigned message send reference (overflows).
247    pub message_reference: u8,
248
249    /// The total amount of messages within this multipart.
250    pub total: u8,
251
252    /// The current received message index.
253    pub index: u8,
254}
255impl TryFrom<Vec<u8>> for SmsMultipartHeader {
256    type Error = &'static str;
257
258    fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
259        if data.len() != 3 {
260            return Err("Invalid user data length!");
261        }
262        Ok(Self {
263            message_reference: data[0],
264            total: data[1],
265            index: data[2],
266        })
267    }
268}