feat: parse message push notification content and replace internal formatting (#693)

* parse message push notification content and replace internal formatting

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: don't unwrap the error I was using for db debugging

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: emojis & remove compiler warning

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: emojis in pushd

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* feat: report errors in the render function to sentry

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

---------

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
This commit is contained in:
Tom
2026-03-27 17:15:05 -07:00
committed by GitHub
parent 6107f242fd
commit d1e72cee42
12 changed files with 451 additions and 52 deletions

11
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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,

View File

@@ -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() {

View File

@@ -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<Timestamp> },
CallStarted {
by: String,
finished_at: Option<Timestamp>,
},
}
/// Name and / or avatar override information
@@ -201,6 +204,9 @@ auto_derived!(
pub image: Option<String>,
/// 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<String>,
/// 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),

View File

@@ -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<String>,
pub role_mentions: HashSet<String>,
pub channel_mentions: HashSet<String>,
pub emojis: HashSet<String>,
pub mentions_everyone: bool,
pub mentions_online: bool
pub mentions_online: bool,
}
struct MessageParserIterator<'a, I> {
inner: I,
temp: VecDeque<MessageToken<'a>>
temp: VecDeque<MessageToken<'a>>,
}
impl<'a, I: Iterator<Item = MessageToken<'a>>> Iterator for MessageParserIterator<'a, I> {
@@ -55,11 +61,11 @@ impl<'a, I: Iterator<Item = MessageToken<'a>>> 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<Item = MessageToken<'a>>> Iterator for MessageParserIterato
}
}
pub fn parse_message_iter(text: &str) -> impl Iterator<Item = MessageToken> + '_ {
pub fn parse_message_iter(text: &str) -> impl Iterator<Item = MessageToken<'_>> + '_ {
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>, <%01FD58YK5W7QRV5H3D64KTQYX3>, <#01FD58YK5W7QRV5H3D64KTQYX3> @everyone and @online. :01FD58YK5W7QRV5H3D64KTQYX3:").collect::<Vec<_>>();
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::<Vec<_>>();
let output = parse_message_iter(
"<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3><#01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online:01FD58YK5W7QRV5H3D64KTQYX3:",
)
.collect::<Vec<_>>();
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::<Vec<_>>();
let output = parse_message_iter(
"```\n<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3><#01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online:01FD58YK5W7QRV5H3D64KTQYX3:\n```",
)
.collect::<Vec<_>>();
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::<Vec<_>>();
let output = parse_message_iter(
"```\n<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3><#01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online:01FD58YK5W7QRV5H3D64KTQYX3:",
)
.collect::<Vec<_>>();
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::<Vec<_>>();
let output = parse_message_iter(
"`<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3><#01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online:01FD58YK5W7QRV5H3D64KTQYX3:`",
)
.collect::<Vec<_>>();
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::<Vec<_>>();
let output = parse_message_iter(
"`<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3><#01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online:01FD58YK5W7QRV5H3D64KTQYX3:",
)
.collect::<Vec<_>>();
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::<Vec<_>>();
let output = parse_message_iter("```rust\n<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3><#01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online:01FD58YK5W7QRV5H3D64KTQYX3:```").collect::<Vec<_>>();
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::<Vec<_>>();
let output =
parse_message_iter("``this `should` not `ping` @everyone``").collect::<Vec<_>>();
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::<Vec<_>>();
let output =
parse_message_iter("i am ~~not~~ pinging \\`@everyone` ok.").collect::<Vec<_>>();
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);
}
}
}

View File

@@ -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"

View File

@@ -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");

View File

@@ -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<u8>,
) -> 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");

View File

@@ -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,

View File

@@ -0,0 +1,2 @@
mod renderer;
pub use renderer::render_notification_content;

View File

@@ -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<Conn> {
_get_conn().await.map_err(|_| create_error!(InternalError))
}
pub async fn render_notification_content(
notification: &PushNotification,
db: &Database,
) -> Result<String> {
let parsed = parse_message(&notification.body);
let user_mentions: HashMap<String, String>;
let channel_mentions: HashMap<String, String>;
let emojis: HashMap<String, String>;
let roles: HashMap<String, String>;
let server_id: Option<String> = 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<F>(
items: HashSet<String>,
server_id: Option<&str>,
db: &Database,
getter: F,
invalid_string: String,
) -> HashMap<String, String>
where
F: AsyncFn(&str, Option<&str>, &Database) -> Result<String>,
{
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<String> {
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<String> {
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<String> {
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<String> {
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<String> {
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<R: regex::Replacer>(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);
}
}