remove: old chats core

This commit is contained in:
trisua 2025-09-05 22:08:16 -04:00
parent 918d47d873
commit 6d333378a4
33 changed files with 9 additions and 3377 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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