diff --git a/Cargo.lock b/Cargo.lock index ac4ed100..c68b822f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6660,9 +6660,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -6672,9 +6672,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.10" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -7149,9 +7149,12 @@ dependencies = [ "iso8601-timestamp", "log", "pretty_env_logger", + "redis-kiss", + "regex", "revolt-config", "revolt-database", "revolt-models", + "revolt-parser", "revolt-presence", "revolt-result", "revolt_a2", diff --git a/crates/core/config/Revolt.toml b/crates/core/config/Revolt.toml index 289ab0df..3bb33111 100644 --- a/crates/core/config/Revolt.toml +++ b/crates/core/config/Revolt.toml @@ -89,6 +89,11 @@ production = true # Increasing this will resolve mentions faster, but will consume more memory while resolving. mass_mention_chunk_size = 200 +# How long pushd will cache resolved names for rendered message notifications. +# Increasing this will result in lower database usage, but may result in a situation where a user/channel/role name changes +# and the notifications still resolve to the old name. +render_cache_time = 60 + # none of these should need changing exchange = "revolt.notifications" message_queue = "notifications.origin.message" diff --git a/crates/core/config/src/lib.rs b/crates/core/config/src/lib.rs index 6183cca9..b9e60a4f 100644 --- a/crates/core/config/src/lib.rs +++ b/crates/core/config/src/lib.rs @@ -248,6 +248,7 @@ pub struct Pushd { pub production: bool, pub exchange: String, pub mass_mention_chunk_size: usize, + pub render_cache_time: usize, // Queues pub message_queue: String, diff --git a/crates/core/database/src/models/messages/model.rs b/crates/core/database/src/models/messages/model.rs index 3db6df49..fc417d79 100644 --- a/crates/core/database/src/models/messages/model.rs +++ b/crates/core/database/src/models/messages/model.rs @@ -388,6 +388,7 @@ impl Message { mut role_mentions, mut mentions_everyone, mut mentions_online, + .. } = message_mentions; if allow_mass_mentions && server_id.is_some() && !role_mentions.is_empty() { diff --git a/crates/core/models/src/v0/messages.rs b/crates/core/models/src/v0/messages.rs index 67af69f8..e1183175 100644 --- a/crates/core/models/src/v0/messages.rs +++ b/crates/core/models/src/v0/messages.rs @@ -133,7 +133,10 @@ auto_derived!( #[serde(rename = "message_unpinned")] MessageUnpinned { id: String, by: String }, #[serde(rename = "call_started")] - CallStarted { by: String, finished_at: Option }, + CallStarted { + by: String, + finished_at: Option, + }, } /// Name and / or avatar override information @@ -201,6 +204,9 @@ auto_derived!( pub image: Option, /// Message content or system message information pub body: String, + /// The raw body, if the body has been rendered + #[serde(skip_serializing_if = "Option::is_none")] + pub raw_body: Option, /// Unique tag, usually the channel ID pub tag: String, /// Timestamp at which this notification was created @@ -508,6 +514,7 @@ impl PushNotification { icon, image, body, + raw_body: None, tag: channel.id().to_string(), timestamp, url: format!("{}/channel/{}/{}", config.hosts.app, channel.id(), msg.id), diff --git a/crates/core/parser/src/lib.rs b/crates/core/parser/src/lib.rs index 4ebbac63..bb5b50d7 100644 --- a/crates/core/parser/src/lib.rs +++ b/crates/core/parser/src/lib.rs @@ -16,23 +16,29 @@ pub enum MessageToken<'a> { UserMention(&'a str), #[regex("<%(?&id)>", |lex| &lex.slice()[2..lex.slice().len() - 1],)] RoleMention(&'a str), + #[regex("<#(?&id)>", |lex| &lex.slice()[2..lex.slice().len() - 1],)] + ChannelMention(&'a str), + #[regex(":(?&id):", |lex| &lex.slice()[1..lex.slice().len() - 1],)] + Emoji(&'a str), #[token("@everyone")] MentionEveryone, #[token("@online")] - MentionOnline + MentionOnline, } #[derive(Debug, Clone, PartialEq, Default)] pub struct MessageResults { pub user_mentions: HashSet, pub role_mentions: HashSet, + pub channel_mentions: HashSet, + pub emojis: HashSet, pub mentions_everyone: bool, - pub mentions_online: bool + pub mentions_online: bool, } struct MessageParserIterator<'a, I> { inner: I, - temp: VecDeque> + temp: VecDeque>, } impl<'a, I: Iterator>> Iterator for MessageParserIterator<'a, I> { @@ -55,11 +61,11 @@ impl<'a, I: Iterator>> Iterator for MessageParserIterato if next_token == Some(MessageToken::CodeblockMarker(ty)) { self.temp.clear(); self.temp.push_back(MessageToken::CodeblockMarker(ty)); - break next_token + break next_token; } else if let Some(token) = next_token { self.temp.push_back(token); } else { - break Some(MessageToken::CodeblockMarker(ty)) + break Some(MessageToken::CodeblockMarker(ty)); } } } else { @@ -69,10 +75,10 @@ impl<'a, I: Iterator>> Iterator for MessageParserIterato } } -pub fn parse_message_iter(text: &str) -> impl Iterator + '_ { +pub fn parse_message_iter(text: &str) -> impl Iterator> + '_ { MessageParserIterator { inner: MessageToken::lexer(text).flatten(), - temp: VecDeque::new() + temp: VecDeque::new(), } } @@ -82,13 +88,23 @@ pub fn parse_message(text: &str) -> MessageResults { for token in parse_message_iter(text) { match token { MessageToken::Escape => {} - MessageToken::CodeblockMarker(_) => {}, - MessageToken::UserMention(id) => { results.user_mentions.insert(id.to_string()); }, - MessageToken::RoleMention(id) => { results.role_mentions.insert(id.to_string()); }, + MessageToken::CodeblockMarker(_) => {} + MessageToken::UserMention(id) => { + results.user_mentions.insert(id.to_string()); + } + MessageToken::RoleMention(id) => { + results.role_mentions.insert(id.to_string()); + } + MessageToken::ChannelMention(id) => { + results.channel_mentions.insert(id.to_string()); + } + MessageToken::Emoji(id) => { + results.emojis.insert(id.to_string()); + } MessageToken::MentionEveryone => results.mentions_everyone = true, MessageToken::MentionOnline => results.mentions_online = true, }; - }; + } results } @@ -109,7 +125,10 @@ mod tests { let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>.").collect::>(); assert_eq!(output.len(), 1); - assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3")); + assert_eq!( + output[0], + MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3") + ); } #[test] @@ -117,7 +136,10 @@ mod tests { let output = parse_message_iter("Hello <%01FD58YK5W7QRV5H3D64KTQYX3>.").collect::>(); assert_eq!(output.len(), 1); - assert_eq!(output[0], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3")); + assert_eq!( + output[0], + MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3") + ); } #[test] @@ -138,29 +160,57 @@ mod tests { #[test] fn test_everything() { - let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>, <%01FD58YK5W7QRV5H3D64KTQYX3>, @everyone and @online.").collect::>(); + let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>, <%01FD58YK5W7QRV5H3D64KTQYX3>, <#01FD58YK5W7QRV5H3D64KTQYX3> @everyone and @online. :01FD58YK5W7QRV5H3D64KTQYX3:").collect::>(); - assert_eq!(output.len(), 4); - assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3")); - assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3")); - assert_eq!(output[2], MessageToken::MentionEveryone); - assert_eq!(output[3], MessageToken::MentionOnline); + assert_eq!(output.len(), 6); + assert_eq!( + output[0], + MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3") + ); + assert_eq!( + output[1], + MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3") + ); + assert_eq!( + output[2], + MessageToken::ChannelMention("01FD58YK5W7QRV5H3D64KTQYX3") + ); + assert_eq!(output[3], MessageToken::MentionEveryone); + assert_eq!(output[4], MessageToken::MentionOnline); + assert_eq!(output[5], MessageToken::Emoji("01FD58YK5W7QRV5H3D64KTQYX3")); } #[test] fn test_everything_no_spaces() { - let output = parse_message_iter("<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online").collect::>(); + let output = parse_message_iter( + "<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3><#01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online:01FD58YK5W7QRV5H3D64KTQYX3:", + ) + .collect::>(); - assert_eq!(output.len(), 4); - assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3")); - assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3")); - assert_eq!(output[2], MessageToken::MentionEveryone); - assert_eq!(output[3], MessageToken::MentionOnline); + assert_eq!(output.len(), 6); + assert_eq!( + output[0], + MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3") + ); + assert_eq!( + output[1], + MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3") + ); + assert_eq!( + output[2], + MessageToken::ChannelMention("01FD58YK5W7QRV5H3D64KTQYX3") + ); + assert_eq!(output[3], MessageToken::MentionEveryone); + assert_eq!(output[4], MessageToken::MentionOnline); + assert_eq!(output[5], MessageToken::Emoji("01FD58YK5W7QRV5H3D64KTQYX3")); } #[test] fn test_codeblock_no_mentions() { - let output = parse_message_iter("```\n<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online\n```").collect::>(); + let output = parse_message_iter( + "```\n<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3><#01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online:01FD58YK5W7QRV5H3D64KTQYX3:\n```", + ) + .collect::>(); assert_eq!(output.len(), 2); assert_eq!(output[0], MessageToken::CodeblockMarker(3)); @@ -169,19 +219,36 @@ mod tests { #[test] fn test_uncontained_codeblock_should_mention() { - let output = parse_message_iter("```\n<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online").collect::>(); + let output = parse_message_iter( + "```\n<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3><#01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online:01FD58YK5W7QRV5H3D64KTQYX3:", + ) + .collect::>(); - assert_eq!(output.len(), 5); + assert_eq!(output.len(), 7); assert_eq!(output[0], MessageToken::CodeblockMarker(3)); - assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3")); - assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3")); - assert_eq!(output[3], MessageToken::MentionEveryone); - assert_eq!(output[4], MessageToken::MentionOnline); + assert_eq!( + output[1], + MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3") + ); + assert_eq!( + output[2], + MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3") + ); + assert_eq!( + output[3], + MessageToken::ChannelMention("01FD58YK5W7QRV5H3D64KTQYX3") + ); + assert_eq!(output[4], MessageToken::MentionEveryone); + assert_eq!(output[5], MessageToken::MentionOnline); + assert_eq!(output[6], MessageToken::Emoji("01FD58YK5W7QRV5H3D64KTQYX3")); } #[test] fn test_inline_codeblock_no_mentions() { - let output = parse_message_iter("`<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online`").collect::>(); + let output = parse_message_iter( + "`<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3><#01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online:01FD58YK5W7QRV5H3D64KTQYX3:`", + ) + .collect::>(); assert_eq!(output.len(), 2); assert_eq!(output[0], MessageToken::CodeblockMarker(1)); @@ -190,19 +257,33 @@ mod tests { #[test] fn test_uncontained_inline_codeblock_should_mention() { - let output = parse_message_iter("`<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online").collect::>(); + let output = parse_message_iter( + "`<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3><#01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online:01FD58YK5W7QRV5H3D64KTQYX3:", + ) + .collect::>(); - assert_eq!(output.len(), 5); + assert_eq!(output.len(), 6); assert_eq!(output[0], MessageToken::CodeblockMarker(1)); - assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3")); - assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3")); - assert_eq!(output[3], MessageToken::MentionEveryone); - assert_eq!(output[4], MessageToken::MentionOnline); + assert_eq!( + output[1], + MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3") + ); + assert_eq!( + output[2], + MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3") + ); + assert_eq!( + output[3], + MessageToken::ChannelMention("01FD58YK5W7QRV5H3D64KTQYX3") + ); + assert_eq!(output[4], MessageToken::MentionEveryone); + assert_eq!(output[5], MessageToken::MentionOnline); + assert_eq!(output[6], MessageToken::Emoji("01FD58YK5W7QRV5H3D64KTQYX3")); } #[test] fn test_codeblock_with_language_no_mentions() { - let output = parse_message_iter("```rust\n<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online```").collect::>(); + let output = parse_message_iter("```rust\n<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3><#01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online:01FD58YK5W7QRV5H3D64KTQYX3:```").collect::>(); assert_eq!(output.len(), 2); assert_eq!(output[0], MessageToken::CodeblockMarker(3)); @@ -220,7 +301,8 @@ mod tests { #[test] fn test_double_inline_codeblock_with_backticks_inside() { - let output = parse_message_iter("``this `should` not `ping` @everyone``").collect::>(); + let output = + parse_message_iter("``this `should` not `ping` @everyone``").collect::>(); assert_eq!(output.len(), 2); assert_eq!(output[0], MessageToken::CodeblockMarker(2)); @@ -238,7 +320,8 @@ mod tests { #[test] fn test_escaped_codeblock() { - let output = parse_message_iter("i am ~~not~~ pinging \\`@everyone` ok.").collect::>(); + let output = + parse_message_iter("i am ~~not~~ pinging \\`@everyone` ok.").collect::>(); assert_eq!(output.len(), 3); assert_eq!(output[0], MessageToken::Escape); @@ -253,4 +336,4 @@ mod tests { assert_eq!(output.len(), 1); assert_eq!(output[0], MessageToken::Escape); } -} \ No newline at end of file +} diff --git a/crates/daemons/pushd/Cargo.toml b/crates/daemons/pushd/Cargo.toml index 6efb02ba..9ea3d886 100644 --- a/crates/daemons/pushd/Cargo.toml +++ b/crates/daemons/pushd/Cargo.toml @@ -18,6 +18,7 @@ revolt-models = { version = "0.11.5", path = "../../core/models", features = [ revolt-presence = { version = "0.11.5", path = "../../core/presence", features = [ "redis-is-patched", ] } +revolt-parser = { version = "0.11.5", path = "../../core/parser" } anyhow = { version = "1.0.98" } @@ -26,6 +27,7 @@ fcm_v1 = "0.3.0" web-push = "0.10.0" isahc = { optional = true, version = "1.7", features = ["json"] } revolt_a2 = { version = "0.10", default-features = false, features = ["ring"] } +redis-kiss = "0.1.4" tokio = "1.39.2" async-trait = "0.1.81" ulid = "1.0.0" @@ -35,6 +37,8 @@ authifier = "1.0.16" log = "0.4.11" pretty_env_logger = "0.4.0" +regex = "1.12.3" + #serialization serde_json = "1" revolt_optional_struct = "0.2.0" diff --git a/crates/daemons/pushd/src/consumers/inbound/mass_mention.rs b/crates/daemons/pushd/src/consumers/inbound/mass_mention.rs index cb6f3c44..8d43cce8 100644 --- a/crates/daemons/pushd/src/consumers/inbound/mass_mention.rs +++ b/crates/daemons/pushd/src/consumers/inbound/mass_mention.rs @@ -3,7 +3,7 @@ use std::{ hash::RandomState, }; -use crate::consumers::inbound::internal::*; +use crate::{consumers::inbound::internal::*, utils}; use amqprs::{ channel::{BasicPublishArguments, Channel}, connection::Connection, @@ -17,6 +17,7 @@ use revolt_database::{ MessageFlagsValue, }; use revolt_models::v0::{MessageFlags, PushNotification}; +use revolt_result::ToRevoltError; pub struct MassMessageConsumer { #[allow(dead_code)] @@ -129,7 +130,17 @@ impl MassMessageConsumer { ) -> Result<()> { let config = revolt_config::config().await; let content = String::from_utf8(content)?; - let payload: MassMessageSentPayload = serde_json::from_str(content.as_str())?; + let mut payload: MassMessageSentPayload = serde_json::from_str(content.as_str())?; + + for push in payload.notifications.iter_mut() { + if let Ok(body) = utils::render_notification_content(push, &self.db) + .await + .to_internal_error() + { + push.raw_body = Some(push.body.clone()); + push.body = body; + } + } debug!("Received mass message event"); diff --git a/crates/daemons/pushd/src/consumers/inbound/message.rs b/crates/daemons/pushd/src/consumers/inbound/message.rs index 420c3707..45de0b0b 100644 --- a/crates/daemons/pushd/src/consumers/inbound/message.rs +++ b/crates/daemons/pushd/src/consumers/inbound/message.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use crate::consumers::inbound::internal::*; +use crate::{consumers::inbound::internal::*, utils}; use amqprs::{ channel::{BasicPublishArguments, Channel}, connection::Connection, @@ -11,6 +11,7 @@ use anyhow::Result; use async_trait::async_trait; use log::debug; use revolt_database::{events::rabbit::*, Database}; +use revolt_result::ToRevoltError; pub struct MessageConsumer { #[allow(dead_code)] @@ -64,7 +65,15 @@ impl MessageConsumer { content: Vec, ) -> Result<()> { let content = String::from_utf8(content)?; - let payload: MessageSentPayload = serde_json::from_str(content.as_str())?; + let mut payload: MessageSentPayload = serde_json::from_str(content.as_str())?; + + if let Ok(body) = utils::render_notification_content(&payload.notification, &self.db) + .await + .to_internal_error() + { + payload.notification.raw_body = Some(payload.notification.body); + payload.notification.body = body; + } debug!("Received message event on origin"); diff --git a/crates/daemons/pushd/src/main.rs b/crates/daemons/pushd/src/main.rs index 472caafe..eaa2e1b8 100644 --- a/crates/daemons/pushd/src/main.rs +++ b/crates/daemons/pushd/src/main.rs @@ -14,6 +14,7 @@ use revolt_config::{config, Settings}; use tokio::sync::Notify; mod consumers; +mod utils; use consumers::{ inbound::{ ack::AckConsumer, dm_call::DmCallConsumer, fr_accepted::FRAcceptedConsumer, diff --git a/crates/daemons/pushd/src/utils/mod.rs b/crates/daemons/pushd/src/utils/mod.rs new file mode 100644 index 00000000..62032e80 --- /dev/null +++ b/crates/daemons/pushd/src/utils/mod.rs @@ -0,0 +1,2 @@ +mod renderer; +pub use renderer::render_notification_content; diff --git a/crates/daemons/pushd/src/utils/renderer.rs b/crates/daemons/pushd/src/utils/renderer.rs new file mode 100644 index 00000000..ef16c9d8 --- /dev/null +++ b/crates/daemons/pushd/src/utils/renderer.rs @@ -0,0 +1,272 @@ +use redis_kiss::{get_connection as _get_conn, AsyncCommands, Conn}; +use regex::Regex; +use revolt_config::config; +use revolt_database::{Channel, Database}; +use revolt_models::v0::PushNotification; +use revolt_parser::parse_message; +use revolt_result::{create_error, Result, ToRevoltError}; +use std::{ + borrow::Cow, + collections::{HashMap, HashSet}, +}; +use tokio::join; + +async fn get_connection() -> Result { + _get_conn().await.map_err(|_| create_error!(InternalError)) +} + +pub async fn render_notification_content( + notification: &PushNotification, + db: &Database, +) -> Result { + let parsed = parse_message(¬ification.body); + + let user_mentions: HashMap; + let channel_mentions: HashMap; + let emojis: HashMap; + let roles: HashMap; + + let server_id: Option = get_channel_server_id(notification.channel.id(), db) + .await + .map(Some) + .or(Ok(None))?; + + if server_id.is_some() { + (user_mentions, channel_mentions, emojis, roles) = join!( + get_items( + parsed.user_mentions, + server_id.as_deref(), + db, + get_user_display_name, + "Unknown User".to_string() + ), + get_items( + parsed.channel_mentions, + server_id.as_deref(), + db, + get_channel_name, + "Unknown Channel".to_string() + ), + get_items( + parsed.emojis, + server_id.as_deref(), + db, + get_emoji_name, + "Unknown Emoji".to_string() + ), + get_items( + parsed.role_mentions, + server_id.as_deref(), + db, + get_role_name, + "Unknown Role".to_string() + ), + ); + } else { + roles = HashMap::default(); + + (user_mentions, channel_mentions, emojis) = join!( + get_items( + parsed.user_mentions, + server_id.as_deref(), + db, + get_user_display_name, + "Unknown User".to_string() + ), + get_items( + parsed.channel_mentions, + server_id.as_deref(), + db, + get_channel_name, + "Unknown Channel".to_string() + ), + get_items( + parsed.emojis, + server_id.as_deref(), + db, + get_emoji_name, + "Unknown Emoji".to_string() + ), + ); + } + + let mut body = Cow::Borrowed(notification.body.as_str()); + + for (uid, name) in user_mentions { + replace_all_in_place( + Regex::new(format!("<@{uid}>").as_str()).unwrap(), + &mut body, + format!("@{name}").as_str(), + ); + } + + for (uid, name) in channel_mentions { + replace_all_in_place( + Regex::new(format!("<#{uid}>").as_str()).unwrap(), + &mut body, + format!("#{name}").as_str(), + ); + } + + for (uid, name) in roles { + replace_all_in_place( + Regex::new(format!("<%{uid}>").as_str()).unwrap(), + &mut body, + format!("%{name}").as_str(), + ); + } + + for (uid, name) in emojis { + replace_all_in_place( + Regex::new(format!(":{uid}:").as_str()).unwrap(), + &mut body, + format!(":{name}:").as_str(), + ); + } + + Ok(body.to_string()) +} + +async fn get_items( + items: HashSet, + server_id: Option<&str>, + db: &Database, + getter: F, + invalid_string: String, +) -> HashMap +where + F: AsyncFn(&str, Option<&str>, &Database) -> Result, +{ + let mut resp = HashMap::default(); + + for obj_id in items { + resp.insert( + obj_id.clone(), + getter(&obj_id, server_id, db) + .await + .unwrap_or(invalid_string.clone()), + ); + } + + resp +} + +// Getters + +async fn get_user_display_name(id: &str, server: Option<&str>, db: &Database) -> Result { + let config = config().await; + + let mut conn = get_connection().await?; + let key = format!("pushd-user-cache:{}:{id}", server.unwrap_or("GLOBAL")); + + if let Ok(name) = conn.get(key.clone()).await { + return Ok(name); + } + + if let Some(server) = server { + let member = db.fetch_member(server, id).await?; + if let Some(nickname) = member.nickname { + conn.set_ex::<_, _, ()>(key, nickname.clone(), config.pushd.render_cache_time) + .await + .to_internal_error()?; + return Ok(nickname); + } + } + + let user = db.fetch_user(id).await?; + let name = user.display_name.unwrap_or(user.username); + + conn.set_ex::<_, _, ()>(key, name.clone(), config.pushd.render_cache_time) + .await + .to_internal_error()?; + + Ok(name) +} + +async fn get_channel_name(id: &str, _server: Option<&str>, db: &Database) -> Result { + let config = config().await; + + let mut conn = get_connection().await?; + let key = format!("pushd-channel-cache:{id}"); + + if let Ok(name) = conn.get(key.clone()).await { + return Ok(name); + } + + let channel = db.fetch_channel(id).await?; + let name = match channel { + Channel::DirectMessage { .. } => "DM Channel".to_string(), + Channel::Group { name, .. } | Channel::TextChannel { name, .. } => name, + Channel::SavedMessages { .. } => "Err".to_string(), + }; + + conn.set_ex::<_, _, ()>(key, name.clone(), config.pushd.render_cache_time) + .await + .to_internal_error()?; + + Ok(name) +} + +async fn get_role_name(id: &str, server: Option<&str>, db: &Database) -> Result { + let server = server.unwrap(); // Must be passed, but the interface must stay the same as the other getters + let config: revolt_config::Settings = config().await; + + let mut conn = get_connection().await?; + let key = format!("pushd-role-cache:{server}:{id}"); + + if let Ok(name) = conn.get(key.clone()).await { + return Ok(name); + } + + let server = db.fetch_server(server).await?; + let name = server + .roles + .get(id) + .ok_or_else(|| create_error!(NotFound))? + .name + .clone(); + + conn.set_ex::<_, _, ()>(key, name.clone(), config.pushd.render_cache_time) + .await + .to_internal_error()?; + + Ok(name) +} + +async fn get_emoji_name(id: &str, _server: Option<&str>, db: &Database) -> Result { + let config: revolt_config::Settings = config().await; + + let mut conn = get_connection().await?; + let key = format!("pushd-emoji-cache:{id}"); + + if let Ok(name) = conn.get(key.clone()).await { + return Ok(name); + } + + let emoji = db.fetch_emoji(id).await?; + let name = emoji.name; + + conn.set_ex::<_, _, ()>(key, name.clone(), config.pushd.render_cache_time) + .await + .to_internal_error()?; + + Ok(name) +} + +// utility + +async fn get_channel_server_id(channel_id: &str, db: &Database) -> Result { + match db.fetch_channel(channel_id).await? { + Channel::DirectMessage { .. } | Channel::Group { .. } | Channel::SavedMessages { .. } => { + Err(create_error!(NotFound)) + } + Channel::TextChannel { server, .. } => Ok(server), + } +} + +fn replace_all_in_place(regex: Regex, s: &mut Cow<'_, str>, replacer: R) { + let new = regex.replace_all(s, replacer); + if let Cow::Owned(o) = new { + *s = Cow::Owned(o); + } +}