feat: add message reactions + interactions object
This commit is contained in:
1850
crates/quark/assets/emojis.txt
Normal file
1850
crates/quark/assets/emojis.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -78,6 +78,29 @@ pub enum EventV1 {
|
||||
/// Delete message
|
||||
MessageDelete { id: String, channel: String },
|
||||
|
||||
/// New reaction to a message
|
||||
MessageReact {
|
||||
id: String,
|
||||
channel_id: String,
|
||||
user_id: String,
|
||||
emoji_id: String,
|
||||
},
|
||||
|
||||
/// Remove user's reaction from message
|
||||
MessageUnreact {
|
||||
id: String,
|
||||
channel_id: String,
|
||||
user_id: String,
|
||||
emoji_id: String,
|
||||
},
|
||||
|
||||
/// Remove a reaction from message
|
||||
MessageRemoveReaction {
|
||||
id: String,
|
||||
channel_id: String,
|
||||
emoji_id: String,
|
||||
},
|
||||
|
||||
/// Bulk delete messages
|
||||
BulkMessageDelete { channel: String, ids: Vec<String> },
|
||||
|
||||
|
||||
@@ -64,4 +64,22 @@ impl AbstractMessage for DummyDb {
|
||||
) -> Result<Vec<Message>> {
|
||||
Ok(vec![self.fetch_message(channel).await.unwrap()])
|
||||
}
|
||||
|
||||
/// Add a new reaction to a message
|
||||
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
|
||||
info!("Add to {id} with {emoji} and {user}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a reaction from a message
|
||||
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
|
||||
info!("Remove {emoji} from {id} for {user}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove reaction from a message
|
||||
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
|
||||
info!("Clear {emoji} on {id}");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@ use crate::{
|
||||
events::client::EventV1,
|
||||
models::{
|
||||
message::{
|
||||
AppendMessage, BulkMessageResponse, PartialMessage, SendableEmbed, SystemMessage,
|
||||
AppendMessage, BulkMessageResponse, Interactions, PartialMessage, SendableEmbed,
|
||||
SystemMessage,
|
||||
},
|
||||
Channel, Message, User,
|
||||
Channel, Emoji, Message, User,
|
||||
},
|
||||
presence::presence_filter_online,
|
||||
tasks::ack::AckEvent,
|
||||
@@ -191,6 +192,79 @@ impl Message {
|
||||
Err(Error::PayloadTooLarge)
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a reaction to a message
|
||||
pub async fn add_reaction(&self, db: &Database, user: &User, emoji: &str) -> Result<()> {
|
||||
// Check if the emoji is whitelisted
|
||||
if !self.interactions.can_use(emoji) {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
|
||||
// Check if the emoji is usable by us
|
||||
if !Emoji::can_use(db, emoji).await? {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
|
||||
// Send reaction event
|
||||
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;
|
||||
|
||||
// Add emoji
|
||||
db.add_reaction(&self.id, emoji, &user.id).await
|
||||
}
|
||||
|
||||
/// Remove a reaction from a message
|
||||
pub async fn remove_reaction(&self, db: &Database, user: &str, emoji: &str) -> Result<()> {
|
||||
// Check if it actually exists
|
||||
let empty = if let Some(users) = self.reactions.get(emoji) {
|
||||
if !users.contains(user) {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
|
||||
users.len() == 1
|
||||
} else {
|
||||
return Err(Error::NotFound);
|
||||
};
|
||||
|
||||
// Send reaction event
|
||||
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 {
|
||||
// If empty, remove the reaction entirely
|
||||
db.clear_reaction(&self.id, emoji).await
|
||||
} else {
|
||||
// Otherwise only remove that one reaction
|
||||
db.remove_reaction(&self.id, emoji, user).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a reaction from a message
|
||||
pub async fn clear_reaction(&self, db: &Database, emoji: &str) -> Result<()> {
|
||||
// Send reaction event
|
||||
EventV1::MessageRemoveReaction {
|
||||
id: self.id.to_string(),
|
||||
channel_id: self.channel.to_string(),
|
||||
emoji_id: emoji.to_string(),
|
||||
}
|
||||
.p(self.channel.to_string())
|
||||
.await;
|
||||
|
||||
// Write to database
|
||||
db.clear_reaction(&self.id, emoji).await
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoUsers {
|
||||
@@ -324,3 +398,40 @@ impl BulkMessageResponse {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Interactions {
|
||||
/// Validate interactions info is correct
|
||||
pub async fn validate(&self, db: &Database) -> Result<()> {
|
||||
if let Some(reactions) = &self.reactions {
|
||||
if reactions.len() > 20 {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
|
||||
for reaction in reactions {
|
||||
if !Emoji::can_use(db, reaction).await? {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if we can use a given emoji to react
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if default initialisation of fields
|
||||
pub fn is_default(&self) -> bool {
|
||||
!self.restrict_reactions && self.reactions.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
use std::{collections::HashSet, str::FromStr};
|
||||
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{
|
||||
events::client::EventV1,
|
||||
models::{emoji::EmojiParent, Emoji},
|
||||
Database, Result,
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
/// Permissible emojis
|
||||
static ref PERMISSIBLE_EMOJIS: HashSet<String> = include_str!(crate::asset!("emojis.txt"))
|
||||
.split('\n')
|
||||
.map(|x| x.into())
|
||||
.collect();
|
||||
}
|
||||
|
||||
impl Emoji {
|
||||
/// Get parent id
|
||||
fn parent(&self) -> &str {
|
||||
@@ -33,4 +45,14 @@ impl Emoji {
|
||||
|
||||
db.detach_emoji(&self).await
|
||||
}
|
||||
|
||||
/// Check whether we can use a given emoji
|
||||
pub async fn can_use(db: &Database, emoji: &str) -> Result<bool> {
|
||||
if Ulid::from_str(emoji).is_ok() {
|
||||
db.fetch_emoji(emoji).await?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(PERMISSIBLE_EMOJIS.contains(emoji))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,4 +279,70 @@ impl AbstractMessage for MongoDb {
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Add a new reaction to a message
|
||||
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": id
|
||||
},
|
||||
doc! {
|
||||
"$addToSet": {
|
||||
format!("reactions.{emoji}"): user
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "update_one",
|
||||
with: "message",
|
||||
})
|
||||
}
|
||||
|
||||
/// Remove a reaction from a message
|
||||
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": id
|
||||
},
|
||||
doc! {
|
||||
"$pull": {
|
||||
format!("reactions.{emoji}"): user
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "update_one",
|
||||
with: "message",
|
||||
})
|
||||
}
|
||||
|
||||
/// Remove reaction from a message
|
||||
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": id
|
||||
},
|
||||
doc! {
|
||||
"$unset": {
|
||||
format!("reactions.{emoji}"): 1
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "update_one",
|
||||
with: "message",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::util::regex::RE_COLOUR;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
@@ -13,6 +14,11 @@ use crate::{
|
||||
types::january::Embed,
|
||||
};
|
||||
|
||||
/// Utility function to check if a boolean value is false
|
||||
pub fn if_false(t: &bool) -> bool {
|
||||
!t
|
||||
}
|
||||
|
||||
/// # Reply
|
||||
///
|
||||
/// Representation of a message reply before it is sent.
|
||||
@@ -88,6 +94,17 @@ pub struct Masquerade {
|
||||
pub colour: Option<String>,
|
||||
}
|
||||
|
||||
/// Information to guide interactions on this message
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, Validate, Default)]
|
||||
pub struct Interactions {
|
||||
/// Reactions which should always appear and be distinct
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub reactions: Option<HashSet<String>>,
|
||||
/// Whether reactions should be restricted to the given list
|
||||
#[serde(skip_serializing_if = "if_false", default)]
|
||||
pub restrict_reactions: bool,
|
||||
}
|
||||
|
||||
/// Representation of a Message on Revolt
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, OptionalStruct, Default)]
|
||||
#[optional_derive(Serialize, Deserialize, JsonSchema, Debug, Default, Clone)]
|
||||
@@ -127,6 +144,12 @@ pub struct Message {
|
||||
/// Array of message ids this message is replying to
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub replies: Option<Vec<String>>,
|
||||
/// Hashmap of emoji IDs to array of user IDs
|
||||
#[serde(skip_serializing_if = "HashMap::is_empty", default)]
|
||||
pub reactions: HashMap<String, HashSet<String>>,
|
||||
/// Information about how this message should be interacted with
|
||||
#[serde(skip_serializing_if = "Interactions::is_default", default)]
|
||||
pub interactions: Interactions,
|
||||
/// Name and / or avatar overrides for this message
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub masquerade: Option<Masquerade>,
|
||||
|
||||
@@ -65,8 +65,8 @@ pub enum Permission {
|
||||
UploadFiles = 1 << 27,
|
||||
/// Masquerade messages using custom nickname and avatar
|
||||
Masquerade = 1 << 28,
|
||||
|
||||
// % 1 bits reserved
|
||||
/// React to messages with emojis
|
||||
React = 1 << 29,
|
||||
|
||||
// * Voice permissions
|
||||
/// Connect to a voice channel
|
||||
|
||||
@@ -42,4 +42,13 @@ pub trait AbstractMessage: Sync + Send {
|
||||
after: Option<String>,
|
||||
sort: MessageSort,
|
||||
) -> Result<Vec<Message>>;
|
||||
|
||||
/// Add a new reaction to a message
|
||||
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>;
|
||||
|
||||
/// Remove a reaction from a message
|
||||
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>;
|
||||
|
||||
/// Remove reaction from a message
|
||||
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()>;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::models::{Bot, Channel, Emoji, Invite, Member, Message, Server, ServerBan, User};
|
||||
use crate::presence::presence_is_online;
|
||||
use crate::{Database, Result};
|
||||
use crate::{Database, Error, Result};
|
||||
|
||||
/// Reference to some object in the database
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -44,6 +44,16 @@ impl Ref {
|
||||
db.fetch_message(&self.id).await
|
||||
}
|
||||
|
||||
/// Fetch message in channel from Ref
|
||||
pub async fn as_message_in(&self, db: &Database, channel: &str) -> Result<Message> {
|
||||
let message = self.as_message(db).await?;
|
||||
if message.channel != channel {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
/// Fetch bot from Ref
|
||||
pub async fn as_bot(&self, db: &Database) -> Result<Bot> {
|
||||
db.fetch_bot(&self.id).await
|
||||
|
||||
Reference in New Issue
Block a user