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