Skip to main content

slack_morphism/models/common/
mod.rs

1use rsb_derive::Builder;
2use rvstruct::ValueStruct;
3use serde::{Deserialize, Serialize};
4use serde_with::{serde_as, skip_serializing_none};
5use std::hash::Hash;
6use std::*;
7use url::Url;
8
9pub(crate) mod datetime;
10
11pub use datetime::*;
12
13use datetime::unix_seconds;
14
15mod user;
16
17pub use user::*;
18
19mod team;
20
21pub use team::*;
22
23mod channel;
24
25pub use channel::*;
26
27mod pin;
28
29pub use pin::*;
30
31mod reaction;
32
33pub use reaction::*;
34
35mod star;
36
37pub use star::*;
38
39mod bot;
40
41pub use bot::*;
42
43mod icon;
44
45pub use icon::*;
46
47mod formatters;
48
49pub use formatters::*;
50
51mod emoji;
52
53pub use emoji::*;
54mod assistant;
55pub use assistant::*;
56
57#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
58pub struct SlackTs(pub String);
59
60impl SlackTs {
61    /// Converts a Slack timestamp (`<seconds>.<microseconds>`) to an instant.
62    ///
63    /// The fractional part is optional and is interpreted as microseconds,
64    /// padded or truncated to six digits.
65    pub fn to_date_time_opt(&self) -> Option<SlackUtcDateTime> {
66        let value = self.value();
67        let (seconds_part, micros_part) = match value.split_once('.') {
68            Some((seconds, micros)) => (seconds, Some(micros)),
69            None => (value.as_str(), None),
70        };
71
72        let seconds: i64 = seconds_part.parse().ok()?;
73
74        let micros: u32 = match micros_part {
75            Some(micros_part) => {
76                let mut digits: String = micros_part.chars().take(6).collect();
77                while digits.len() < 6 {
78                    digits.push('0');
79                }
80                digits.parse().ok()?
81            }
82            None => 0,
83        };
84
85        datetime::from_unix_seconds_micros(seconds, micros)
86    }
87}
88
89#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
90pub struct SlackScheduledMid(pub String);
91
92#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
93pub struct SlackTeamId(pub String);
94
95#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
96pub struct SlackEnterpriseSubteamId(pub String);
97
98#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
99pub struct SlackAppId(pub String);
100
101#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
102pub struct SlackCanvasId(pub String);
103
104/// A section inside a canvas, e.g. `temp:C:eBa219af721c664422cb90a52fac`.
105#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
106pub struct SlackCanvasSectionId(pub String);
107
108#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
109pub struct SlackChannelId(pub String);
110
111#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
112pub struct SlackClientMessageId(pub String);
113
114impl SlackTextFormat for SlackChannelId {
115    fn to_slack_format(&self) -> String {
116        format!("<#{}>", self.value())
117    }
118}
119
120#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
121pub struct SlackChannelType(pub String);
122
123#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
124pub struct SlackConversationId(pub String);
125
126#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
127pub struct SlackActionId(pub String);
128
129#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
130pub struct SlackActionType(pub String);
131
132#[derive(Debug, Eq, Hash, PartialEq, Clone, Serialize, Deserialize, ValueStruct)]
133pub struct SlackUserId(pub String);
134
135#[derive(Debug, Eq, Hash, PartialEq, Clone, Serialize, Deserialize, ValueStruct)]
136pub struct SlackUserGroupId(pub String);
137
138impl SlackTextFormat for SlackUserId {
139    fn to_slack_format(&self) -> String {
140        format!("<@{}>", self.value())
141    }
142}
143
144impl SlackTextFormat for SlackUserGroupId {
145    fn to_slack_format(&self) -> String {
146        format!("<!subteam^{}>", self.value())
147    }
148}
149
150#[derive(Debug, Eq, Hash, PartialEq, Clone, Serialize, Deserialize, ValueStruct)]
151pub struct SlackBotId(pub String);
152
153#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
154pub struct SlackDateTime(#[serde(with = "unix_seconds")] pub SlackUtcDateTime);
155
156impl SlackDateTime {
157    pub fn now() -> Self {
158        Self(datetime::now())
159    }
160}
161
162#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
163pub struct SlackDate(pub String);
164
165impl SlackDate {
166    pub fn to_naive_date(&self) -> Option<SlackCivilDate> {
167        datetime::parse_civil_date(self.value())
168    }
169}
170
171#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
172pub struct SlackLocale(pub String);
173
174#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
175pub struct SlackCursorId(pub String);
176
177#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
178pub struct SlackColor(pub String);
179
180#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
181pub struct SlackCallbackId(pub String);
182
183#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
184pub struct SlackTriggerId(pub String);
185
186#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
187pub struct SlackViewId(pub String);
188
189#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
190pub struct SlackCommandId(pub String);
191
192#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
193pub struct SlackClientId(pub String);
194
195#[derive(Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
196pub struct SlackClientSecret(pub String);
197
198impl fmt::Debug for SlackClientSecret {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
200        write!(f, "SlackClientSecret(len:{})", self.value().len())
201    }
202}
203
204#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
205pub struct SlackApiTokenScope(pub String);
206
207#[derive(Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
208pub struct SlackVerificationToken(pub String);
209
210impl fmt::Debug for SlackVerificationToken {
211    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
212        write!(f, "SlackVerificationToken(len:{})", self.value().len())
213    }
214}
215
216#[derive(Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
217pub struct SlackSigningSecret(pub String);
218
219impl fmt::Debug for SlackSigningSecret {
220    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
221        write!(f, "SlackSigningSecret(len:{})", self.value().len())
222    }
223}
224
225#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
226pub struct EmailAddress(pub String);
227
228#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
229pub struct PhoneNumber(pub String);
230
231#[serde_as]
232#[skip_serializing_none]
233#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
234pub struct SlackResponseMetadata {
235    #[serde(default)]
236    #[serde_as(as = "serde_with::NoneAsEmptyString")]
237    pub next_cursor: Option<SlackCursorId>,
238}
239
240#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
241pub enum SlackConversationType {
242    #[serde(rename = "im")]
243    Im,
244    #[serde(rename = "mpim")]
245    Mpim,
246    #[serde(rename = "private_channel")]
247    Private,
248    #[serde(rename = "public_channel")]
249    Public,
250}
251
252#[allow(clippy::to_string_trait_impl)]
253impl ToString for SlackConversationType {
254    fn to_string(&self) -> String {
255        match self {
256            SlackConversationType::Im => "im".into(),
257            SlackConversationType::Mpim => "mpim".into(),
258            SlackConversationType::Private => "private_channel".into(),
259            SlackConversationType::Public => "public_channel".into(),
260        }
261    }
262}
263
264#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
265pub struct SlackResponseUrl(pub Url);
266
267#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
268pub struct SlackWebSocketsUrl(pub Url);
269
270impl SlackWebSocketsUrl {
271    pub fn to_debug_url(&self) -> Self {
272        let mut url = self.value().clone();
273        url.query_pairs_mut()
274            .append_pair("debug_reconnects", "true");
275        Self(url)
276    }
277}
278
279#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
280pub struct SlackTeamUrl(pub Url);
281
282#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
283pub struct SlackUnfurlId(pub String);
284
285#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
286pub struct SlackMimeType(pub String);
287
288#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
289pub struct SlackEmoji(pub String);
290
291impl SlackEmoji {
292    pub const SMILE: &'static str = ":smile:";
293    pub const SPEECH_BALLOON: &'static str = ":speech_balloon:";
294    pub const HEAVY_CHECK_MARK: &'static str = ":heavy_check_mark:";
295}
296
297#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
298pub enum SlackShortcutType {
299    #[serde(rename = "message")]
300    Message,
301    #[serde(rename = "global")]
302    Global,
303}
304
305#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
306pub struct SlackEventType(pub String);
307
308#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
309pub struct SlackAppContextEntityType(pub String);
310
311#[derive(Debug, Eq, Hash, PartialEq, Clone, Serialize, Deserialize, ValueStruct)]
312pub struct SlackEnterpriseId(pub String);
313
314/// This type is needed since Slack allowes invalid URLs in some places like Rich sections
315/// and we still need to read them on the client side, so we store it as a string
316/// but provide convinent messages to convert to Url if possible.
317#[derive(Debug, Eq, Hash, PartialEq, Clone, Serialize, Deserialize, ValueStruct)]
318pub struct SlackRelaxedUrl(pub String);
319
320impl SlackRelaxedUrl {
321    pub fn to_url(&self) -> Result<Url, url::ParseError> {
322        self.try_into()
323    }
324}
325
326impl TryFrom<&SlackRelaxedUrl> for Url {
327    type Error = url::ParseError;
328
329    fn try_from(relaxed_url: &SlackRelaxedUrl) -> Result<Self, Self::Error> {
330        Url::parse(relaxed_url.value())
331    }
332}
333
334impl TryFrom<SlackRelaxedUrl> for Url {
335    type Error = url::ParseError;
336
337    fn try_from(relaxed_url: SlackRelaxedUrl) -> Result<Self, Self::Error> {
338        Url::parse(relaxed_url.value())
339    }
340}
341
342impl From<Url> for SlackRelaxedUrl {
343    fn from(url: Url) -> Self {
344        SlackRelaxedUrl(url.to_string())
345    }
346}
347
348impl From<&Url> for SlackRelaxedUrl {
349    fn from(url: &Url) -> Self {
350        SlackRelaxedUrl(url.to_string())
351    }
352}
353
354#[cfg(test)]
355mod test {
356    use super::*;
357    use serde_json::json;
358
359    fn test_date_time() -> SlackUtcDateTime {
360        "2020-01-01T00:42:42Z".parse::<SlackUtcDateTime>().unwrap()
361    }
362
363    #[test]
364    fn test_slack_date_time() {
365        let dt = SlackDateTime(test_date_time());
366        let json = serde_json::to_value(&dt).unwrap();
367        assert_eq!(json.as_u64().unwrap(), 1577839362);
368
369        let parsed: SlackDateTime = serde_json::from_value(json!(1577839362)).unwrap();
370        assert_eq!(parsed, dt);
371        assert_eq!(serde_json::from_value::<SlackDateTime>(json).unwrap(), dt);
372    }
373
374    #[test]
375    fn test_slack_ts_to_date_time_with_micros() {
376        let ts = SlackTs("1577839362.000400".to_string());
377        let dt = ts.to_date_time_opt().unwrap();
378
379        assert_eq!(datetime::unix_seconds(&dt), 1577839362);
380        assert_eq!(
381            Some(dt),
382            datetime::from_unix_seconds_micros(1577839362, 400)
383        );
384        assert_ne!(Some(dt), datetime::from_unix_seconds_micros(1577839362, 0));
385    }
386
387    #[test]
388    fn test_slack_ts_to_date_time_without_fraction() {
389        let ts = SlackTs("1577839362".to_string());
390        let dt = ts.to_date_time_opt().unwrap();
391
392        assert_eq!(datetime::unix_seconds(&dt), 1577839362);
393        assert_eq!(Some(dt), datetime::from_unix_seconds_micros(1577839362, 0));
394    }
395
396    #[test]
397    fn test_slack_ts_to_date_time_invalid() {
398        assert_eq!(SlackTs("garbage".to_string()).to_date_time_opt(), None);
399    }
400
401    #[test]
402    fn debug_url_keeps_existing_query() {
403        let url = SlackWebSocketsUrl(Url::parse("wss://wss.slack.com/link/?ticket=abc").unwrap());
404
405        assert_eq!(
406            url.to_debug_url(),
407            SlackWebSocketsUrl(
408                Url::parse("wss://wss.slack.com/link/?ticket=abc&debug_reconnects=true").unwrap()
409            )
410        );
411    }
412
413    #[test]
414    fn debug_url_adds_query_when_absent() {
415        let url = SlackWebSocketsUrl(Url::parse("wss://wss.slack.com/link/").unwrap());
416
417        assert_eq!(
418            url.to_debug_url(),
419            SlackWebSocketsUrl(
420                Url::parse("wss://wss.slack.com/link/?debug_reconnects=true").unwrap()
421            )
422        );
423    }
424
425    #[test]
426    fn test_slack_date_to_naive_date() {
427        assert!(SlackDate("2020-01-01".to_string())
428            .to_naive_date()
429            .is_some());
430        assert_eq!(SlackDate("2020-13-01".to_string()).to_naive_date(), None);
431        assert_eq!(SlackDate("garbage".to_string()).to_naive_date(), None);
432    }
433}