tetratto-core 15.0.2

The core behind Tetratto
Documentation
use pathbufd::PathBufD;
use serde::{Serialize, Deserialize};
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
use crate::config::Config;
use std::{
    collections::HashMap,
    fs::{exists, remove_file, write},
};
use super::{Error, Result};

#[derive(Serialize, Deserialize, PartialEq, Eq)]
pub enum MediaType {
    #[serde(alias = "image/webp")]
    Webp,
    #[serde(alias = "image/avif")]
    Avif,
    #[serde(alias = "image/png")]
    Png,
    #[serde(alias = "image/jpg")]
    Jpg,
    #[serde(alias = "image/gif")]
    Gif,
    #[serde(alias = "image/carpgraph")]
    Carpgraph,
}

impl MediaType {
    pub fn extension(&self) -> &str {
        match self {
            Self::Webp => "webp",
            Self::Avif => "avif",
            Self::Png => "png",
            Self::Jpg => "jpg",
            Self::Gif => "gif",
            Self::Carpgraph => "carpgraph",
        }
    }

    pub fn mime(&self) -> String {
        format!("image/{}", self.extension())
    }
}

#[derive(Serialize, Deserialize)]
pub struct UploadMetadata {
    pub what: MediaType,
    #[serde(default)]
    pub alt: String,
    #[serde(default)]
    pub kv: HashMap<String, String>,
}

impl UploadMetadata {
    pub fn validate_kv(&self) -> Result<()> {
        for x in &self.kv {
            if x.0.len() > 32 {
                return Err(Error::DataTooLong("key".to_string()));
            }

            if x.1.len() > 128 {
                return Err(Error::DataTooLong("value".to_string()));
            }
        }

        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
pub struct MediaUpload {
    pub id: usize,
    pub created: usize,
    pub owner: usize,
    pub bucket: String,
    pub metadata: UploadMetadata,
}

impl MediaUpload {
    /// Create a new [`MediaUpload`].
    pub fn new(what: MediaType, owner: usize, bucket: String) -> Self {
        Self {
            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
            created: unix_epoch_timestamp(),
            owner,
            bucket,
            metadata: UploadMetadata {
                alt: String::new(),
                what,
                kv: HashMap::new(),
            },
        }
    }

    /// Get the path to the fs file for this upload.
    pub fn path(&self, config: &Config) -> PathBufD {
        PathBufD::current()
            .extend(&[config.dirs.media.as_str(), "uploads"])
            .join(format!("{}.{}", self.id, self.metadata.what.extension()))
    }

    /// Write to this upload in the file system.
    pub fn write(&self, config: &Config, bytes: &[u8]) -> Result<()> {
        match write(self.path(config), bytes) {
            Ok(_) => Ok(()),
            Err(e) => Err(Error::MiscError(e.to_string())),
        }
    }

    /// Delete this upload in the file system.
    pub fn remove(&self, config: &Config) -> Result<()> {
        let path = self.path(config);

        if !exists(&path).unwrap() {
            return Ok(());
        }

        match remove_file(path) {
            Ok(_) => Ok(()),
            Err(e) => Err(Error::MiscError(e.to_string())),
        }
    }
}

#[derive(Serialize, Deserialize)]
pub struct CustomEmoji {
    pub id: usize,
    pub created: usize,
    pub owner: usize,
    pub community: usize,
    pub upload_id: usize,
    pub name: String,
    pub is_animated: bool,
}

pub type EmojiParserResult = Vec<(String, usize, String)>;

impl CustomEmoji {
    /// Create a new [`CustomEmoji`].
    pub fn new(
        owner: usize,
        community: usize,
        upload_id: usize,
        name: String,
        is_animated: bool,
    ) -> Self {
        Self {
            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
            created: unix_epoch_timestamp(),
            owner,
            community,
            upload_id,
            name,
            is_animated,
        }
    }

    /// Replace emojis in the given input string.
    pub fn replace(input: &str) -> String {
        let res = Self::parse(input);
        let mut out = input.to_string();

        for emoji in res {
            if emoji.1 == 0 {
                out = out.replace(
                    &emoji.0,
                    match emoji.2.as_str() {
                        "100" => "💯",
                        "thumbs_up" => "👍",
                        "thumbs_down" => "👎",
                        _ => match emojis::get_by_shortcode(&emoji.2) {
                            Some(e) => e.as_str(),
                            None => &emoji.0,
                        },
                    },
                );
            } else {
                out = out.replace(
                    &emoji.0,
                    &format!(
                        "<img class=\"emoji\" src=\"/api/v1/communities/{}/emojis/{}\" />",
                        emoji.1, emoji.2
                    ),
                );
            }
        }

        out
    }

    /// Parse text for emojis.
    ///
    /// Another "great" parser, just like the mentions parser.
    ///
    /// # Returns
    /// `(capture, community id, emoji name)`
    pub fn parse(input: &str) -> EmojiParserResult {
        let mut out = Vec::new();
        let mut buffer: String = String::new();

        let mut escape: bool = false;
        let mut in_emoji: bool = false;

        let mut chars = input.chars();
        while let Some(char) = chars.next() {
            if char == '\\' && !escape {
                escape = true;
                continue;
            } else if char == ':' && !escape {
                let mut community_id: String = String::new();
                let mut accepting_community_id_chars: bool = true;
                let mut emoji_name: String = String::new();

                for (char_count, char) in (0_u32..).zip(chars.by_ref()) {
                    if (char == ':') | (char == ' ') {
                        in_emoji = false;
                        break;
                    }

                    if char.is_ascii_digit() && accepting_community_id_chars {
                        community_id.push(char);
                    } else if char == '.' {
                        // the period closes the community id
                        accepting_community_id_chars = false;
                    } else {
                        emoji_name.push(char);
                    }

                    if char_count >= 4 && community_id.is_empty() {
                        accepting_community_id_chars = false;
                    }
                }

                out.push((
                    format!(
                        ":{}{emoji_name}:",
                        if !community_id.is_empty() {
                            format!("{community_id}.")
                        } else {
                            String::new()
                        }
                    ),
                    community_id.parse::<usize>().unwrap_or(0),
                    emoji_name,
                ));

                continue;
            } else if in_emoji {
                buffer.push(char);
            }

            escape = false;
        }

        out
    }
}