onechatsocial_models/v0/
messages.rs

1use std::time::SystemTime;
2
3use indexmap::{IndexMap, IndexSet};
4use once_cell::sync::Lazy;
5use regex::Regex;
6use onechatsocial_config::config;
7
8#[cfg(feature = "validator")]
9use validator::Validate;
10
11use iso8601_timestamp::Timestamp;
12
13use super::{Embed, File, MessageWebhook, User, Webhook, RE_COLOUR};
14
15pub static RE_MENTION: Lazy<Regex> =
16    Lazy::new(|| Regex::new(r"<@([0-9A-HJKMNP-TV-Z]{26})>").unwrap());
17
18auto_derived_partial!(
19    /// Message
20    pub struct Message {
21        /// Unique Id
22        #[serde(rename = "_id")]
23        pub id: String,
24        /// Unique value generated by client sending this message
25        #[serde(skip_serializing_if = "Option::is_none")]
26        pub nonce: Option<String>,
27        /// Id of the channel this message was sent in
28        pub channel: String,
29        /// Id of the user or webhook that sent this message
30        pub author: String,
31        /// The webhook that sent this message
32        #[serde(skip_serializing_if = "Option::is_none")]
33        pub webhook: Option<MessageWebhook>,
34        /// Message content
35        #[serde(skip_serializing_if = "Option::is_none")]
36        pub content: Option<String>,
37        /// System message
38        #[serde(skip_serializing_if = "Option::is_none")]
39        pub system: Option<SystemMessage>,
40        /// Array of attachments
41        #[serde(skip_serializing_if = "Option::is_none")]
42        pub attachments: Option<Vec<File>>,
43        /// Time at which this message was last edited
44        #[serde(skip_serializing_if = "Option::is_none")]
45        pub edited: Option<Timestamp>,
46        /// Attached embeds to this message
47        #[serde(skip_serializing_if = "Option::is_none")]
48        pub embeds: Option<Vec<Embed>>,
49        /// Array of user ids mentioned in this message
50        #[serde(skip_serializing_if = "Option::is_none")]
51        pub mentions: Option<Vec<String>>,
52        /// Array of message ids this message is replying to
53        #[serde(skip_serializing_if = "Option::is_none")]
54        pub replies: Option<Vec<String>>,
55        /// Hashmap of emoji IDs to array of user IDs
56        #[serde(skip_serializing_if = "IndexMap::is_empty", default)]
57        pub reactions: IndexMap<String, IndexSet<String>>,
58        /// Information about how this message should be interacted with
59        #[serde(skip_serializing_if = "Interactions::is_default", default)]
60        pub interactions: Interactions,
61        /// Name and / or avatar overrides for this message
62        #[serde(skip_serializing_if = "Option::is_none")]
63        pub masquerade: Option<Masquerade>,
64    },
65    "PartialMessage"
66);
67
68auto_derived!(
69    /// System Event
70    #[serde(tag = "type")]
71    pub enum SystemMessage {
72        #[serde(rename = "text")]
73        Text { content: String },
74        #[serde(rename = "user_added")]
75        UserAdded { id: String, by: String },
76        #[serde(rename = "user_remove")]
77        UserRemove { id: String, by: String },
78        #[serde(rename = "user_joined")]
79        UserJoined { id: String },
80        #[serde(rename = "user_left")]
81        UserLeft { id: String },
82        #[serde(rename = "user_kicked")]
83        UserKicked { id: String },
84        #[serde(rename = "user_banned")]
85        UserBanned { id: String },
86        #[serde(rename = "channel_renamed")]
87        ChannelRenamed { name: String, by: String },
88        #[serde(rename = "channel_description_changed")]
89        ChannelDescriptionChanged { by: String },
90        #[serde(rename = "channel_icon_changed")]
91        ChannelIconChanged { by: String },
92        #[serde(rename = "channel_ownership_changed")]
93        ChannelOwnershipChanged { from: String, to: String },
94    }
95
96    /// Name and / or avatar override information
97    #[cfg_attr(feature = "validator", derive(Validate))]
98    pub struct Masquerade {
99        /// Replace the display name shown on this message
100        #[serde(skip_serializing_if = "Option::is_none")]
101        #[validate(length(min = 1, max = 32))]
102        pub name: Option<String>,
103        /// Replace the avatar shown on this message (URL to image file)
104        #[serde(skip_serializing_if = "Option::is_none")]
105        #[validate(length(min = 1, max = 256))]
106        pub avatar: Option<String>,
107        /// Replace the display role colour shown on this message
108        ///
109        /// Must have `ManageRole` permission to use
110        #[serde(skip_serializing_if = "Option::is_none")]
111        #[validate(length(min = 1, max = 128), regex = "RE_COLOUR")]
112        pub colour: Option<String>,
113    }
114
115    /// Information to guide interactions on this message
116    #[derive(Default)]
117    pub struct Interactions {
118        /// Reactions which should always appear and be distinct
119        #[serde(skip_serializing_if = "Option::is_none", default)]
120        pub reactions: Option<IndexSet<String>>,
121        /// Whether reactions should be restricted to the given list
122        ///
123        /// Can only be set to true if reactions list is of at least length 1
124        #[serde(skip_serializing_if = "crate::if_false", default)]
125        pub restrict_reactions: bool,
126    }
127
128    /// Appended Information
129    pub struct AppendMessage {
130        /// Additional embeds to include in this message
131        #[serde(skip_serializing_if = "Option::is_none")]
132        pub embeds: Option<Vec<Embed>>,
133    }
134
135    /// Message Sort
136    ///
137    /// Sort used for retrieving messages
138    #[derive(Default)]
139    pub enum MessageSort {
140        /// Sort by the most relevant messages
141        #[default]
142        Relevance,
143        /// Sort by the newest messages first
144        Latest,
145        /// Sort by the oldest messages first
146        Oldest,
147    }
148
149    /// Push Notification
150    pub struct PushNotification {
151        /// Known author name
152        pub author: String,
153        /// URL to author avatar
154        pub icon: String,
155        /// URL to first matching attachment
156        #[serde(skip_serializing_if = "Option::is_none")]
157        pub image: Option<String>,
158        /// Message content or system message information
159        pub body: String,
160        /// Unique tag, usually the channel ID
161        pub tag: String,
162        /// Timestamp at which this notification was created
163        pub timestamp: u64,
164        /// URL to open when clicking notification
165        pub url: String,
166    }
167
168    /// Representation of a text embed before it is sent.
169    #[derive(Default)]
170    #[cfg_attr(feature = "validator", derive(Validate))]
171    pub struct SendableEmbed {
172        #[validate(length(min = 1, max = 128))]
173        pub icon_url: Option<String>,
174        #[validate(length(min = 1, max = 256))]
175        pub url: Option<String>,
176        #[validate(length(min = 1, max = 100))]
177        pub title: Option<String>,
178        #[validate(length(min = 1, max = 2000))]
179        pub description: Option<String>,
180        pub media: Option<String>,
181        #[validate(length(min = 1, max = 128), regex = "RE_COLOUR")]
182        pub colour: Option<String>,
183    }
184
185    /// What this message should reply to and how
186    pub struct ReplyIntent {
187        /// Message Id
188        pub id: String,
189        /// Whether this reply should mention the message's author
190        pub mention: bool,
191    }
192
193    /// Message to send
194    #[cfg_attr(feature = "validator", derive(Validate))]
195    pub struct DataMessageSend {
196        /// Unique token to prevent duplicate message sending
197        ///
198        /// **This is deprecated and replaced by `Idempotency-Key`!**
199        #[validate(length(min = 1, max = 64))]
200        pub nonce: Option<String>,
201
202        /// Message content to send
203        #[validate(length(min = 0, max = 2000))]
204        pub content: Option<String>,
205        /// Attachments to include in message
206        pub attachments: Option<Vec<String>>,
207        /// Messages to reply to
208        pub replies: Option<Vec<ReplyIntent>>,
209        /// Embeds to include in message
210        ///
211        /// Text embed content contributes to the content length cap
212        #[validate]
213        pub embeds: Option<Vec<SendableEmbed>>,
214        /// Masquerade to apply to this message
215        #[validate]
216        pub masquerade: Option<Masquerade>,
217        /// Information about how this message should be interacted with
218        pub interactions: Option<Interactions>,
219    }
220);
221
222/// Message Author Abstraction
223pub enum MessageAuthor<'a> {
224    User(&'a User),
225    Webhook(&'a Webhook),
226    System {
227        username: &'a str,
228        avatar: Option<&'a str>,
229    },
230}
231
232impl Interactions {
233    /// Check if default initialisation of fields
234    pub fn is_default(&self) -> bool {
235        !self.restrict_reactions && self.reactions.is_none()
236    }
237}
238
239impl<'a> MessageAuthor<'a> {
240    pub fn id(&self) -> &str {
241        match self {
242            MessageAuthor::User(user) => &user.id,
243            MessageAuthor::Webhook(webhook) => &webhook.id,
244            MessageAuthor::System { .. } => "00000000000000000000000000",
245        }
246    }
247
248    pub fn avatar(&self) -> Option<&str> {
249        match self {
250            MessageAuthor::User(user) => user.avatar.as_ref().map(|file| file.id.as_str()),
251            MessageAuthor::Webhook(webhook) => webhook.avatar.as_ref().map(|file| file.id.as_str()),
252            MessageAuthor::System { avatar, .. } => *avatar,
253        }
254    }
255
256    pub fn username(&self) -> &str {
257        match self {
258            MessageAuthor::User(user) => &user.username,
259            MessageAuthor::Webhook(webhook) => &webhook.name,
260            MessageAuthor::System { username, .. } => username,
261        }
262    }
263}
264
265impl From<SystemMessage> for String {
266    fn from(s: SystemMessage) -> String {
267        match s {
268            SystemMessage::Text { content } => content,
269            SystemMessage::UserAdded { .. } => "User added to the channel.".to_string(),
270            SystemMessage::UserRemove { .. } => "User removed from the channel.".to_string(),
271            SystemMessage::UserJoined { .. } => "User joined the channel.".to_string(),
272            SystemMessage::UserLeft { .. } => "User left the channel.".to_string(),
273            SystemMessage::UserKicked { .. } => "User kicked from the channel.".to_string(),
274            SystemMessage::UserBanned { .. } => "User banned from the channel.".to_string(),
275            SystemMessage::ChannelRenamed { .. } => "Channel renamed.".to_string(),
276            SystemMessage::ChannelDescriptionChanged { .. } => {
277                "Channel description changed.".to_string()
278            }
279            SystemMessage::ChannelIconChanged { .. } => "Channel icon changed.".to_string(),
280            SystemMessage::ChannelOwnershipChanged { .. } => {
281                "Channel ownership changed.".to_string()
282            }
283        }
284    }
285}
286
287impl PushNotification {
288    /// Create a new notification from a given message, author and channel ID
289    pub async fn from(msg: Message, author: Option<MessageAuthor<'_>>, channel_id: &str) -> Self {
290        let config = config().await;
291
292        let icon = if let Some(author) = &author {
293            if let Some(avatar) = author.avatar() {
294                format!("{}/avatars/{}", config.hosts.autumn, avatar)
295            } else {
296                format!("{}/users/{}/default_avatar", config.hosts.api, author.id())
297            }
298        } else {
299            format!("{}/assets/logo.png", config.hosts.app)
300        };
301
302        let image = msg.attachments.and_then(|attachments| {
303            attachments
304                .first()
305                .map(|v| format!("{}/attachments/{}", config.hosts.autumn, v.id))
306        });
307
308        let body = if let Some(sys) = msg.system {
309            sys.into()
310        } else if let Some(text) = msg.content {
311            text
312        } else {
313            "Empty Message".to_string()
314        };
315
316        let timestamp = SystemTime::now()
317            .duration_since(SystemTime::UNIX_EPOCH)
318            .expect("Time went backwards")
319            .as_secs();
320
321        Self {
322            author: author
323                .map(|x| x.username().to_string())
324                .unwrap_or_else(|| "Revolt".to_string()),
325            icon,
326            image,
327            body,
328            tag: channel_id.to_string(),
329            timestamp,
330            url: format!("{}/channel/{}/{}", config.hosts.app, channel_id, msg.id),
331        }
332    }
333}