tetratto-core 12.0.1

The core behind Tetratto
Documentation
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 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:{}");
}