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 pub struct Message {
21 #[serde(rename = "_id")]
23 pub id: String,
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub nonce: Option<String>,
27 pub channel: String,
29 pub author: String,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub webhook: Option<MessageWebhook>,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub content: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub system: Option<SystemMessage>,
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub attachments: Option<Vec<File>>,
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub edited: Option<Timestamp>,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub embeds: Option<Vec<Embed>>,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub mentions: Option<Vec<String>>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub replies: Option<Vec<String>>,
55 #[serde(skip_serializing_if = "IndexMap::is_empty", default)]
57 pub reactions: IndexMap<String, IndexSet<String>>,
58 #[serde(skip_serializing_if = "Interactions::is_default", default)]
60 pub interactions: Interactions,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub masquerade: Option<Masquerade>,
64 },
65 "PartialMessage"
66);
67
68auto_derived!(
69 #[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 #[cfg_attr(feature = "validator", derive(Validate))]
98 pub struct Masquerade {
99 #[serde(skip_serializing_if = "Option::is_none")]
101 #[validate(length(min = 1, max = 32))]
102 pub name: Option<String>,
103 #[serde(skip_serializing_if = "Option::is_none")]
105 #[validate(length(min = 1, max = 256))]
106 pub avatar: Option<String>,
107 #[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 #[derive(Default)]
117 pub struct Interactions {
118 #[serde(skip_serializing_if = "Option::is_none", default)]
120 pub reactions: Option<IndexSet<String>>,
121 #[serde(skip_serializing_if = "crate::if_false", default)]
125 pub restrict_reactions: bool,
126 }
127
128 pub struct AppendMessage {
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub embeds: Option<Vec<Embed>>,
133 }
134
135 #[derive(Default)]
139 pub enum MessageSort {
140 #[default]
142 Relevance,
143 Latest,
145 Oldest,
147 }
148
149 pub struct PushNotification {
151 pub author: String,
153 pub icon: String,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub image: Option<String>,
158 pub body: String,
160 pub tag: String,
162 pub timestamp: u64,
164 pub url: String,
166 }
167
168 #[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 pub struct ReplyIntent {
187 pub id: String,
189 pub mention: bool,
191 }
192
193 #[cfg_attr(feature = "validator", derive(Validate))]
195 pub struct DataMessageSend {
196 #[validate(length(min = 1, max = 64))]
200 pub nonce: Option<String>,
201
202 #[validate(length(min = 0, max = 2000))]
204 pub content: Option<String>,
205 pub attachments: Option<Vec<String>>,
207 pub replies: Option<Vec<ReplyIntent>>,
209 #[validate]
213 pub embeds: Option<Vec<SendableEmbed>>,
214 #[validate]
216 pub masquerade: Option<Masquerade>,
217 pub interactions: Option<Interactions>,
219 }
220);
221
222pub 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 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 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}