tetratto_core/model/
uploads.rs

1use pathbufd::PathBufD;
2use serde::{Serialize, Deserialize};
3use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
4use crate::config::Config;
5use std::fs::{write, exists, remove_file};
6use super::{Error, Result};
7
8#[derive(Serialize, Deserialize, PartialEq, Eq)]
9pub enum MediaType {
10    #[serde(alias = "image/webp")]
11    Webp,
12    #[serde(alias = "image/avif")]
13    Avif,
14    #[serde(alias = "image/png")]
15    Png,
16    #[serde(alias = "image/jpg")]
17    Jpg,
18    #[serde(alias = "image/gif")]
19    Gif,
20    #[serde(alias = "image/carpgraph")]
21    Carpgraph,
22}
23
24impl MediaType {
25    pub fn extension(&self) -> &str {
26        match self {
27            Self::Webp => "webp",
28            Self::Avif => "avif",
29            Self::Png => "png",
30            Self::Jpg => "jpg",
31            Self::Gif => "gif",
32            Self::Carpgraph => "carpgraph",
33        }
34    }
35
36    pub fn mime(&self) -> String {
37        format!("image/{}", self.extension())
38    }
39}
40
41#[derive(Serialize, Deserialize)]
42pub struct MediaUpload {
43    pub id: usize,
44    pub created: usize,
45    pub owner: usize,
46    pub what: MediaType,
47    pub alt: String,
48}
49
50impl MediaUpload {
51    /// Create a new [`MediaUpload`].
52    pub fn new(what: MediaType, owner: usize) -> Self {
53        Self {
54            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
55            created: unix_epoch_timestamp(),
56            owner,
57            what,
58            alt: String::new(),
59        }
60    }
61
62    /// Get the path to the fs file for this upload.
63    pub fn path(&self, config: &Config) -> PathBufD {
64        PathBufD::current()
65            .extend(&[config.dirs.media.as_str(), "uploads"])
66            .join(format!("{}.{}", self.id, self.what.extension()))
67    }
68
69    /// Write to this upload in the file system.
70    pub fn write(&self, config: &Config, bytes: &[u8]) -> Result<()> {
71        match write(self.path(config), bytes) {
72            Ok(_) => Ok(()),
73            Err(e) => Err(Error::MiscError(e.to_string())),
74        }
75    }
76
77    /// Delete this upload in the file system.
78    pub fn remove(&self, config: &Config) -> Result<()> {
79        let path = self.path(config);
80
81        if !exists(&path).unwrap() {
82            return Ok(());
83        }
84
85        match remove_file(path) {
86            Ok(_) => Ok(()),
87            Err(e) => Err(Error::MiscError(e.to_string())),
88        }
89    }
90}
91
92#[derive(Serialize, Deserialize)]
93pub struct CustomEmoji {
94    pub id: usize,
95    pub created: usize,
96    pub owner: usize,
97    pub community: usize,
98    pub upload_id: usize,
99    pub name: String,
100    pub is_animated: bool,
101}
102
103pub type EmojiParserResult = Vec<(String, usize, String)>;
104
105impl CustomEmoji {
106    /// Create a new [`CustomEmoji`].
107    pub fn new(
108        owner: usize,
109        community: usize,
110        upload_id: usize,
111        name: String,
112        is_animated: bool,
113    ) -> Self {
114        Self {
115            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
116            created: unix_epoch_timestamp(),
117            owner,
118            community,
119            upload_id,
120            name,
121            is_animated,
122        }
123    }
124
125    /// Replace emojis in the given input string.
126    pub fn replace(input: &str) -> String {
127        let res = Self::parse(input);
128        let mut out = input.to_string();
129
130        for emoji in res {
131            if emoji.1 == 0 {
132                out = out.replace(
133                    &emoji.0,
134                    match emoji.2.as_str() {
135                        "100" => "💯",
136                        "thumbs_up" => "👍",
137                        "thumbs_down" => "👎",
138                        _ => match emojis::get_by_shortcode(&emoji.2) {
139                            Some(e) => e.as_str(),
140                            None => &emoji.0,
141                        },
142                    },
143                );
144            } else {
145                out = out.replace(
146                    &emoji.0,
147                    &format!(
148                        "<img class=\"emoji\" src=\"/api/v1/communities/{}/emojis/{}\" />",
149                        emoji.1, emoji.2
150                    ),
151                );
152            }
153        }
154
155        out
156    }
157
158    /// Parse text for emojis.
159    ///
160    /// Another "great" parser, just like the mentions parser.
161    ///
162    /// # Returns
163    /// `(capture, community id, emoji name)`
164    pub fn parse(input: &str) -> EmojiParserResult {
165        let mut out = Vec::new();
166        let mut buffer: String = String::new();
167
168        let mut escape: bool = false;
169        let mut in_emoji: bool = false;
170
171        let mut chars = input.chars();
172        while let Some(char) = chars.next() {
173            if char == '\\' && !escape {
174                escape = true;
175                continue;
176            } else if char == ':' && !escape {
177                let mut community_id: String = String::new();
178                let mut accepting_community_id_chars: bool = true;
179                let mut emoji_name: String = String::new();
180
181                for (char_count, char) in (0_u32..).zip(chars.by_ref()) {
182                    if (char == ':') | (char == ' ') {
183                        in_emoji = false;
184                        break;
185                    }
186
187                    if char.is_ascii_digit() && accepting_community_id_chars {
188                        community_id.push(char);
189                    } else if char == '.' {
190                        // the period closes the community id
191                        accepting_community_id_chars = false;
192                    } else {
193                        emoji_name.push(char);
194                    }
195
196                    if char_count >= 4 && community_id.is_empty() {
197                        accepting_community_id_chars = false;
198                    }
199                }
200
201                out.push((
202                    format!(
203                        ":{}{emoji_name}:",
204                        if !community_id.is_empty() {
205                            format!("{community_id}.")
206                        } else {
207                            String::new()
208                        }
209                    ),
210                    community_id.parse::<usize>().unwrap_or(0),
211                    emoji_name,
212                ));
213
214                continue;
215            } else if in_emoji {
216                buffer.push(char);
217            }
218
219            escape = false;
220        }
221
222        out
223    }
224}