#
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}