use std::collections::HashSet;
use indexmap::{IndexMap, IndexSet};
use iso8601_timestamp::Timestamp;
use revolt_config::config;
use revolt_models::v0::{
self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageSort, MessageWebhook,
PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION,
};
use revolt_permissions::{ChannelPermission, PermissionValue};
use revolt_result::Result;
use ulid::Ulid;
use validator::Validate;
use crate::{
events::client::EventV1,
tasks::{self, ack::AckEvent},
util::idempotency::IdempotencyKey,
Channel, Database, Emoji, File, User,
};
auto_derived_partial!(
pub struct Message {
#[serde(rename = "_id")]
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub nonce: Option<String>,
pub channel: String,
pub author: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub webhook: Option<MessageWebhook>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<SystemMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attachments: Option<Vec<File>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub edited: Option<Timestamp>,
#[serde(skip_serializing_if = "Option::is_none")]
pub embeds: Option<Vec<Embed>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mentions: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub replies: Option<Vec<String>>,
#[serde(skip_serializing_if = "IndexMap::is_empty", default)]
pub reactions: IndexMap<String, IndexSet<String>>,
#[serde(skip_serializing_if = "Interactions::is_default", default)]
pub interactions: Interactions,
#[serde(skip_serializing_if = "Option::is_none")]
pub masquerade: Option<Masquerade>,
},
"PartialMessage"
);
auto_derived!(
#[serde(tag = "type")]
pub enum SystemMessage {
#[serde(rename = "text")]
Text { content: String },
#[serde(rename = "user_added")]
UserAdded { id: String, by: String },
#[serde(rename = "user_remove")]
UserRemove { id: String, by: String },
#[serde(rename = "user_joined")]
UserJoined { id: String },
#[serde(rename = "user_left")]
UserLeft { id: String },
#[serde(rename = "user_kicked")]
UserKicked { id: String },
#[serde(rename = "user_banned")]
UserBanned { id: String },
#[serde(rename = "channel_renamed")]
ChannelRenamed { name: String, by: String },
#[serde(rename = "channel_description_changed")]
ChannelDescriptionChanged { by: String },
#[serde(rename = "channel_icon_changed")]
ChannelIconChanged { by: String },
#[serde(rename = "channel_ownership_changed")]
ChannelOwnershipChanged { from: String, to: String },
}
pub struct Masquerade {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub colour: Option<String>,
}
#[derive(Default)]
pub struct Interactions {
#[serde(skip_serializing_if = "Option::is_none", default)]
pub reactions: Option<IndexSet<String>>,
#[serde(skip_serializing_if = "crate::if_false", default)]
pub restrict_reactions: bool,
}
pub struct AppendMessage {
#[serde(skip_serializing_if = "Option::is_none")]
pub embeds: Option<Vec<Embed>>,
}
#[serde(untagged)]
pub enum MessageTimePeriod {
Relative {
nearby: String,
},
Absolute {
before: Option<String>,
after: Option<String>,
sort: Option<MessageSort>,
},
}
#[derive(Default)]
pub struct MessageFilter {
pub channel: Option<String>,
pub author: Option<String>,
pub query: Option<String>,
}
pub struct MessageQuery {
pub limit: Option<i64>,
#[serde(flatten)]
pub filter: MessageFilter,
#[serde(flatten)]
pub time_period: MessageTimePeriod,
}
);
#[allow(clippy::derivable_impls)]
impl Default for Message {
fn default() -> Self {
Self {
id: Default::default(),
nonce: None,
channel: Default::default(),
author: Default::default(),
webhook: None,
content: None,
system: None,
attachments: None,
edited: None,
embeds: None,
mentions: None,
replies: None,
reactions: Default::default(),
interactions: Default::default(),
masquerade: None,
}
}
}
#[allow(clippy::disallowed_methods)]
impl Message {
pub async fn create_from_api(
db: &Database,
channel: Channel,
data: DataMessageSend,
author: MessageAuthor<'_>,
mut idempotency: IdempotencyKey,
generate_embeds: bool,
allow_mentions: bool,
) -> Result<Message> {
let config = config().await;
Message::validate_sum(
&data.content,
data.embeds.as_deref().unwrap_or_default(),
config.features.limits.default.message_length,
)?;
idempotency
.consume_nonce(data.nonce)
.await
.map_err(|_| create_error!(InvalidOperation))?;
if (data.content.as_ref().map_or(true, |v| v.is_empty()))
&& (data.attachments.as_ref().map_or(true, |v| v.is_empty()))
&& (data.embeds.as_ref().map_or(true, |v| v.is_empty()))
{
return Err(create_error!(EmptyMessage));
}
if let Some(interactions) = &data.interactions {
if interactions.restrict_reactions {
let disallowed = if let Some(list) = &interactions.reactions {
list.is_empty()
} else {
true
};
if disallowed {
return Err(create_error!(InvalidProperty));
}
}
}
let (author_id, webhook) = match &author {
MessageAuthor::User(user) => (user.id.clone(), None),
MessageAuthor::Webhook(webhook) => (webhook.id.clone(), Some((*webhook).clone())),
MessageAuthor::System { .. } => ("00000000000000000000000000".to_string(), None),
};
let message_id = Ulid::new().to_string();
let mut message = Message {
id: message_id.clone(),
channel: channel.id(),
masquerade: data.masquerade.map(|masquerade| masquerade.into()),
interactions: data
.interactions
.map(|interactions| interactions.into())
.unwrap_or_default(),
author: author_id,
webhook: webhook.map(|w| w.into()),
..Default::default()
};
let mut mentions = HashSet::new();
if allow_mentions {
if let Some(content) = &data.content {
for capture in RE_MENTION.captures_iter(content) {
if let Some(mention) = capture.get(1) {
mentions.insert(mention.as_str().to_string());
}
}
}
}
let mut replies = HashSet::new();
if let Some(entries) = data.replies {
if entries.len() > config.features.limits.default.message_replies {
return Err(create_error!(TooManyReplies {
max: config.features.limits.default.message_replies,
}));
}
for ReplyIntent { id, mention } in entries {
let message = db.fetch_message(&id).await?;
if mention && allow_mentions {
mentions.insert(message.author.to_owned());
}
replies.insert(message.id);
}
}
if !mentions.is_empty() {
message.mentions.replace(mentions.into_iter().collect());
}
if !replies.is_empty() {
message
.replies
.replace(replies.into_iter().collect::<Vec<String>>());
}
let mut attachments = vec![];
if data
.attachments
.as_ref()
.is_some_and(|v| v.len() > config.features.limits.default.message_attachments)
{
return Err(create_error!(TooManyAttachments {
max: config.features.limits.default.message_attachments,
}));
}
if data
.embeds
.as_ref()
.is_some_and(|v| v.len() > config.features.limits.default.message_embeds)
{
return Err(create_error!(TooManyEmbeds {
max: config.features.limits.default.message_embeds,
}));
}
for attachment_id in data.attachments.as_deref().unwrap_or_default() {
attachments.push(
db.find_and_use_attachment(attachment_id, "attachments", "message", &message_id)
.await?,
);
}
if !attachments.is_empty() {
message.attachments.replace(attachments);
}
for sendable_embed in data.embeds.unwrap_or_default() {
message.attach_sendable_embed(db, sendable_embed).await?;
}
message.content = data.content;
message.nonce = Some(idempotency.into_key());
message.send(db, author, &channel, generate_embeds).await?;
Ok(message)
}
pub async fn send_without_notifications(
&mut self,
db: &Database,
is_dm: bool,
generate_embeds: bool,
) -> Result<()> {
db.insert_message(self).await?;
EventV1::Message(self.clone().into())
.p(self.channel.to_string())
.await;
tasks::last_message_id::queue(self.channel.to_string(), self.id.to_string(), is_dm).await;
if let Some(mentions) = &self.mentions {
for user in mentions {
tasks::ack::queue(
self.channel.to_string(),
user.to_string(),
AckEvent::AddMention {
ids: vec![self.id.to_string()],
},
)
.await;
}
}
if generate_embeds {
if let Some(content) = &self.content {
tasks::process_embeds::queue(
self.channel.to_string(),
self.id.to_string(),
content.clone(),
)
.await;
}
}
Ok(())
}
pub async fn send(
&mut self,
db: &Database,
author: MessageAuthor<'_>,
channel: &Channel,
generate_embeds: bool,
) -> Result<()> {
self.send_without_notifications(
db,
matches!(channel, Channel::DirectMessage { .. }),
generate_embeds,
)
.await?;
crate::tasks::web_push::queue(
{
match channel {
Channel::DirectMessage { recipients, .. }
| Channel::Group { recipients, .. } => recipients.clone(),
Channel::TextChannel { .. } => self.mentions.clone().unwrap_or_default(),
_ => vec![],
}
},
PushNotification::from(self.clone().into(), Some(author), &channel.id()).await,
)
.await;
Ok(())
}
pub async fn create_embed(&self, db: &Database, embed: SendableEmbed) -> Result<Embed> {
embed.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let media = if let Some(id) = embed.media {
Some(
db.find_and_use_attachment(&id, "attachments", "message", &self.id)
.await?,
)
} else {
None
};
Ok(Embed::Text(Text {
icon_url: embed.icon_url,
url: embed.url,
title: embed.title,
description: embed.description,
media: media.map(|m| m.into()),
colour: embed.colour,
}))
}
pub async fn update(&mut self, db: &Database, partial: PartialMessage) -> Result<()> {
self.apply_options(partial.clone());
db.update_message(&self.id, &partial).await?;
EventV1::MessageUpdate {
id: self.id.clone(),
channel: self.channel.clone(),
data: partial.into(),
}
.p(self.channel.clone())
.await;
Ok(())
}
pub async fn fetch_with_users(
db: &Database,
query: MessageQuery,
perspective: &User,
include_users: Option<bool>,
server_id: Option<String>,
) -> Result<BulkMessageResponse> {
let messages: Vec<v0::Message> = db
.fetch_messages(query)
.await?
.into_iter()
.map(Into::into)
.collect();
if let Some(true) = include_users {
let user_ids = messages
.iter()
.map(|m| m.author.clone())
.collect::<HashSet<String>>()
.into_iter()
.collect::<Vec<String>>();
let users = User::fetch_many_ids_as_mutuals(db, perspective, &user_ids).await?;
Ok(BulkMessageResponse::MessagesAndUsers {
messages,
users,
members: if let Some(server_id) = server_id {
Some(
db.fetch_members(&server_id, &user_ids)
.await?
.into_iter()
.map(Into::into)
.collect(),
)
} else {
None
},
})
} else {
Ok(BulkMessageResponse::JustMessages(messages))
}
}
pub async fn append(
db: &Database,
id: String,
channel: String,
append: AppendMessage,
) -> Result<()> {
db.append_message(&id, &append).await?;
EventV1::MessageAppend {
id,
channel: channel.to_string(),
append: append.into(),
}
.p(channel)
.await;
Ok(())
}
pub async fn attach_sendable_embed(
&mut self,
db: &Database,
embed: v0::SendableEmbed,
) -> Result<()> {
let media: Option<v0::File> = if let Some(id) = embed.media {
Some(
db.find_and_use_attachment(&id, "attachments", "message", &self.id)
.await?
.into(),
)
} else {
None
};
let embed = v0::Embed::Text(v0::Text {
icon_url: embed.icon_url,
url: embed.url,
title: embed.title,
description: embed.description,
media,
colour: embed.colour,
});
if let Some(embeds) = &mut self.embeds {
embeds.push(embed);
} else {
self.embeds = Some(vec![embed]);
}
Ok(())
}
pub async fn add_reaction(&self, db: &Database, user: &User, emoji: &str) -> Result<()> {
let config = config().await;
if self.reactions.len() >= config.features.limits.default.message_reactions
&& !self.reactions.contains_key(emoji)
{
return Err(create_error!(InvalidOperation));
}
if !self.interactions.can_use(emoji) {
return Err(create_error!(InvalidOperation));
}
if !Emoji::can_use(db, emoji).await? {
return Err(create_error!(InvalidOperation));
}
EventV1::MessageReact {
id: self.id.to_string(),
channel_id: self.channel.to_string(),
user_id: user.id.to_string(),
emoji_id: emoji.to_string(),
}
.p(self.channel.to_string())
.await;
db.add_reaction(&self.id, emoji, &user.id).await
}
pub fn validate_sum(
content: &Option<String>,
embeds: &[SendableEmbed],
max_length: usize,
) -> Result<()> {
let mut running_total = 0;
if let Some(content) = content {
running_total += content.len();
}
for embed in embeds {
if let Some(desc) = &embed.description {
running_total += desc.len();
}
}
if running_total <= max_length {
Ok(())
} else {
Err(create_error!(PayloadTooLarge))
}
}
pub async fn delete(self, db: &Database) -> Result<()> {
let file_ids: Vec<String> = self
.attachments
.map(|files| files.iter().map(|file| file.id.to_string()).collect())
.unwrap_or_default();
if !file_ids.is_empty() {
db.mark_attachments_as_deleted(&file_ids).await?;
}
db.delete_message(&self.id).await?;
EventV1::MessageDelete {
id: self.id,
channel: self.channel.clone(),
}
.p(self.channel)
.await;
Ok(())
}
pub async fn bulk_delete(db: &Database, channel: &str, ids: Vec<String>) -> Result<()> {
let valid_ids = db
.fetch_messages_by_id(&ids)
.await?
.into_iter()
.filter(|msg| msg.channel == channel)
.map(|msg| msg.id)
.collect::<Vec<String>>();
db.delete_messages(channel, &valid_ids).await?;
EventV1::BulkMessageDelete {
channel: channel.to_string(),
ids: valid_ids,
}
.p(channel.to_string())
.await;
Ok(())
}
pub async fn remove_reaction(&self, db: &Database, user: &str, emoji: &str) -> Result<()> {
let empty = if let Some(users) = self.reactions.get(emoji) {
if !users.contains(user) {
return Err(create_error!(NotFound));
}
users.len() == 1
} else {
return Err(create_error!(NotFound));
};
EventV1::MessageUnreact {
id: self.id.to_string(),
channel_id: self.channel.to_string(),
user_id: user.to_string(),
emoji_id: emoji.to_string(),
}
.p(self.channel.to_string())
.await;
if empty {
db.clear_reaction(&self.id, emoji).await
} else {
db.remove_reaction(&self.id, emoji, user).await
}
}
pub async fn clear_reaction(&self, db: &Database, emoji: &str) -> Result<()> {
EventV1::MessageRemoveReaction {
id: self.id.to_string(),
channel_id: self.channel.to_string(),
emoji_id: emoji.to_string(),
}
.p(self.channel.to_string())
.await;
db.clear_reaction(&self.id, emoji).await
}
}
impl SystemMessage {
pub fn into_message(self, channel: String) -> Message {
Message {
id: Ulid::new().to_string(),
channel,
author: "00000000000000000000000000".to_string(),
system: Some(self),
..Default::default()
}
}
}
impl Interactions {
pub async fn validate(&self, db: &Database, permissions: &PermissionValue) -> Result<()> {
let config = config().await;
if let Some(reactions) = &self.reactions {
permissions.throw_if_lacking_channel_permission(ChannelPermission::React)?;
if reactions.len() > config.features.limits.default.message_reactions {
return Err(create_error!(InvalidOperation));
}
for reaction in reactions {
if !Emoji::can_use(db, reaction).await? {
return Err(create_error!(InvalidOperation));
}
}
}
Ok(())
}
pub fn can_use(&self, emoji: &str) -> bool {
if self.restrict_reactions {
if let Some(reactions) = &self.reactions {
reactions.contains(emoji)
} else {
false
}
} else {
true
}
}
pub fn is_default(&self) -> bool {
!self.restrict_reactions && self.reactions.is_none()
}
}