add: chat message reactions
This commit is contained in:
parent
a4298f95f6
commit
a37312fecf
20 changed files with 557 additions and 25 deletions
|
@ -389,6 +389,46 @@ impl DataManager {
|
|||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// delete stackblocks
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"DELETE FROM stackblocks WHERE owner = $1",
|
||||
&[&(id as i64)]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// delete journals
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"DELETE FROM journals WHERE owner = $1",
|
||||
&[&(id as i64)]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// delete notes
|
||||
let res = execute!(&conn, "DELETE FROM notes WHERE owner = $1", &[&(id as i64)]);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// delete message reactions
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"DELETE FROM message_reactions WHERE owner = $1",
|
||||
&[&(id as i64)]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// delete user follows... individually since it requires updating user counts
|
||||
for follow in self.get_userfollows_by_receiver_all(id).await? {
|
||||
self.delete_userfollow(follow.id, &user, true).await?;
|
||||
|
|
|
@ -38,6 +38,7 @@ impl DataManager {
|
|||
execute!(&conn, common::CREATE_TABLE_STACKBLOCKS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_JOURNALS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_NOTES).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_MESSAGE_REACTIONS).unwrap();
|
||||
|
||||
self.0
|
||||
.1
|
||||
|
|
|
@ -25,3 +25,4 @@ pub const CREATE_TABLE_APPS: &str = include_str!("./sql/create_apps.sql");
|
|||
pub const CREATE_TABLE_STACKBLOCKS: &str = include_str!("./sql/create_stackblocks.sql");
|
||||
pub const CREATE_TABLE_JOURNALS: &str = include_str!("./sql/create_journals.sql");
|
||||
pub const CREATE_TABLE_NOTES: &str = include_str!("./sql/create_notes.sql");
|
||||
pub const CREATE_TABLE_MESSAGE_REACTIONS: &str = include_str!("./sql/create_message_reactions.sql");
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
CREATE TABLE IF NOT EXISTS message_reactions (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
created BIGINT NOT NULL,
|
||||
owner BIGINT NOT NULL,
|
||||
message BIGINT NOT NULL,
|
||||
emoji TEXT NOT NULL,
|
||||
UNIQUE (owner, message, emoji)
|
||||
)
|
|
@ -5,5 +5,6 @@ CREATE TABLE IF NOT EXISTS messages (
|
|||
created BIGINT NOT NULL,
|
||||
edited BIGINT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
context TEXT NOT NULL
|
||||
context TEXT NOT NULL,
|
||||
reactions TEXT NOT NULL
|
||||
)
|
||||
|
|
|
@ -12,5 +12,6 @@ CREATE TABLE IF NOT EXISTS questions (
|
|||
dislikes INT NOT NULL,
|
||||
-- ...
|
||||
context TEXT NOT NULL,
|
||||
ip TEXT NOT NULL
|
||||
ip TEXT NOT NULL,
|
||||
drawings TEXT NOT NULL
|
||||
)
|
||||
|
|
183
crates/core/src/database/message_reactions.rs
Normal file
183
crates/core/src/database/message_reactions.rs
Normal file
|
@ -0,0 +1,183 @@
|
|||
use oiseau::{cache::Cache, query_rows};
|
||||
use crate::model::{
|
||||
Error, Result,
|
||||
auth::{Notification, User},
|
||||
permissions::FinePermission,
|
||||
channels::MessageReaction,
|
||||
};
|
||||
use crate::{auto_method, DataManager};
|
||||
|
||||
use oiseau::{PostgresRow, execute, get, query_row, params};
|
||||
|
||||
impl DataManager {
|
||||
/// Get a [`MessageReaction`] from an SQL row.
|
||||
pub(crate) fn get_message_reaction_from_row(x: &PostgresRow) -> MessageReaction {
|
||||
MessageReaction {
|
||||
id: get!(x->0(i64)) as usize,
|
||||
created: get!(x->1(i64)) as usize,
|
||||
owner: get!(x->2(i64)) as usize,
|
||||
message: get!(x->3(i64)) as usize,
|
||||
emoji: get!(x->4(String)),
|
||||
}
|
||||
}
|
||||
|
||||
auto_method!(get_message_reaction_by_id()@get_message_reaction_from_row -> "SELECT * FROM message_reactions WHERE id = $1" --name="message_reaction" --returns=MessageReaction --cache-key-tmpl="atto.message_reaction:{}");
|
||||
|
||||
/// Get message_reactions by `owner` and `message`.
|
||||
pub async fn get_message_reactions_by_owner_message(
|
||||
&self,
|
||||
owner: usize,
|
||||
message: usize,
|
||||
) -> Result<Vec<MessageReaction>> {
|
||||
let conn = match self.0.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = query_rows!(
|
||||
&conn,
|
||||
"SELECT * FROM message_reactions WHERE owner = $1 AND message = $2",
|
||||
&[&(owner as i64), &(message as i64)],
|
||||
|x| { Self::get_message_reaction_from_row(x) }
|
||||
);
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound("message_reaction".to_string()));
|
||||
}
|
||||
|
||||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
/// Get a message_reaction by `owner`, `message`, and `emoji`.
|
||||
pub async fn get_message_reaction_by_owner_message_emoji(
|
||||
&self,
|
||||
owner: usize,
|
||||
message: usize,
|
||||
emoji: &str,
|
||||
) -> Result<MessageReaction> {
|
||||
let conn = match self.0.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = query_row!(
|
||||
&conn,
|
||||
"SELECT * FROM message_reactions WHERE owner = $1 AND message = $2 AND emoji = $3",
|
||||
params![&(owner as i64), &(message as i64), &emoji],
|
||||
|x| { Ok(Self::get_message_reaction_from_row(x)) }
|
||||
);
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound("message_reaction".to_string()));
|
||||
}
|
||||
|
||||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
/// Create a new message_reaction in the database.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - a mock [`MessageReaction`] object to insert
|
||||
pub async fn create_message_reaction(&self, data: MessageReaction, user: &User) -> Result<()> {
|
||||
let conn = match self.0.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let mut message = self.get_message_by_id(data.message).await?;
|
||||
let channel = self.get_channel_by_id(message.channel).await?;
|
||||
|
||||
// ...
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"INSERT INTO message_reactions VALUES ($1, $2, $3, $4, $5)",
|
||||
params![
|
||||
&(data.id as i64),
|
||||
&(data.created as i64),
|
||||
&(data.owner as i64),
|
||||
&(data.message as i64),
|
||||
&data.emoji
|
||||
]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// incr corresponding
|
||||
if let Some(x) = message.reactions.get(&data.emoji) {
|
||||
message.reactions.insert(data.emoji.clone(), x + 1);
|
||||
} else {
|
||||
message.reactions.insert(data.emoji.clone(), 1);
|
||||
}
|
||||
|
||||
self.update_message_reactions(message.id, message.reactions)
|
||||
.await?;
|
||||
|
||||
// send notif
|
||||
if message.owner != user.id {
|
||||
self
|
||||
.create_notification(Notification::new(
|
||||
"Your message has received a reaction!".to_string(),
|
||||
format!(
|
||||
"[@{}](/api/v1/auth/user/find/{}) has reacted \"{}\" to your [message](/chats/{}/{}?message={})!",
|
||||
user.username, user.id, data.emoji, channel.community, channel.id, message.id
|
||||
),
|
||||
message.owner,
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
|
||||
// return
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_message_reaction(&self, id: usize, user: &User) -> Result<()> {
|
||||
let message_reaction = self.get_message_reaction_by_id(id).await?;
|
||||
|
||||
if user.id != message_reaction.owner
|
||||
&& !user.permissions.check(FinePermission::MANAGE_REACTIONS)
|
||||
{
|
||||
return Err(Error::NotAllowed);
|
||||
}
|
||||
|
||||
let mut message = self.get_message_by_id(message_reaction.message).await?;
|
||||
|
||||
let conn = match self.0.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"DELETE FROM message_reactions WHERE id = $1",
|
||||
&[&(id as i64)]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
self.0
|
||||
.1
|
||||
.remove(format!("atto.message_reaction:{}", id))
|
||||
.await;
|
||||
|
||||
// decr message reaction count
|
||||
if let Some(x) = message.reactions.get(&message_reaction.emoji) {
|
||||
if *x == 1 {
|
||||
// there are no 0 of this reaction
|
||||
message.reactions.remove(&message_reaction.emoji);
|
||||
} else {
|
||||
// decr 1
|
||||
message.reactions.insert(message_reaction.emoji, x - 1);
|
||||
}
|
||||
}
|
||||
|
||||
self.update_message_reactions(message.id, message.reactions)
|
||||
.await?;
|
||||
|
||||
// return
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -31,6 +31,7 @@ impl DataManager {
|
|||
edited: get!(x->4(i64)) as usize,
|
||||
content: get!(x->5(String)),
|
||||
context: serde_json::from_str(&get!(x->6(String))).unwrap(),
|
||||
reactions: serde_json::from_str(&get!(x->7(String))).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -218,7 +219,7 @@ impl DataManager {
|
|||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"INSERT INTO messages VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||
"INSERT INTO messages VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
|
||||
params![
|
||||
&(data.id as i64),
|
||||
&(data.channel as i64),
|
||||
|
@ -226,7 +227,8 @@ impl DataManager {
|
|||
&(data.created as i64),
|
||||
&(data.edited as i64),
|
||||
&data.content,
|
||||
&serde_json::to_string(&data.context).unwrap()
|
||||
&serde_json::to_string(&data.context).unwrap(),
|
||||
&serde_json::to_string(&data.reactions).unwrap(),
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -357,4 +359,6 @@ impl DataManager {
|
|||
// return
|
||||
Ok(())
|
||||
}
|
||||
|
||||
auto_method!(update_message_reactions(HashMap<String, usize>) -> "UPDATE messages SET reactions = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.message:{}");
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ mod ipbans;
|
|||
mod ipblocks;
|
||||
mod journals;
|
||||
mod memberships;
|
||||
mod message_reactions;
|
||||
mod messages;
|
||||
mod notes;
|
||||
mod notifications;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
|
||||
|
||||
|
@ -79,6 +81,7 @@ pub struct Message {
|
|||
pub edited: usize,
|
||||
pub content: String,
|
||||
pub context: MessageContext,
|
||||
pub reactions: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
|
@ -93,6 +96,7 @@ impl Message {
|
|||
edited: now,
|
||||
content,
|
||||
context: MessageContext,
|
||||
reactions: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -105,3 +109,25 @@ impl Default for MessageContext {
|
|||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct MessageReaction {
|
||||
pub id: usize,
|
||||
pub created: usize,
|
||||
pub owner: usize,
|
||||
pub message: usize,
|
||||
pub emoji: String,
|
||||
}
|
||||
|
||||
impl MessageReaction {
|
||||
/// Create a new [`MessageReaction`].
|
||||
pub fn new(owner: usize, message: usize, emoji: String) -> Self {
|
||||
Self {
|
||||
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
|
||||
created: unix_epoch_timestamp(),
|
||||
owner,
|
||||
message,
|
||||
emoji,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue