authbeam/
database.rs

1use std::collections::BTreeMap;
2
3use crate::layout::LayoutComponent;
4use crate::model::{
5    DatabaseError, FinePermission, IpBan, IpBanCreate, IpBlock, IpBlockCreate, Item, ItemCreate,
6    ItemEdit, ItemEditContent, ItemStatus, ItemType, Profile, ProfileCreate, ProfileMetadata,
7    RelationshipStatus, TokenContext, Transaction, TransactionCreate, UserLabel, Warning,
8    WarningCreate,
9};
10use crate::model::{Group, Notification, NotificationCreate, UserFollow};
11use hcaptcha_no_wasm::Hcaptcha;
12use rainbeam_shared::snow::AlmostSnowflake;
13use reqwest::Client as HttpClient;
14use serde::{Deserialize, Serialize};
15
16use databeam::{query as sqlquery, utility, prelude::*};
17use pathbufd::{PathBufD, pathd};
18
19pub use rainbeam_shared::config::HCaptchaConfig;
20
21pub type Result<T> = std::result::Result<T, DatabaseError>;
22use std::sync::LazyLock;
23
24use crate::{cache_sync, from_row, update_profile_count, simplify};
25
26/// Custom keys allowed to be used as metadata options.
27pub static ALLOWED_CUSTOM_KEYS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
28    vec![
29        "sparkler:display_name",
30        "sparkler:status_note",
31        "sparkler:status_emoji",
32        "sparkler:limited_friend_requests",
33        "sparkler:limited_chats",
34        "sparkler:private_profile",
35        "sparkler:allow_drawings",
36        "sparkler:biography",
37        "sparkler:sidebar",
38        "sparkler:avatar_url",
39        "sparkler:banner_url",
40        "sparkler:banner_fit",
41        "sparkler:website_theme",
42        "sparkler:allow_profile_themes",
43        "sparkler:motivational_header",
44        "sparkler:warning",
45        "sparkler:anonymous_username",
46        "sparkler:anonymous_avatar",
47        "sparkler:pinned",
48        "sparkler:profile_theme",
49        "sparkler:desktop_tl_logo",
50        "sparkler:custom_css",
51        "sparkler:color_surface",
52        "sparkler:color_lowered",
53        "sparkler:color_super_lowered",
54        "sparkler:color_raised",
55        "sparkler:color_super_raised",
56        "sparkler:color_text",
57        "sparkler:color_text_raised",
58        "sparkler:color_text_lowered",
59        "sparkler:color_link",
60        "sparkler:color_primary",
61        "sparkler:color_primary_lowered",
62        "sparkler:color_primary_raised",
63        "sparkler:color_text_primary",
64        "sparkler:color_shadow",
65        "sparkler:lock_profile",
66        "sparkler:disallow_anonymous",
67        "sparkler:disallow_anonymous_comments",
68        "sparkler:require_account",
69        "sparkler:private_social",
70        "sparkler:filter",
71        "rainbeam:verify_url",
72        "rainbeam:verify_code",
73        "rainbeam:market_theme_template",
74        "rainbeam:nsfw_profile",
75        "rainbeam:share_hashtag",
76        "rainbeam:authenticated_only",
77        "rainbeam:force_default_layout",
78        "rainbeam:disallow_response_comments",
79        "rainbeam:view_password",
80        "rainbeam:do_not_send_global_questions_to_inbox",
81        "rainbeam:do_not_clear_inbox_count_on_view",
82        "rainbeam:do_not_send_global_questions_to_friends",
83    ]
84});
85
86#[derive(Serialize, Deserialize, Clone, Debug)]
87pub struct ServerOptions {
88    /// If new registrations are enabled
89    #[serde(default)]
90    pub registration_enabled: bool,
91    /// HCaptcha configuration
92    #[serde(default)]
93    pub captcha: HCaptchaConfig,
94    /// The header to read user IP from
95    #[serde(default)]
96    pub real_ip_header: Option<String>,
97    /// The directory to serve static assets from
98    #[serde(default)]
99    pub static_dir: PathBufD,
100    /// The location of media uploads on the file system
101    #[serde(default)]
102    pub media_dir: PathBufD,
103    /// The origin of the public server (ex: "https://rainbeam.net")
104    ///
105    /// Used in embeds and links.
106    #[serde(default)]
107    pub host: String,
108    /// The server ID for ID generation
109    pub snowflake_server_id: usize,
110    /// A list of image hosts that are blocked
111    #[serde(default)]
112    pub blocked_hosts: Vec<String>,
113}
114
115impl Default for ServerOptions {
116    fn default() -> Self {
117        Self {
118            registration_enabled: true,
119            captcha: HCaptchaConfig::default(),
120            real_ip_header: Option::None,
121            static_dir: PathBufD::default(),
122            media_dir: PathBufD::default(),
123            host: String::new(),
124            snowflake_server_id: 1234567890,
125            blocked_hosts: Vec::new(),
126        }
127    }
128}
129
130/// Database connector
131#[derive(Clone)]
132pub struct Database {
133    pub base: StarterDatabase,
134    pub config: ServerOptions,
135    pub http: HttpClient,
136}
137
138impl Database {
139    /// Create a new [`Database`]
140    pub async fn new(
141        database_options: databeam::DatabaseOpts,
142        server_options: ServerOptions,
143    ) -> Self {
144        let base = StarterDatabase::new(database_options).await;
145
146        Self {
147            base: base.clone(),
148            http: HttpClient::new(),
149            config: server_options,
150        }
151    }
152
153    /// Pull [`databeam::DatabaseOpts`] from env
154    pub fn env_options() -> databeam::DatabaseOpts {
155        use std::env::var;
156        databeam::DatabaseOpts {
157            r#type: match var("DB_TYPE") {
158                Ok(v) => Option::Some(v),
159                Err(_) => Option::None,
160            },
161            host: match var("DB_HOST") {
162                Ok(v) => Option::Some(v),
163                Err(_) => Option::None,
164            },
165            user: var("DB_USER").unwrap_or(String::new()),
166            pass: var("DB_PASS").unwrap_or(String::new()),
167            name: var("DB_NAME").unwrap_or(String::new()),
168        }
169    }
170
171    /// Init database
172    pub async fn init(&self) {
173        // create tables
174        let c = &self.base.db.client;
175
176        let _ = sqlquery(
177            "CREATE TABLE IF NOT EXISTS \"xprofiles\" (
178                id                 TEXT,
179                username           TEXT,
180                password           TEXT,
181                tokens             TEXT,
182                metadata           TEXT,
183                joined             TEXT,
184                gid                TEXT,
185                salt               TEXT,
186                ips                TEXT,
187                badges             TEXT,
188                tier               TEXT,
189                token_context      TEXT,
190                coins              TEXT DEFAULT '0',
191                links              TEXT DEFAULT '{}',
192                layout             TEXT DEFAULT '{\"json\":\"default.json\"}',
193                question_count     TEXT DEFAULT '0',
194                response_count     TEXT DEFAULT '0',
195                totp               TEXT DEFAULT '',
196                recovery_codes     TEXT DEFAULT '[]'
197                notification_count TEXT DEFAULT '0',
198                inbox_count        TEXT DEFAULT '0'
199                labels             TEXT DEFAULT '[]',
200            )",
201        )
202        .execute(c)
203        .await;
204
205        let _ = sqlquery(
206            "CREATE TABLE IF NOT EXISTS \"xgroups\" (
207                name        TEXT,
208                id          TEXT,
209                permissions INTEGER
210            )",
211        )
212        .execute(c)
213        .await;
214
215        let _ = sqlquery(
216            "CREATE TABLE IF NOT EXISTS \"xfollows\" (
217                user      TEXT,
218                following TEXT
219            )",
220        )
221        .execute(c)
222        .await;
223
224        let _ = sqlquery(
225            "CREATE TABLE IF NOT EXISTS \"xnotifications\" (
226                title     TEXT,
227                content   TEXT,
228                address   TEXT,
229                timestamp TEXT,
230                id        TEXT,
231                recipient TEXT
232            )",
233        )
234        .execute(c)
235        .await;
236
237        let _ = sqlquery(
238            "CREATE TABLE IF NOT EXISTS \"xwarnings\" (
239                id        TEXT,
240                content   TEXT,
241                timestamp TEXT,
242                recipient TEXT,
243                moderator TEXT
244            )",
245        )
246        .execute(c)
247        .await;
248
249        let _ = sqlquery(
250            "CREATE TABLE IF NOT EXISTS \"xbans\" (
251                id        TEXT,
252                ip        TEXT,
253                reason    TEXT,
254                moderator TEXT,
255                timestamp TEXT
256            )",
257        )
258        .execute(c)
259        .await;
260
261        let _ = sqlquery(
262            "CREATE TABLE IF NOT EXISTS \"xrelationships\" (
263                one       TEXT,
264                two       TEXT,
265                status    TEXT,
266                timestamp TEXT
267            )",
268        )
269        .execute(c)
270        .await;
271
272        let _ = sqlquery(
273            "CREATE TABLE IF NOT EXISTS \"xipblocks\" (
274                id        TEXT,
275                ip        TEXT,
276                user      TEXT,
277                context   TEXT,
278                timestamp TEXT
279            )",
280        )
281        .execute(c)
282        .await;
283
284        let _ = sqlquery(
285            "CREATE TABLE IF NOT EXISTS \"xlabels\" (
286                id        TEXT,
287                name      TEXT,
288                timestamp TEXT,
289                creator   TEXT
290            )",
291        )
292        .execute(c)
293        .await;
294
295        let _ = sqlquery(
296            // "xugc_transactions" to not interfere with real money transactions
297            "CREATE TABLE IF NOT EXISTS \"xugc_transactions\" (
298                id        TEXT,
299                amount    TEXT,
300                item      TEXT,
301                timestamp TEXT,
302                customer  TEXT,
303                merchant  TEXT
304            )",
305        )
306        .execute(c)
307        .await;
308
309        let _ = sqlquery(
310            "CREATE TABLE IF NOT EXISTS \"xugc_items\" (
311                id          TEXT,
312                name        TEXT,
313                description TEXT,
314                cost        TEXT,
315                content     TEXT,
316                type        TEXT,
317                status      TEXT,
318                timestamp   TEXT,
319                creator     TEXT
320            )",
321        )
322        .execute(c)
323        .await;
324    }
325
326    // util
327
328    /// Create a moderator audit log entry.
329    pub async fn audit(&self, actor_id: &str, content: &str) -> Result<()> {
330        match self
331            .create_notification(
332                NotificationCreate {
333                    title: format!("[{actor_id}](/+u/{actor_id})"),
334                    content: content.to_string(),
335                    address: format!("/+u/{actor_id}"),
336                    recipient: "*(audit)".to_string(), // all staff, audit registry
337                },
338                None,
339            )
340            .await
341        {
342            Ok(_) => Ok(()),
343            Err(_) => Err(DatabaseError::Other),
344        }
345    }
346
347    // profiles
348
349    /// Get profile given the `row` data.
350    pub async fn gimme_profile(&self, row: BTreeMap<String, String>) -> Result<Box<Profile>> {
351        let id = from_row!(row->id());
352
353        let metadata: ProfileMetadata = from_row!(row->metadata(json); DatabaseError::ValueError);
354        let do_not_clear_inbox_count_on_view =
355            metadata.is_true("rainbeam:do_not_clear_inbox_count_on_view");
356
357        Ok(Box::new(Profile {
358            id: id.clone(),
359            username: from_row!(row->username()),
360            password: from_row!(row->password()),
361            salt: from_row!(row->salt(); &String::new()),
362            tokens: from_row!(row->tokens(json); DatabaseError::ValueError),
363            ips: from_row!(row->ips(json); DatabaseError::ValueError),
364            token_context: from_row!(row->token_context(json); DatabaseError::ValueError),
365            metadata,
366            badges: from_row!(row->badges(json); DatabaseError::ValueError),
367            group: from_row!(row->gid(i32); 0),
368            joined: from_row!(row->joined(u128); 0),
369            tier: from_row!(row->tier(i32); 0),
370            labels: from_row!(row->labels(json); DatabaseError::ValueError),
371            coins: from_row!(row->coins(i32); 0),
372            links: from_row!(row->links(json); DatabaseError::ValueError),
373            layout: from_row!(row->layout(json); DatabaseError::ValueError),
374            question_count: from_row!(row->question_count(usize); 0),
375            response_count: cache_sync!(
376                |row, id| response_count->(update_profile_response_count in self) {1}
377            ),
378            totp: from_row!(row->totp()),
379            recovery_codes: from_row!(row->recovery_codes(json); DatabaseError::ValueError),
380            notification_count: from_row!(row->notification_count(usize); 0),
381            inbox_count: if do_not_clear_inbox_count_on_view {
382                // sync
383                cache_sync!(|row, id| inbox_count->(update_profile_inbox_count in self) {1})
384            } else {
385                from_row!(row->inbox_count(usize); 0)
386            },
387        }))
388    }
389
390    // GET
391    fn is_digit(&self, input: &str) -> bool {
392        for char in input.chars() {
393            if !char.is_numeric() {
394                return false;
395            }
396        }
397
398        true
399    }
400
401    /// Get a profile's username given its `id`.
402    pub async fn get_profile_username(&self, id: &str) -> String {
403        // fetch from database
404        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
405            "SELECT \"username\" FROM \"xprofiles\" WHERE \"id\" = ?"
406        } else {
407            "SELECT \"username\" FROM \"xprofiles\" WHERE \"id\" = $1"
408        };
409
410        let c = &self.base.db.client;
411        let row = match sqlquery(query).bind::<&str>(id).fetch_one(c).await {
412            Ok(u) => self.base.textify_row(u).0,
413            Err(_) => return String::new(),
414        };
415
416        // return
417        from_row!(row->username())
418    }
419
420    /// Fetch a profile correctly.
421    pub async fn get_profile(&self, id: &str) -> Result<Box<Profile>> {
422        let mut id = id.to_string();
423        if id.starts_with("ANSWERED:") {
424            // we use the "ANSWERED" prefix whenever we answer a question so it doesn't show up in inboxes
425            id = id.replace("ANSWERED:", "");
426        }
427
428        if id == "@" {
429            return Ok(Box::new(Profile::global()));
430        } else if id.starts_with("anonymous#") | (id == "anonymous") | (id == "#") {
431            let tag = Profile::anonymous_tag(&id);
432            return Ok(Box::new(Profile::anonymous(tag.3)));
433        } else if (id == "0") | (id == "system") {
434            return Ok(Box::new(Profile::system()));
435        }
436
437        // remove circle tag
438        if id.contains("%") {
439            id = id
440                .split("%")
441                .collect::<Vec<&str>>()
442                .get(0)
443                .unwrap()
444                .to_string();
445        }
446
447        // handle legacy IDs (usernames)
448        if (id.len() <= 32) && (!self.is_digit(&id) | (id.len() < 18)) {
449            return match self.get_profile_by_username(&id).await {
450                Ok(ua) => Ok(ua),
451                Err(e) => return Err(e),
452            };
453        }
454
455        match self.get_profile_by_id(&id).await {
456            Ok(ua) => Ok(ua),
457            Err(e) => return Err(e),
458        }
459    }
460
461    /// Get a [`Profile`] by their hashed ID.
462    ///
463    /// # Arguments:
464    /// * `hashed` - `String` of the profile's hashed ID
465    pub async fn get_profile_by_hashed(&self, hashed: &str) -> Result<Box<Profile>> {
466        // fetch from database
467        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
468            "SELECT * FROM \"xprofiles\" WHERE \"tokens\" LIKE ?"
469        } else {
470            "SELECT * FROM \"xprofiles\" WHERE \"tokens\" LIKE $1"
471        };
472
473        let c = &self.base.db.client;
474        let row = match sqlquery(query)
475            .bind::<&str>(&format!("%\"{hashed}\"%"))
476            .fetch_one(c)
477            .await
478        {
479            Ok(u) => self.base.textify_row(u).0,
480            Err(_) => return Err(DatabaseError::Other),
481        };
482
483        // return
484        Ok(match self.gimme_profile(row).await {
485            Ok(ua) => ua,
486            Err(e) => return Err(e),
487        })
488    }
489
490    /// Get a user by their unhashed ID (hashes ID and then calls [`Database::get_profile_by_hashed()`]).
491    ///
492    /// # Arguments:
493    /// * `unhashed` - `String` of the user's unhashed ID
494    pub async fn get_profile_by_unhashed(&self, unhashed: &str) -> Result<Box<Profile>> {
495        self.get_profile_by_hashed(&utility::hash(unhashed.to_string()))
496            .await
497    }
498
499    /// Get a [`Profile`] by their IP.
500    ///
501    /// # Arguments:
502    /// * `hashed` - `String` of the profile's IP
503    pub async fn get_profile_by_ip(&self, ip: &str) -> Result<Box<Profile>> {
504        // fetch from database
505        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
506            "SELECT * FROM \"xprofiles\" WHERE \"ips\" LIKE ?"
507        } else {
508            "SELECT * FROM \"xprofiles\" WHERE \"ips\" LIKE $1"
509        };
510
511        let c = &self.base.db.client;
512        let row = match sqlquery(query)
513            .bind::<&str>(&format!("%\"{ip}\"%"))
514            .fetch_one(c)
515            .await
516        {
517            Ok(u) => self.base.textify_row(u).0,
518            Err(_) => return Err(DatabaseError::Other),
519        };
520
521        // return
522        Ok(match self.gimme_profile(row).await {
523            Ok(ua) => ua,
524            Err(e) => return Err(e),
525        })
526    }
527
528    /// Get a user by their username.
529    ///
530    /// # Arguments:
531    /// * `username` - `String` of the user's username
532    pub async fn get_profile_by_username(&self, username: &str) -> Result<Box<Profile>> {
533        let username = username.to_lowercase();
534
535        // check in cache
536        let cached = self
537            .base
538            .cache
539            .get(format!("rbeam.auth.profile:{}", username))
540            .await;
541
542        if cached.is_some() {
543            match serde_json::from_str::<Profile>(cached.unwrap().as_str()) {
544                Ok(p) => return Ok(Box::new(p)),
545                Err(_) => {
546                    self.base
547                        .cache
548                        .remove(format!("rbeam.auth.profile:{}", username))
549                        .await;
550                }
551            };
552        }
553
554        // ...
555        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
556            "SELECT * FROM \"xprofiles\" WHERE \"username\" = ?"
557        } else {
558            "SELECT * FROM \"xprofiles\" WHERE \"username\" = $1"
559        };
560
561        let c = &self.base.db.client;
562        let row = match sqlquery(query).bind::<&str>(&username).fetch_one(c).await {
563            Ok(r) => self.base.textify_row(r).0,
564            Err(_) => return Err(DatabaseError::NotFound),
565        };
566
567        // store in cache
568        let user = match self.gimme_profile(row).await {
569            Ok(ua) => ua,
570            Err(e) => return Err(e),
571        };
572
573        self.base
574            .cache
575            .set(
576                format!("rbeam.auth.profile:{}", username),
577                serde_json::to_string::<Profile>(&user).unwrap(),
578            )
579            .await;
580
581        // return
582        Ok(user)
583    }
584
585    /// Get a user by their id.
586    ///
587    /// # Arguments:
588    /// * `id` - `String` of the user's username
589    pub async fn get_profile_by_id(&self, id: &str) -> Result<Box<Profile>> {
590        let id = id.to_lowercase();
591
592        // check in cache
593        let cached = self
594            .base
595            .cache
596            .get(format!("rbeam.auth.profile:{}", id))
597            .await;
598
599        if cached.is_some() {
600            match serde_json::from_str::<Profile>(cached.unwrap().as_str()) {
601                Ok(p) => return Ok(Box::new(p)),
602                Err(_) => {
603                    self.base
604                        .cache
605                        .remove(format!("rbeam.auth.profile:{}", id))
606                        .await;
607                }
608            };
609        }
610
611        // ...
612        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
613            "SELECT * FROM \"xprofiles\" WHERE \"id\" = ?"
614        } else {
615            "SELECT * FROM \"xprofiles\" WHERE \"id\" = $1"
616        };
617
618        let c = &self.base.db.client;
619        let row = match sqlquery(query).bind::<&String>(&id).fetch_one(c).await {
620            Ok(r) => self.base.textify_row(r).0,
621            Err(_) => return Err(DatabaseError::NotFound),
622        };
623
624        // store in cache
625        let user = match self.gimme_profile(row).await {
626            Ok(ua) => ua,
627            Err(e) => return Err(e),
628        };
629
630        self.base
631            .cache
632            .set(
633                format!("rbeam.auth.profile:{}", id),
634                serde_json::to_string::<Profile>(&user).unwrap(),
635            )
636            .await;
637
638        // return
639        Ok(user)
640    }
641
642    /// Validate a username.
643    pub fn validate_username(username: &str) -> Result<()> {
644        let banned_usernames = &[
645            "admin",
646            "account",
647            "anonymous",
648            "login",
649            "sign_up",
650            "settings",
651            "api",
652            "intents",
653            "circles",
654            "chats",
655            "sites",
656            "responses",
657            "questions",
658            "comments",
659            "response",
660            "question",
661            "comment",
662            "pages",
663            "inbox",
664            "system",
665            "market",
666            ".well-known",
667            "static",
668        ];
669
670        let regex = regex::RegexBuilder::new(r"[^\w_\-\.!]+")
671            .multi_line(true)
672            .build()
673            .unwrap();
674
675        if regex.captures(&username).is_some() {
676            return Err(DatabaseError::ValueError);
677        }
678
679        if (username.len() < 2) | (username.len() > 500) {
680            return Err(DatabaseError::ValueError);
681        }
682
683        if banned_usernames.contains(&username) {
684            return Err(DatabaseError::ValueError);
685        }
686
687        Ok(())
688    }
689
690    // SET
691    /// Create a new user given their username. Returns their unhashed token.
692    ///
693    /// # Arguments:
694    /// * `username` - `String` of the user's `username`
695    /// * `user_ip` - the ip address of the user registering
696    pub async fn create_profile(&self, props: ProfileCreate, user_ip: &str) -> Result<String> {
697        if self.config.registration_enabled == false {
698            return Err(DatabaseError::NotAllowed);
699        }
700
701        // ...
702        let username = props.username.trim();
703        let password = props.password.trim();
704
705        // check captcha
706        if let Err(_) = props
707            .valid_response(&self.config.captcha.secret, None)
708            .await
709        {
710            return Err(DatabaseError::NotAllowed);
711        }
712
713        // make sure user doesn't already exists
714        if let Ok(_) = &self.get_profile_by_username(username).await {
715            return Err(DatabaseError::UsernameTaken);
716        };
717
718        // check username
719        if let Err(e) = Database::validate_username(username) {
720            return Err(e);
721        }
722
723        // ...
724        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
725            "INSERT INTO \"xprofiles\" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
726        } else {
727            "INSERT INTO \"xprofiles\" VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)"
728        };
729
730        let user_token_unhashed: &str = &databeam::utility::uuid();
731        let user_token_hashed: &str = &databeam::utility::hash(user_token_unhashed.to_string());
732        let salt: &str = &rainbeam_shared::hash::salt();
733
734        let timestamp = utility::unix_epoch_timestamp();
735
736        let c = &self.base.db.client;
737        match sqlquery(query)
738            // .bind::<&str>(&databeam::utility::uuid())
739            .bind::<&str>(&AlmostSnowflake::new(self.config.snowflake_server_id).to_string())
740            .bind::<&str>(&username.to_lowercase())
741            .bind::<&str>(&rainbeam_shared::hash::hash_salted(
742                password.to_string(),
743                salt.to_string(),
744            ))
745            .bind::<&str>(&serde_json::to_string::<Vec<&str>>(&vec![user_token_hashed]).unwrap())
746            .bind::<&str>(
747                &serde_json::to_string::<ProfileMetadata>(&ProfileMetadata::default()).unwrap(),
748            )
749            .bind::<&String>(&timestamp.to_string())
750            .bind::<i8>(0)
751            .bind::<&str>(&salt)
752            .bind::<&str>(&serde_json::to_string::<Vec<&str>>(&vec![user_ip]).unwrap())
753            .bind::<&str>("[]")
754            .bind::<i8>(0)
755            .bind::<&str>("[]")
756            .bind::<i8>(0)
757            .bind::<&str>("{}")
758            .bind::<&str>("{\"json\":\"default.json\"}")
759            .bind::<i8>(0)
760            .bind::<i8>(0)
761            .bind::<&str>("")
762            .bind::<&str>("[]")
763            .bind::<i8>(0)
764            .bind::<i8>(0)
765            .bind::<&str>("[]")
766            .execute(c)
767            .await
768        {
769            Ok(_) => Ok(user_token_unhashed.to_string()),
770            Err(_) => Err(DatabaseError::Other),
771        }
772    }
773
774    update_profile_count!(update_profile_question_count, question_count);
775    update_profile_count!(update_profile_response_count, response_count);
776    update_profile_count!(update_profile_inbox_count, inbox_count);
777    update_profile_count!(update_profile_notification_count, notification_count);
778
779    /// Update a [`Profile`]'s metadata by its `id`.
780    pub async fn update_profile_metadata(
781        &self,
782        id: &str,
783        mut metadata: ProfileMetadata,
784    ) -> Result<()> {
785        // make sure user exists
786        let profile = match self.get_profile(&id).await {
787            Ok(ua) => ua,
788            Err(e) => return Err(e),
789        };
790
791        // check metadata kv
792        for kv in metadata.kv.clone() {
793            if !ALLOWED_CUSTOM_KEYS.contains(&kv.0.as_str()) {
794                metadata.kv.remove(&kv.0);
795            }
796        }
797
798        if !metadata.check() {
799            return Err(DatabaseError::TooLong);
800        }
801
802        // update user
803        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
804            "UPDATE \"xprofiles\" SET \"metadata\" = ? WHERE \"id\" = ?"
805        } else {
806            "UPDATE \"xprofiles\" SET (\"metadata\") = ($1) WHERE \"id\" = $2"
807        };
808
809        let c = &self.base.db.client;
810        let meta = &serde_json::to_string(&metadata).unwrap();
811        match sqlquery(query)
812            .bind::<&str>(meta)
813            .bind::<&str>(&id)
814            .execute(c)
815            .await
816        {
817            Ok(_) => {
818                self.base
819                    .cache
820                    .remove(format!("rbeam.auth.profile:{}", profile.username))
821                    .await;
822
823                self.base
824                    .cache
825                    .remove(format!("rbeam.auth.profile:{}", profile.id))
826                    .await;
827
828                Ok(())
829            }
830            Err(_) => Err(DatabaseError::Other),
831        }
832    }
833
834    /// Update a [`Profile`]'s tokens (and IPs/token_contexts) by its `id`
835    pub async fn update_profile_tokens(
836        &self,
837        id: &str,
838        tokens: Vec<String>,
839        ips: Vec<String>,
840        token_context: Vec<TokenContext>,
841    ) -> Result<()> {
842        // make sure user exists
843        let ua = match self.get_profile(&id).await {
844            Ok(ua) => ua,
845            Err(e) => return Err(e),
846        };
847
848        // update user
849        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
850            "UPDATE \"xprofiles\" SET \"tokens\" = ?, \"ips\" = ?, \"token_context\" = ? WHERE \"id\" = ?"
851        } else {
852            "UPDATE \"xprofiles\" SET (\"tokens\", \"ips\") = ($1, $2, $3) WHERE \"id\" = $4"
853        };
854
855        let c = &self.base.db.client;
856
857        let tokens = &serde_json::to_string(&tokens).unwrap();
858        let ips = &serde_json::to_string(&ips).unwrap();
859        let token_context = &serde_json::to_string(&token_context).unwrap();
860
861        match sqlquery(query)
862            .bind::<&str>(tokens)
863            .bind::<&str>(ips)
864            .bind::<&str>(token_context)
865            .bind::<&str>(&ua.id)
866            .execute(c)
867            .await
868        {
869            Ok(_) => {
870                self.base
871                    .cache
872                    .remove(format!("rbeam.auth.profile:{}", ua.username))
873                    .await;
874
875                self.base
876                    .cache
877                    .remove(format!("rbeam.auth.profile:{}", ua.id))
878                    .await;
879
880                Ok(())
881            }
882            Err(_) => Err(DatabaseError::Other),
883        }
884    }
885
886    /// Update a [`Profile`]'s badges by its `id`
887    pub async fn update_profile_badges(
888        &self,
889        id: &str,
890        badges: Vec<(String, String, String)>,
891    ) -> Result<()> {
892        // make sure user exists
893        let ua = match self.get_profile(&id).await {
894            Ok(ua) => ua,
895            Err(e) => return Err(e),
896        };
897
898        // update user
899        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
900            "UPDATE \"xprofiles\" SET \"badges\" = ? WHERE \"id\" = ?"
901        } else {
902            "UPDATE \"xprofiles\" SET (\"badges\") = ($1) WHERE \"id\" = $2"
903        };
904
905        let c = &self.base.db.client;
906        let badges = &serde_json::to_string(&badges).unwrap();
907
908        match sqlquery(query)
909            .bind::<&str>(badges)
910            .bind::<&str>(&id)
911            .execute(c)
912            .await
913        {
914            Ok(_) => {
915                self.base
916                    .cache
917                    .remove(format!("rbeam.auth.profile:{}", ua.username))
918                    .await;
919
920                self.base
921                    .cache
922                    .remove(format!("rbeam.auth.profile:{}", ua.id))
923                    .await;
924
925                Ok(())
926            }
927            Err(_) => Err(DatabaseError::Other),
928        }
929    }
930
931    /// Update a [`Profile`]'s labels by its `id`
932    pub async fn update_profile_labels(&self, id: &str, labels: Vec<i64>) -> Result<()> {
933        // make sure user exists
934        let ua = match self.get_profile(&id).await {
935            Ok(ua) => ua,
936            Err(e) => return Err(e),
937        };
938
939        // update user
940        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
941            "UPDATE \"xprofiles\" SET \"labels\" = ? WHERE \"id\" = ?"
942        } else {
943            "UPDATE \"xprofiles\" SET (\"labels\") = ($1) WHERE \"id\" = $2"
944        };
945
946        let c = &self.base.db.client;
947        let labels = &serde_json::to_string(&labels).unwrap();
948
949        match sqlquery(query)
950            .bind::<&str>(labels)
951            .bind::<&str>(&id)
952            .execute(c)
953            .await
954        {
955            Ok(_) => {
956                self.base
957                    .cache
958                    .remove(format!("rbeam.auth.profile:{}", ua.username))
959                    .await;
960
961                self.base
962                    .cache
963                    .remove(format!("rbeam.auth.profile:{}", ua.id))
964                    .await;
965
966                Ok(())
967            }
968            Err(_) => Err(DatabaseError::Other),
969        }
970    }
971
972    /// Update a [`Profile`]'s links by its `id`
973    pub async fn update_profile_links(
974        &self,
975        id: &str,
976        links: BTreeMap<String, String>,
977    ) -> Result<()> {
978        // make sure user exists
979        let ua = match self.get_profile(&id).await {
980            Ok(ua) => ua,
981            Err(e) => return Err(e),
982        };
983
984        // update user
985        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
986            "UPDATE \"xprofiles\" SET \"links\" = ? WHERE \"id\" = ?"
987        } else {
988            "UPDATE \"xprofiles\" SET (\"links\") = ($1) WHERE \"id\" = $2"
989        };
990
991        let c = &self.base.db.client;
992        let links = &serde_json::to_string(&links).unwrap();
993
994        match sqlquery(query)
995            .bind::<&str>(links)
996            .bind::<&str>(&id)
997            .execute(c)
998            .await
999        {
1000            Ok(_) => {
1001                self.base
1002                    .cache
1003                    .remove(format!("rbeam.auth.profile:{}", ua.username))
1004                    .await;
1005
1006                self.base
1007                    .cache
1008                    .remove(format!("rbeam.auth.profile:{}", ua.id))
1009                    .await;
1010
1011                Ok(())
1012            }
1013            Err(_) => Err(DatabaseError::Other),
1014        }
1015    }
1016
1017    /// Update a [`Profile`]'s links by its `id`
1018    pub async fn update_profile_layout(&self, id: &str, layout: LayoutComponent) -> Result<()> {
1019        // make sure user exists
1020        let ua = match self.get_profile(&id).await {
1021            Ok(ua) => ua,
1022            Err(e) => return Err(e),
1023        };
1024
1025        // update user
1026        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1027            "UPDATE \"xprofiles\" SET \"layout\" = ? WHERE \"id\" = ?"
1028        } else {
1029            "UPDATE \"xprofiles\" SET (\"layout\") = ($1) WHERE \"id\" = $2"
1030        };
1031
1032        let c = &self.base.db.client;
1033        let layout = &serde_json::to_string(&layout).unwrap();
1034
1035        match sqlquery(query)
1036            .bind::<&str>(layout)
1037            .bind::<&str>(&id)
1038            .execute(c)
1039            .await
1040        {
1041            Ok(_) => {
1042                self.base
1043                    .cache
1044                    .remove(format!("rbeam.auth.profile:{}", ua.username))
1045                    .await;
1046
1047                self.base
1048                    .cache
1049                    .remove(format!("rbeam.auth.profile:{}", ua.id))
1050                    .await;
1051
1052                Ok(())
1053            }
1054            Err(_) => Err(DatabaseError::Other),
1055        }
1056    }
1057
1058    /// Update a [`Profile`]'s tier by its ID
1059    pub async fn update_profile_tier(&self, id: &str, tier: i32) -> Result<()> {
1060        // make sure user exists
1061        let ua = match self.get_profile(&id).await {
1062            Ok(ua) => ua,
1063            Err(e) => return Err(e),
1064        };
1065
1066        // update user
1067        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1068            "UPDATE \"xprofiles\" SET \"tier\" = ? WHERE \"id\" = ?"
1069        } else {
1070            "UPDATE \"xprofiles\" SET (\"tier\") = ($1) WHERE \"id\" = $2"
1071        };
1072
1073        let c = &self.base.db.client;
1074        match sqlquery(query)
1075            .bind::<i32>(tier)
1076            .bind::<&str>(&id)
1077            .execute(c)
1078            .await
1079        {
1080            Ok(_) => {
1081                self.base
1082                    .cache
1083                    .remove(format!("rbeam.auth.profile:{}", ua.username))
1084                    .await;
1085
1086                self.base
1087                    .cache
1088                    .remove(format!("rbeam.auth.profile:{}", ua.id))
1089                    .await;
1090
1091                Ok(())
1092            }
1093            Err(_) => Err(DatabaseError::Other),
1094        }
1095    }
1096
1097    /// Update a [`Profile`]'s `gid` by its `id`
1098    pub async fn update_profile_group(&self, id: &str, group: i32) -> Result<()> {
1099        // make sure user exists
1100        let ua = match self.get_profile(&id).await {
1101            Ok(ua) => ua,
1102            Err(e) => return Err(e),
1103        };
1104
1105        // update user
1106        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1107            "UPDATE \"xprofiles\" SET \"gid\" = ? WHERE \"id\" = ?"
1108        } else {
1109            "UPDATE \"xprofiles\" SET (\"gid\") = ($1) WHERE \"id\" = $2"
1110        };
1111
1112        let c = &self.base.db.client;
1113        match sqlquery(query)
1114            .bind::<&i32>(&group)
1115            .bind::<&str>(&id)
1116            .execute(c)
1117            .await
1118        {
1119            Ok(_) => {
1120                self.base
1121                    .cache
1122                    .remove(format!("rbeam.auth.profile:{}", ua.username))
1123                    .await;
1124
1125                self.base
1126                    .cache
1127                    .remove(format!("rbeam.auth.profile:{}", ua.id))
1128                    .await;
1129
1130                Ok(())
1131            }
1132            Err(_) => Err(DatabaseError::Other),
1133        }
1134    }
1135
1136    /// Update a [`Profile`]'s coins by its ID
1137    ///
1138    /// # Arguments
1139    /// * `coins` - the amount to ADD to the existing coins value
1140    pub async fn update_profile_coins(&self, id: &str, coins: i32) -> Result<()> {
1141        // make sure user exists
1142        let ua = match self.get_profile(&id).await {
1143            Ok(ua) => ua,
1144            Err(e) => return Err(e),
1145        };
1146
1147        // update user
1148        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1149            "UPDATE \"xprofiles\" SET \"coins\" = ? WHERE \"id\" = ?"
1150        } else {
1151            "UPDATE \"xprofiles\" SET (\"coins\") = ($1) WHERE \"id\" = $2"
1152        };
1153
1154        let c = &self.base.db.client;
1155        match sqlquery(query)
1156            .bind::<i32>(ua.coins + coins)
1157            .bind::<&str>(&id)
1158            .execute(c)
1159            .await
1160        {
1161            Ok(_) => {
1162                self.base
1163                    .cache
1164                    .remove(format!("rbeam.auth.profile:{}", ua.username))
1165                    .await;
1166
1167                self.base
1168                    .cache
1169                    .remove(format!("rbeam.auth.profile:{}", ua.id))
1170                    .await;
1171
1172                Ok(())
1173            }
1174            Err(_) => Err(DatabaseError::Other),
1175        }
1176    }
1177
1178    /// Update a [`Profile`]'s `password` by its name and password
1179    pub async fn update_profile_password(
1180        &self,
1181        id: &str,
1182        password: &str,
1183        new_password: &str,
1184        do_password_check: bool,
1185    ) -> Result<()> {
1186        // make sure user exists
1187        let ua = match self.get_profile(&id).await {
1188            Ok(ua) => ua,
1189            Err(e) => return Err(e),
1190        };
1191
1192        // check password
1193        if do_password_check {
1194            let password_hashed = rainbeam_shared::hash::hash_salted(password.to_string(), ua.salt);
1195
1196            if password_hashed != ua.password {
1197                return Err(DatabaseError::NotAllowed);
1198            }
1199        }
1200
1201        // update user
1202        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1203            "UPDATE \"xprofiles\" SET \"password\" = ?, \"salt\" = ? WHERE \"id\" = ?"
1204        } else {
1205            "UPDATE \"xprofiles\" SET (\"password\", \"salt\") = ($1, $2) WHERE \"id\" = $3"
1206        };
1207
1208        let new_salt = rainbeam_shared::hash::salt();
1209
1210        let c = &self.base.db.client;
1211        match sqlquery(query)
1212            .bind::<&str>(&rainbeam_shared::hash::hash_salted(
1213                new_password.to_string(),
1214                new_salt.clone(),
1215            ))
1216            .bind::<&str>(&new_salt)
1217            .bind::<&str>(&id)
1218            .execute(c)
1219            .await
1220        {
1221            Ok(_) => {
1222                self.base
1223                    .cache
1224                    .remove(format!("rbeam.auth.profile:{}", ua.username))
1225                    .await;
1226
1227                self.base
1228                    .cache
1229                    .remove(format!("rbeam.auth.profile:{}", ua.id))
1230                    .await;
1231
1232                Ok(())
1233            }
1234            Err(_) => Err(DatabaseError::Other),
1235        }
1236    }
1237
1238    /// Update a [`Profile`]'s `username` by its id and password
1239    pub async fn update_profile_username(
1240        &self,
1241        id: &str,
1242        password: &str,
1243        new_name: &str,
1244    ) -> Result<()> {
1245        let new_name = new_name.to_lowercase();
1246
1247        // make sure user exists
1248        let ua = match self.get_profile(&id).await {
1249            Ok(ua) => ua,
1250            Err(e) => return Err(e),
1251        };
1252
1253        // make sure username isn't in use
1254        if let Ok(_) = self.get_profile_by_username(&new_name).await {
1255            return Err(DatabaseError::MustBeUnique);
1256        }
1257
1258        // check username
1259        if let Err(e) = Database::validate_username(&new_name) {
1260            return Err(e);
1261        }
1262
1263        // check password
1264        let password_hashed = rainbeam_shared::hash::hash_salted(password.to_string(), ua.salt);
1265
1266        if password_hashed != ua.password {
1267            return Err(DatabaseError::NotAllowed);
1268        }
1269
1270        // update user
1271        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1272            "UPDATE \"xprofiles\" SET \"username\" = ? WHERE \"id\" = ?"
1273        } else {
1274            "UPDATE \"xprofiles\" SET (\"username\") = ($1) WHERE \"id\" = $2"
1275        };
1276
1277        let c = &self.base.db.client;
1278        match sqlquery(query)
1279            .bind::<&str>(&new_name)
1280            .bind::<&str>(&id)
1281            .execute(c)
1282            .await
1283        {
1284            Ok(_) => {
1285                self.base
1286                    .cache
1287                    .remove(format!("rbeam.auth.profile:{}", ua.username))
1288                    .await;
1289
1290                self.base
1291                    .cache
1292                    .remove(format!("rbeam.auth.profile:{}", ua.id))
1293                    .await;
1294
1295                Ok(())
1296            }
1297            Err(_) => Err(DatabaseError::Other),
1298        }
1299    }
1300
1301    /// Delete a profile
1302    ///
1303    /// **VALIDATION SHOULD BE DONE *BEFORE* THIS!!**
1304    async fn delete_profile(&self, id: &str) -> Result<()> {
1305        let user = self.get_profile_by_id(&id).await.unwrap();
1306
1307        // delete user
1308        let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1309            "DELETE FROM \"xprofiles\" WHERE \"id\" = ?"
1310        } else {
1311            "DELETE FROM \"xprofiles\" WHERE \"id\" = $1"
1312        };
1313
1314        let c = &self.base.db.client;
1315        match sqlquery(query).bind::<&str>(&id).execute(c).await {
1316            Ok(_) => {
1317                let query: &str =
1318                    if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1319                        "DELETE FROM \"xnotifications\" WHERE \"recipient\" = ?"
1320                    } else {
1321                        "DELETE FROM \"xnotifications\" WHERE \"recipient\" = $1"
1322                    };
1323
1324                if let Err(_) = sqlquery(query).bind::<&str>(&id).execute(c).await {
1325                    return Err(DatabaseError::Other);
1326                };
1327
1328                let query: &str =
1329                    if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1330                        "DELETE FROM \"xwarnings\" WHERE \"recipient\" = ?"
1331                    } else {
1332                        "DELETE FROM \"xwarnings\" WHERE \"recipient\" = $1"
1333                    };
1334
1335                if let Err(_) = sqlquery(query).bind::<&str>(&id).execute(c).await {
1336                    return Err(DatabaseError::Other);
1337                };
1338
1339                let query: &str =
1340                    if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1341                        "DELETE FROM \"xfollows\" WHERE \"user\" = ? OR \"following\" = ?"
1342                    } else {
1343                        "DELETE FROM \"xfollows\" WHERE \"user\" = $1 OR \"following\" = $2"
1344                    };
1345
1346                if let Err(_) = sqlquery(query)
1347                    .bind::<&str>(&id)
1348                    .bind::<&str>(&id)
1349                    .execute(c)
1350                    .await
1351                {
1352                    return Err(DatabaseError::Other);
1353                };
1354
1355                // rainbeam crate stuff
1356                // questions to user
1357                let query: &str =
1358                    if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1359                        "DELETE FROM \"xquestions\" WHERE \"recipient\" = ?"
1360                    } else {
1361                        "DELETE FROM \"xquestions\" WHERE \"recipient\" = $1"
1362                    };
1363
1364                if let Err(_) = sqlquery(query).bind::<&str>(&id).execute(c).await {
1365                    return Err(DatabaseError::Other);
1366                };
1367
1368                // questions by user
1369                let query: &str =
1370                    if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1371                        "DELETE FROM \"xquestions\" WHERE \"author\" = ?"
1372                    } else {
1373                        "DELETE FROM \"xquestions\" WHERE \"author\" = $1"
1374                    };
1375
1376                if let Err(_) = sqlquery(query).bind::<&str>(&id).execute(c).await {
1377                    return Err(DatabaseError::Other);
1378                };
1379
1380                // responses by user
1381                let query: &str =
1382                    if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1383                        "DELETE FROM \"xresponses\" WHERE \"author\" = ?"
1384                    } else {
1385                        "DELETE FROM \"xresponses\" WHERE \"author\" = $1"
1386                    };
1387
1388                if let Err(_) = sqlquery(query).bind::<&str>(&id).execute(c).await {
1389                    return Err(DatabaseError::Other);
1390                };
1391
1392                // responses to questions by user
1393                let query: &str =
1394                    if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1395                        "DELETE FROM \"xresponses\" WHERE \"question\" LIKE ?"
1396                    } else {
1397                        "DELETE FROM \"xresponses\" WHERE \"question\" LIKE $1"
1398                    };
1399
1400                if let Err(_) = sqlquery(query)
1401                    .bind::<&str>(&format!("%\"author\":\"{id}\"%"))
1402                    .execute(c)
1403                    .await
1404                {
1405                    return Err(DatabaseError::Other);
1406                };
1407
1408                self.base
1409                    .cache
1410                    .remove(format!("rbeam.app.response_count:{}", id))
1411                    .await;
1412
1413                self.base
1414                    .cache
1415                    .remove(format!("rbeam.app.global_question_count:{}", id))
1416                    .await;
1417
1418                // relationships involving user
1419                let query: &str =
1420                    if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1421                        "DELETE FROM \"xrelationships\" WHERE \"one\" = ? OR \"two\" = ?"
1422                    } else {
1423                        "DELETE FROM \"xrelationships\" WHERE \"one\" = $1 OR \"two\" = $2"
1424                    };
1425
1426                if let Err(_) = sqlquery(query)
1427                    .bind::<&str>(&id)
1428                    .bind::<&str>(&id)
1429                    .execute(c)
1430                    .await
1431                {
1432                    return Err(DatabaseError::Other);
1433                };
1434
1435                self.base
1436                    .cache
1437                    .remove(format!("rbeam.app.friends_count:{}", id))
1438                    .await;
1439
1440                // ipblocks by user
1441                let query: &str =
1442                    if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1443                        "DELETE FROM \"xipblocks\" WHERE \"user\" = ?"
1444                    } else {
1445                        "DELETE FROM \"xipblocks\" WHERE \"user\" = $1"
1446                    };
1447
1448                if let Err(_) = sqlquery(query).bind::<&str>(&id).execute(c).await {
1449                    return Err(DatabaseError::Other);
1450                };
1451
1452                // transactions with user
1453                let query: &str = if (self.base.db.r#type == "sqlite")
1454                    | (self.base.db.r#type == "mysql")
1455                {
1456                    "DELETE FROM \"xugc_transactions\" WHERE \"customer\" = ? OR \"merchant\" = ?"
1457                } else {
1458                    "DELETE FROM \"xugc_transactions\" WHERE \"customer\" = $1 OR \"merchant\" = $2"
1459                };
1460
1461                if let Err(_) = sqlquery(query)
1462                    .bind::<&str>(&id)
1463                    .bind::<&str>(&id)
1464                    .execute(c)
1465                    .await
1466                {
1467                    return Err(DatabaseError::Other);
1468                };
1469
1470                // items by user
1471                let query: &str =
1472                    if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1473                        "DELETE FROM \"xugc_items\" WHERE \"creator\" = ?"
1474                    } else {
1475                        "DELETE FROM \"xugc_items\" WHERE \"creator\" = $1"
1476                    };
1477
1478                if let Err(_) = sqlquery(query).bind::<&str>(&id).execute(c).await {
1479                    return Err(DatabaseError::Other);
1480                };
1481
1482                // ...
1483                self.base
1484                    .cache
1485                    .remove(format!("rbeam.auth.profile:{}", id))
1486                    .await;
1487
1488                self.base
1489                    .cache
1490                    .remove(format!("rbeam.auth.profile:{}", user.username))
1491                    .await;
1492
1493                self.base
1494                    .cache
1495                    .remove(format!("rbeam.auth.followers_count:{}", id))
1496                    .await;
1497
1498                self.base
1499                    .cache
1500                    .remove(format!("rbeam.auth.following_count:{}", id))
1501                    .await;
1502
1503                self.base
1504                    .cache
1505                    .remove(format!("rbeam.auth.notification_count:{}", id))
1506                    .await;
1507
1508                // delete images
1509                if !self.config.media_dir.to_string().is_empty() {
1510                    let avatar = pathd!("{}/avatars/{}.avif", self.config.media_dir, id);
1511                    if let Ok(_) = rainbeam_shared::fs::fstat(&avatar) {
1512                        if let Err(_) = rainbeam_shared::fs::remove_file(avatar) {
1513                            return Err(DatabaseError::Other);
1514                        }
1515                    }
1516
1517                    let banner = pathd!("{}/banners/{}.avif", self.config.media_dir, id);
1518                    if let Ok(_) = rainbeam_shared::fs::fstat(&banner) {
1519                        if let Err(_) = rainbeam_shared::fs::remove_file(banner) {
1520                            return Err(DatabaseError::Other);
1521                        }
1522                    }
1523                }
1524
1525                // ...
1526                Ok(())
1527            }
1528            Err(_) => Err(DatabaseError::Other),
1529        }
1530    }
1531
1532    /// Delete an existing [`Profile`] by its `id`
1533    pub async fn delete_profile_by_id(&self, id: &str) -> Result<()> {
1534        let user = match self.get_profile_by_id(&id).await {
1535            Ok(ua) => ua,
1536            Err(e) => return Err(e),
1537        };
1538
1539        // make sure they aren't a manager
1540        let group = match self.get_group_by_id(user.group).await {
1541            Ok(g) => g,
1542            Err(_) => return Err(DatabaseError::Other),
1543        };
1544
1545        if group.permissions.check(FinePermission::DELETE_USER) {
1546            return Err(DatabaseError::NotAllowed);
1547        }
1548
1549        // delete
1550        self.delete_profile(id).await
1551    }
1552
1553    // groups
1554
1555    // GET
1556    /// Get a group by its id
1557    ///
1558    /// # Arguments:
1559    /// * `username` - `String` of the user's username
1560    pub async fn get_group_by_id(&self, id: i32) -> Result<Group> {
1561        // check in cache
1562        let cached = self.base.cache.get(format!("rbeam.auth.gid:{}", id)).await;
1563
1564        if cached.is_some() {
1565            return Ok(serde_json::from_str::<Group>(cached.unwrap().as_str()).unwrap());
1566        }
1567
1568        // ...
1569        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1570            "SELECT * FROM \"xgroups\" WHERE \"id\" = ?"
1571        } else {
1572            "SELECT * FROM \"xgroups\" WHERE \"id\" = $1"
1573        };
1574
1575        let c = &self.base.db.client;
1576        let row = match sqlquery(query).bind::<&i32>(&id).fetch_one(c).await {
1577            Ok(r) => self.base.textify_row(r).0,
1578            Err(_) => return Ok(Group::default()),
1579        };
1580
1581        // store in cache
1582        let group = Group {
1583            name: from_row!(row->name()),
1584            id: row.get("id").unwrap().parse::<i32>().unwrap(),
1585            permissions: match serde_json::from_str(row.get("permissions").unwrap()) {
1586                Ok(m) => m,
1587                Err(_) => return Err(DatabaseError::ValueError),
1588            },
1589        };
1590
1591        self.base
1592            .cache
1593            .set(
1594                format!("rbeam.auth.gid:{}", id),
1595                serde_json::to_string::<Group>(&group).unwrap(),
1596            )
1597            .await;
1598
1599        // return
1600        Ok(group)
1601    }
1602
1603    // profiles
1604
1605    // GET
1606    /// Get an existing [`UserFollow`]
1607    ///
1608    /// # Arguments:
1609    /// * `user`
1610    /// * `following`
1611    pub async fn get_follow(&self, user: &str, following: &str) -> Result<UserFollow> {
1612        // fetch from database
1613        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1614            "SELECT * FROM \"xfollows\" WHERE \"user\" = ? AND \"following\" = ?"
1615        } else {
1616            "SELECT * FROM \"xfollows\" WHERE \"user\" = $1 AND \"following\" = $2"
1617        };
1618
1619        let c = &self.base.db.client;
1620        let row = match sqlquery(query)
1621            .bind::<&str>(&user)
1622            .bind::<&str>(&following)
1623            .fetch_one(c)
1624            .await
1625        {
1626            Ok(u) => self.base.textify_row(u).0,
1627            Err(_) => return Err(DatabaseError::Other),
1628        };
1629
1630        // return
1631        Ok(UserFollow {
1632            user: from_row!(row->user()),
1633            following: from_row!(row->following()),
1634        })
1635    }
1636
1637    /// Get all existing [`UserFollow`]s where `following` is the value of `user`
1638    ///
1639    /// # Arguments:
1640    /// * `user`
1641    pub async fn get_followers(
1642        &self,
1643        user: &str,
1644    ) -> Result<Vec<(UserFollow, Box<Profile>, Box<Profile>)>> {
1645        // fetch from database
1646        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1647            "SELECT * FROM \"xfollows\" WHERE \"following\" = ?"
1648        } else {
1649            "SELECT * FROM \"xfollows\" WHERE \"following\" = $1"
1650        };
1651
1652        let c = &self.base.db.client;
1653        let res = match sqlquery(query).bind::<&str>(&user).fetch_all(c).await {
1654            Ok(u) => {
1655                let mut out = Vec::new();
1656
1657                for row in u {
1658                    let row = self.base.textify_row(row).0;
1659
1660                    let user = from_row!(row->user());
1661                    let following = from_row!(row->following());
1662
1663                    out.push((
1664                        UserFollow {
1665                            user: user.clone(),
1666                            following: following.clone(),
1667                        },
1668                        match self.get_profile_by_id(&user).await {
1669                            Ok(ua) => ua,
1670                            Err(e) => return Err(e),
1671                        },
1672                        match self.get_profile_by_id(&following).await {
1673                            Ok(ua) => ua,
1674                            Err(e) => return Err(e),
1675                        },
1676                    ))
1677                }
1678
1679                out
1680            }
1681            Err(_) => return Err(DatabaseError::Other),
1682        };
1683
1684        // return
1685        Ok(res)
1686    }
1687
1688    /// Get all existing [`UserFollow`]s where `following` is the value of `user`, 12 at a time
1689    ///
1690    /// # Arguments:
1691    /// * `user`
1692    /// * `page`
1693    pub async fn get_followers_paginated(
1694        &self,
1695        user: &str,
1696        page: i32,
1697    ) -> Result<Vec<(UserFollow, Box<Profile>, Box<Profile>)>> {
1698        // fetch from database
1699        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1700            format!(
1701                "SELECT * FROM \"xfollows\" WHERE \"following\" = ? LIMIT 12 OFFSET {}",
1702                page * 12
1703            )
1704        } else {
1705            format!(
1706                "SELECT * FROM \"xfollows\" WHERE \"following\" = $1 LIMIT 12 OFFSET {}",
1707                page * 12
1708            )
1709        };
1710
1711        let c = &self.base.db.client;
1712        let res = match sqlquery(&query).bind::<&str>(&user).fetch_all(c).await {
1713            Ok(u) => {
1714                let mut out = Vec::new();
1715
1716                for row in u {
1717                    let row = self.base.textify_row(row).0;
1718
1719                    let user = from_row!(row->user());
1720                    let following = from_row!(row->following());
1721
1722                    out.push((
1723                        UserFollow {
1724                            user: user.clone(),
1725                            following: following.clone(),
1726                        },
1727                        match self.get_profile_by_id(&user).await {
1728                            Ok(ua) => ua,
1729                            Err(_) => continue,
1730                        },
1731                        match self.get_profile_by_id(&following).await {
1732                            Ok(ua) => ua,
1733                            Err(_) => continue,
1734                        },
1735                    ))
1736                }
1737
1738                out
1739            }
1740            Err(_) => return Err(DatabaseError::Other),
1741        };
1742
1743        // return
1744        Ok(res)
1745    }
1746
1747    /// Get the number of followers `user` has
1748    ///
1749    /// # Arguments:
1750    /// * `user`
1751    pub async fn get_followers_count(&self, user: &str) -> usize {
1752        // attempt to fetch from cache
1753        if let Some(count) = self
1754            .base
1755            .cache
1756            .get(format!("rbeam.auth.followers_count:{}", user))
1757            .await
1758        {
1759            return count.parse::<usize>().unwrap_or(0);
1760        };
1761
1762        // fetch from database
1763        let count = self.get_followers(user).await.unwrap_or(Vec::new()).len();
1764
1765        self.base
1766            .cache
1767            .set(
1768                format!("rbeam.auth.followers_count:{}", user),
1769                count.to_string(),
1770            )
1771            .await;
1772
1773        count
1774    }
1775
1776    /// Get all existing [`UserFollow`]s where `user` is the value of `user`
1777    ///
1778    /// # Arguments:
1779    /// * `user`
1780    pub async fn get_following(
1781        &self,
1782        user: &str,
1783    ) -> Result<Vec<(UserFollow, Box<Profile>, Box<Profile>)>> {
1784        // fetch from database
1785        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1786            "SELECT * FROM \"xfollows\" WHERE \"user\" = ?"
1787        } else {
1788            "SELECT * FROM \"xfollows\" WHERE \"user\" = $1"
1789        };
1790
1791        let c = &self.base.db.client;
1792        let res = match sqlquery(query).bind::<&str>(&user).fetch_all(c).await {
1793            Ok(u) => {
1794                let mut out = Vec::new();
1795
1796                for row in u {
1797                    let row = self.base.textify_row(row).0;
1798
1799                    let user = from_row!(row->user());
1800                    let following = from_row!(row->following());
1801
1802                    out.push((
1803                        UserFollow {
1804                            user: user.clone(),
1805                            following: following.clone(),
1806                        },
1807                        match self.get_profile_by_id(&user).await {
1808                            Ok(ua) => ua,
1809                            Err(_) => continue,
1810                        },
1811                        match self.get_profile_by_id(&following).await {
1812                            Ok(ua) => ua,
1813                            Err(_) => continue,
1814                        },
1815                    ))
1816                }
1817
1818                out
1819            }
1820            Err(_) => return Err(DatabaseError::Other),
1821        };
1822
1823        // return
1824        Ok(res)
1825    }
1826
1827    /// Get all existing [`UserFollow`]s where `user` is the value of `user`, 12 at a time
1828    ///
1829    /// # Arguments:
1830    /// * `user`
1831    /// * `page`
1832    pub async fn get_following_paginated(
1833        &self,
1834        user: &str,
1835        page: i32,
1836    ) -> Result<Vec<(UserFollow, Box<Profile>, Box<Profile>)>> {
1837        // fetch from database
1838        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1839            format!(
1840                "SELECT * FROM \"xfollows\" WHERE \"user\" = ? LIMIT 12 OFFSET {}",
1841                page * 12
1842            )
1843        } else {
1844            format!(
1845                "SELECT * FROM \"xfollows\" WHERE \"user\" = $1 LIMIT 12 OFFSET {}",
1846                page * 12
1847            )
1848        };
1849
1850        let c = &self.base.db.client;
1851        let res = match sqlquery(&query).bind::<&str>(&user).fetch_all(c).await {
1852            Ok(u) => {
1853                let mut out = Vec::new();
1854
1855                for row in u {
1856                    let row = self.base.textify_row(row).0;
1857
1858                    let user = from_row!(row->user());
1859                    let following = from_row!(row->following());
1860
1861                    out.push((
1862                        UserFollow {
1863                            user: user.clone(),
1864                            following: following.clone(),
1865                        },
1866                        match self.get_profile_by_id(&user).await {
1867                            Ok(ua) => ua,
1868                            Err(_) => continue,
1869                        },
1870                        match self.get_profile_by_id(&following).await {
1871                            Ok(ua) => ua,
1872                            Err(_) => continue,
1873                        },
1874                    ))
1875                }
1876
1877                out
1878            }
1879            Err(_) => return Err(DatabaseError::Other),
1880        };
1881
1882        // return
1883        Ok(res)
1884    }
1885
1886    /// Get the number of users `user` is following
1887    ///
1888    /// # Arguments:
1889    /// * `user`
1890    pub async fn get_following_count(&self, user: &str) -> usize {
1891        // attempt to fetch from cache
1892        if let Some(count) = self
1893            .base
1894            .cache
1895            .get(format!("rbeam.auth.following_count:{}", user))
1896            .await
1897        {
1898            return count.parse::<usize>().unwrap_or(0);
1899        };
1900
1901        // fetch from database
1902        let count = self.get_following(user).await.unwrap_or(Vec::new()).len();
1903
1904        self.base
1905            .cache
1906            .set(
1907                format!("rbeam.auth.following_count:{}", user),
1908                count.to_string(),
1909            )
1910            .await;
1911
1912        count
1913    }
1914
1915    // SET
1916    /// Toggle the following status of `user` on `following` ([`UserFollow`])
1917    ///
1918    /// # Arguments:
1919    /// * `props` - [`UserFollow`]
1920    pub async fn toggle_user_follow(&self, props: &mut UserFollow) -> Result<()> {
1921        // users cannot be the same
1922        if props.user == props.following {
1923            return Err(DatabaseError::Other);
1924        }
1925
1926        // make sure both users exist
1927        let user_1 = match self.get_profile(&props.user).await {
1928            Ok(ua) => ua,
1929            Err(e) => return Err(e),
1930        };
1931
1932        // make sure both users exist
1933        if let Err(e) = self.get_profile(&props.following).await {
1934            return Err(e);
1935        };
1936
1937        // check if follow exists
1938        if let Ok(_) = self.get_follow(&props.user, &props.following).await {
1939            // delete
1940            let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1941                "DELETE FROM \"xfollows\" WHERE \"user\" = ? AND \"following\" = ?"
1942            } else {
1943                "DELETE FROM \"xfollows\" WHERE \"user\" = $1 AND \"following\" = $2"
1944            };
1945
1946            let c = &self.base.db.client;
1947            match sqlquery(&query)
1948                .bind::<&str>(&props.user)
1949                .bind::<&str>(&props.following)
1950                .execute(c)
1951                .await
1952            {
1953                Ok(_) => {
1954                    self.base
1955                        .cache
1956                        .decr(format!("rbeam.auth.following_count:{}", props.user))
1957                        .await;
1958
1959                    self.base
1960                        .cache
1961                        .decr(format!("rbeam.auth.followers_count:{}", props.following))
1962                        .await;
1963
1964                    return Ok(());
1965                }
1966                Err(_) => return Err(DatabaseError::Other),
1967            };
1968        }
1969
1970        // return
1971        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
1972            "INSERT INTO \"xfollows\" VALUES (?, ?)"
1973        } else {
1974            "INSERT INTO \"xfollows\" VALUES ($1, $2)"
1975        };
1976
1977        let c = &self.base.db.client;
1978        match sqlquery(&query)
1979            .bind::<&str>(&props.user)
1980            .bind::<&str>(&props.following)
1981            .execute(c)
1982            .await
1983        {
1984            Ok(_) => {
1985                // bump counts
1986                self.base
1987                    .cache
1988                    .incr(format!("rbeam.auth.following_count:{}", props.user))
1989                    .await;
1990
1991                self.base
1992                    .cache
1993                    .incr(format!("rbeam.auth.followers_count:{}", props.following))
1994                    .await;
1995
1996                // create notification
1997                if let Err(e) = self
1998                    .create_notification(
1999                        NotificationCreate {
2000                            title: format!(
2001                                "[@{}](/+u/{}) followed you!",
2002                                user_1.username, user_1.id
2003                            ),
2004                            content: String::new(),
2005                            address: format!("/+u/{}", user_1.id),
2006                            recipient: props.following.clone(),
2007                        },
2008                        None,
2009                    )
2010                    .await
2011                {
2012                    return Err(e);
2013                };
2014
2015                // return
2016                Ok(())
2017            }
2018            Err(_) => Err(DatabaseError::Other),
2019        }
2020    }
2021
2022    /// Force remove the following status of `user` on `following` ([`UserFollow`])
2023    ///
2024    /// # Arguments:
2025    /// * `props` - [`UserFollow`]
2026    pub async fn force_remove_user_follow(&self, props: &mut UserFollow) -> Result<()> {
2027        // users cannot be the same
2028        if props.user == props.following {
2029            return Err(DatabaseError::Other);
2030        }
2031
2032        // check if follow exists
2033        if let Ok(_) = self.get_follow(&props.user, &props.following).await {
2034            // delete
2035            let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
2036                "DELETE FROM \"xfollows\" WHERE \"user\" = ? AND \"following\" = ?"
2037            } else {
2038                "DELETE FROM \"xfollows\" WHERE \"user\" = $1 AND \"following\" = $2"
2039            };
2040
2041            let c = &self.base.db.client;
2042            match sqlquery(&query)
2043                .bind::<&str>(&props.user)
2044                .bind::<&str>(&props.following)
2045                .execute(c)
2046                .await
2047            {
2048                Ok(_) => {
2049                    self.base
2050                        .cache
2051                        .decr(format!("rbeam.auth.following_count:{}", props.user))
2052                        .await;
2053
2054                    self.base
2055                        .cache
2056                        .decr(format!("rbeam.auth.followers_count:{}", props.following))
2057                        .await;
2058
2059                    return Ok(());
2060                }
2061                Err(_) => return Err(DatabaseError::Other),
2062            };
2063        }
2064
2065        // return
2066        // we can only remove following here, not add it
2067        Ok(())
2068    }
2069
2070    // notifications
2071
2072    // GET
2073    /// Get an existing notification
2074    ///
2075    /// # Arguments
2076    /// * `id`
2077    pub async fn get_notification(&self, id: &str) -> Result<Notification> {
2078        // check in cache
2079        match self
2080            .base
2081            .cache
2082            .get(format!("rbeam.auth.notification:{}", id))
2083            .await
2084        {
2085            Some(c) => return Ok(serde_json::from_str::<Notification>(c.as_str()).unwrap()),
2086            None => (),
2087        };
2088
2089        // pull from database
2090        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
2091            "SELECT * FROM \"xnotifications\" WHERE \"id\" = ?"
2092        } else {
2093            "SELECT * FROM \"xnotifications\" WHERE \"id\" = $1"
2094        };
2095
2096        let c = &self.base.db.client;
2097        let res = match sqlquery(&query).bind::<&str>(&id).fetch_one(c).await {
2098            Ok(p) => self.base.textify_row(p).0,
2099            Err(_) => return Err(DatabaseError::NotFound),
2100        };
2101
2102        // return
2103        let notification = Notification {
2104            title: from_row!(res->title()),
2105            content: from_row!(res->content()),
2106            address: from_row!(res->address()),
2107            timestamp: from_row!(res->timestamp(u128); 0),
2108            id: from_row!(res->id()),
2109            recipient: from_row!(res->recipient()),
2110        };
2111
2112        // store in cache
2113        self.base
2114            .cache
2115            .set(
2116                format!("rbeam.auth.notification:{}", id),
2117                serde_json::to_string::<Notification>(&notification).unwrap(),
2118            )
2119            .await;
2120
2121        // return
2122        Ok(notification)
2123    }
2124
2125    /// Get all notifications by their recipient
2126    ///
2127    /// # Arguments
2128    /// * `recipient`
2129    pub async fn get_notifications_by_recipient(
2130        &self,
2131        recipient: &str,
2132    ) -> Result<Vec<Notification>> {
2133        // pull from database
2134        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
2135            "SELECT * FROM \"xnotifications\" WHERE \"recipient\" = ? ORDER BY \"timestamp\" DESC"
2136        } else {
2137            "SELECT * FROM \"xnotifications\" WHERE \"recipient\" = $1 ORDER BY \"timestamp\" DESC"
2138        };
2139
2140        let c = &self.base.db.client;
2141        let res = match sqlquery(&query)
2142            .bind::<&str>(&recipient.to_lowercase())
2143            .fetch_all(c)
2144            .await
2145        {
2146            Ok(p) => {
2147                let mut out: Vec<Notification> = Vec::new();
2148
2149                for row in p {
2150                    let res = self.base.textify_row(row).0;
2151                    out.push(Notification {
2152                        title: from_row!(res->title()),
2153                        content: from_row!(res->content()),
2154                        address: from_row!(res->address()),
2155                        timestamp: from_row!(res->timestamp(u128); 0),
2156                        id: from_row!(res->id()),
2157                        recipient: from_row!(res->recipient()),
2158                    });
2159                }
2160
2161                out
2162            }
2163            Err(_) => return Err(DatabaseError::NotFound),
2164        };
2165
2166        // return
2167        Ok(res)
2168    }
2169
2170    /// Get the number of notifications by their recipient
2171    ///
2172    /// # Arguments
2173    /// * `recipient`
2174    pub async fn get_notification_count_by_recipient_cache(&self, recipient: &str) -> usize {
2175        // attempt to fetch from cache
2176        if let Some(count) = self
2177            .base
2178            .cache
2179            .get(format!("rbeam.auth.notification_count:{}", recipient))
2180            .await
2181        {
2182            return count.parse::<usize>().unwrap_or(0);
2183        };
2184
2185        // fetch from database
2186        let count = self
2187            .get_notifications_by_recipient(recipient)
2188            .await
2189            .unwrap_or(Vec::new())
2190            .len();
2191
2192        self.base
2193            .cache
2194            .set(
2195                format!("rbeam.auth.notification_count:{}", recipient),
2196                count.to_string(),
2197            )
2198            .await;
2199
2200        count
2201    }
2202
2203    /// Get the number of notifications by their recipient
2204    ///
2205    /// # Arguments
2206    /// * `recipient`
2207    pub async fn get_notification_count_by_recipient(&self, recipient: &str) -> usize {
2208        match self.get_profile(recipient).await {
2209            Ok(x) => x.notification_count,
2210            Err(_) => 0,
2211        }
2212    }
2213
2214    /// Get all notifications by their recipient, 12 at a time
2215    ///
2216    /// # Arguments
2217    /// * `recipient`
2218    /// * `page`
2219    pub async fn get_notifications_by_recipient_paginated(
2220        &self,
2221        recipient: &str,
2222        page: i32,
2223    ) -> Result<Vec<Notification>> {
2224        // pull from database
2225        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
2226            format!("SELECT * FROM \"xnotifications\" WHERE \"recipient\" = ? ORDER BY \"timestamp\" DESC LIMIT 12 OFFSET {}", page * 12)
2227        } else {
2228            format!("SELECT * FROM \"xnotifications\" WHERE \"recipient\" = $1 ORDER BY \"timestamp\" DESC LIMIT 12 OFFSET {}", page * 12)
2229        };
2230
2231        let c = &self.base.db.client;
2232        let res = match sqlquery(&query)
2233            .bind::<&str>(&recipient.to_lowercase())
2234            .fetch_all(c)
2235            .await
2236        {
2237            Ok(p) => {
2238                let mut out: Vec<Notification> = Vec::new();
2239
2240                for row in p {
2241                    let res = self.base.textify_row(row).0;
2242                    out.push(Notification {
2243                        title: from_row!(res->title()),
2244                        content: from_row!(res->content()),
2245                        address: from_row!(res->address()),
2246                        timestamp: from_row!(res->timestamp(u128); 0),
2247                        id: from_row!(res->id()),
2248                        recipient: from_row!(res->recipient()),
2249                    });
2250                }
2251
2252                out
2253            }
2254            Err(_) => return Err(DatabaseError::NotFound),
2255        };
2256
2257        // return
2258        Ok(res)
2259    }
2260
2261    // SET
2262    /// Create a new notification
2263    ///
2264    /// # Arguments
2265    /// * `props` - [`NotificationCreate`]
2266    pub async fn create_notification(
2267        &self,
2268        props: NotificationCreate,
2269        id: Option<String>,
2270    ) -> Result<()> {
2271        let notification = Notification {
2272            title: props.title,
2273            content: props.content,
2274            address: props.address,
2275            timestamp: utility::unix_epoch_timestamp(),
2276            id: if let Some(id) = id {
2277                id
2278            } else {
2279                AlmostSnowflake::new(self.config.snowflake_server_id).to_string()
2280            },
2281            recipient: props.recipient,
2282        };
2283
2284        // create notification
2285        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
2286            "INSERT INTO \"xnotifications\" VALUES (?, ?, ?, ?, ?, ?)"
2287        } else {
2288            "INSERT INTO \"xnotifications\" VALUES ($1, $2, $3, $4, $5, $6)"
2289        };
2290
2291        let c = &self.base.db.client;
2292        match sqlquery(&query)
2293            .bind::<&str>(&notification.title)
2294            .bind::<&str>(&notification.content)
2295            .bind::<&str>(&notification.address)
2296            .bind::<&str>(&notification.timestamp.to_string())
2297            .bind::<&str>(&notification.id)
2298            .bind::<&str>(&notification.recipient)
2299            .execute(c)
2300            .await
2301        {
2302            Ok(_) => {
2303                // incr notifications count
2304                self.base
2305                    .cache
2306                    .incr(format!(
2307                        "rbeam.auth.notification_count:{}",
2308                        notification.recipient
2309                    ))
2310                    .await;
2311
2312                // check recipient
2313                if !notification.recipient.starts_with("*") {
2314                    let recipient =
2315                        simplify!(self.get_profile(&notification.recipient).await; Result);
2316
2317                    simplify!(
2318                        self.update_profile_notification_count(
2319                            &notification.recipient,
2320                            recipient.notification_count + 1,
2321                        )
2322                        .await; Err
2323                    );
2324                }
2325
2326                // ...
2327                return Ok(());
2328            }
2329            Err(_) => return Err(DatabaseError::Other),
2330        };
2331    }
2332
2333    /// Delete an existing notification
2334    ///
2335    /// Notifications can only be deleted by their recipient.
2336    ///
2337    /// # Arguments
2338    /// * `id` - the ID of the notification
2339    /// * `user` - the user doing this
2340    pub async fn delete_notification(&self, id: &str, user: Box<Profile>) -> Result<()> {
2341        // make sure notification exists
2342        let notification = match self.get_notification(id).await {
2343            Ok(n) => n,
2344            Err(e) => return Err(e),
2345        };
2346
2347        // check username
2348        if user.id != notification.recipient {
2349            // check permission
2350            let group = match self.get_group_by_id(user.group).await {
2351                Ok(g) => g,
2352                Err(_) => return Err(DatabaseError::Other),
2353            };
2354
2355            if !group
2356                .permissions
2357                .check(FinePermission::MANAGE_NOTIFICATIONS)
2358            {
2359                return Err(DatabaseError::NotAllowed);
2360            }
2361        }
2362
2363        // delete notification
2364        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
2365            "DELETE FROM \"xnotifications\" WHERE \"id\" = ?"
2366        } else {
2367            "DELETE FROM \"xnotifications\" WHERE \"id\" = $1"
2368        };
2369
2370        let c = &self.base.db.client;
2371        match sqlquery(&query).bind::<&str>(&id).execute(c).await {
2372            Ok(_) => {
2373                // decr notifications count
2374                self.base
2375                    .cache
2376                    .decr(format!(
2377                        "rbeam.auth.notification_count:{}",
2378                        notification.recipient
2379                    ))
2380                    .await;
2381
2382                // remove from cache
2383                self.base
2384                    .cache
2385                    .remove(format!("rbeam.auth.notification:{}", id))
2386                    .await;
2387
2388                // return
2389                return Ok(());
2390            }
2391            Err(_) => return Err(DatabaseError::Other),
2392        };
2393    }
2394
2395    /// Delete all existing notifications by their recipient
2396    ///
2397    /// # Arguments
2398    /// * `id` - the ID of the notification
2399    /// * `user` - the user doing this
2400    pub async fn delete_notifications_by_recipient(
2401        &self,
2402        recipient: &str,
2403        user: Box<Profile>,
2404    ) -> Result<()> {
2405        // make sure notifications exists
2406        let notifications = match self.get_notifications_by_recipient(recipient).await {
2407            Ok(n) => n,
2408            Err(e) => return Err(e),
2409        };
2410
2411        // check username
2412        if user.id != recipient {
2413            // check permission
2414            let group = match self.get_group_by_id(user.group).await {
2415                Ok(g) => g,
2416                Err(_) => return Err(DatabaseError::Other),
2417            };
2418
2419            if !group
2420                .permissions
2421                .check(FinePermission::MANAGE_NOTIFICATIONS)
2422            {
2423                return Err(DatabaseError::NotAllowed);
2424            }
2425        }
2426
2427        // delete notifications
2428        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
2429            "DELETE FROM \"xnotifications\" WHERE \"recipient\" = ?"
2430        } else {
2431            "DELETE FROM \"xnotifications\" WHERE \"recipient\" = $1"
2432        };
2433
2434        let c = &self.base.db.client;
2435        match sqlquery(&query).bind::<&str>(&recipient).execute(c).await {
2436            Ok(_) => {
2437                // clear notifications count
2438                self.base
2439                    .cache
2440                    .remove(format!("rbeam.auth.notification_count:{}", recipient))
2441                    .await;
2442
2443                // clear cache for all deleted notifications
2444                for notification in notifications {
2445                    // remove from cache
2446                    self.base
2447                        .cache
2448                        .remove(format!("rbeam.auth.notification:{}", notification.id))
2449                        .await;
2450                }
2451
2452                // return
2453                return Ok(());
2454            }
2455            Err(_) => return Err(DatabaseError::Other),
2456        };
2457    }
2458
2459    // warnings
2460
2461    // GET
2462    /// Get an existing warning
2463    ///
2464    /// # Arguments
2465    /// * `id`
2466    pub async fn get_warning(&self, id: &str) -> Result<Warning> {
2467        // check in cache
2468        match self
2469            .base
2470            .cache
2471            .get(format!("rbeam.auth.warning:{}", id))
2472            .await
2473        {
2474            Some(c) => return Ok(serde_json::from_str::<Warning>(c.as_str()).unwrap()),
2475            None => (),
2476        };
2477
2478        // pull from database
2479        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
2480            "SELECT * FROM \"xwarnings\" WHERE \"id\" = ?"
2481        } else {
2482            "SELECT * FROM \"xwarnings\" WHERE \"id\" = $1"
2483        };
2484
2485        let c = &self.base.db.client;
2486        let res = match sqlquery(&query).bind::<&str>(&id).fetch_one(c).await {
2487            Ok(p) => self.base.textify_row(p).0,
2488            Err(_) => return Err(DatabaseError::NotFound),
2489        };
2490
2491        // return
2492        let warning = Warning {
2493            id: from_row!(res->id()),
2494            content: from_row!(res->content()),
2495            timestamp: from_row!(res->timestamp(u128); 0),
2496            recipient: from_row!(res->recipient()),
2497            moderator: match self.get_profile_by_id(res.get("moderator").unwrap()).await {
2498                Ok(ua) => ua,
2499                Err(e) => return Err(e),
2500            },
2501        };
2502
2503        // store in cache
2504        self.base
2505            .cache
2506            .set(
2507                format!("rbeam.auth.warning:{}", id),
2508                serde_json::to_string::<Warning>(&warning).unwrap(),
2509            )
2510            .await;
2511
2512        // return
2513        Ok(warning)
2514    }
2515
2516    /// Get all warnings by their recipient
2517    ///
2518    /// # Arguments
2519    /// * `recipient`
2520    /// * `user` - the user doing this
2521    pub async fn get_warnings_by_recipient(
2522        &self,
2523        recipient: &str,
2524        user: Box<Profile>,
2525    ) -> Result<Vec<Warning>> {
2526        // make sure user is a manager
2527        let group = match self.get_group_by_id(user.group).await {
2528            Ok(g) => g,
2529            Err(_) => return Err(DatabaseError::Other),
2530        };
2531
2532        if !group.permissions.check(FinePermission::MANAGE_WARNINGS) {
2533            return Err(DatabaseError::NotAllowed);
2534        }
2535
2536        // pull from database
2537        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
2538            "SELECT * FROM \"xwarnings\" WHERE \"recipient\" = ? ORDER BY \"timestamp\" DESC"
2539        } else {
2540            "SELECT * FROM \"xwarnings\" WHERE \"recipient\" = $1 ORDER BY \"timestamp\" DESC"
2541        };
2542
2543        let c = &self.base.db.client;
2544        let res = match sqlquery(&query)
2545            .bind::<&str>(&recipient.to_lowercase())
2546            .fetch_all(c)
2547            .await
2548        {
2549            Ok(p) => {
2550                let mut out: Vec<Warning> = Vec::new();
2551
2552                for row in p {
2553                    let res = self.base.textify_row(row).0;
2554                    out.push(Warning {
2555                        id: from_row!(res->id()),
2556                        content: from_row!(res->content()),
2557                        timestamp: from_row!(res->timestamp(u128); 0),
2558                        recipient: from_row!(res->recipient()),
2559                        moderator: match self.get_profile_by_id(res.get("moderator").unwrap()).await
2560                        {
2561                            Ok(ua) => ua,
2562                            Err(_) => continue,
2563                        },
2564                    });
2565                }
2566
2567                out
2568            }
2569            Err(_) => return Err(DatabaseError::NotFound),
2570        };
2571
2572        // return
2573        Ok(res)
2574    }
2575
2576    // SET
2577    /// Create a new warning
2578    ///
2579    /// # Arguments
2580    /// * `props` - [`WarningCreate`]
2581    /// * `user` - the user creating this warning
2582    pub async fn create_warning(&self, props: WarningCreate, user: Box<Profile>) -> Result<()> {
2583        // make sure user is a manager
2584        let group = match self.get_group_by_id(user.group).await {
2585            Ok(g) => g,
2586            Err(_) => return Err(DatabaseError::Other),
2587        };
2588
2589        if !group.permissions.check(FinePermission::MANAGE_WARNINGS) {
2590            return Err(DatabaseError::NotAllowed);
2591        }
2592
2593        // ...
2594        let warning = Warning {
2595            id: utility::random_id(),
2596            content: props.content,
2597            timestamp: utility::unix_epoch_timestamp(),
2598            recipient: props.recipient,
2599            moderator: user,
2600        };
2601
2602        // create notification
2603        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
2604            "INSERT INTO \"xwarnings\" VALUES (?, ?, ?, ?, ?)"
2605        } else {
2606            "INSERT INTO \"xwarnings\" VALUES ($1, $2, $3, $4, $5)"
2607        };
2608
2609        let c = &self.base.db.client;
2610        match sqlquery(&query)
2611            .bind::<&str>(&warning.id)
2612            .bind::<&str>(&warning.content)
2613            .bind::<&str>(&warning.timestamp.to_string())
2614            .bind::<&str>(&warning.recipient)
2615            .bind::<&str>(&warning.moderator.id)
2616            .execute(c)
2617            .await
2618        {
2619            Ok(_) => {
2620                // create notification for recipient
2621                if let Err(e) = self
2622                    .create_notification(
2623                        NotificationCreate {
2624                            title: "You have received an account warning!".to_string(),
2625                            content: warning.content,
2626                            address: String::new(),
2627                            recipient: warning.recipient,
2628                        },
2629                        None,
2630                    )
2631                    .await
2632                {
2633                    return Err(e);
2634                };
2635
2636                // ...
2637                return Ok(());
2638            }
2639            Err(_) => return Err(DatabaseError::Other),
2640        };
2641    }
2642
2643    /// Delete an existing warning
2644    ///
2645    /// Warnings can only be deleted by their moderator or admins.
2646    ///
2647    /// # Arguments
2648    /// * `id` - the ID of the warning
2649    /// * `user` - the user doing this
2650    pub async fn delete_warning(&self, id: &str, user: Box<Profile>) -> Result<()> {
2651        // make sure warning exists
2652        let warning = match self.get_warning(id).await {
2653            Ok(n) => n,
2654            Err(e) => return Err(e),
2655        };
2656
2657        // check id
2658        if user.id != warning.moderator.id {
2659            // check permission
2660            let group = match self.get_group_by_id(user.group).await {
2661                Ok(g) => g,
2662                Err(_) => return Err(DatabaseError::Other),
2663            };
2664
2665            if !group.permissions.check(FinePermission::MANAGE_WARNINGS) {
2666                return Err(DatabaseError::NotAllowed);
2667            }
2668        }
2669
2670        // delete warning
2671        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
2672            "DELETE FROM \"xwarnings\" WHERE \"id\" = ?"
2673        } else {
2674            "DELETE FROM \"xwarnings\" WHERE \"id\" = $1"
2675        };
2676
2677        let c = &self.base.db.client;
2678        match sqlquery(&query).bind::<&str>(&id).execute(c).await {
2679            Ok(_) => {
2680                // remove from cache
2681                self.base
2682                    .cache
2683                    .remove(format!("rbeam.auth.warning:{}", id))
2684                    .await;
2685
2686                // return
2687                return Ok(());
2688            }
2689            Err(_) => return Err(DatabaseError::Other),
2690        };
2691    }
2692
2693    // ip bans
2694
2695    // GET
2696    /// Get an existing [`IpBan`]
2697    ///
2698    /// # Arguments
2699    /// * `id`
2700    pub async fn get_ipban(&self, id: &str) -> Result<IpBan> {
2701        // check in cache
2702        match self
2703            .base
2704            .cache
2705            .get(format!("rbeam.auth.ipban:{}", id))
2706            .await
2707        {
2708            Some(c) => return Ok(serde_json::from_str::<IpBan>(c.as_str()).unwrap()),
2709            None => (),
2710        };
2711
2712        // pull from database
2713        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
2714            "SELECT * FROM \"xbans\" WHERE \"id\" = ?"
2715        } else {
2716            "SELECT * FROM \"xbans\" WHERE \"id\" = $1"
2717        };
2718
2719        let c = &self.base.db.client;
2720        let res = match sqlquery(&query).bind::<&str>(&id).fetch_one(c).await {
2721            Ok(p) => self.base.textify_row(p).0,
2722            Err(_) => return Err(DatabaseError::NotFound),
2723        };
2724
2725        // return
2726        let ban = IpBan {
2727            id: from_row!(res->id()),
2728            ip: from_row!(res->ip()),
2729            reason: from_row!(res->reason()),
2730            moderator: match self.get_profile_by_id(res.get("moderator").unwrap()).await {
2731                Ok(ua) => ua,
2732                Err(e) => return Err(e),
2733            },
2734            timestamp: from_row!(res->timestamp(u128); 0),
2735        };
2736
2737        // store in cache
2738        self.base
2739            .cache
2740            .set(
2741                format!("rbeam.auth.ipban:{}", id),
2742                serde_json::to_string::<IpBan>(&ban).unwrap(),
2743            )
2744            .await;
2745
2746        // return
2747        Ok(ban)
2748    }
2749
2750    /// Get an existing [`IpBan`] by its IP
2751    ///
2752    /// # Arguments
2753    /// * `ip`
2754    pub async fn get_ipban_by_ip(&self, ip: &str) -> Result<IpBan> {
2755        // pull from database
2756        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
2757            "SELECT * FROM \"xbans\" WHERE \"ip\" = ?"
2758        } else {
2759            "SELECT * FROM \"xbans\" WHERE \"ip\" = $1"
2760        };
2761
2762        let c = &self.base.db.client;
2763        let res = match sqlquery(&query).bind::<&str>(&ip).fetch_one(c).await {
2764            Ok(p) => self.base.textify_row(p).0,
2765            Err(_) => return Err(DatabaseError::NotFound),
2766        };
2767
2768        // return
2769        let ban = IpBan {
2770            id: from_row!(res->id()),
2771            ip: from_row!(res->ip()),
2772            reason: from_row!(res->reason()),
2773            moderator: match self.get_profile_by_id(res.get("moderator").unwrap()).await {
2774                Ok(ua) => ua,
2775                Err(e) => return Err(e),
2776            },
2777            timestamp: from_row!(res->timestamp(u128); 0),
2778        };
2779
2780        // return
2781        Ok(ban)
2782    }
2783
2784    /// Get all [`IpBan`]s
2785    ///
2786    /// # Arguments
2787    /// * `user` - the user doing this
2788    pub async fn get_ipbans(&self, user: Box<Profile>) -> Result<Vec<IpBan>> {
2789        // make sure user is a manager
2790        let group = match self.get_group_by_id(user.group).await {
2791            Ok(g) => g,
2792            Err(_) => return Err(DatabaseError::Other),
2793        };
2794
2795        if !group.permissions.check(FinePermission::BAN_IP) {
2796            return Err(DatabaseError::NotAllowed);
2797        }
2798
2799        // pull from database
2800        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
2801            "SELECT * FROM \"xbans\" ORDER BY \"timestamp\" DESC"
2802        } else {
2803            "SELECT * FROM \"xbans\" ORDER BY \"timestamp\" DESC"
2804        };
2805
2806        let c = &self.base.db.client;
2807        let res = match sqlquery(&query).fetch_all(c).await {
2808            Ok(p) => {
2809                let mut out: Vec<IpBan> = Vec::new();
2810
2811                for row in p {
2812                    let res = self.base.textify_row(row).0;
2813                    out.push(IpBan {
2814                        id: from_row!(res->id()),
2815                        ip: from_row!(res->ip()),
2816                        reason: from_row!(res->reason()),
2817                        moderator: match self.get_profile_by_id(res.get("moderator").unwrap()).await
2818                        {
2819                            Ok(ua) => ua,
2820                            Err(_) => continue,
2821                        },
2822                        timestamp: from_row!(res->timestamp(u128); 0),
2823                    });
2824                }
2825
2826                out
2827            }
2828            Err(_) => return Err(DatabaseError::NotFound),
2829        };
2830
2831        // return
2832        Ok(res)
2833    }
2834
2835    // SET
2836    /// Create a new [`IpBan`]
2837    ///
2838    /// # Arguments
2839    /// * `props` - [`IpBanCreate`]
2840    /// * `user` - the user creating this ban
2841    pub async fn create_ipban(&self, props: IpBanCreate, user: Box<Profile>) -> Result<()> {
2842        // make sure user is a helper
2843        let group = match self.get_group_by_id(user.group).await {
2844            Ok(g) => g,
2845            Err(_) => return Err(DatabaseError::Other),
2846        };
2847
2848        if !group.permissions.check(FinePermission::BAN_IP) {
2849            return Err(DatabaseError::NotAllowed);
2850        } else {
2851            let actor_id = user.id.clone();
2852            if let Err(e) = self
2853                .create_notification(
2854                    NotificationCreate {
2855                        title: format!("[{actor_id}](/+u/{actor_id})"),
2856                        content: format!("Banned an IP: {}", props.ip),
2857                        address: format!("/+u/{actor_id}"),
2858                        recipient: "*(audit)".to_string(), // all staff, audit
2859                    },
2860                    None,
2861                )
2862                .await
2863            {
2864                return Err(e);
2865            }
2866        }
2867
2868        // make sure this ip isn't already banned
2869        if self.get_ipban_by_ip(&props.ip).await.is_ok() {
2870            return Err(DatabaseError::MustBeUnique);
2871        }
2872
2873        // ...
2874        let ban = IpBan {
2875            id: utility::random_id(),
2876            ip: props.ip,
2877            reason: props.reason,
2878            moderator: user,
2879            timestamp: utility::unix_epoch_timestamp(),
2880        };
2881
2882        // create notification
2883        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
2884            "INSERT INTO \"xbans\" VALUES (?, ?, ?, ?, ?)"
2885        } else {
2886            "INSERT INTO \"xbans\" VALUES ($1, $2, $3, $4, $5)"
2887        };
2888
2889        let c = &self.base.db.client;
2890        match sqlquery(&query)
2891            .bind::<&str>(&ban.id)
2892            .bind::<&str>(&ban.ip)
2893            .bind::<&str>(&ban.reason)
2894            .bind::<&str>(&ban.moderator.id)
2895            .bind::<&str>(&ban.timestamp.to_string())
2896            .execute(c)
2897            .await
2898        {
2899            Ok(_) => return Ok(()),
2900            Err(_) => return Err(DatabaseError::Other),
2901        };
2902    }
2903
2904    /// Delete an existing IpBan
2905    ///
2906    /// # Arguments
2907    /// * `id` - the ID of the ban
2908    /// * `user` - the user doing this
2909    pub async fn delete_ipban(&self, id: &str, user: Box<Profile>) -> Result<()> {
2910        // make sure ban exists
2911        let ipban = match self.get_ipban(id).await {
2912            Ok(n) => n,
2913            Err(e) => return Err(e),
2914        };
2915
2916        // check id
2917        if user.id != ipban.moderator.id {
2918            // check permission
2919            let group = match self.get_group_by_id(user.group).await {
2920                Ok(g) => g,
2921                Err(_) => return Err(DatabaseError::Other),
2922            };
2923
2924            if !group.permissions.check(FinePermission::UNBAN_IP) {
2925                return Err(DatabaseError::NotAllowed);
2926            } else {
2927                let actor_id = user.id.clone();
2928                if let Err(e) = self
2929                    .create_notification(
2930                        NotificationCreate {
2931                            title: format!("[{actor_id}](/+u/{actor_id})"),
2932                            content: format!("Unbanned an IP: {}", ipban.ip),
2933                            address: format!("/+u/{actor_id}"),
2934                            recipient: "*(audit)".to_string(), // all staff, audit
2935                        },
2936                        None,
2937                    )
2938                    .await
2939                {
2940                    return Err(e);
2941                }
2942            }
2943        }
2944
2945        // delete ban
2946        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
2947            "DELETE FROM \"xbans\" WHERE \"id\" = ?"
2948        } else {
2949            "DELETE FROM \"xbans\" WHERE \"id\" = $1"
2950        };
2951
2952        let c = &self.base.db.client;
2953        match sqlquery(&query).bind::<&str>(&id).execute(c).await {
2954            Ok(_) => {
2955                // remove from cache
2956                self.base
2957                    .cache
2958                    .remove(format!("rbeam.auth.ipban:{}", id))
2959                    .await;
2960
2961                // return
2962                return Ok(());
2963            }
2964            Err(_) => return Err(DatabaseError::Other),
2965        };
2966    }
2967
2968    // relationships
2969
2970    /// Get the membership status of the given user and the other user
2971    ///
2972    /// # Arguments
2973    /// * `user` - the ID of the first user
2974    /// * `other` - the ID of the second user
2975    pub async fn get_user_relationship(
2976        &self,
2977        user: &str,
2978        other: &str,
2979    ) -> (RelationshipStatus, String, String) {
2980        // pull from database
2981        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
2982            "SELECT * FROM \"xrelationships\" WHERE (\"one\" = ? AND \"two\" = ?) OR (\"one\" = ? AND \"two\" = ?)"
2983        } else {
2984            "SELECT * FROM \"xrelationships\" WHERE (\"one\" = $1 AND \"two\" = $2) OR (\"one\" = $3 AND \"two\" = $4)"
2985        };
2986
2987        let c = &self.base.db.client;
2988        let res = match sqlquery(&query)
2989            .bind::<&str>(&user)
2990            .bind::<&str>(&other)
2991            .bind::<&str>(&other)
2992            .bind::<&str>(&user)
2993            .fetch_one(c)
2994            .await
2995        {
2996            Ok(p) => self.base.textify_row(p).0,
2997            Err(_) => {
2998                return (
2999                    RelationshipStatus::default(),
3000                    user.to_string(),
3001                    other.to_string(),
3002                )
3003            }
3004        };
3005
3006        // return
3007        (
3008            serde_json::from_str(&res.get("status").unwrap()).unwrap(),
3009            res.get("one").unwrap().to_string(),
3010            res.get("two").unwrap().to_string(),
3011        )
3012    }
3013
3014    /// Set the relationship of user `one` and `two`
3015    ///
3016    /// # Arguments
3017    /// * `one` - the ID of the first user
3018    /// * `two` - the ID of the second user
3019    /// * `status` - the new relationship status, setting to "Unknown" will remove the relationship
3020    /// * `disable_notifications`
3021    pub async fn set_user_relationship_status(
3022        &self,
3023        one: &str,
3024        two: &str,
3025        status: RelationshipStatus,
3026        disable_notifications: bool,
3027    ) -> Result<()> {
3028        // get current membership status
3029        let mut relationship = self.get_user_relationship(one, two).await;
3030
3031        if relationship.0 == status {
3032            return Ok(());
3033        }
3034
3035        let mut uone = match self.get_profile(&relationship.1).await {
3036            Ok(ua) => ua,
3037            Err(e) => return Err(e),
3038        };
3039
3040        let mut utwo = match self.get_profile(&relationship.2).await {
3041            Ok(ua) => ua,
3042            Err(e) => return Err(e),
3043        };
3044
3045        // ...
3046        match status {
3047            RelationshipStatus::Blocked => {
3048                // if the relationship exists but we aren't user one, delete it
3049                if relationship.0 != RelationshipStatus::Unknown && uone.id != one {
3050                    // delete
3051                    let query =
3052                        if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
3053                            "DELETE FROM \"xrelationships\" WHERE \"one\" = ? AND \"two\" = ?"
3054                        } else {
3055                            "DELETE FROM \"xrelationships\" WHERE \"one\" = ? AND \"two\" = ?"
3056                        };
3057
3058                    let c = &self.base.db.client;
3059                    if let Err(_) = sqlquery(&query)
3060                        .bind::<&str>(&uone.id)
3061                        .bind::<&str>(&utwo.id)
3062                        .execute(c)
3063                        .await
3064                    {
3065                        return Err(DatabaseError::Other);
3066                    };
3067
3068                    relationship.0 = RelationshipStatus::Unknown; // act like it never happened
3069                    uone.id = one.to_string();
3070                    utwo.id = two.to_string();
3071                }
3072
3073                // ...
3074                if relationship.0 != RelationshipStatus::Unknown {
3075                    if relationship.0 == RelationshipStatus::Friends {
3076                        // decr friendship counts since we were previously friends but are not now
3077                        self.base
3078                            .cache
3079                            .decr(format!("rbeam.app.friends_count:{}", uone.id))
3080                            .await;
3081
3082                        self.base
3083                            .cache
3084                            .decr(format!("rbeam.app.friends_count:{}", utwo.id))
3085                            .await;
3086                    }
3087
3088                    // update
3089                    let query = if (self.base.db.r#type == "sqlite")
3090                        | (self.base.db.r#type == "mysql")
3091                    {
3092                        "UPDATE \"xrelationships\" SET \"status\" = ? WHERE \"one\" = ? AND \"two\" = ?"
3093                    } else {
3094                        "UPDATE \"xrelationships\" SET (\"status\") = (?) WHERE \"one\" = ? AND \"two\" = ?"
3095                    };
3096
3097                    let c = &self.base.db.client;
3098                    if let Err(_) = sqlquery(&query)
3099                        .bind::<&str>(&serde_json::to_string(&status).unwrap())
3100                        .bind::<&str>(&uone.id)
3101                        .bind::<&str>(&utwo.id)
3102                        .execute(c)
3103                        .await
3104                    {
3105                        return Err(DatabaseError::Other);
3106                    };
3107                } else {
3108                    // add
3109                    let query =
3110                        if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
3111                            "INSERT INTO \"xrelationships\" VALUES (?, ?, ?, ?)"
3112                        } else {
3113                            "INSERT INTO \"xrelationships\" VALUES ($1, $2, $3, $4)"
3114                        };
3115
3116                    let c = &self.base.db.client;
3117                    if let Err(_) = sqlquery(&query)
3118                        .bind::<&str>(&uone.id)
3119                        .bind::<&str>(&utwo.id)
3120                        .bind::<&str>(&serde_json::to_string(&status).unwrap())
3121                        .bind::<&str>(&rainbeam_shared::unix_epoch_timestamp().to_string())
3122                        .execute(c)
3123                        .await
3124                    {
3125                        return Err(DatabaseError::Other);
3126                    };
3127                }
3128            }
3129            RelationshipStatus::Pending => {
3130                // check utwo permissions
3131                if utwo.metadata.is_true("sparkler:limited_friend_requests") {
3132                    // make sure utwo is following uone
3133                    if let Err(_) = self.get_follow(&utwo.id, &uone.id).await {
3134                        return Err(DatabaseError::NotAllowed);
3135                    }
3136                }
3137
3138                // add
3139                let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
3140                {
3141                    "INSERT INTO \"xrelationships\" VALUES (?, ?, ?, ?)"
3142                } else {
3143                    "INSERT INTO \"xrelationships\" VALUES ($1, $2, $3, $4)"
3144                };
3145
3146                let c = &self.base.db.client;
3147                if let Err(_) = sqlquery(&query)
3148                    .bind::<&str>(&uone.id)
3149                    .bind::<&str>(&utwo.id)
3150                    .bind::<&str>(&serde_json::to_string(&status).unwrap())
3151                    .bind::<&str>(&rainbeam_shared::unix_epoch_timestamp().to_string())
3152                    .execute(c)
3153                    .await
3154                {
3155                    return Err(DatabaseError::Other);
3156                };
3157
3158                // create notification
3159                if !disable_notifications {
3160                    if let Err(_) = self
3161                        .create_notification(
3162                            NotificationCreate {
3163                                title: format!(
3164                                    "[@{}](/+u/{}) has sent you a friend request!",
3165                                    uone.username, uone.id
3166                                ),
3167                                content: format!("{} wants to be your friend.", uone.username),
3168                                address: format!("/@{}/relationship/friend_accept", uone.id),
3169                                recipient: utwo.id,
3170                            },
3171                            None,
3172                        )
3173                        .await
3174                    {
3175                        return Err(DatabaseError::Other);
3176                    };
3177                };
3178            }
3179            RelationshipStatus::Friends => {
3180                // update
3181                let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
3182                {
3183                    "UPDATE \"xrelationships\" SET \"status\" = ? WHERE \"one\" = ? AND \"two\" = ?"
3184                } else {
3185                    "UPDATE \"xrelationships\" SET (\"status\") = (?) WHERE \"one\" = ? AND \"two\" = ?"
3186                };
3187
3188                let c = &self.base.db.client;
3189                if let Err(_) = sqlquery(&query)
3190                    .bind::<&str>(&serde_json::to_string(&status).unwrap())
3191                    .bind::<&str>(&uone.id)
3192                    .bind::<&str>(&utwo.id)
3193                    .execute(c)
3194                    .await
3195                {
3196                    return Err(DatabaseError::Other);
3197                };
3198
3199                self.base
3200                    .cache
3201                    .incr(format!("rbeam.app.friends_count:{}", uone.id))
3202                    .await;
3203
3204                self.base
3205                    .cache
3206                    .incr(format!("rbeam.app.friends_count:{}", utwo.id))
3207                    .await;
3208
3209                // create notification
3210                if !disable_notifications {
3211                    if let Err(_) = self
3212                        .create_notification(
3213                            NotificationCreate {
3214                                title: "Your friend request has been accepted!".to_string(),
3215                                content: format!(
3216                                    "[@{}](/@{}) has accepted your friend request.",
3217                                    utwo.username, utwo.username
3218                                ),
3219                                address: String::new(),
3220                                recipient: uone.id,
3221                            },
3222                            None,
3223                        )
3224                        .await
3225                    {
3226                        return Err(DatabaseError::Other);
3227                    };
3228                };
3229            }
3230            RelationshipStatus::Unknown => {
3231                // delete
3232                let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
3233                {
3234                    "DELETE FROM \"xrelationships\" WHERE \"one\" = ? AND \"two\" = ?"
3235                } else {
3236                    "DELETE FROM \"xrelationships\" WHERE \"one\" = ? AND \"two\" = ?"
3237                };
3238
3239                let c = &self.base.db.client;
3240                if let Err(_) = sqlquery(&query)
3241                    .bind::<&str>(&uone.id)
3242                    .bind::<&str>(&utwo.id)
3243                    .execute(c)
3244                    .await
3245                {
3246                    return Err(DatabaseError::Other);
3247                };
3248
3249                if relationship.0 == RelationshipStatus::Friends {
3250                    // decr friendship counts since we were previously friends but are not now
3251                    self.base
3252                        .cache
3253                        .decr(format!("rbeam.app.friends_count:{}", uone.id))
3254                        .await;
3255
3256                    self.base
3257                        .cache
3258                        .decr(format!("rbeam.app.friends_count:{}", utwo.id))
3259                        .await;
3260                }
3261            }
3262        }
3263
3264        // return
3265        Ok(())
3266    }
3267
3268    /// Get all relationships owned by `user` (ownership is the relationship's `one` field)
3269    ///
3270    /// # Arguments
3271    /// * `user`
3272    pub async fn get_user_relationships(
3273        &self,
3274        user: &str,
3275    ) -> Result<Vec<(Box<Profile>, RelationshipStatus)>> {
3276        // pull from database
3277        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
3278            "SELECT * FROM \"xrelationships\" WHERE \"one\" = ?"
3279        } else {
3280            "SELECT * FROM \"xrelationships\" WHERE \"one\" = $1"
3281        };
3282
3283        let c = &self.base.db.client;
3284        match sqlquery(&query).bind::<&str>(&user).fetch_all(c).await {
3285            Ok(p) => {
3286                let mut out = Vec::new();
3287
3288                for row in p {
3289                    let res = self.base.textify_row(row).0;
3290
3291                    // get profile
3292                    let profile = match self.get_profile(res.get("two").unwrap()).await {
3293                        Ok(c) => c,
3294                        Err(e) => return Err(e),
3295                    };
3296
3297                    // add to out
3298                    out.push((
3299                        profile,
3300                        serde_json::from_str(&res.get("status").unwrap()).unwrap(),
3301                    ));
3302                }
3303
3304                Ok(out)
3305            }
3306            Err(_) => return Err(DatabaseError::Other),
3307        }
3308    }
3309
3310    /// Get all relationships owned by `user` (ownership is the relationship's `one` field)
3311    ///
3312    /// # Arguments
3313    /// * `user`
3314    /// * `status`
3315    pub async fn get_user_relationships_of_status(
3316        &self,
3317        user: &str,
3318        status: RelationshipStatus,
3319    ) -> Result<Vec<(Box<Profile>, RelationshipStatus)>> {
3320        // pull from database
3321        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
3322            "SELECT * FROM \"xrelationships\" WHERE \"one\" = ? AND \"status\" = ?"
3323        } else {
3324            "SELECT * FROM \"xrelationships\" WHERE \"one\" = $1 AND \"status\" = $2"
3325        };
3326
3327        let c = &self.base.db.client;
3328        match sqlquery(&query)
3329            .bind::<&str>(&user)
3330            .bind::<&str>(&serde_json::to_string(&status).unwrap())
3331            .fetch_all(c)
3332            .await
3333        {
3334            Ok(p) => {
3335                let mut out = Vec::new();
3336
3337                for row in p {
3338                    let res = self.base.textify_row(row).0;
3339
3340                    // get profile
3341                    let profile = match self.get_profile(res.get("two").unwrap()).await {
3342                        Ok(c) => c,
3343                        Err(_) => continue,
3344                    };
3345
3346                    // add to out
3347                    out.push((
3348                        profile,
3349                        serde_json::from_str(&res.get("status").unwrap()).unwrap(),
3350                    ));
3351                }
3352
3353                Ok(out)
3354            }
3355            Err(_) => return Err(DatabaseError::Other),
3356        }
3357    }
3358
3359    /// Get all relationships where `user` is either `one` or `two` and the status is `status`
3360    ///
3361    /// # Arguments
3362    /// * `user`
3363    /// * `status`
3364    pub async fn get_user_participating_relationships_of_status(
3365        &self,
3366        user: &str,
3367        status: RelationshipStatus,
3368    ) -> Result<Vec<(Box<Profile>, Box<Profile>)>> {
3369        // pull from database
3370        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
3371            "SELECT * FROM \"xrelationships\" WHERE (\"one\" = ? OR \"two\" = ?) AND \"status\" = ?"
3372        } else {
3373            "SELECT * FROM \"xrelationships\" WHERE (\"one\" = $1 OR \"two\" = $2) AND \"status\" = $3"
3374        };
3375
3376        let c = &self.base.db.client;
3377        match sqlquery(&query)
3378            .bind::<&str>(&user)
3379            .bind::<&str>(&user)
3380            .bind::<&str>(&serde_json::to_string(&status).unwrap())
3381            .fetch_all(c)
3382            .await
3383        {
3384            Ok(p) => {
3385                let mut out = Vec::new();
3386
3387                for row in p {
3388                    let res = self.base.textify_row(row).0;
3389
3390                    // get profiles
3391                    let profile = match self.get_profile(res.get("one").unwrap()).await {
3392                        Ok(c) => c,
3393                        Err(_) => continue,
3394                    };
3395
3396                    let profile_2 = match self.get_profile(res.get("two").unwrap()).await {
3397                        Ok(c) => c,
3398                        Err(_) => continue,
3399                    };
3400
3401                    // add to out
3402                    out.push((profile, profile_2));
3403                }
3404
3405                Ok(out)
3406            }
3407            Err(_) => return Err(DatabaseError::Other),
3408        }
3409    }
3410
3411    /// Get all relationships where `user` is either `one` or `two` and the status is `status`, 12 at a time
3412    ///
3413    /// # Arguments
3414    /// * `user`
3415    /// * `status`
3416    /// * `page`
3417    pub async fn get_user_participating_relationships_of_status_paginated(
3418        &self,
3419        user: &str,
3420        status: RelationshipStatus,
3421        page: i32,
3422    ) -> Result<Vec<(Box<Profile>, Box<Profile>)>> {
3423        // pull from database
3424        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
3425            format!("SELECT * FROM \"xrelationships\" WHERE (\"one\" = ? OR \"two\" = ?) AND \"status\" = ? ORDER BY \"timestamp\" DESC LIMIT 12 OFFSET {}", page * 12)
3426        } else {
3427            format!("SELECT * FROM \"xrelationships\" WHERE (\"one\" = $1 OR \"two\" = $2) AND \"status\" = $3 ORDER BY \"timestamp\" DESC LIMIT 12 OFFSET {}", page * 12)
3428        };
3429
3430        let c = &self.base.db.client;
3431        match sqlquery(&query)
3432            .bind::<&str>(&user)
3433            .bind::<&str>(&user)
3434            .bind::<&str>(&serde_json::to_string(&status).unwrap())
3435            .fetch_all(c)
3436            .await
3437        {
3438            Ok(p) => {
3439                let mut out = Vec::new();
3440
3441                for row in p {
3442                    let res = self.base.textify_row(row).0;
3443
3444                    // get profiles
3445                    let profile = match self.get_profile(res.get("one").unwrap()).await {
3446                        Ok(c) => c,
3447                        Err(e) => return Err(e),
3448                    };
3449
3450                    let profile_2 = match self.get_profile(res.get("two").unwrap()).await {
3451                        Ok(c) => c,
3452                        Err(e) => return Err(e),
3453                    };
3454
3455                    // add to out
3456                    out.push((profile, profile_2));
3457                }
3458
3459                Ok(out)
3460            }
3461            Err(_) => return Err(DatabaseError::Other),
3462        }
3463    }
3464
3465    /// Get the number of friends a user has
3466    ///
3467    /// # Arguments
3468    /// * `id`
3469    pub async fn get_friendship_count_by_user(&self, id: &str) -> usize {
3470        // attempt to fetch from cache
3471        if let Some(count) = self
3472            .base
3473            .cache
3474            .get(format!("rbeam.app.friends_count:{}", id))
3475            .await
3476        {
3477            return count.parse::<usize>().unwrap_or(0);
3478        };
3479
3480        // fetch from database
3481        let count = self
3482            .get_user_participating_relationships_of_status(id, RelationshipStatus::Friends)
3483            .await
3484            .unwrap_or(Vec::new())
3485            .len();
3486
3487        self.base
3488            .cache
3489            .set(format!("rbeam.app.friends_count:{}", id), count.to_string())
3490            .await;
3491
3492        count
3493    }
3494
3495    // ip blocks
3496
3497    // GET
3498    /// Get an existing [`IpBlock`]
3499    ///
3500    /// # Arguments
3501    /// * `id`
3502    pub async fn get_ipblock(&self, id: &str) -> Result<IpBlock> {
3503        // check in cache
3504        match self
3505            .base
3506            .cache
3507            .get(format!("rbeam.auth.ipblock:{}", id))
3508            .await
3509        {
3510            Some(c) => return Ok(serde_json::from_str::<IpBlock>(c.as_str()).unwrap()),
3511            None => (),
3512        };
3513
3514        // pull from database
3515        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
3516            "SELECT * FROM \"xipblocks\" WHERE \"id\" = ?"
3517        } else {
3518            "SELECT * FROM \"xipblocks\" WHERE \"id\" = $1"
3519        };
3520
3521        let c = &self.base.db.client;
3522        let res = match sqlquery(&query).bind::<&str>(&id).fetch_one(c).await {
3523            Ok(p) => self.base.textify_row(p).0,
3524            Err(_) => return Err(DatabaseError::NotFound),
3525        };
3526
3527        // return
3528        let block = IpBlock {
3529            id: from_row!(res->id()),
3530            ip: from_row!(res->ip()),
3531            user: from_row!(res->user()),
3532            context: from_row!(res->context()),
3533            timestamp: from_row!(res->timestamp(u128); 0),
3534        };
3535
3536        // store in cache
3537        self.base
3538            .cache
3539            .set(
3540                format!("rbeam.auth.ipblock:{}", id),
3541                serde_json::to_string::<IpBlock>(&block).unwrap(),
3542            )
3543            .await;
3544
3545        // return
3546        Ok(block)
3547    }
3548
3549    /// Get an existing [`IpBlock`] by its IP and its `user`
3550    ///
3551    /// # Arguments
3552    /// * `ip`
3553    /// * `user`
3554    pub async fn get_ipblock_by_ip(&self, ip: &str, user: &str) -> Result<IpBlock> {
3555        // pull from database
3556        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
3557            "SELECT * FROM \"xipblocks\" WHERE \"ip\" = ? AND \"user\" = ?"
3558        } else {
3559            "SELECT * FROM \"xipblocks\" WHERE \"ip\" = $1 AND \"user\" = $2"
3560        };
3561
3562        let c = &self.base.db.client;
3563        let res = match sqlquery(&query)
3564            .bind::<&str>(&ip)
3565            .bind::<&str>(&user)
3566            .fetch_one(c)
3567            .await
3568        {
3569            Ok(p) => self.base.textify_row(p).0,
3570            Err(_) => return Err(DatabaseError::NotFound),
3571        };
3572
3573        // return
3574        let block = IpBlock {
3575            id: from_row!(res->id()),
3576            ip: from_row!(res->ip()),
3577            user: from_row!(res->user()),
3578            context: from_row!(res->context()),
3579            timestamp: from_row!(res->timestamp(u128); 0),
3580        };
3581
3582        // return
3583        Ok(block)
3584    }
3585
3586    /// Get all [`IpBlocks`]s for the given `query_user`
3587    ///
3588    /// # Arguments
3589    /// * `query_user` - the ID of the user the blocks belong to
3590    pub async fn get_ipblocks(&self, query_user: &str) -> Result<Vec<IpBlock>> {
3591        // pull from database
3592        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
3593            "SELECT * FROM \"xipblocks\" WHERE \"user\" = ? ORDER BY \"timestamp\" DESC"
3594        } else {
3595            "SELECT * FROM \"xipblocks\" WHERE \"user\" = $1 ORDER BY \"timestamp\" DESC"
3596        };
3597
3598        let c = &self.base.db.client;
3599        let res = match sqlquery(&query)
3600            .bind::<&str>(&query_user)
3601            .fetch_all(c)
3602            .await
3603        {
3604            Ok(p) => {
3605                let mut out: Vec<IpBlock> = Vec::new();
3606
3607                for row in p {
3608                    let res = self.base.textify_row(row).0;
3609                    out.push(IpBlock {
3610                        id: from_row!(res->id()),
3611                        ip: from_row!(res->ip()),
3612                        user: from_row!(res->user()),
3613                        context: from_row!(res->context()),
3614                        timestamp: from_row!(res->timestamp(u128); 0),
3615                    });
3616                }
3617
3618                out
3619            }
3620            Err(_) => return Err(DatabaseError::NotFound),
3621        };
3622
3623        // return
3624        Ok(res)
3625    }
3626
3627    // SET
3628    /// Create a new [`IpBlock`]
3629    ///
3630    /// # Arguments
3631    /// * `props` - [`IpBlockCreate`]
3632    /// * `user` - the user creating this block
3633    pub async fn create_ipblock(&self, props: IpBlockCreate, user: Box<Profile>) -> Result<()> {
3634        // make sure this ip isn't already banned
3635        if self.get_ipblock_by_ip(&props.ip, &user.id).await.is_ok() {
3636            return Err(DatabaseError::MustBeUnique);
3637        }
3638
3639        // ...
3640        let block = IpBlock {
3641            id: utility::random_id(),
3642            ip: props.ip,
3643            user: user.id,
3644            context: props.context,
3645            timestamp: utility::unix_epoch_timestamp(),
3646        };
3647
3648        // create notification
3649        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
3650            "INSERT INTO \"xipblocks\" VALUES (?, ?, ?, ?, ?)"
3651        } else {
3652            "INSERT INTO \"xipblocks\" VALUES ($1, $2, $3, $4, $5)"
3653        };
3654
3655        let c = &self.base.db.client;
3656        match sqlquery(&query)
3657            .bind::<&str>(&block.id)
3658            .bind::<&str>(&block.ip)
3659            .bind::<&str>(&block.user)
3660            .bind::<&str>(&block.context)
3661            .bind::<&str>(&block.timestamp.to_string())
3662            .execute(c)
3663            .await
3664        {
3665            Ok(_) => return Ok(()),
3666            Err(_) => return Err(DatabaseError::Other),
3667        };
3668    }
3669
3670    /// Delete an existing IpBlock
3671    ///
3672    /// # Arguments
3673    /// * `id` - the ID of the block
3674    /// * `user` - the user doing this
3675    pub async fn delete_ipblock(&self, id: &str, user: Box<Profile>) -> Result<()> {
3676        // make sure block exists
3677        let block = match self.get_ipblock(id).await {
3678            Ok(n) => n,
3679            Err(e) => return Err(e),
3680        };
3681
3682        // check id
3683        if user.id != block.user {
3684            // check permission
3685            let group = match self.get_group_by_id(user.group).await {
3686                Ok(g) => g,
3687                Err(_) => return Err(DatabaseError::Other),
3688            };
3689
3690            if !group.permissions.check(FinePermission::UNBAN_IP) {
3691                return Err(DatabaseError::NotAllowed);
3692            } else {
3693                let actor_id = user.id.clone();
3694                if let Err(e) = self
3695                    .create_notification(
3696                        NotificationCreate {
3697                            title: format!("[{actor_id}](/+u/{actor_id})"),
3698                            content: format!("Unblocked an IP: {}", block.ip),
3699                            address: format!("/+u/{actor_id}"),
3700                            recipient: "*(audit)".to_string(), // all staff, audit
3701                        },
3702                        None,
3703                    )
3704                    .await
3705                {
3706                    return Err(e);
3707                }
3708            }
3709        }
3710
3711        // delete block
3712        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
3713            "DELETE FROM \"xipblocks\" WHERE \"id\" = ?"
3714        } else {
3715            "DELETE FROM \"xipblocks\" WHERE \"id\" = $1"
3716        };
3717
3718        let c = &self.base.db.client;
3719        match sqlquery(&query).bind::<&str>(&id).execute(c).await {
3720            Ok(_) => {
3721                // remove from cache
3722                self.base
3723                    .cache
3724                    .remove(format!("rbeam.auth.ipblock:{}", id))
3725                    .await;
3726
3727                // return
3728                return Ok(());
3729            }
3730            Err(_) => return Err(DatabaseError::Other),
3731        };
3732    }
3733
3734    // labels
3735
3736    /// Get a [`UserLabel`] from a database result
3737    pub async fn gimme_label(&self, res: BTreeMap<String, String>) -> Result<UserLabel> {
3738        Ok(UserLabel {
3739            id: from_row!(res->id(i64); 0),
3740            name: from_row!(res->name()),
3741            timestamp: from_row!(res->timestamp(u128); 0),
3742            creator: from_row!(res->creator()),
3743        })
3744    }
3745
3746    /// Get an existing label
3747    ///
3748    /// # Arguments
3749    /// * `id`
3750    pub async fn get_label(&self, id: i64) -> Result<UserLabel> {
3751        // check in cache
3752        match self
3753            .base
3754            .cache
3755            .get(format!("rbeam.auth.label:{}", id))
3756            .await
3757        {
3758            Some(c) => match serde_json::from_str::<UserLabel>(c.as_str()) {
3759                Ok(c) => return Ok(c),
3760                Err(_) => {
3761                    self.base
3762                        .cache
3763                        .remove(format!("rbeam.auth.label:{}", id))
3764                        .await;
3765                }
3766            },
3767            None => (),
3768        };
3769
3770        // pull from database
3771        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
3772            "SELECT * FROM \"xlabels\" WHERE \"id\" = ?"
3773        } else {
3774            "SELECT * FROM \"xlabels\" WHERE \"id\" = $1"
3775        };
3776
3777        let c = &self.base.db.client;
3778        let res = match sqlquery(&query).bind::<i64>(id).fetch_one(c).await {
3779            Ok(p) => self.base.textify_row(p).0,
3780            Err(_) => return Err(DatabaseError::NotFound),
3781        };
3782
3783        // return
3784        let label = match self.gimme_label(res).await {
3785            Ok(l) => l,
3786            Err(e) => return Err(e),
3787        };
3788
3789        // store in cache
3790        self.base
3791            .cache
3792            .set(
3793                format!("rbeam.auth.label:{}", id),
3794                serde_json::to_string::<UserLabel>(&label).unwrap(),
3795            )
3796            .await;
3797
3798        // return
3799        Ok(label)
3800    }
3801
3802    /// Create a new user label
3803    ///
3804    /// # Arguments
3805    /// * `name` - the name of the label
3806    /// * `id` - the ID of the label
3807    /// * `author` - the ID of the user creating the label
3808    pub async fn create_label(&self, name: &str, id: i64, author: &str) -> Result<UserLabel> {
3809        // check author permissions
3810        let author = match self.get_profile(author).await {
3811            Ok(ua) => ua,
3812            Err(e) => return Err(e),
3813        };
3814
3815        let group = match self.get_group_by_id(author.group).await {
3816            Ok(g) => g,
3817            Err(e) => return Err(e),
3818        };
3819
3820        if !group.permissions.check(FinePermission::CREATE_LABEL) {
3821            return Err(DatabaseError::NotAllowed);
3822        }
3823
3824        // check name length
3825        if name.len() < 2 {
3826            return Err(DatabaseError::Other);
3827        }
3828
3829        if name.len() > 32 {
3830            return Err(DatabaseError::Other);
3831        }
3832
3833        // ...
3834        let label = UserLabel {
3835            // id: utility::random_id(),
3836            id,
3837            name: name.to_string(),
3838            timestamp: utility::unix_epoch_timestamp(),
3839            creator: author.id,
3840        };
3841
3842        // create label
3843        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
3844            "INSERT INTO \"xlabels\" VALUES (?, ?, ?, ?)"
3845        } else {
3846            "INSERT INTO \"xlabels\" VALUES ($1, $2, $3, $4)"
3847        };
3848
3849        let c = &self.base.db.client;
3850        match sqlquery(&query)
3851            .bind::<i64>(label.id)
3852            .bind::<&str>(&label.name)
3853            .bind::<&str>(&label.timestamp.to_string())
3854            .bind::<&str>(&label.creator)
3855            .execute(c)
3856            .await
3857        {
3858            Ok(_) => Ok(label),
3859            Err(_) => Err(DatabaseError::Other),
3860        }
3861    }
3862
3863    /// Create a new user label
3864    ///
3865    /// # Arguments
3866    /// * `id` - the ID of the label
3867    /// * `author` - the ID of the user creating the label
3868    pub async fn delete_label(&self, id: i64, author: Box<Profile>) -> Result<()> {
3869        // check author permissions
3870        let group = match self.get_group_by_id(author.group).await {
3871            Ok(g) => g,
3872            Err(e) => return Err(e),
3873        };
3874
3875        if !group.permissions.check(FinePermission::MANAGE_LABELS) {
3876            return Err(DatabaseError::NotAllowed);
3877        }
3878
3879        // delete label
3880        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
3881            "DELETE FROM \"xlabels\" WHERE \"id\" = ?"
3882        } else {
3883            "DELETE FROM \"xlabels\" WHERE \"id\" = $1"
3884        };
3885
3886        let c = &self.base.db.client;
3887        let res = match sqlquery(&query).bind::<i64>(id).execute(c).await {
3888            Ok(_) => Ok(()),
3889            Err(_) => Err(DatabaseError::Other),
3890        };
3891
3892        self.base
3893            .cache
3894            .remove(format!("rbeam.auth.label:{}", id))
3895            .await;
3896
3897        res
3898    }
3899
3900    // ugc transactions
3901
3902    // Get item given the `row` data
3903    pub async fn gimme_transaction(
3904        &self,
3905        row: BTreeMap<String, String>,
3906    ) -> Result<(Transaction, Option<Item>)> {
3907        let item = row.get("item").unwrap();
3908
3909        Ok((
3910            Transaction {
3911                id: from_row!(row->id()),
3912                amount: row.get("amount").unwrap().parse::<i32>().unwrap_or(0),
3913                item: item.clone(),
3914                timestamp: row.get("timestamp").unwrap().parse::<u128>().unwrap(),
3915                customer: from_row!(row->customer()),
3916                merchant: from_row!(row->merchant()),
3917            },
3918            match self.get_item(item).await {
3919                Ok(i) => Some(i),
3920                Err(_) => None,
3921            },
3922        ))
3923    }
3924
3925    // GET
3926    /// Get an existing transaction
3927    ///
3928    /// # Arguments
3929    /// * `id`
3930    pub async fn get_transaction(&self, id: &str) -> Result<(Transaction, Option<Item>)> {
3931        // check in cache
3932        match self
3933            .base
3934            .cache
3935            .get(format!("rbeam.auth.econ.transaction:{}", id))
3936            .await
3937        {
3938            Some(c) => match serde_json::from_str::<BTreeMap<String, String>>(c.as_str()) {
3939                Ok(c) => {
3940                    return Ok(match self.gimme_transaction(c).await {
3941                        Ok(t) => t,
3942                        Err(e) => return Err(e),
3943                    })
3944                }
3945                Err(_) => {
3946                    self.base
3947                        .cache
3948                        .remove(format!("rbeam.auth.econ.transaction:{}", id))
3949                        .await;
3950                }
3951            },
3952            None => (),
3953        };
3954
3955        // pull from database
3956        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
3957            "SELECT * FROM \"xugc_transactions\" WHERE \"id\" = ?"
3958        } else {
3959            "SELECT * FROM \"xugc_transactions\" WHERE \"id\" = $1"
3960        };
3961
3962        let c = &self.base.db.client;
3963        let res = match sqlquery(&query).bind::<&str>(&id).fetch_one(c).await {
3964            Ok(p) => self.base.textify_row(p).0,
3965            Err(_) => return Err(DatabaseError::NotFound),
3966        };
3967
3968        // return
3969        let transaction = match self.gimme_transaction(res).await {
3970            Ok(t) => t,
3971            Err(e) => return Err(e),
3972        };
3973
3974        // store in cache
3975        self.base
3976            .cache
3977            .set(
3978                format!("rbeam.auth.econ.transaction:{}", id),
3979                serde_json::to_string::<Transaction>(&transaction.0).unwrap(),
3980            )
3981            .await;
3982
3983        // return
3984        Ok(transaction)
3985    }
3986
3987    /// Get an existing transaction given the `customer` and the `item`
3988    ///
3989    /// # Arguments
3990    /// * `user`
3991    /// * `item`
3992    pub async fn get_transaction_by_customer_item(
3993        &self,
3994        customer: &str,
3995        item: &str,
3996    ) -> Result<(Transaction, Option<Item>)> {
3997        // pull from database
3998        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
3999            "SELECT * FROM \"xugc_transactions\" WHERE \"customer\" = ? AND \"item\" = ?"
4000        } else {
4001            "SELECT * FROM \"xugc_transactions\" WHERE \"customer\" = $1 AND \"item\" = $2"
4002        };
4003
4004        let c = &self.base.db.client;
4005        let res = match sqlquery(&query)
4006            .bind::<&str>(&customer)
4007            .bind::<&str>(&item)
4008            .fetch_one(c)
4009            .await
4010        {
4011            Ok(p) => self.base.textify_row(p).0,
4012            Err(_) => return Err(DatabaseError::NotFound),
4013        };
4014
4015        // return
4016        let transaction = match self.gimme_transaction(res).await {
4017            Ok(t) => t,
4018            Err(e) => return Err(e),
4019        };
4020
4021        // return
4022        Ok(transaction)
4023    }
4024
4025    /// Get all transactions where the customer is the given user ID, 12 at a time
4026    ///
4027    /// # Arguments
4028    /// * `user`
4029    /// * `page`
4030    ///
4031    /// # Returns
4032    /// `Vec<(Transaction, Customer, Merchant)>`
4033    pub async fn get_transactions_by_customer_paginated(
4034        &self,
4035        user: &str,
4036        page: i32,
4037    ) -> Result<Vec<((Transaction, Option<Item>), Box<Profile>, Box<Profile>)>> {
4038        // pull from database
4039        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
4040            format!("SELECT * FROM \"xugc_transactions\" WHERE \"customer\" = ? ORDER BY \"timestamp\" DESC LIMIT 12 OFFSET {}", page * 12)
4041        } else {
4042            format!("SELECT * FROM \"xugc_transactions\" WHERE \"customer\" = $1 ORDER BY \"timestamp\" DESC LIMIT 12 OFFSET {}", page * 12)
4043        };
4044
4045        let c = &self.base.db.client;
4046        let res = match sqlquery(&query)
4047            .bind::<&str>(&user)
4048            .bind::<&str>(&user)
4049            .fetch_all(c)
4050            .await
4051        {
4052            Ok(p) => {
4053                let mut out = Vec::new();
4054
4055                for row in p {
4056                    let res = self.base.textify_row(row).0;
4057                    let transaction = match self.gimme_transaction(res).await {
4058                        Ok(t) => t,
4059                        Err(e) => return Err(e),
4060                    };
4061
4062                    let customer = transaction.0.customer.clone();
4063                    let merchant = transaction.0.merchant.clone();
4064
4065                    out.push((
4066                        transaction.clone(),
4067                        match self.get_profile(&customer).await {
4068                            Ok(ua) => ua,
4069                            Err(_) => continue,
4070                        },
4071                        match self.get_profile(&merchant).await {
4072                            Ok(ua) => ua,
4073                            Err(_) => continue,
4074                        },
4075                    ));
4076                }
4077
4078                out
4079            }
4080            Err(_) => return Err(DatabaseError::Other),
4081        };
4082
4083        // return
4084        Ok(res)
4085    }
4086
4087    /// Get all transactions by the given user ID, 12 at a time
4088    ///
4089    /// # Arguments
4090    /// * `user`
4091    /// * `page`
4092    ///
4093    /// # Returns
4094    /// `Vec<(Transaction, Customer, Merchant)>`
4095    pub async fn get_participating_transactions_paginated(
4096        &self,
4097        user: &str,
4098        page: i32,
4099    ) -> Result<Vec<((Transaction, Option<Item>), Box<Profile>, Box<Profile>)>> {
4100        // pull from database
4101        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
4102            format!("SELECT * FROM \"xugc_transactions\" WHERE \"customer\" = ? OR \"merchant\" = ? ORDER BY \"timestamp\" DESC LIMIT 12 OFFSET {}", page * 12)
4103        } else {
4104            format!("SELECT * FROM \"xugc_transactions\" WHERE \"customer\" = $1 OR \"merchant\" = $2 ORDER BY \"timestamp\" DESC LIMIT 12 OFFSET {}", page * 12)
4105        };
4106
4107        let c = &self.base.db.client;
4108        let res = match sqlquery(&query)
4109            .bind::<&str>(&user)
4110            .bind::<&str>(&user)
4111            .fetch_all(c)
4112            .await
4113        {
4114            Ok(p) => {
4115                let mut out = Vec::new();
4116
4117                for row in p {
4118                    let res = self.base.textify_row(row).0;
4119                    let transaction = match self.gimme_transaction(res).await {
4120                        Ok(t) => t,
4121                        Err(e) => return Err(e),
4122                    };
4123
4124                    let customer = transaction.0.customer.clone();
4125                    let merchant = transaction.0.merchant.clone();
4126
4127                    out.push((
4128                        transaction.clone(),
4129                        match self.get_profile(&customer).await {
4130                            Ok(ua) => ua,
4131                            Err(_) => continue,
4132                        },
4133                        match self.get_profile(&merchant).await {
4134                            Ok(ua) => ua,
4135                            Err(_) => continue,
4136                        },
4137                    ));
4138                }
4139
4140                out
4141            }
4142            Err(_) => return Err(DatabaseError::Other),
4143        };
4144
4145        // return
4146        Ok(res)
4147    }
4148
4149    // SET
4150    /// Create a new transaction
4151    ///
4152    /// # Arguments
4153    /// * `props` - [`TransactionCreate`]
4154    /// * `customer` - the user in the `customer` field of the transaction
4155    pub async fn create_transaction(
4156        &self,
4157        props: TransactionCreate,
4158        customer: &str,
4159    ) -> Result<Transaction> {
4160        // make sure customer and merchant exist
4161        let customer = match self.get_profile(customer).await {
4162            Ok(ua) => ua,
4163            Err(e) => return Err(e),
4164        };
4165
4166        let merchant = match self.get_profile(&props.merchant).await {
4167            Ok(ua) => ua,
4168            Err(e) => return Err(e),
4169        };
4170
4171        // make sure customer can afford this
4172        if props.amount.is_negative() {
4173            if (customer.coins + props.amount) < 0 {
4174                return Err(DatabaseError::TooExpensive);
4175            }
4176        }
4177
4178        // ...
4179        let transaction = Transaction {
4180            // id: utility::random_id(),
4181            id: AlmostSnowflake::new(self.config.snowflake_server_id)
4182                .to_string()
4183                .to_string(),
4184            amount: props.amount,
4185            item: props.item,
4186            timestamp: utility::unix_epoch_timestamp(),
4187            customer: customer.id.clone(),
4188            merchant: merchant.id.clone(),
4189        };
4190
4191        // create transaction
4192        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
4193            "INSERT INTO \"xugc_transactions\" VALUES (?, ?, ?, ?, ?, ?)"
4194        } else {
4195            "INSERT INTO \"xugc_transactions\" VALUES ($1, $2, $3, $4, $5, $6)"
4196        };
4197
4198        let c = &self.base.db.client;
4199        match sqlquery(&query)
4200            .bind::<&str>(&transaction.id)
4201            .bind::<i32>(transaction.amount)
4202            .bind::<&str>(&transaction.item)
4203            .bind::<&str>(&transaction.timestamp.to_string())
4204            .bind::<&str>(&transaction.customer)
4205            .bind::<&str>(&transaction.merchant)
4206            .execute(c)
4207            .await
4208        {
4209            Ok(_) => {
4210                // update balances
4211                if let Err(e) = self
4212                    .update_profile_coins(&customer.id, transaction.amount)
4213                    .await
4214                {
4215                    return Err(e);
4216                };
4217
4218                if let Err(e) = self
4219                    .update_profile_coins(&merchant.id, transaction.amount.abs())
4220                    .await
4221                {
4222                    return Err(e);
4223                };
4224
4225                // send notification
4226                if (customer.id != merchant.id) && (merchant.id != "0") {
4227                    if let Err(e) = self
4228                        .create_notification(
4229                            NotificationCreate {
4230                                title: "Purchased data now available!".to_string(),
4231                                content: "Data from an item you purchased is now available."
4232                                    .to_string(),
4233                                address: format!(
4234                                    "/market/item/{}#/preview",
4235                                    transaction.item.clone()
4236                                ),
4237                                recipient: customer.id,
4238                            },
4239                            None,
4240                        )
4241                        .await
4242                    {
4243                        return Err(e);
4244                    }
4245                }
4246
4247                // ...
4248                return Ok(transaction);
4249            }
4250            Err(_) => return Err(DatabaseError::Other),
4251        };
4252    }
4253
4254    // ugc items
4255
4256    // Get transaction given the `row` data
4257    pub fn gimme_item(&self, row: BTreeMap<String, String>) -> Result<Item> {
4258        Ok(Item {
4259            id: from_row!(row->id()),
4260            name: from_row!(row->name()),
4261            description: from_row!(row->description()),
4262            cost: row.get("cost").unwrap().parse::<i32>().unwrap_or(0),
4263            content: from_row!(row->content()),
4264            r#type: match serde_json::from_str(row.get("type").unwrap()) {
4265                Ok(v) => v,
4266                Err(_) => return Err(DatabaseError::ValueError),
4267            },
4268            status: match serde_json::from_str(row.get("status").unwrap()) {
4269                Ok(v) => v,
4270                Err(_) => return Err(DatabaseError::ValueError),
4271            },
4272            timestamp: row.get("timestamp").unwrap().parse::<u128>().unwrap(),
4273            creator: from_row!(row->creator()),
4274        })
4275    }
4276
4277    // GET
4278    /// Get an existing item
4279    ///
4280    /// # Arguments
4281    /// * `id`
4282    pub async fn get_item(&self, id: &str) -> Result<Item> {
4283        if id == "0" {
4284            // this item can be charged for things that don't relate to an item
4285            return Ok(Item {
4286                id: "0".to_string(),
4287                name: "System cost".to_string(),
4288                description: String::new(),
4289                cost: -1,
4290                content: String::new(),
4291                r#type: ItemType::Text,
4292                status: ItemStatus::Approved,
4293                timestamp: 0,
4294                creator: "0".to_string(),
4295            });
4296        }
4297
4298        // check in cache
4299        match self
4300            .base
4301            .cache
4302            .get(format!("rbeam.auth.econ.item:{}", id))
4303            .await
4304        {
4305            Some(c) => match serde_json::from_str::<Item>(c.as_str()) {
4306                Ok(c) => return Ok(c),
4307                Err(_) => {
4308                    self.base
4309                        .cache
4310                        .remove(format!("rbeam.auth.econ.item:{}", id))
4311                        .await;
4312                }
4313            },
4314            None => (),
4315        };
4316
4317        // pull from database
4318        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
4319            "SELECT * FROM \"xugc_items\" WHERE \"id\" = ?"
4320        } else {
4321            "SELECT * FROM \"xugc_items\" WHERE \"id\" = $1"
4322        };
4323
4324        let c = &self.base.db.client;
4325        let res = match sqlquery(&query).bind::<&str>(&id).fetch_one(c).await {
4326            Ok(p) => self.base.textify_row(p).0,
4327            Err(_) => return Err(DatabaseError::NotFound),
4328        };
4329
4330        // return
4331        let item = match self.gimme_item(res) {
4332            Ok(t) => t,
4333            Err(e) => return Err(e),
4334        };
4335
4336        // store in cache
4337        self.base
4338            .cache
4339            .set(
4340                format!("rbeam.auth.econ.item:{}", id),
4341                serde_json::to_string::<Item>(&item).unwrap(),
4342            )
4343            .await;
4344
4345        // return
4346        Ok(item)
4347    }
4348
4349    /// Get all items by their creator, 12 at a time
4350    ///
4351    /// # Arguments
4352    /// * `user`
4353    /// * `page`
4354    ///
4355    /// # Returns
4356    /// `Vec<(Item, Box<Profile>)>`
4357    pub async fn get_items_by_creator_paginated(
4358        &self,
4359        user: &str,
4360        page: i32,
4361    ) -> Result<Vec<(Item, Box<Profile>)>> {
4362        // pull from database
4363        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
4364            format!("SELECT * FROM \"xugc_items\" WHERE \"creator\" = ? ORDER BY \"timestamp\" DESC LIMIT 12 OFFSET {}", page * 12)
4365        } else {
4366            format!("SELECT * FROM \"xugc_items\" WHERE \"creator\" = $1 ORDER BY \"timestamp\" DESC LIMIT 12 OFFSET {}", page * 12)
4367        };
4368
4369        let c = &self.base.db.client;
4370        let res = match sqlquery(&query).bind::<&str>(&user).fetch_all(c).await {
4371            Ok(p) => {
4372                let mut out = Vec::new();
4373
4374                for row in p {
4375                    let res = self.base.textify_row(row).0;
4376                    let item = match self.gimme_item(res) {
4377                        Ok(t) => t,
4378                        Err(e) => return Err(e),
4379                    };
4380
4381                    let creator = item.creator.clone();
4382
4383                    out.push((
4384                        item,
4385                        match self.get_profile(&creator).await {
4386                            Ok(ua) => ua,
4387                            Err(_) => continue,
4388                        },
4389                    ));
4390                }
4391
4392                out
4393            }
4394            Err(_) => return Err(DatabaseError::Other),
4395        };
4396
4397        // return
4398        Ok(res)
4399    }
4400
4401    /// Get all items by their creator and type, 12 at a time
4402    ///
4403    /// # Arguments
4404    /// * `user`
4405    /// * `type`
4406    /// * `page`
4407    ///
4408    /// # Returns
4409    /// `Vec<(Item, Box<Profile>)>`
4410    pub async fn get_items_by_creator_type_paginated(
4411        &self,
4412        user: &str,
4413        r#type: ItemType,
4414        page: i32,
4415    ) -> Result<Vec<(Item, Box<Profile>)>> {
4416        // pull from database
4417        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
4418            format!("SELECT * FROM \"xugc_items\" WHERE \"creator\" = ? AND \"type\" = ? ORDER BY \"timestamp\" DESC LIMIT 12 OFFSET {}", page * 12)
4419        } else {
4420            format!("SELECT * FROM \"xugc_items\" WHERE \"creator\" = $1 AND \"type\" = $2 ORDER BY \"timestamp\" DESC LIMIT 12 OFFSET {}", page * 12)
4421        };
4422
4423        let c = &self.base.db.client;
4424        let res = match sqlquery(&query)
4425            .bind::<&str>(&user)
4426            .bind::<&str>(&serde_json::to_string(&r#type).unwrap())
4427            .fetch_all(c)
4428            .await
4429        {
4430            Ok(p) => {
4431                let mut out = Vec::new();
4432
4433                for row in p {
4434                    let res = self.base.textify_row(row).0;
4435                    let item = match self.gimme_item(res) {
4436                        Ok(t) => t,
4437                        Err(e) => return Err(e),
4438                    };
4439
4440                    let creator = item.creator.clone();
4441
4442                    out.push((
4443                        item,
4444                        match self.get_profile(&creator).await {
4445                            Ok(ua) => ua,
4446                            Err(_) => continue,
4447                        },
4448                    ));
4449                }
4450
4451                out
4452            }
4453            Err(_) => return Err(DatabaseError::Other),
4454        };
4455
4456        // return
4457        Ok(res)
4458    }
4459
4460    /// Get all items by their status, 12 at a time
4461    ///
4462    /// # Arguments
4463    /// * `user`
4464    /// * `page`
4465    ///
4466    /// # Returns
4467    /// `Vec<(Item, Box<Profile>)>`
4468    pub async fn get_items_by_status_searched_paginated(
4469        &self,
4470        status: ItemStatus,
4471        page: i32,
4472        search: &str,
4473    ) -> Result<Vec<(Item, Box<Profile>)>> {
4474        // pull from database
4475        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
4476            format!("SELECT * FROM \"xugc_items\" WHERE \"status\" = ? AND \"name\" LIKE ? AND \"cost\" != '-1' ORDER BY \"timestamp\" DESC LIMIT 12 OFFSET {}", page * 12)
4477        } else {
4478            format!("SELECT * FROM \"xugc_items\" WHERE \"status\" = $1 AND \"name\" LIKE $2 AND \"cost\" != '-1' ORDER BY \"timestamp\" DESC LIMIT 12 OFFSET {}", page * 12)
4479        };
4480
4481        let c = &self.base.db.client;
4482        let res = match sqlquery(&query)
4483            .bind::<&str>(&serde_json::to_string(&status).unwrap())
4484            .bind::<&str>(&format!("%{search}%"))
4485            .fetch_all(c)
4486            .await
4487        {
4488            Ok(p) => {
4489                let mut out = Vec::new();
4490
4491                for row in p {
4492                    let res = self.base.textify_row(row).0;
4493                    let item = match self.gimme_item(res) {
4494                        Ok(t) => t,
4495                        Err(e) => return Err(e),
4496                    };
4497
4498                    let creator = item.creator.clone();
4499
4500                    out.push((
4501                        item,
4502                        match self.get_profile(&creator).await {
4503                            Ok(ua) => ua,
4504                            Err(_) => continue,
4505                        },
4506                    ));
4507                }
4508
4509                out
4510            }
4511            Err(_) => return Err(DatabaseError::Other),
4512        };
4513
4514        // return
4515        Ok(res)
4516    }
4517
4518    /// Get all items by their type, 12 at a time
4519    ///
4520    /// # Arguments
4521    /// * `type`
4522    /// * `page`
4523    ///
4524    /// # Returns
4525    /// `Vec<(Item, Box<Profile>)>`
4526    pub async fn get_items_by_type_paginated(
4527        &self,
4528        r#type: ItemType,
4529        page: i32,
4530    ) -> Result<Vec<(Item, Box<Profile>)>> {
4531        // pull from database
4532        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
4533            format!("SELECT * FROM \"xugc_items\" WHERE \"type\" = ? ORDER BY \"timestamp\" DESC LIMIT 12 OFFSET {}", page * 12)
4534        } else {
4535            format!("SELECT * FROM \"xugc_items\" WHERE \"type\" = $2 ORDER BY \"timestamp\" DESC LIMIT 12 OFFSET {}", page * 12)
4536        };
4537
4538        let c = &self.base.db.client;
4539        let res = match sqlquery(&query)
4540            .bind::<&str>(&serde_json::to_string(&r#type).unwrap())
4541            .fetch_all(c)
4542            .await
4543        {
4544            Ok(p) => {
4545                let mut out = Vec::new();
4546
4547                for row in p {
4548                    let res = self.base.textify_row(row).0;
4549                    let item = match self.gimme_item(res) {
4550                        Ok(t) => t,
4551                        Err(e) => return Err(e),
4552                    };
4553
4554                    let creator = item.creator.clone();
4555
4556                    out.push((
4557                        item,
4558                        match self.get_profile(&creator).await {
4559                            Ok(ua) => ua,
4560                            Err(_) => continue,
4561                        },
4562                    ));
4563                }
4564
4565                out
4566            }
4567            Err(_) => return Err(DatabaseError::Other),
4568        };
4569
4570        // return
4571        Ok(res)
4572    }
4573
4574    // SET
4575    /// Create a new item
4576    ///
4577    /// # Arguments
4578    /// * `props` - [`ItemCreate`]
4579    /// * `creator` - the user in the `creator` field of the item
4580    pub async fn create_item(&self, props: ItemCreate, creator: &str) -> Result<Item> {
4581        // check values
4582        if props.content.len() > (64 * 128 * 2) {
4583            return Err(DatabaseError::TooLong);
4584        }
4585
4586        if props.content.len() < 2 {
4587            return Err(DatabaseError::ValueError);
4588        }
4589
4590        if props.name.len() > (64 * 2) {
4591            return Err(DatabaseError::TooLong);
4592        }
4593
4594        if props.name.len() < 2 {
4595            return Err(DatabaseError::ValueError);
4596        }
4597
4598        if props.description.len() > (64 * 128) {
4599            return Err(DatabaseError::TooLong);
4600        }
4601
4602        // if we're creating a module, the cost HAS to be -1 (offsale)
4603        if props.r#type == ItemType::Module && props.cost != -1 {
4604            return Err(DatabaseError::NotAllowed);
4605        }
4606
4607        // ...
4608        if props.cost.is_negative() && props.cost != -1 {
4609            return Err(DatabaseError::NotAllowed);
4610        }
4611
4612        let item = Item {
4613            // id: utility::random_id(),
4614            id: AlmostSnowflake::new(self.config.snowflake_server_id).to_string(),
4615            name: props.name,
4616            description: props.description,
4617            cost: props.cost,
4618            content: props.content,
4619            r#type: props.r#type,
4620            status: ItemStatus::Pending,
4621            timestamp: utility::unix_epoch_timestamp(),
4622            creator: creator.to_string(),
4623        };
4624
4625        // subtract creation cost from creator
4626        /* if let Err(e) = self
4627            .create_transaction(
4628                TransactionCreate {
4629                    merchant: "0",
4630                    item: "0",
4631                    amount: -25,
4632                },
4633                creator.clone(),
4634            )
4635            .await
4636        {
4637            return Err(e);
4638        }; */
4639
4640        // create item
4641        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
4642            "INSERT INTO \"xugc_items\" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
4643        } else {
4644            "INSERT INTO \"xugc_items\" VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)"
4645        };
4646
4647        let c = &self.base.db.client;
4648        match sqlquery(&query)
4649            .bind::<&str>(&item.id)
4650            .bind::<&str>(&item.name)
4651            .bind::<&str>(&item.description)
4652            .bind::<i32>(item.cost)
4653            .bind::<&str>(&item.content)
4654            .bind::<&str>(&serde_json::to_string(&item.r#type).unwrap())
4655            .bind::<&str>(&serde_json::to_string(&item.status).unwrap())
4656            .bind::<&str>(&item.timestamp.to_string())
4657            .bind::<&str>(&item.creator)
4658            .execute(c)
4659            .await
4660        {
4661            Ok(_) => {
4662                // buy item (for free)
4663                if let Err(e) = self
4664                    .create_transaction(
4665                        TransactionCreate {
4666                            merchant: creator.to_string(),
4667                            item: item.id.clone(),
4668                            amount: 0,
4669                        },
4670                        creator,
4671                    )
4672                    .await
4673                {
4674                    return Err(e);
4675                };
4676
4677                // ...
4678                return Ok(item);
4679            }
4680            Err(_) => return Err(DatabaseError::Other),
4681        };
4682    }
4683
4684    /// Update item status
4685    ///
4686    /// # Arguments
4687    /// * `id`
4688    /// * `status` - [`ItemStatus`]
4689    /// * `user`
4690    pub async fn update_item_status(
4691        &self,
4692        id: &str,
4693        status: ItemStatus,
4694        user: Box<Profile>,
4695    ) -> Result<()> {
4696        // make sure item exists and check permission
4697        let item = match self.get_item(id).await {
4698            Ok(i) => i,
4699            Err(e) => return Err(e),
4700        };
4701
4702        // check permission
4703        let group = match self.get_group_by_id(user.group).await {
4704            Ok(g) => g,
4705            Err(_) => return Err(DatabaseError::Other),
4706        };
4707
4708        if !group.permissions.check(FinePermission::ECON_MASTER) {
4709            return Err(DatabaseError::NotAllowed);
4710        }
4711
4712        // update item
4713        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
4714            "UPDATE \"xugc_items\" SET \"status\" = ? WHERE \"id\" = ?"
4715        } else {
4716            "UPDATE \"xugc_items\" SET (\"status\") = ($1) WHERE \"id\" = $2"
4717        };
4718
4719        let c = &self.base.db.client;
4720        match sqlquery(&query)
4721            .bind::<&str>(&serde_json::to_string(&status).unwrap())
4722            .bind::<&str>(&id)
4723            .execute(c)
4724            .await
4725        {
4726            Ok(_) => {
4727                if let Err(e) = self
4728                    .create_notification(
4729                        NotificationCreate {
4730                            title: "Item status updated!".to_string(),
4731                            content: format!(
4732                                "An item you created has been updated to the status of \"{}\"",
4733                                status.to_string()
4734                            ),
4735                            address: format!("/market/item/{}", item.id.clone()),
4736                            recipient: item.creator,
4737                        },
4738                        None,
4739                    )
4740                    .await
4741                {
4742                    return Err(e);
4743                }
4744
4745                // remove from cache
4746                self.base
4747                    .cache
4748                    .remove(format!("rbeam.auth.econ.item:{}", id))
4749                    .await;
4750
4751                // ...
4752                return Ok(());
4753            }
4754            Err(_) => return Err(DatabaseError::Other),
4755        };
4756    }
4757
4758    /// Update item fields
4759    ///
4760    /// # Arguments
4761    /// * `id`
4762    /// * `props` - [`ItemEdit`]
4763    /// * `user`
4764    pub async fn update_item(&self, id: &str, props: ItemEdit, user: Box<Profile>) -> Result<()> {
4765        // make sure item exists and check permission
4766        let item = match self.get_item(id).await {
4767            Ok(i) => i,
4768            Err(e) => return Err(e),
4769        };
4770
4771        // check values
4772        if props.name.len() > (64 * 2) {
4773            return Err(DatabaseError::TooLong);
4774        }
4775
4776        if props.name.len() < 2 {
4777            return Err(DatabaseError::ValueError);
4778        }
4779
4780        if props.description.len() > (64 * 128) {
4781            return Err(DatabaseError::TooLong);
4782        }
4783
4784        // if we're creating a module, the cost HAS to be -1 (offsale)
4785        if item.r#type == ItemType::Module && props.cost != -1 {
4786            return Err(DatabaseError::NotAllowed);
4787        }
4788
4789        // ...
4790        if props.cost.is_negative() && props.cost != -1 {
4791            return Err(DatabaseError::NotAllowed);
4792        }
4793
4794        // check permission
4795        let group = match self.get_group_by_id(user.group).await {
4796            Ok(g) => g,
4797            Err(_) => return Err(DatabaseError::Other),
4798        };
4799
4800        if user.id != item.creator {
4801            if !group.permissions.check(FinePermission::ECON_MASTER) {
4802                return Err(DatabaseError::NotAllowed);
4803            }
4804        }
4805
4806        // update item
4807        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
4808            "UPDATE \"xugc_items\" SET \"name\" = ?, \"description\" = ?, \"cost\" = ? WHERE \"id\" = ?"
4809        } else {
4810            "UPDATE \"xugc_items\" SET (\"name\", \"description\", \"cost\") = ($1, $2, $3) WHERE \"id\" = $4"
4811        };
4812
4813        let c = &self.base.db.client;
4814        match sqlquery(&query)
4815            .bind::<&str>(&props.name)
4816            .bind::<&str>(&props.description)
4817            .bind::<i32>(props.cost)
4818            .bind::<&str>(&id)
4819            .execute(c)
4820            .await
4821        {
4822            Ok(_) => {
4823                // remove from cache
4824                self.base
4825                    .cache
4826                    .remove(format!("rbeam.auth.econ.item:{}", id))
4827                    .await;
4828
4829                // ...
4830                return Ok(());
4831            }
4832            Err(_) => return Err(DatabaseError::Other),
4833        };
4834    }
4835
4836    /// Update item content
4837    ///
4838    /// # Arguments
4839    /// * `id`
4840    /// * `props` - [`ItemEditContent`]
4841    /// * `user`
4842    pub async fn update_item_content(
4843        &self,
4844        id: &str,
4845        props: ItemEditContent,
4846        user: Box<Profile>,
4847    ) -> Result<()> {
4848        // make sure item exists and check permission
4849        let item = match self.get_item(id).await {
4850            Ok(i) => i,
4851            Err(e) => return Err(e),
4852        };
4853
4854        // we cannot change the content of a module
4855        // this would allow people to lie about the checksum of their wasm package
4856        //
4857        // doing this also ensures that people create a new asset for each version of their package,
4858        // meaning old versions will still verify properly
4859        if item.r#type == ItemType::Module {
4860            return Err(DatabaseError::NotAllowed);
4861        }
4862
4863        // check values
4864        if props.content.len() > (64 * 128 * 2) {
4865            return Err(DatabaseError::TooLong);
4866        }
4867
4868        if props.content.len() < 2 {
4869            return Err(DatabaseError::ValueError);
4870        }
4871
4872        // check permission
4873        let group = match self.get_group_by_id(user.group).await {
4874            Ok(g) => g,
4875            Err(_) => return Err(DatabaseError::Other),
4876        };
4877
4878        if user.id != item.creator {
4879            if !group.permissions.check(FinePermission::ECON_MASTER) {
4880                return Err(DatabaseError::NotAllowed);
4881            }
4882        }
4883
4884        // update item
4885        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
4886            "UPDATE \"xugc_items\" SET \"content\" = ? WHERE \"id\" = ?"
4887        } else {
4888            "UPDATE \"xugc_items\" SET (\"content\") = ($1) WHERE \"id\" = $2"
4889        };
4890
4891        let c = &self.base.db.client;
4892        match sqlquery(&query)
4893            .bind::<&str>(&props.content)
4894            .bind::<&str>(&id)
4895            .execute(c)
4896            .await
4897        {
4898            Ok(_) => {
4899                // remove from cache
4900                self.base
4901                    .cache
4902                    .remove(format!("rbeam.auth.econ.item:{}", id))
4903                    .await;
4904
4905                // ...
4906                return Ok(());
4907            }
4908            Err(_) => return Err(DatabaseError::Other),
4909        };
4910    }
4911
4912    /// Delete an existing item
4913    ///
4914    /// Items can only be deleted by their creator.
4915    ///
4916    /// # Arguments
4917    /// * `id` - the ID of the item
4918    /// * `user` - the user doing this
4919    pub async fn delete_item(&self, id: &str, user: Box<Profile>) -> Result<()> {
4920        // make sure item exists
4921        let item = match self.get_item(id).await {
4922            Ok(n) => n,
4923            Err(e) => return Err(e),
4924        };
4925
4926        // check username
4927        if item.creator != user.id {
4928            // check permission
4929            let group = match self.get_group_by_id(user.group).await {
4930                Ok(g) => g,
4931                Err(_) => return Err(DatabaseError::Other),
4932            };
4933
4934            if !group.permissions.check(FinePermission::ECON_MASTER) {
4935                return Err(DatabaseError::NotAllowed);
4936            }
4937        }
4938
4939        // delete item
4940        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
4941            "DELETE FROM \"xugc_items\" WHERE \"id\" = ?"
4942        } else {
4943            "DELETE FROM \"xugc_items\" WHERE \"id\" = $1"
4944        };
4945
4946        let c = &self.base.db.client;
4947        match sqlquery(&query).bind::<&str>(&id).execute(c).await {
4948            Ok(_) => {
4949                // remove from cache
4950                self.base
4951                    .cache
4952                    .remove(format!("rbeam.auth.econ.item:{}", id))
4953                    .await;
4954
4955                // return
4956                return Ok(());
4957            }
4958            Err(_) => return Err(DatabaseError::Other),
4959        };
4960    }
4961
4962    // totp
4963
4964    /// Update the profile's TOTP secret.
4965    pub async fn update_profile_totp_secret(
4966        &self,
4967        id: &str,
4968        secret: &str,
4969        recovery: &Vec<String>,
4970    ) -> Result<()> {
4971        let ua = match self.get_profile_by_id(&id).await {
4972            Ok(ua) => ua,
4973            Err(e) => return Err(e),
4974        };
4975
4976        // update profile
4977        let query = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
4978            "UPDATE \"xprofiles\" SET \"totp\" = ?, \"recovery_codes\" = ? WHERE \"id\" = ?"
4979        } else {
4980            "UPDATE \"xprofiles\" SET (\"totp\", \"recovery_codes\") = ($1, $2) WHERE \"id\" = $3"
4981        };
4982
4983        let c = &self.base.db.client;
4984        match sqlquery(&query)
4985            .bind::<&str>(&secret)
4986            .bind::<&str>(&serde_json::to_string(&recovery).unwrap())
4987            .bind::<&str>(&id)
4988            .execute(c)
4989            .await
4990        {
4991            Ok(_) => {
4992                self.base
4993                    .cache
4994                    .remove(format!("rbeam.auth.profile:{id}"))
4995                    .await;
4996
4997                self.base
4998                    .cache
4999                    .remove(format!("rbeam.auth.profile:{}", ua.username))
5000                    .await;
5001
5002                Ok(())
5003            }
5004            Err(_) => Err(DatabaseError::Other),
5005        }
5006    }
5007
5008    /// Enable TOTP for a profile.
5009    ///
5010    /// # Returns
5011    /// `Result<(secret, qr base64)>`
5012    pub async fn enable_totp(
5013        &self,
5014        as_user: Box<Profile>,
5015        id: &str,
5016    ) -> Result<(String, String, Vec<String>)> {
5017        let profile = match self.get_profile(&id).await {
5018            Ok(p) => p,
5019            Err(e) => return Err(e),
5020        };
5021
5022        if profile.id != as_user.id {
5023            return Err(DatabaseError::NotAllowed);
5024        }
5025
5026        let secret = totp_rs::Secret::default().to_string();
5027        let recovery = Database::generate_totp_recovery_codes();
5028
5029        // update profile
5030        if let Err(e) = self
5031            .update_profile_totp_secret(id, &secret, &recovery)
5032            .await
5033        {
5034            return Err(e);
5035        }
5036
5037        // fetch profile again (with totp information)
5038        let profile = match self.get_profile(&id).await {
5039            Ok(p) => p,
5040            Err(e) => return Err(e),
5041        };
5042
5043        // get totp
5044        let totp = profile.totp(Some(
5045            self.config
5046                .host
5047                .replace("http://", "")
5048                .replace("https://", "")
5049                .replace(":", "_"),
5050        ));
5051
5052        if totp.is_none() {
5053            return Err(DatabaseError::Other);
5054        }
5055
5056        let totp = totp.unwrap();
5057
5058        // generate qr
5059        let qr = match totp.get_qr_base64() {
5060            Ok(q) => q,
5061            Err(_) => return Err(DatabaseError::Other),
5062        };
5063
5064        // return
5065        Ok((totp.get_secret_base32(), qr, recovery))
5066    }
5067
5068    /// Validate a given TOTP code for the given profile.
5069    pub fn check_totp(&self, ua: &Box<Profile>, code: &str) -> bool {
5070        let totp = ua.totp(Some(
5071            self.config
5072                .host
5073                .replace("http://", "")
5074                .replace("https://", "")
5075                .replace(":", "_"),
5076        ));
5077
5078        if let Some(totp) = totp {
5079            return !code.is_empty()
5080                && (totp.check_current(code).unwrap()
5081                    | ua.recovery_codes.contains(&code.to_string()));
5082        }
5083
5084        true
5085    }
5086
5087    /// Generate 8 random recovery codes for TOTP.
5088    pub fn generate_totp_recovery_codes() -> Vec<String> {
5089        let mut out: Vec<String> = Vec::new();
5090
5091        for _ in 0..9 {
5092            out.push(rainbeam_shared::hash::salt())
5093        }
5094
5095        out
5096    }
5097}