remove: old chats core
This commit is contained in:
parent
918d47d873
commit
6d333378a4
33 changed files with 9 additions and 3377 deletions
|
@ -1,325 +0,0 @@
|
|||
use oiseau::cache::Cache;
|
||||
use crate::model::moderation::AuditLogEntry;
|
||||
use crate::model::{
|
||||
Error, Result, auth::User, permissions::FinePermission,
|
||||
communities_permissions::CommunityPermission, channels::Channel,
|
||||
};
|
||||
use crate::{auto_method, DataManager};
|
||||
use oiseau::{PostgresRow, execute, get, query_row, query_rows, params};
|
||||
|
||||
impl DataManager {
|
||||
/// Get a [`Channel`] from an SQL row.
|
||||
pub(crate) fn get_channel_from_row(x: &PostgresRow) -> Channel {
|
||||
Channel {
|
||||
id: get!(x->0(i64)) as usize,
|
||||
community: get!(x->1(i64)) as usize,
|
||||
owner: get!(x->2(i64)) as usize,
|
||||
created: get!(x->3(i64)) as usize,
|
||||
minimum_role_read: get!(x->4(i32)) as u32,
|
||||
minimum_role_write: get!(x->5(i32)) as u32,
|
||||
position: get!(x->6(i32)) as usize,
|
||||
members: serde_json::from_str(&get!(x->7(String))).unwrap(),
|
||||
title: get!(x->8(String)),
|
||||
last_message: get!(x->9(i64)) as usize,
|
||||
}
|
||||
}
|
||||
|
||||
auto_method!(get_channel_by_id(usize as i64)@get_channel_from_row -> "SELECT * FROM channels WHERE id = $1" --name="channel" --returns=Channel --cache-key-tmpl="atto.channel:{}");
|
||||
|
||||
/// Get all member profiles from a channel members list.
|
||||
pub async fn fill_members(
|
||||
&self,
|
||||
members: &Vec<usize>,
|
||||
ignore_users: Vec<usize>,
|
||||
) -> Result<Vec<User>> {
|
||||
let mut out = Vec::new();
|
||||
|
||||
for member in members {
|
||||
if ignore_users.contains(member) {
|
||||
continue;
|
||||
}
|
||||
|
||||
out.push(self.get_user_by_id(member.to_owned()).await?);
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Get all channels by community.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `community` - the ID of the community to fetch channels for
|
||||
pub async fn get_channels_by_community(&self, community: usize) -> Result<Vec<Channel>> {
|
||||
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 channels WHERE community = $1 ORDER BY position ASC",
|
||||
&[&(community as i64)],
|
||||
|x| { Self::get_channel_from_row(x) }
|
||||
);
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound("channel".to_string()));
|
||||
}
|
||||
|
||||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
/// Get all channels by user.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `user` - the ID of the user to fetch channels for
|
||||
pub async fn get_channels_by_user(&self, user: usize) -> Result<Vec<Channel>> {
|
||||
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 channels WHERE (owner = $1 OR members LIKE $2) AND community = 0 ORDER BY last_message DESC",
|
||||
params![&(user as i64), &format!("%{user}%")],
|
||||
|x| { Self::get_channel_from_row(x) }
|
||||
);
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound("channel".to_string()));
|
||||
}
|
||||
|
||||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
/// Get a channel given its `owner` and a member.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `owner` - the ID of the owner
|
||||
/// * `member` - the ID of the member
|
||||
pub async fn get_channel_by_owner_member(
|
||||
&self,
|
||||
owner: usize,
|
||||
member: usize,
|
||||
) -> Result<Channel> {
|
||||
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 channels WHERE owner = $1 AND members = $2 AND community = 0 ORDER BY created DESC",
|
||||
params![&(owner as i64), &format!("[{member}]")],
|
||||
|x| { Ok(Self::get_channel_from_row(x)) }
|
||||
);
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound("channel".to_string()));
|
||||
}
|
||||
|
||||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
/// Create a new channel in the database.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - a mock [`Channel`] object to insert
|
||||
pub async fn create_channel(&self, data: Channel) -> Result<()> {
|
||||
let user = self.get_user_by_id(data.owner).await?;
|
||||
|
||||
// check user permission in community
|
||||
if data.community != 0 {
|
||||
let membership = self
|
||||
.get_membership_by_owner_community(user.id, data.community)
|
||||
.await?;
|
||||
|
||||
if !membership.role.check(CommunityPermission::MANAGE_CHANNELS)
|
||||
&& !user.permissions.check(FinePermission::MANAGE_CHANNELS)
|
||||
{
|
||||
return Err(Error::NotAllowed);
|
||||
}
|
||||
}
|
||||
// check members
|
||||
else {
|
||||
for member in &data.members {
|
||||
if self
|
||||
.get_userblock_by_initiator_receiver(member.to_owned(), data.owner)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Err(Error::NotAllowed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
let conn = match self.0.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"INSERT INTO channels VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
|
||||
params![
|
||||
&(data.id as i64),
|
||||
&(data.community as i64),
|
||||
&(data.owner as i64),
|
||||
&(data.created as i64),
|
||||
&(data.minimum_role_read as i32),
|
||||
&(data.minimum_role_write as i32),
|
||||
&(data.position as i32),
|
||||
&serde_json::to_string(&data.members).unwrap(),
|
||||
&data.title,
|
||||
&(data.last_message as i64)
|
||||
]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_channel(&self, id: usize, user: &User) -> Result<()> {
|
||||
let channel = self.get_channel_by_id(id).await?;
|
||||
|
||||
// check user permission in community
|
||||
if user.id != channel.owner {
|
||||
let membership = self
|
||||
.get_membership_by_owner_community(user.id, channel.community)
|
||||
.await?;
|
||||
|
||||
if !membership.role.check(CommunityPermission::MANAGE_CHANNELS) {
|
||||
return Err(Error::NotAllowed);
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
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 channels WHERE id = $1", &[&(id as i64)]);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// delete messages
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"DELETE FROM messages WHERE channel = $1",
|
||||
&[&(id as i64)]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// ...
|
||||
self.0.1.remove(format!("atto.channel:{}", id)).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_channel_member(&self, id: usize, user: User, member: String) -> Result<()> {
|
||||
let mut y = self.get_channel_by_id(id).await?;
|
||||
|
||||
if user.id != y.owner && member != user.username {
|
||||
if !user.permissions.check(FinePermission::MANAGE_CHANNELS) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_audit_log_entry(AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `add_channel_member` with x value `{member}`"),
|
||||
))
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
// check permissions
|
||||
let member = self.get_user_by_username(&member).await?;
|
||||
|
||||
if self
|
||||
.get_userblock_by_initiator_receiver(member.id, user.id)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Err(Error::NotAllowed);
|
||||
}
|
||||
|
||||
// ...
|
||||
y.members.push(member.id);
|
||||
|
||||
let conn = match self.0.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"UPDATE channels SET members = $1 WHERE id = $2",
|
||||
params![&serde_json::to_string(&y.members).unwrap(), &(id as i64)]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
self.0.1.remove(format!("atto.channel:{}", id)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_channel_member(&self, id: usize, user: User, member: usize) -> Result<()> {
|
||||
let mut y = self.get_channel_by_id(id).await?;
|
||||
|
||||
if user.id != y.owner && member != user.id {
|
||||
if !user.permissions.check(FinePermission::MANAGE_CHANNELS) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_audit_log_entry(AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `remove_channel_member` with x value `{member}`"),
|
||||
))
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
y.members
|
||||
.remove(match y.members.iter().position(|x| *x == member) {
|
||||
Some(i) => i,
|
||||
None => return Err(Error::GeneralNotFound("member".to_string())),
|
||||
});
|
||||
|
||||
let conn = match self.0.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"UPDATE channels SET members = $1 WHERE id = $2",
|
||||
params![&serde_json::to_string(&y.members).unwrap(), &(id as i64)]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
self.0.1.remove(format!("atto.channel:{}", id)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
auto_method!(update_channel_title(&str)@get_channel_by_id:FinePermission::MANAGE_CHANNELS; -> "UPDATE channels SET title = $1 WHERE id = $2" --cache-key-tmpl="atto.channel:{}");
|
||||
auto_method!(update_channel_position(i32)@get_channel_by_id:FinePermission::MANAGE_CHANNELS; -> "UPDATE channels SET position = $1 WHERE id = $2" --cache-key-tmpl="atto.channel:{}");
|
||||
auto_method!(update_channel_minimum_role_read(i32)@get_channel_by_id:FinePermission::MANAGE_CHANNELS; -> "UPDATE channels SET minimum_role_read = $1 WHERE id = $2" --cache-key-tmpl="atto.channel:{}");
|
||||
auto_method!(update_channel_minimum_role_write(i32)@get_channel_by_id:FinePermission::MANAGE_CHANNELS; -> "UPDATE channels SET minimum_role_write = $1 WHERE id = $2" --cache-key-tmpl="atto.channel:{}");
|
||||
auto_method!(update_channel_members(Vec<usize>)@get_channel_by_id:FinePermission::MANAGE_CHANNELS; -> "UPDATE channels SET members = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.channel:{}");
|
||||
auto_method!(update_channel_last_message(i64) -> "UPDATE channels SET last_message = $1 WHERE id = $2" --cache-key-tmpl="atto.channel:{}");
|
||||
}
|
|
@ -26,8 +26,6 @@ impl DataManager {
|
|||
execute!(&conn, common::CREATE_TABLE_REQUESTS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_QUESTIONS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_IPBLOCKS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_CHANNELS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_MESSAGES).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_EMOJIS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_STACKS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_DRAFTS).unwrap();
|
||||
|
@ -37,7 +35,6 @@ 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();
|
||||
execute!(&conn, common::CREATE_TABLE_INVITE_CODES).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_DOMAINS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_SERVICES).unwrap();
|
||||
|
|
|
@ -375,11 +375,6 @@ impl DataManager {
|
|||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// remove channels
|
||||
for channel in self.get_channels_by_community(id).await? {
|
||||
self.delete_channel(channel.id, &user).await?;
|
||||
}
|
||||
|
||||
// remove images
|
||||
let avatar = PathBufD::current().extend(&[
|
||||
self.0.0.dirs.media.as_str(),
|
||||
|
|
|
@ -14,8 +14,6 @@ pub const CREATE_TABLE_USER_WARNINGS: &str = include_str!("./sql/create_user_war
|
|||
pub const CREATE_TABLE_REQUESTS: &str = include_str!("./sql/create_requests.sql");
|
||||
pub const CREATE_TABLE_QUESTIONS: &str = include_str!("./sql/create_questions.sql");
|
||||
pub const CREATE_TABLE_IPBLOCKS: &str = include_str!("./sql/create_ipblocks.sql");
|
||||
pub const CREATE_TABLE_CHANNELS: &str = include_str!("./sql/create_channels.sql");
|
||||
pub const CREATE_TABLE_MESSAGES: &str = include_str!("./sql/create_messages.sql");
|
||||
pub const CREATE_TABLE_EMOJIS: &str = include_str!("./sql/create_emojis.sql");
|
||||
pub const CREATE_TABLE_STACKS: &str = include_str!("./sql/create_stacks.sql");
|
||||
pub const CREATE_TABLE_DRAFTS: &str = include_str!("./sql/create_drafts.sql");
|
||||
|
@ -25,7 +23,6 @@ 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");
|
||||
pub const CREATE_TABLE_INVITE_CODES: &str = include_str!("./sql/create_invite_codes.sql");
|
||||
pub const CREATE_TABLE_DOMAINS: &str = include_str!("./sql/create_domains.sql");
|
||||
pub const CREATE_TABLE_SERVICES: &str = include_str!("./sql/create_services.sql");
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
CREATE TABLE IF NOT EXISTS channels (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
community BIGINT NOT NULL,
|
||||
owner BIGINT NOT NULL,
|
||||
created BIGINT NOT NULL,
|
||||
minimum_role_read INT NOT NULL,
|
||||
minimum_role_write INT NOT NULL,
|
||||
position INT NOT NULL,
|
||||
members TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
last_message BIGINT NOT NULL
|
||||
)
|
|
@ -1,8 +0,0 @@
|
|||
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)
|
||||
)
|
|
@ -1,10 +0,0 @@
|
|||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
channel BIGINT NOT NULL,
|
||||
owner BIGINT NOT NULL,
|
||||
created BIGINT NOT NULL,
|
||||
edited BIGINT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
context TEXT NOT NULL,
|
||||
reactions TEXT NOT NULL
|
||||
)
|
|
@ -1,183 +0,0 @@
|
|||
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(())
|
||||
}
|
||||
}
|
|
@ -1,380 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
use oiseau::cache::Cache;
|
||||
use crate::model::auth::Notification;
|
||||
use crate::model::moderation::AuditLogEntry;
|
||||
use crate::model::socket::{SocketMessage, SocketMethod};
|
||||
use crate::model::{
|
||||
Error, Result, auth::User, permissions::FinePermission,
|
||||
communities_permissions::CommunityPermission, channels::Message,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use tetratto_shared::unix_epoch_timestamp;
|
||||
use crate::{auto_method, DataManager};
|
||||
|
||||
use oiseau::{PostgresRow, cache::redis::Commands};
|
||||
|
||||
use oiseau::{execute, get, query_rows, params};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DeleteMessageEvent {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
impl DataManager {
|
||||
/// Get a [`Message`] from an SQL row.
|
||||
pub(crate) fn get_message_from_row(x: &PostgresRow) -> Message {
|
||||
Message {
|
||||
id: get!(x->0(i64)) as usize,
|
||||
channel: get!(x->1(i64)) as usize,
|
||||
owner: get!(x->2(i64)) as usize,
|
||||
created: get!(x->3(i64)) as usize,
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
auto_method!(get_message_by_id(usize as i64)@get_message_from_row -> "SELECT * FROM messages WHERE id = $1" --name="message" --returns=Message --cache-key-tmpl="atto.message:{}");
|
||||
|
||||
/// Complete a vector of just messages with their owner as well.
|
||||
///
|
||||
/// # Returns
|
||||
/// `(message, owner, group with previous messages in ui)`
|
||||
pub async fn fill_messages(
|
||||
&self,
|
||||
messages: Vec<Message>,
|
||||
ignore_users: &[usize],
|
||||
) -> Result<Vec<(Message, User, bool)>> {
|
||||
let mut out: Vec<(Message, User, bool)> = Vec::new();
|
||||
|
||||
let mut users: HashMap<usize, User> = HashMap::new();
|
||||
for (i, message) in messages.iter().enumerate() {
|
||||
let next_owner: usize = match messages.get(i + 1) {
|
||||
Some(m) => m.owner,
|
||||
None => 0,
|
||||
};
|
||||
|
||||
let owner = message.owner;
|
||||
|
||||
if ignore_users.contains(&owner) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(user) = users.get(&owner) {
|
||||
out.push((message.to_owned(), user.clone(), next_owner == owner));
|
||||
} else {
|
||||
let user = self.get_user_by_id_with_void(owner).await?;
|
||||
users.insert(owner, user.clone());
|
||||
out.push((message.to_owned(), user, next_owner == owner));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Get all messages by channel (paginated).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `channel` - the ID of the community to fetch channels for
|
||||
/// * `batch` - the limit of items in each page
|
||||
/// * `page` - the page number
|
||||
pub async fn get_messages_by_channel(
|
||||
&self,
|
||||
channel: usize,
|
||||
batch: usize,
|
||||
page: usize,
|
||||
) -> Result<Vec<Message>> {
|
||||
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 messages WHERE channel = $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
|
||||
&[&(channel as i64), &(batch as i64), &((page * batch) as i64)],
|
||||
|x| { Self::get_message_from_row(x) }
|
||||
);
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound("message".to_string()));
|
||||
}
|
||||
|
||||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
/// Create a new message in the database.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - a mock [`Message`] object to insert
|
||||
pub async fn create_message(&self, mut data: Message) -> Result<()> {
|
||||
if data.content.len() < 2 {
|
||||
return Err(Error::DataTooLong("content".to_string()));
|
||||
}
|
||||
|
||||
if data.content.len() > 2048 {
|
||||
return Err(Error::DataTooLong("content".to_string()));
|
||||
}
|
||||
|
||||
let owner = self.get_user_by_id(data.owner).await?;
|
||||
let channel = self.get_channel_by_id(data.channel).await?;
|
||||
|
||||
// check user permission in community
|
||||
let membership = self
|
||||
.get_membership_by_owner_community(owner.id, channel.community)
|
||||
.await?;
|
||||
|
||||
// check user permission to post in channel
|
||||
if !channel.check_post(owner.id, Some(membership.role)) {
|
||||
return Err(Error::NotAllowed);
|
||||
}
|
||||
|
||||
// send mention notifications
|
||||
let mut already_notified: HashMap<String, User> = HashMap::new();
|
||||
for username in User::parse_mentions(&data.content) {
|
||||
let user = {
|
||||
if let Some(ua) = already_notified.get(&username) {
|
||||
ua.to_owned()
|
||||
} else {
|
||||
let user = self.get_user_by_username(&username).await?;
|
||||
|
||||
// check blocked status
|
||||
if self
|
||||
.get_userblock_by_initiator_receiver(user.id, data.owner)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Err(Error::NotAllowed);
|
||||
}
|
||||
|
||||
// check private status
|
||||
if user.settings.private_profile {
|
||||
if self
|
||||
.get_userfollow_by_initiator_receiver(user.id, data.owner)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return Err(Error::NotAllowed);
|
||||
}
|
||||
}
|
||||
|
||||
// check if the user can read the channel
|
||||
let membership = self
|
||||
.get_membership_by_owner_community(user.id, channel.community)
|
||||
.await?;
|
||||
|
||||
if !channel.check_read(user.id, Some(membership.role)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// create notif
|
||||
self.create_notification(Notification::new(
|
||||
"You've been mentioned in a message!".to_string(),
|
||||
format!(
|
||||
"[@{}](/api/v1/auth/user/find/{}) has mentioned you in their [message](/chats/{}/{}?message={}).",
|
||||
owner.username, owner.id, channel.community, data.channel, data.id
|
||||
),
|
||||
user.id,
|
||||
))
|
||||
.await?;
|
||||
|
||||
// ...
|
||||
already_notified.insert(username.to_owned(), user.clone());
|
||||
user
|
||||
}
|
||||
};
|
||||
|
||||
data.content = data.content.replace(
|
||||
&format!("@{username}"),
|
||||
&format!(
|
||||
"<a href=\"/api/v1/auth/user/find/{}\" target=\"_top\">@{username}</a>",
|
||||
user.id
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// send notifs to members (if this message isn't associated with a channel)
|
||||
if channel.community == 0 {
|
||||
for member in [channel.members, vec![channel.owner]].concat() {
|
||||
if member == owner.id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let user = self.get_user_by_id(member).await?;
|
||||
if user.channel_mutes.contains(&channel.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut notif = Notification::new(
|
||||
"You've received a new message!".to_string(),
|
||||
format!(
|
||||
"[@{}](/api/v1/auth/user/find/{}) has sent a [message](/chats/{}/{}?message={}) in [{}](/chats/{}/{}).",
|
||||
owner.username,
|
||||
owner.id,
|
||||
channel.community,
|
||||
data.channel,
|
||||
data.id,
|
||||
channel.title,
|
||||
channel.community,
|
||||
data.channel
|
||||
),
|
||||
member,
|
||||
);
|
||||
|
||||
notif.tag = format!("chats/{}", channel.id);
|
||||
self.create_notification(notif).await?;
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
let conn = match self.0.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"INSERT INTO messages VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
|
||||
params![
|
||||
&(data.id as i64),
|
||||
&(data.channel as i64),
|
||||
&(data.owner as i64),
|
||||
&(data.created as i64),
|
||||
&(data.edited as i64),
|
||||
&data.content,
|
||||
&serde_json::to_string(&data.context).unwrap(),
|
||||
&serde_json::to_string(&data.reactions).unwrap(),
|
||||
]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// post event
|
||||
let mut con = self.0.1.get_con().await;
|
||||
|
||||
if let Err(e) = con.publish::<String, String, ()>(
|
||||
if channel.community != 0 {
|
||||
// broadcast to community ws
|
||||
format!("chats/{}", channel.community)
|
||||
} else {
|
||||
// broadcast to channel ws
|
||||
format!("chats/{}", channel.id)
|
||||
},
|
||||
serde_json::to_string(&SocketMessage {
|
||||
method: SocketMethod::Message,
|
||||
data: serde_json::to_string(&(data.channel.to_string(), data)).unwrap(),
|
||||
})
|
||||
.unwrap(),
|
||||
) {
|
||||
return Err(Error::MiscError(e.to_string()));
|
||||
}
|
||||
|
||||
// update channel position
|
||||
self.update_channel_last_message(channel.id, unix_epoch_timestamp() as i64)
|
||||
.await?;
|
||||
|
||||
// ...
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_message(&self, id: usize, user: User) -> Result<()> {
|
||||
let message = self.get_message_by_id(id).await?;
|
||||
let channel = self.get_channel_by_id(message.channel).await?;
|
||||
|
||||
// check user permission in community
|
||||
if user.id != message.owner {
|
||||
let membership = self
|
||||
.get_membership_by_owner_community(user.id, channel.community)
|
||||
.await?;
|
||||
|
||||
if !membership.role.check(CommunityPermission::MANAGE_MESSAGES)
|
||||
&& !user.permissions.check(FinePermission::MANAGE_MESSAGES)
|
||||
{
|
||||
return Err(Error::NotAllowed);
|
||||
} else if user.permissions.check(FinePermission::MANAGE_MESSAGES) {
|
||||
self.create_audit_log_entry(AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `delete_message` with x value `{id}`"),
|
||||
))
|
||||
.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 messages 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:{}", id)).await;
|
||||
|
||||
// post event
|
||||
let mut con = self.0.1.get_con().await;
|
||||
|
||||
if let Err(e) = con.publish::<String, String, ()>(
|
||||
if channel.community != 0 {
|
||||
// broadcast to community ws
|
||||
format!("chats/{}", channel.community)
|
||||
} else {
|
||||
// broadcast to channel ws
|
||||
format!("chats/{}", channel.id)
|
||||
},
|
||||
serde_json::to_string(&SocketMessage {
|
||||
method: SocketMethod::Delete,
|
||||
data: serde_json::to_string(&DeleteMessageEvent { id: id.to_string() }).unwrap(),
|
||||
})
|
||||
.unwrap(),
|
||||
) {
|
||||
return Err(Error::MiscError(e.to_string()));
|
||||
}
|
||||
|
||||
// ...
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_message_content(&self, id: usize, user: User, x: String) -> Result<()> {
|
||||
let y = self.get_message_by_id(id).await?;
|
||||
|
||||
if user.id != y.owner {
|
||||
if !user.permissions.check(FinePermission::MANAGE_MESSAGES) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_audit_log_entry(AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `update_message_content` with x value `{id}`"),
|
||||
))
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
let conn = match self.0.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"UPDATE messages SET content = $1, edited = $2 WHERE id = $2",
|
||||
params![&x, &(unix_epoch_timestamp() as i64), &(id as i64)]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// return
|
||||
Ok(())
|
||||
}
|
||||
|
||||
auto_method!(update_message_reactions(HashMap<String, usize>) -> "UPDATE messages SET reactions = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.message:{}");
|
||||
}
|
|
@ -3,7 +3,6 @@ pub mod app_data;
|
|||
mod apps;
|
||||
mod audit_log;
|
||||
mod auth;
|
||||
mod channels;
|
||||
mod common;
|
||||
mod communities;
|
||||
pub mod connections;
|
||||
|
@ -17,8 +16,6 @@ mod ipblocks;
|
|||
mod journals;
|
||||
mod letters;
|
||||
mod memberships;
|
||||
mod message_reactions;
|
||||
mod messages;
|
||||
mod notes;
|
||||
mod notifications;
|
||||
mod polls;
|
||||
|
|
|
@ -1,133 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
|
||||
|
||||
use super::communities_permissions::CommunityPermission;
|
||||
|
||||
/// A channel is a more "chat-like" feed in communities.
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Channel {
|
||||
pub id: usize,
|
||||
pub community: usize,
|
||||
pub owner: usize,
|
||||
pub created: usize,
|
||||
/// The minimum role (as bits) that can read this channel.
|
||||
pub minimum_role_read: u32,
|
||||
/// The minimum role (as bits) that can write to this channel.
|
||||
pub minimum_role_write: u32,
|
||||
/// The position of this channel in the UI.
|
||||
///
|
||||
/// Top (0) to bottom.
|
||||
pub position: usize,
|
||||
/// The members of the chat (ids). Should be empty if `community > 0`.
|
||||
///
|
||||
/// The owner should not be a member of the channel since any member can update members.
|
||||
pub members: Vec<usize>,
|
||||
/// The title of the channel.
|
||||
pub title: String,
|
||||
/// The timestamp of the last message in the channel.
|
||||
pub last_message: usize,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
/// Create a new [`Channel`].
|
||||
pub fn new(community: usize, owner: usize, position: usize, title: String) -> Self {
|
||||
let created = unix_epoch_timestamp();
|
||||
|
||||
Self {
|
||||
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
|
||||
community,
|
||||
owner,
|
||||
created,
|
||||
minimum_role_read: (CommunityPermission::DEFAULT | CommunityPermission::MEMBER).bits(),
|
||||
minimum_role_write: (CommunityPermission::DEFAULT | CommunityPermission::MEMBER).bits(),
|
||||
position,
|
||||
members: Vec::new(),
|
||||
title,
|
||||
last_message: created,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the given `uid` can post in the channel.
|
||||
pub fn check_post(&self, uid: usize, membership: Option<CommunityPermission>) -> bool {
|
||||
let mut is_member = false;
|
||||
|
||||
if let Some(membership) = membership {
|
||||
is_member = membership.bits() >= self.minimum_role_write
|
||||
}
|
||||
|
||||
(uid == self.owner) | is_member | self.members.contains(&uid)
|
||||
}
|
||||
|
||||
/// Check if the given `uid` can post in the channel.
|
||||
pub fn check_read(&self, uid: usize, membership: Option<CommunityPermission>) -> bool {
|
||||
let mut is_member = false;
|
||||
|
||||
if let Some(membership) = membership {
|
||||
is_member = membership.bits() >= self.minimum_role_read
|
||||
}
|
||||
|
||||
(uid == self.owner) | is_member | self.members.contains(&uid)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Message {
|
||||
pub id: usize,
|
||||
pub channel: usize,
|
||||
pub owner: usize,
|
||||
pub created: usize,
|
||||
pub edited: usize,
|
||||
pub content: String,
|
||||
pub context: MessageContext,
|
||||
pub reactions: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub fn new(channel: usize, owner: usize, content: String) -> Self {
|
||||
let now = unix_epoch_timestamp();
|
||||
|
||||
Self {
|
||||
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
|
||||
channel,
|
||||
owner,
|
||||
created: now,
|
||||
edited: now,
|
||||
content,
|
||||
context: MessageContext,
|
||||
reactions: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct MessageContext;
|
||||
|
||||
impl Default for MessageContext {
|
||||
fn default() -> Self {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@ bitflags! {
|
|||
const MANAGE_PINS = 1 << 7;
|
||||
const MANAGE_COMMUNITY = 1 << 8;
|
||||
const MANAGE_QUESTIONS = 1 << 9;
|
||||
const MANAGE_CHANNELS = 1 << 10;
|
||||
const UNUSED_0 = 1 << 10;
|
||||
const MANAGE_MESSAGES = 1 << 11;
|
||||
const MANAGE_EMOJIS = 1 << 12;
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ pub mod addr;
|
|||
pub mod apps;
|
||||
pub mod auth;
|
||||
pub mod carp;
|
||||
pub mod channels;
|
||||
pub mod communities;
|
||||
pub mod communities_permissions;
|
||||
pub mod economy;
|
||||
|
|
|
@ -30,7 +30,7 @@ bitflags! {
|
|||
const SUPPORTER = 1 << 19;
|
||||
const MANAGE_REQUESTS = 1 << 20;
|
||||
const MANAGE_QUESTIONS = 1 << 21;
|
||||
const MANAGE_CHANNELS = 1 << 22;
|
||||
const UNUSED_0 = 1 << 22;
|
||||
const MANAGE_MESSAGES = 1 << 23;
|
||||
const MANAGE_UPLOADS = 1 << 24;
|
||||
const MANAGE_EMOJIS = 1 << 25;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue