db_sqlx_postgres/
lib.rs

1// Copyright (C) 2022  Aravinth Manivannan <realaravinth@batsense.net>
2// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
3//
4// SPDX-License-Identifier: AGPL-3.0-or-later
5
6use std::str::FromStr;
7
8use db_core::dev::*;
9
10use sqlx::postgres::PgPoolOptions;
11use sqlx::types::time::OffsetDateTime;
12use sqlx::ConnectOptions;
13use sqlx::PgPool;
14use uuid::Uuid;
15
16pub mod errors;
17#[cfg(test)]
18pub mod tests;
19
20#[derive(Clone)]
21pub struct Database {
22    pub pool: PgPool,
23}
24
25/// Use an existing database pool
26pub struct Conn(pub PgPool);
27
28/// Connect to database
29pub enum ConnectionOptions {
30    /// fresh connection
31    Fresh(Fresh),
32    /// existing connection
33    Existing(Conn),
34}
35
36pub struct Fresh {
37    pub pool_options: PgPoolOptions,
38    pub disable_logging: bool,
39    pub url: String,
40}
41
42pub mod dev {
43    pub use super::errors::*;
44    pub use super::Database;
45    pub use db_core::dev::*;
46    pub use sqlx::Error;
47}
48
49pub mod prelude {
50    pub use super::*;
51}
52
53#[async_trait]
54impl Connect for ConnectionOptions {
55    type Pool = Database;
56    async fn connect(self) -> DBResult<Self::Pool> {
57        let pool = match self {
58            Self::Fresh(fresh) => {
59                let mut connect_options =
60                    sqlx::postgres::PgConnectOptions::from_str(&fresh.url).unwrap();
61                if fresh.disable_logging {
62                    connect_options = connect_options.disable_statement_logging();
63                }
64                fresh
65                    .pool_options
66                    .connect_with(connect_options)
67                    .await
68                    .map_err(|e| DBError::DBError(Box::new(e)))?
69            }
70
71            Self::Existing(conn) => conn.0,
72        };
73        Ok(Database { pool })
74    }
75}
76
77use dev::*;
78
79#[async_trait]
80impl Migrate for Database {
81    async fn migrate(&self) -> DBResult<()> {
82        sqlx::migrate!("./migrations/")
83            .run(&self.pool)
84            .await
85            .map_err(|e| DBError::DBError(Box::new(e)))?;
86        Ok(())
87    }
88}
89
90#[async_trait]
91impl MCDatabase for Database {
92    /// ping DB
93    async fn ping(&self) -> bool {
94        use sqlx::Connection;
95
96        if let Ok(mut con) = self.pool.acquire().await {
97            con.ping().await.is_ok()
98        } else {
99            false
100        }
101    }
102
103    /// register a new user
104    async fn register(&self, p: &Register) -> DBResult<()> {
105        let res = if let Some(email) = &p.email {
106            sqlx::query!(
107                "insert into mcaptcha_users 
108        (name , password, email, secret) values ($1, $2, $3, $4)",
109                &p.username,
110                &p.hash,
111                &email,
112                &p.secret,
113            )
114            .execute(&self.pool)
115            .await
116        } else {
117            sqlx::query!(
118                "INSERT INTO mcaptcha_users 
119        (name , password,  secret) VALUES ($1, $2, $3)",
120                &p.username,
121                &p.hash,
122                &p.secret,
123            )
124            .execute(&self.pool)
125            .await
126        };
127        res.map_err(map_register_err)?;
128        Ok(())
129    }
130
131    /// delete a user
132    async fn delete_user(&self, username: &str) -> DBResult<()> {
133        sqlx::query!("DELETE FROM mcaptcha_users WHERE name = ($1)", username)
134            .execute(&self.pool)
135            .await
136            .map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
137        Ok(())
138    }
139
140    /// check if username exists
141    async fn username_exists(&self, username: &str) -> DBResult<bool> {
142        let res = sqlx::query!(
143            "SELECT EXISTS (SELECT 1 from mcaptcha_users WHERE name = $1)",
144            username,
145        )
146        .fetch_one(&self.pool)
147        .await
148        .map_err(map_register_err)?;
149
150        let mut resp = false;
151        if let Some(x) = res.exists {
152            resp = x;
153        }
154
155        Ok(resp)
156    }
157
158    /// get user email
159    async fn get_email(&self, username: &str) -> DBResult<Option<String>> {
160        struct Email {
161            email: Option<String>,
162        }
163
164        let res = sqlx::query_as!(
165            Email,
166            "SELECT email FROM mcaptcha_users WHERE name = $1",
167            username
168        )
169        .fetch_one(&self.pool)
170        .await
171        .map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
172        Ok(res.email)
173    }
174
175    /// check if email exists
176    async fn email_exists(&self, email: &str) -> DBResult<bool> {
177        let res = sqlx::query!(
178            "SELECT EXISTS (SELECT 1 from mcaptcha_users WHERE email = $1)",
179            email
180        )
181        .fetch_one(&self.pool)
182        .await
183        .map_err(map_register_err)?;
184
185        let mut resp = false;
186        if let Some(x) = res.exists {
187            resp = x;
188        }
189
190        Ok(resp)
191    }
192
193    /// update a user's email
194    async fn update_email(&self, p: &UpdateEmail) -> DBResult<()> {
195        sqlx::query!(
196            "UPDATE mcaptcha_users set email = $1
197            WHERE name = $2",
198            &p.new_email,
199            &p.username,
200        )
201        .execute(&self.pool)
202        .await
203        .map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
204
205        Ok(())
206    }
207
208    /// get a user's password
209    async fn get_password(&self, l: &Login) -> DBResult<NameHash> {
210        struct Password {
211            name: String,
212            password: String,
213        }
214
215        let rec = match l {
216            Login::Username(u) => sqlx::query_as!(
217                Password,
218                r#"SELECT name, password  FROM mcaptcha_users WHERE name = ($1)"#,
219                u,
220            )
221            .fetch_one(&self.pool)
222            .await
223            .map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?,
224
225            Login::Email(e) => sqlx::query_as!(
226                Password,
227                r#"SELECT name, password  FROM mcaptcha_users WHERE email = ($1)"#,
228                e,
229            )
230            .fetch_one(&self.pool)
231            .await
232            .map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?,
233        };
234
235        let res = NameHash {
236            hash: rec.password,
237            username: rec.name,
238        };
239
240        Ok(res)
241    }
242
243    /// update user's password
244    async fn update_password(&self, p: &NameHash) -> DBResult<()> {
245        sqlx::query!(
246            "UPDATE mcaptcha_users set password = $1
247            WHERE name = $2",
248            &p.hash,
249            &p.username,
250        )
251        .execute(&self.pool)
252        .await
253        .map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
254
255        Ok(())
256    }
257
258    /// update username
259    async fn update_username(&self, current: &str, new: &str) -> DBResult<()> {
260        sqlx::query!(
261            "UPDATE mcaptcha_users set name = $1
262            WHERE name = $2",
263            new,
264            current,
265        )
266        .execute(&self.pool)
267        .await
268        .map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
269
270        Ok(())
271    }
272
273    /// get a user's secret
274    async fn get_secret(&self, username: &str) -> DBResult<Secret> {
275        let secret = sqlx::query_as!(
276            Secret,
277            r#"SELECT secret  FROM mcaptcha_users WHERE name = ($1)"#,
278            username,
279        )
280        .fetch_one(&self.pool)
281        .await
282        .map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
283
284        Ok(secret)
285    }
286
287    /// get a user's secret from a captcha key
288    async fn get_secret_from_captcha(&self, key: &str) -> DBResult<Secret> {
289        let secret = sqlx::query_as!(
290            Secret,
291            r#"SELECT secret  FROM mcaptcha_users WHERE ID = (
292                    SELECT user_id FROM mcaptcha_config WHERE key = $1
293                    )"#,
294            key,
295        )
296        .fetch_one(&self.pool)
297        .await
298        .map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
299
300        Ok(secret)
301    }
302
303    /// update a user's secret
304    async fn update_secret(&self, username: &str, secret: &str) -> DBResult<()> {
305        sqlx::query!(
306            "UPDATE mcaptcha_users set secret = $1
307        WHERE name = $2",
308            &secret,
309            &username,
310        )
311        .execute(&self.pool)
312        .await
313        .map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
314
315        Ok(())
316    }
317
318    /// create new captcha
319    async fn create_captcha(&self, username: &str, p: &CreateCaptcha) -> DBResult<()> {
320        sqlx::query!(
321            "INSERT INTO mcaptcha_config
322        (key, user_id, duration, name)
323        VALUES ($1, (SELECT ID FROM mcaptcha_users WHERE name = $2), $3, $4)",
324            p.key,
325            username,
326            p.duration as i32,
327            p.description,
328        )
329        .execute(&self.pool)
330        .await
331        .map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
332
333        Ok(())
334    }
335
336    /// Get captcha config
337    async fn get_captcha_config(&self, username: &str, key: &str) -> DBResult<Captcha> {
338        let captcha = sqlx::query_as!(
339            InternaleCaptchaConfig,
340            "SELECT config_id, duration, name, key from mcaptcha_config WHERE
341                        key = $1 AND
342                        user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2) ",
343            &key,
344            &username,
345        )
346        .fetch_one(&self.pool)
347        .await
348        .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
349
350        Ok(captcha.into())
351    }
352
353    /// Get all captchas belonging to user
354    async fn get_all_user_captchas(&self, username: &str) -> DBResult<Vec<Captcha>> {
355        let mut res = sqlx::query_as!(
356            InternaleCaptchaConfig,
357            "SELECT key, name, config_id, duration FROM mcaptcha_config WHERE
358            user_id = (SELECT ID FROM mcaptcha_users WHERE name = $1) ",
359            &username,
360        )
361        .fetch_all(&self.pool)
362        .await
363        .map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
364
365        let mut captchas = Vec::with_capacity(res.len());
366
367        res.drain(0..).for_each(|r| captchas.push(r.into()));
368
369        Ok(captchas)
370    }
371
372    /// update captcha metadata; doesn't change captcha key
373    async fn update_captcha_metadata(
374        &self,
375        username: &str,
376        p: &CreateCaptcha,
377    ) -> DBResult<()> {
378        sqlx::query!(
379            "UPDATE mcaptcha_config SET name = $1, duration = $2
380            WHERE user_id = (SELECT ID FROM mcaptcha_users WHERE name = $3)
381            AND key = $4",
382            p.description,
383            p.duration,
384            username,
385            p.key,
386        )
387        .execute(&self.pool)
388        .await
389        .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
390
391        Ok(())
392    }
393
394    /// update captcha key; doesn't change metadata
395    async fn update_captcha_key(
396        &self,
397        username: &str,
398        old_key: &str,
399        new_key: &str,
400    ) -> DBResult<()> {
401        sqlx::query!(
402            "UPDATE mcaptcha_config SET key = $1 
403        WHERE key = $2 AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $3)",
404            new_key,
405            old_key,
406            username,
407        )
408        .execute(&self.pool)
409        .await
410        .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
411
412        Ok(())
413    }
414
415    /// Add levels to captcha
416    async fn add_captcha_levels(
417        &self,
418        username: &str,
419        captcha_key: &str,
420        levels: &[Level],
421    ) -> DBResult<()> {
422        use futures::future::try_join_all;
423        let mut futs = Vec::with_capacity(levels.len());
424
425        for level in levels.iter() {
426            let difficulty_factor = level.difficulty_factor as i32;
427            let visitor_threshold = level.visitor_threshold as i32;
428            let fut = sqlx::query!(
429                "INSERT INTO mcaptcha_levels (
430            difficulty_factor, 
431            visitor_threshold,
432            config_id) VALUES  (
433            $1, $2, (
434                SELECT config_id FROM mcaptcha_config WHERE
435                key = ($3) AND user_id = (
436                SELECT ID FROM mcaptcha_users WHERE name = $4
437                    )));",
438                difficulty_factor,
439                visitor_threshold,
440                &captcha_key,
441                username,
442            )
443            .execute(&self.pool);
444            futs.push(fut);
445        }
446
447        try_join_all(futs)
448            .await
449            .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
450
451        let mut futs = Vec::with_capacity(levels.len());
452        for level in levels.iter() {
453            let difficulty_factor = level.difficulty_factor as i32;
454            let visitor_threshold = level.visitor_threshold as i32;
455            let fut = sqlx::query!(
456                "INSERT INTO
457                    mcaptcha_track_nonce (level_id, nonce)
458                VALUES  ((
459                    SELECT
460                        level_id
461                    FROM
462                        mcaptcha_levels
463                    WHERE
464                        config_id = (SELECT config_id FROM mcaptcha_config WHERE key = ($1))
465                    AND
466                        difficulty_factor = $2
467                    AND
468                        visitor_threshold = $3
469                    ), $4);",
470                &captcha_key,
471                difficulty_factor,
472                visitor_threshold,
473                0,
474            )
475            .execute(&self.pool);
476            futs.push(fut);
477        }
478
479        try_join_all(futs)
480            .await
481            .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
482
483        Ok(())
484    }
485
486    /// check if captcha exists
487    async fn captcha_exists(
488        &self,
489        username: Option<&str>,
490        captcha_key: &str,
491    ) -> DBResult<bool> {
492        let mut exists = false;
493
494        match username {
495            Some(username) => {
496                let x = sqlx::query!(
497                    "SELECT EXISTS (
498            SELECT 1 from mcaptcha_config WHERE key = $1 
499            AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)
500            )",
501                    captcha_key,
502                    username
503                )
504                .fetch_one(&self.pool)
505                .await
506                .map_err(map_register_err)?;
507                if let Some(x) = x.exists {
508                    exists = x;
509                };
510            }
511
512            None => {
513                let x = sqlx::query!(
514                    "SELECT EXISTS (SELECT 1 from mcaptcha_config WHERE key = $1)",
515                    &captcha_key,
516                )
517                .fetch_one(&self.pool)
518                .await
519                .map_err(map_register_err)?;
520                if let Some(x) = x.exists {
521                    exists = x;
522                };
523            }
524        };
525
526        Ok(exists)
527    }
528
529    /// Delete all levels of a captcha
530    async fn delete_captcha_levels(
531        &self,
532        username: &str,
533        captcha_key: &str,
534    ) -> DBResult<()> {
535        sqlx::query!(
536            "DELETE FROM mcaptcha_levels 
537        WHERE config_id = (
538            SELECT config_id FROM mcaptcha_config where key = ($1) 
539            AND user_id = (
540            SELECT ID from mcaptcha_users WHERE name = $2
541            )
542            )",
543            captcha_key,
544            username
545        )
546        .execute(&self.pool)
547        .await
548        .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
549
550        Ok(())
551    }
552
553    /// Delete captcha
554    async fn delete_captcha(&self, username: &str, captcha_key: &str) -> DBResult<()> {
555        sqlx::query!(
556            "DELETE FROM mcaptcha_config WHERE key = ($1)
557                AND
558            user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)",
559            captcha_key,
560            username,
561        )
562        .execute(&self.pool)
563        .await
564        .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
565
566        Ok(())
567    }
568
569    /// Get captcha levels
570    async fn get_captcha_levels(
571        &self,
572        username: Option<&str>,
573        captcha_key: &str,
574    ) -> DBResult<Vec<Level>> {
575        struct I32Levels {
576            difficulty_factor: i32,
577            visitor_threshold: i32,
578        }
579        let levels = match username {
580            None => sqlx::query_as!(
581                I32Levels,
582                "SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels  WHERE
583            config_id = (
584                SELECT config_id FROM mcaptcha_config WHERE key = ($1)
585                ) ORDER BY difficulty_factor ASC;",
586                captcha_key,
587            )
588            .fetch_all(&self.pool)
589            .await
590            .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?,
591
592            Some(username) => sqlx::query_as!(
593                I32Levels,
594                "SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels  WHERE
595            config_id = (
596                SELECT config_id FROM mcaptcha_config WHERE key = ($1)
597                AND user_id = (SELECT ID from mcaptcha_users WHERE name = $2)
598                )
599            ORDER BY difficulty_factor ASC;",
600                captcha_key,
601                username
602            )
603            .fetch_all(&self.pool)
604            .await
605            .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?,
606        };
607
608        let mut new_levels = Vec::with_capacity(levels.len());
609        for l in levels.iter() {
610            new_levels.push(Level {
611                difficulty_factor: l.difficulty_factor as u32,
612                visitor_threshold: l.visitor_threshold as u32,
613            });
614        }
615        Ok(new_levels)
616    }
617
618    /// Get captcha's cooldown period
619    async fn get_captcha_cooldown(&self, captcha_key: &str) -> DBResult<i32> {
620        struct DurationResp {
621            duration: i32,
622        }
623
624        let resp = sqlx::query_as!(
625            DurationResp,
626            "SELECT duration FROM mcaptcha_config  
627            WHERE key = $1",
628            captcha_key,
629        )
630        .fetch_one(&self.pool)
631        .await
632        .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
633
634        Ok(resp.duration)
635    }
636    /// Add traffic configuration
637    async fn add_traffic_pattern(
638        &self,
639        username: &str,
640        captcha_key: &str,
641        pattern: &TrafficPattern,
642    ) -> DBResult<()> {
643        sqlx::query!(
644            "INSERT INTO mcaptcha_sitekey_user_provided_avg_traffic (
645            config_id,
646            avg_traffic,
647            peak_sustainable_traffic,
648            broke_my_site_traffic
649            ) VALUES ( 
650             (SELECT config_id FROM mcaptcha_config WHERE key = ($1)
651             AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)
652            ), $3, $4, $5)",
653            //payload.avg_traffic,
654            captcha_key,
655            username,
656            pattern.avg_traffic as i32,
657            pattern.peak_sustainable_traffic as i32,
658            pattern.broke_my_site_traffic.as_ref().map(|v| *v as i32),
659        )
660        .execute(&self.pool)
661        .await
662        .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
663        Ok(())
664    }
665
666    /// Get traffic configuration
667    async fn get_traffic_pattern(
668        &self,
669        username: &str,
670        captcha_key: &str,
671    ) -> DBResult<TrafficPattern> {
672        let res = sqlx::query_as!(
673            InnerTraffic,
674            "SELECT 
675          avg_traffic, 
676          peak_sustainable_traffic, 
677          broke_my_site_traffic 
678        FROM 
679          mcaptcha_sitekey_user_provided_avg_traffic 
680        WHERE 
681          config_id = (
682            SELECT 
683              config_id 
684            FROM 
685              mcaptcha_config 
686            WHERE 
687              KEY = $1 
688              AND user_id = (
689                SELECT 
690                  id 
691                FROM 
692                  mcaptcha_users 
693                WHERE 
694                  NAME = $2
695              )
696          )
697        ",
698            captcha_key,
699            username
700        )
701        .fetch_one(&self.pool)
702        .await
703        .map_err(|e| map_row_not_found_err(e, DBError::TrafficPatternNotFound))?;
704        Ok(res.into())
705    }
706
707    /// Get all easy captcha configurations on instance
708    async fn get_all_easy_captchas(
709        &self,
710        limit: usize,
711        offset: usize,
712    ) -> DBResult<Vec<EasyCaptcha>> {
713        struct InnerEasyCaptcha {
714            key: String,
715            peak_sustainable_traffic: i32,
716            avg_traffic: i32,
717            broke_my_site_traffic: Option<i32>,
718            name: String,
719            username: String,
720        }
721        let mut inner_res = sqlx::query_as!(
722            InnerEasyCaptcha,
723            "SELECT 
724                  mcaptcha_sitekey_user_provided_avg_traffic.avg_traffic, 
725                  mcaptcha_sitekey_user_provided_avg_traffic.peak_sustainable_traffic, 
726                  mcaptcha_sitekey_user_provided_avg_traffic.broke_my_site_traffic,
727                  mcaptcha_config.name,
728                  mcaptcha_users.name as username,
729                  mcaptcha_config.key
730            FROM 
731              mcaptcha_sitekey_user_provided_avg_traffic 
732            INNER JOIN
733                mcaptcha_config
734            ON
735                mcaptcha_config.config_id = mcaptcha_sitekey_user_provided_avg_traffic.config_id
736            INNER JOIN
737                mcaptcha_users
738            ON
739                mcaptcha_config.user_id = mcaptcha_users.ID
740            ORDER BY mcaptcha_config.config_id
741            OFFSET $1 LIMIT $2; ",
742            offset as i32,
743            limit as i32
744        )
745        .fetch_all(&self.pool)
746        .await
747        .map_err(|e| map_row_not_found_err(e, DBError::TrafficPatternNotFound))?;
748        let mut res = Vec::with_capacity(inner_res.len());
749        inner_res.drain(0..).for_each(|v| {
750            res.push(EasyCaptcha {
751                key: v.key,
752                description: v.name,
753                username: v.username,
754                traffic_pattern: TrafficPattern {
755                    broke_my_site_traffic: v
756                        .broke_my_site_traffic
757                        .as_ref()
758                        .map(|v| *v as u32),
759                    avg_traffic: v.avg_traffic as u32,
760                    peak_sustainable_traffic: v.peak_sustainable_traffic as u32,
761                },
762            })
763        });
764        Ok(res)
765    }
766
767    /// Delete traffic configuration
768    async fn delete_traffic_pattern(
769        &self,
770        username: &str,
771        captcha_key: &str,
772    ) -> DBResult<()> {
773        sqlx::query!(
774            "DELETE FROM mcaptcha_sitekey_user_provided_avg_traffic
775        WHERE config_id = (
776            SELECT config_id 
777            FROM 
778                mcaptcha_config 
779            WHERE
780                key = ($1) 
781            AND 
782                user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)
783            );",
784            captcha_key,
785            username,
786        )
787        .execute(&self.pool)
788        .await
789        .map_err(|e| map_row_not_found_err(e, DBError::TrafficPatternNotFound))?;
790        Ok(())
791    }
792
793    /// create new notification
794    async fn create_notification(&self, p: &AddNotification) -> DBResult<()> {
795        let now = now_unix_time_stamp();
796        sqlx::query!(
797            "INSERT INTO mcaptcha_notifications (
798              heading, message, tx, rx, received)
799              VALUES  (
800              $1, $2,
801                  (SELECT ID FROM mcaptcha_users WHERE name = $3),
802                  (SELECT ID FROM mcaptcha_users WHERE name = $4),
803                  $5
804                      );",
805            p.heading,
806            p.message,
807            p.from,
808            p.to,
809            now
810        )
811        .execute(&self.pool)
812        .await
813        .map_err(map_register_err)?;
814
815        Ok(())
816    }
817
818    /// get all unread notifications
819    async fn get_all_unread_notifications(
820        &self,
821        username: &str,
822    ) -> DBResult<Vec<Notification>> {
823        let mut inner_notifications = sqlx::query_file_as!(
824            InnerNotification,
825            "./src/get_all_unread_notifications.sql",
826            &username
827        )
828        .fetch_all(&self.pool)
829        .await
830        .map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
831
832        let mut notifications = Vec::with_capacity(inner_notifications.len());
833
834        inner_notifications
835            .drain(0..)
836            .for_each(|n| notifications.push(n.into()));
837
838        Ok(notifications)
839    }
840
841    /// mark a notification read
842    async fn mark_notification_read(&self, username: &str, id: i32) -> DBResult<()> {
843        sqlx::query_file_as!(
844            Notification,
845            "./src/mark_notification_read.sql",
846            id,
847            &username
848        )
849        .execute(&self.pool)
850        .await
851        .map_err(|e| map_row_not_found_err(e, DBError::NotificationNotFound))?;
852
853        Ok(())
854    }
855
856    /// record PoWConfig fetches
857    async fn record_fetch(&self, key: &str) -> DBResult<()> {
858        let now = now_unix_time_stamp();
859        let _ = sqlx::query!(
860        "INSERT INTO mcaptcha_pow_fetched_stats 
861        (config_id, time) VALUES ((SELECT config_id FROM mcaptcha_config WHERE key = $1), $2)",
862        key,
863        &now,
864    )
865    .execute(&self.pool)
866    .await
867        .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
868        Ok(())
869    }
870
871    /// record PoWConfig solves
872    async fn record_solve(&self, key: &str) -> DBResult<()> {
873        let now = OffsetDateTime::now_utc();
874        let _ = sqlx::query!(
875        "INSERT INTO mcaptcha_pow_solved_stats 
876        (config_id, time) VALUES ((SELECT config_id FROM mcaptcha_config WHERE key = $1), $2)",
877        key,
878        &now,
879    )
880    .execute(&self.pool)
881    .await
882    .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
883        Ok(())
884    }
885
886    /// record PoWConfig confirms
887    async fn record_confirm(&self, key: &str) -> DBResult<()> {
888        let now = now_unix_time_stamp();
889        let _ = sqlx::query!(
890        "INSERT INTO mcaptcha_pow_confirmed_stats 
891        (config_id, time) VALUES ((SELECT config_id FROM mcaptcha_config WHERE key = $1), $2)",
892        key,
893        &now
894    )
895    .execute(&self.pool)
896    .await
897        .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
898        Ok(())
899    }
900
901    /// fetch PoWConfig fetches
902    async fn fetch_config_fetched(&self, user: &str, key: &str) -> DBResult<Vec<i64>> {
903        let records = sqlx::query_as!(
904            Date,
905            "SELECT time FROM mcaptcha_pow_fetched_stats
906            WHERE 
907                config_id = (
908                    SELECT 
909                        config_id FROM mcaptcha_config 
910                    WHERE 
911                        key = $1
912                    AND
913                        user_id = (
914                        SELECT 
915                            ID FROM mcaptcha_users WHERE name = $2))
916                ORDER BY time DESC",
917            &key,
918            &user,
919        )
920        .fetch_all(&self.pool)
921        .await
922        .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
923
924        Ok(Date::dates_to_unix(records))
925    }
926
927    /// fetch PoWConfig solves
928    async fn fetch_solve(&self, user: &str, key: &str) -> DBResult<Vec<i64>> {
929        let records = sqlx::query_as!(
930            Date,
931            "SELECT time FROM mcaptcha_pow_solved_stats 
932            WHERE config_id = (
933                SELECT config_id FROM mcaptcha_config 
934                WHERE 
935                    key = $1
936                AND
937                     user_id = (
938                        SELECT 
939                            ID FROM mcaptcha_users WHERE name = $2)) 
940                ORDER BY time DESC",
941            &key,
942            &user
943        )
944        .fetch_all(&self.pool)
945        .await
946        .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
947
948        Ok(Date::dates_to_unix(records))
949    }
950
951    /// fetch PoWConfig confirms
952    async fn fetch_confirm(&self, user: &str, key: &str) -> DBResult<Vec<i64>> {
953        let records = sqlx::query_as!(
954            Date,
955            "SELECT time FROM mcaptcha_pow_confirmed_stats 
956            WHERE 
957                config_id = (
958                    SELECT config_id FROM mcaptcha_config 
959                WHERE 
960                    key = $1
961                AND
962                     user_id = (
963                        SELECT 
964                            ID FROM mcaptcha_users WHERE name = $2))
965                ORDER BY time DESC",
966            &key,
967            &user
968        )
969        .fetch_all(&self.pool)
970        .await
971        .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
972
973        Ok(Date::dates_to_unix(records))
974    }
975
976    /// record PoW timing
977    async fn analysis_save(
978        &self,
979        captcha_id: &str,
980        d: &CreatePerformanceAnalytics,
981    ) -> DBResult<()> {
982        let _ = sqlx::query!(
983            "INSERT INTO mcaptcha_pow_analytics 
984        (config_id, time, difficulty_factor, worker_type)
985        VALUES ((SELECT config_id FROM mcaptcha_config WHERE key = $1), $2, $3, $4)",
986            captcha_id,
987            d.time as i32,
988            d.difficulty_factor as i32,
989            &d.worker_type,
990        )
991        .execute(&self.pool)
992        .await
993        .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
994        Ok(())
995    }
996
997    /// fetch PoW analytics
998    async fn analytics_fetch(
999        &self,
1000        captcha_id: &str,
1001        limit: usize,
1002        offset: usize,
1003    ) -> DBResult<Vec<PerformanceAnalytics>> {
1004        struct P {
1005            id: i32,
1006            time: i32,
1007            difficulty_factor: i32,
1008            worker_type: String,
1009        }
1010
1011        impl From<P> for PerformanceAnalytics {
1012            fn from(v: P) -> Self {
1013                Self {
1014                    time: v.time as u32,
1015                    difficulty_factor: v.difficulty_factor as u32,
1016                    worker_type: v.worker_type,
1017                    id: v.id as usize,
1018                }
1019            }
1020        }
1021
1022        let mut c = sqlx::query_as!(
1023            P,
1024            "SELECT id, time, difficulty_factor, worker_type FROM mcaptcha_pow_analytics
1025            WHERE 
1026                config_id = (
1027                    SELECT 
1028                        config_id FROM mcaptcha_config 
1029                    WHERE 
1030                        key = $1
1031                        )
1032                ORDER BY ID
1033                OFFSET $2 LIMIT $3
1034                ",
1035            &captcha_id,
1036            offset as i32,
1037            limit as i32
1038        )
1039        .fetch_all(&self.pool)
1040        .await
1041        .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
1042        let mut res = Vec::with_capacity(c.len());
1043        for i in c.drain(0..) {
1044            res.push(i.into())
1045        }
1046
1047        Ok(res)
1048    }
1049
1050    /// Create psuedo ID against campaign ID to publish analytics
1051    async fn analytics_create_psuedo_id_if_not_exists(
1052        &self,
1053        captcha_id: &str,
1054    ) -> DBResult<()> {
1055        let id = Uuid::new_v4();
1056        sqlx::query!(
1057            "
1058            INSERT INTO
1059                mcaptcha_psuedo_campaign_id (config_id, psuedo_id)
1060            VALUES (
1061                (SELECT config_id FROM mcaptcha_config WHERE key = ($1)),
1062                $2
1063            );",
1064            captcha_id,
1065            &id.to_string(),
1066        )
1067        .execute(&self.pool)
1068        .await
1069        .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
1070
1071        Ok(())
1072    }
1073
1074    /// Get psuedo ID from campaign ID
1075    async fn analytics_get_psuedo_id_from_capmaign_id(
1076        &self,
1077        captcha_id: &str,
1078    ) -> DBResult<String> {
1079        let res = sqlx::query_as!(
1080            PsuedoID,
1081            "SELECT psuedo_id FROM
1082                mcaptcha_psuedo_campaign_id
1083            WHERE
1084                 config_id = (SELECT config_id FROM mcaptcha_config WHERE key = ($1));
1085            ",
1086            captcha_id
1087        )
1088        .fetch_one(&self.pool)
1089        .await
1090        .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
1091
1092        Ok(res.psuedo_id)
1093    }
1094
1095    /// Get campaign ID from psuedo ID
1096    async fn analytics_get_capmaign_id_from_psuedo_id(
1097        &self,
1098        psuedo_id: &str,
1099    ) -> DBResult<String> {
1100        struct ID {
1101            key: String,
1102        }
1103
1104        let res = sqlx::query_as!(
1105            ID,
1106            "SELECT
1107                key
1108            FROM
1109                mcaptcha_config
1110            WHERE
1111                 config_id = (
1112                     SELECT
1113                         config_id
1114                     FROM
1115                         mcaptcha_psuedo_campaign_id
1116                     WHERE
1117                         psuedo_id = $1
1118                 );",
1119            psuedo_id
1120        )
1121        .fetch_one(&self.pool)
1122        .await
1123        .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
1124        Ok(res.key)
1125    }
1126
1127    async fn analytics_delete_all_records_for_campaign(
1128        &self,
1129        campaign_id: &str,
1130    ) -> DBResult<()> {
1131        let _ = sqlx::query!(
1132            "
1133        DELETE FROM
1134            mcaptcha_psuedo_campaign_id
1135        WHERE config_id = (
1136            SELECT config_id FROM mcaptcha_config WHERE key = ($1)
1137        );",
1138            campaign_id
1139        )
1140        .execute(&self.pool)
1141        .await;
1142
1143        let _ = sqlx::query!(
1144            "
1145            DELETE FROM
1146                mcaptcha_pow_analytics
1147            WHERE
1148                config_id = (
1149                    SELECT config_id FROM mcaptcha_config WHERE key = $1
1150                    )
1151             ",
1152            campaign_id
1153        )
1154        .execute(&self.pool)
1155        .await;
1156
1157        Ok(())
1158    }
1159
1160    /// Get all psuedo IDs
1161    async fn analytics_get_all_psuedo_ids(&self, page: usize) -> DBResult<Vec<String>> {
1162        const LIMIT: usize = 50;
1163        let offset = LIMIT * page;
1164
1165        let mut res = sqlx::query_as!(
1166            PsuedoID,
1167            "
1168                SELECT
1169                    psuedo_id
1170                FROM
1171                    mcaptcha_psuedo_campaign_id
1172                    ORDER BY ID ASC LIMIT $1 OFFSET $2;",
1173            LIMIT as i64,
1174            offset as i64
1175        )
1176        .fetch_all(&self.pool)
1177        .await
1178        .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
1179
1180        Ok(res.drain(0..).map(|r| r.psuedo_id).collect())
1181    }
1182
1183    /// Track maximum nonce received against captcha levels
1184    async fn update_max_nonce_for_level(
1185        &self,
1186        captcha_key: &str,
1187        difficulty_factor: u32,
1188        latest_nonce: u32,
1189    ) -> DBResult<()> {
1190        sqlx::query!(
1191                "UPDATE mcaptcha_track_nonce SET nonce = $3
1192                WHERE level_id =  (
1193                    SELECT
1194                        level_id
1195                    FROM
1196                        mcaptcha_levels
1197                    WHERE
1198                        config_id = (SELECT config_id FROM mcaptcha_config WHERE key = ($1))
1199                    AND
1200                        difficulty_factor = $2
1201                    )
1202                AND nonce <= $3;",
1203                &captcha_key,
1204                difficulty_factor as i32,
1205                latest_nonce as i32,
1206            )
1207            .execute(&self.pool).await
1208        .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
1209
1210        Ok(())
1211    }
1212
1213    /// Get maximum nonce tracked so far for captcha levels
1214    async fn get_max_nonce_for_level(
1215        &self,
1216        captcha_key: &str,
1217        difficulty_factor: u32,
1218    ) -> DBResult<u32> {
1219        struct X {
1220            nonce: i32,
1221        }
1222
1223        async fn inner_get_max_nonce(
1224            pool: &PgPool,
1225            captcha_key: &str,
1226            difficulty_factor: u32,
1227        ) -> DBResult<X> {
1228            sqlx::query_as!(
1229                X,
1230                "SELECT nonce FROM mcaptcha_track_nonce
1231                WHERE level_id =  (
1232                    SELECT
1233                        level_id
1234                    FROM
1235                        mcaptcha_levels
1236                    WHERE
1237                        config_id = (SELECT config_id FROM mcaptcha_config WHERE key = ($1))
1238                    AND
1239                        difficulty_factor = $2
1240                    );",
1241                &captcha_key,
1242                difficulty_factor as i32,
1243            )
1244        .fetch_one(pool)
1245                .await
1246                .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))
1247        }
1248
1249        let res = inner_get_max_nonce(&self.pool, captcha_key, difficulty_factor).await;
1250        if let Err(DBError::CaptchaNotFound) = res {
1251            sqlx::query!(
1252                "INSERT INTO
1253                    mcaptcha_track_nonce (level_id, nonce)
1254                VALUES  ((
1255                    SELECT
1256                        level_id
1257                    FROM
1258                        mcaptcha_levels
1259                    WHERE
1260                        config_id = (SELECT config_id FROM mcaptcha_config WHERE key = ($1))
1261                    AND
1262                        difficulty_factor = $2
1263                    ), $3);",
1264                &captcha_key,
1265                difficulty_factor as i32,
1266                0,
1267            )
1268            .execute(&self.pool)
1269            .await
1270                .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
1271
1272            let res =
1273                inner_get_max_nonce(&self.pool, captcha_key, difficulty_factor).await?;
1274            Ok(res.nonce as u32)
1275        } else {
1276            let res = res?;
1277            Ok(res.nonce as u32)
1278        }
1279    }
1280
1281    /// Get number of analytics entries that are under a certain duration
1282    async fn stats_get_num_logs_under_time(&self, duration: u32) -> DBResult<usize> {
1283        struct Count {
1284            count: Option<i64>,
1285        }
1286
1287        let count = sqlx::query_as!(
1288        Count,
1289        "SELECT COUNT(difficulty_factor) FROM mcaptcha_pow_analytics WHERE time <= $1;",
1290        duration as i32,
1291    )
1292        .fetch_one(&self.pool)
1293        .await
1294        .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
1295
1296        Ok(count.count.unwrap_or_else(|| 0) as usize)
1297    }
1298
1299    /// Get the entry at a location in the list of analytics entires under a certain time limit
1300    /// and sorted in ascending order
1301    async fn stats_get_entry_at_location_for_time_limit_asc(
1302        &self,
1303        duration: u32,
1304        location: u32,
1305    ) -> DBResult<Option<usize>> {
1306        struct Difficulty {
1307            difficulty_factor: Option<i32>,
1308        }
1309
1310        match sqlx::query_as!(
1311            Difficulty,
1312            "SELECT
1313            difficulty_factor
1314        FROM
1315            mcaptcha_pow_analytics
1316        WHERE
1317            time <= $1
1318        ORDER BY difficulty_factor ASC LIMIT 1 OFFSET $2;",
1319            duration as i32,
1320            location as i64 - 1,
1321        )
1322        .fetch_one(&self.pool)
1323        .await
1324        {
1325            Ok(res) => Ok(Some(res.difficulty_factor.unwrap() as usize)),
1326            Err(sqlx::Error::RowNotFound) => Ok(None),
1327            Err(e) => Err(map_row_not_found_err(e, DBError::CaptchaNotFound)),
1328        }
1329    }
1330}
1331
1332#[derive(Clone)]
1333struct Date {
1334    time: OffsetDateTime,
1335}
1336
1337impl Date {
1338    fn dates_to_unix(mut d: Vec<Self>) -> Vec<i64> {
1339        let mut dates = Vec::with_capacity(d.len());
1340        d.drain(0..)
1341            .for_each(|x| dates.push(x.time.unix_timestamp()));
1342        dates
1343    }
1344}
1345
1346fn now_unix_time_stamp() -> OffsetDateTime {
1347    OffsetDateTime::now_utc()
1348}
1349
1350#[derive(Debug, Clone, Default, PartialEq)]
1351/// Represents notification
1352pub struct InnerNotification {
1353    /// receiver name  of the notification
1354    pub name: Option<String>,
1355    /// heading of the notification
1356    pub heading: Option<String>,
1357    /// message of the notification
1358    pub message: Option<String>,
1359    /// when notification was received
1360    pub received: Option<OffsetDateTime>,
1361    /// db assigned ID of the notification
1362    pub id: Option<i32>,
1363}
1364
1365impl From<InnerNotification> for Notification {
1366    fn from(n: InnerNotification) -> Self {
1367        Notification {
1368            name: n.name,
1369            heading: n.heading,
1370            message: n.message,
1371            received: n.received.map(|t| t.unix_timestamp()),
1372            id: n.id,
1373        }
1374    }
1375}
1376
1377struct PsuedoID {
1378    psuedo_id: String,
1379}
1380
1381#[derive(Clone)]
1382struct InternaleCaptchaConfig {
1383    config_id: i32,
1384    duration: i32,
1385    name: String,
1386    key: String,
1387}
1388
1389impl From<InternaleCaptchaConfig> for Captcha {
1390    fn from(i: InternaleCaptchaConfig) -> Self {
1391        Self {
1392            config_id: i.config_id,
1393            duration: i.duration,
1394            description: i.name,
1395            key: i.key,
1396        }
1397    }
1398}
1399
1400struct InnerTraffic {
1401    peak_sustainable_traffic: i32,
1402    avg_traffic: i32,
1403    broke_my_site_traffic: Option<i32>,
1404}
1405
1406impl From<InnerTraffic> for TrafficPattern {
1407    fn from(v: InnerTraffic) -> Self {
1408        TrafficPattern {
1409            broke_my_site_traffic: v.broke_my_site_traffic.as_ref().map(|v| *v as u32),
1410            avg_traffic: v.avg_traffic as u32,
1411            peak_sustainable_traffic: v.peak_sustainable_traffic as u32,
1412        }
1413    }
1414}