From 8830961e049b169f367bfb0b3860bc838035685e Mon Sep 17 00:00:00 2001 From: realaravinth Date: Sun, 25 Jul 2021 21:15:59 +0530 Subject: [PATCH] stats endpoint --- ...10430032935_mcaptcha_pow_fetched_stats.sql | 2 +- ...210509135118_mcaptcha_pow_solved_stats.sql | 2 +- ...509135154_mcaptcha_pow_confirmed_stats.sql | 2 +- src/api/v1/mcaptcha/captcha.rs | 34 +++++ src/api/v1/mcaptcha/levels.rs | 30 ++-- src/date.rs | 92 +++++++++++ src/main.rs | 1 + src/pages/panel/notifications.rs | 25 +-- src/stats/fetch.rs | 143 +++++++++++------- 9 files changed, 239 insertions(+), 92 deletions(-) create mode 100644 src/date.rs diff --git a/migrations/20210430032935_mcaptcha_pow_fetched_stats.sql b/migrations/20210430032935_mcaptcha_pow_fetched_stats.sql index 4c6f0097..0a770f42 100644 --- a/migrations/20210430032935_mcaptcha_pow_fetched_stats.sql +++ b/migrations/20210430032935_mcaptcha_pow_fetched_stats.sql @@ -1,4 +1,4 @@ CREATE TABLE IF NOT EXISTS mcaptcha_pow_fetched_stats ( config_id INTEGER references mcaptcha_config(config_id) ON DELETE CASCADE, - fetched_at timestamptz NOT NULL DEFAULT now() + time timestamptz NOT NULL DEFAULT now() ); diff --git a/migrations/20210509135118_mcaptcha_pow_solved_stats.sql b/migrations/20210509135118_mcaptcha_pow_solved_stats.sql index 833da2dc..1f9d7214 100644 --- a/migrations/20210509135118_mcaptcha_pow_solved_stats.sql +++ b/migrations/20210509135118_mcaptcha_pow_solved_stats.sql @@ -1,4 +1,4 @@ CREATE TABLE IF NOT EXISTS mcaptcha_pow_solved_stats ( config_id INTEGER references mcaptcha_config(config_id) ON DELETE CASCADE, - solved_at timestamptz NOT NULL DEFAULT now() + time timestamptz NOT NULL DEFAULT now() ); diff --git a/migrations/20210509135154_mcaptcha_pow_confirmed_stats.sql b/migrations/20210509135154_mcaptcha_pow_confirmed_stats.sql index b41d6149..348c7008 100644 --- a/migrations/20210509135154_mcaptcha_pow_confirmed_stats.sql +++ b/migrations/20210509135154_mcaptcha_pow_confirmed_stats.sql @@ -1,4 +1,4 @@ CREATE TABLE IF NOT EXISTS mcaptcha_pow_confirmed_stats ( config_id INTEGER references mcaptcha_config(config_id) ON DELETE CASCADE, - confirmed_at timestamptz NOT NULL DEFAULT now() + time timestamptz NOT NULL DEFAULT now() ); diff --git a/src/api/v1/mcaptcha/captcha.rs b/src/api/v1/mcaptcha/captcha.rs index 30fa48a4..1bcb1136 100644 --- a/src/api/v1/mcaptcha/captcha.rs +++ b/src/api/v1/mcaptcha/captcha.rs @@ -23,12 +23,14 @@ use serde::{Deserialize, Serialize}; use super::get_random; use crate::errors::*; +use crate::stats::fetch::{Stats, StatsUnixTimestamp}; use crate::AppData; pub mod routes { pub struct MCaptcha { pub delete: &'static str, pub update_key: &'static str, + pub stats: &'static str, } impl MCaptcha { @@ -36,6 +38,7 @@ pub mod routes { MCaptcha { update_key: "/api/v1/mcaptcha/update/key", delete: "/api/v1/mcaptcha/delete", + stats: "/api/v1/mcaptcha/stats", } } } @@ -44,6 +47,7 @@ pub mod routes { pub fn services(cfg: &mut web::ServiceConfig) { cfg.service(update_token); cfg.service(delete_mcaptcha); + cfg.service(get_stats); } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -241,6 +245,24 @@ async fn delete_mcaptcha( } } +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct StatsPayload { + pub key: String, +} + +#[my_codegen::post( + path = "crate::V1_API_ROUTES.mcaptcha.stats", + wrap = "crate::CheckLogin" +)] +async fn get_stats( + payload: web::Json, + data: AppData, +) -> ServiceResult { + let stats = Stats::new(&payload.key, &data.db).await?; + let stats = StatsUnixTimestamp::from_stats(&stats); + Ok(HttpResponse::Ok().json(&stats)) +} + // Workflow: // 1. Sign up // 2. Sign in @@ -299,5 +321,17 @@ mod tests { .await; // if updated key doesn't exist in databse, a non 200 result will bereturned assert_eq!(get_token_resp.status(), StatusCode::OK); + + // get stats + let paylod = StatsPayload { key: token_key.key }; + let get_statis_resp = test::call_service( + &app, + post_request!(&paylod, ROUTES.mcaptcha.stats) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + // if updated key doesn't exist in databse, a non 200 result will bereturned + assert_eq!(get_statis_resp.status(), StatusCode::OK); } } diff --git a/src/api/v1/mcaptcha/levels.rs b/src/api/v1/mcaptcha/levels.rs index 53e8ab06..2fae07ca 100644 --- a/src/api/v1/mcaptcha/levels.rs +++ b/src/api/v1/mcaptcha/levels.rs @@ -1,19 +1,19 @@ /* -* Copyright (C) 2021 Aravinth Manivannan -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see . -*/ + * Copyright (C) 2021 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ use actix_identity::Identity; use actix_web::{web, HttpResponse, Responder}; use futures::future::try_join_all; diff --git a/src/date.rs b/src/date.rs new file mode 100644 index 00000000..0f1f200e --- /dev/null +++ b/src/date.rs @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2021 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use sqlx::types::time::OffsetDateTime; + +pub struct Date { + pub time: OffsetDateTime, +} + +pub const MINUTE: i64 = 60; +pub const HOUR: i64 = MINUTE * 60; +pub const DAY: i64 = HOUR * 24; +pub const WEEK: i64 = DAY * 7; + +impl Date { + pub fn format(date: &OffsetDateTime) -> String { + let timestamp = date.unix_timestamp(); + let now = OffsetDateTime::now_utc().unix_timestamp(); + + let difference = now - timestamp; + + if difference >= 3 * WEEK { + date.format("%d-%m-%y") + } else if (DAY..(3 * WEEK)).contains(&difference) { + format!("{} days ago", date.hour()) + } else if (HOUR..DAY).contains(&difference) { + format!("{} hours ago", date.hour()) + } else if (MINUTE..HOUR).contains(&difference) { + format!("{} minutes ago", date.minute()) + } else { + format!("{} seconds ago", date.second()) + } + } + + pub fn print_date(&self) -> String { + Self::format(&self.time) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn print_date_test() { + let mut n = Date { + time: OffsetDateTime::now_utc(), + }; + + let timestamp = n.time.unix_timestamp(); + println!("timestamp: {}", timestamp); + + // seconds test + assert!(n.print_date().contains("seconds ago")); + n.time = OffsetDateTime::from_unix_timestamp(timestamp - 5); + assert!(n.print_date().contains("seconds ago")); + + // minutes test + n.time = OffsetDateTime::from_unix_timestamp(timestamp - MINUTE * 2); + assert!(n.print_date().contains("minutes ago")); + n.time = OffsetDateTime::from_unix_timestamp(timestamp - MINUTE * 56); + assert!(n.print_date().contains("minutes ago")); + + // hours test + n.time = OffsetDateTime::from_unix_timestamp(timestamp - HOUR); + assert!(n.print_date().contains("hours ago")); + n.time = OffsetDateTime::from_unix_timestamp(timestamp - HOUR * 23); + assert!(n.print_date().contains("hours ago")); + + // days test + n.time = OffsetDateTime::from_unix_timestamp(timestamp - 2 * WEEK); + assert!(n.print_date().contains("days ago")); + + // date test + n.time = OffsetDateTime::from_unix_timestamp(timestamp - 6 * WEEK); + let date = n.time.format("%d-%m-%y"); + assert!(n.print_date().contains(&date)) + } +} diff --git a/src/main.rs b/src/main.rs index 48d65135..8a0ba96c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,7 @@ use log::info; mod api; mod data; +mod date; mod docs; mod email; mod errors; diff --git a/src/pages/panel/notifications.rs b/src/pages/panel/notifications.rs index e8258ee0..413c4d69 100644 --- a/src/pages/panel/notifications.rs +++ b/src/pages/panel/notifications.rs @@ -21,14 +21,10 @@ use sailfish::TemplateOnce; use sqlx::types::time::OffsetDateTime; use crate::api::v1::notifications::get::{self, runner}; +use crate::date::Date; use crate::errors::PageResult; use crate::AppData; -const MINUTE: i64 = 60; -const HOUR: i64 = MINUTE * 60; -const DAY: i64 = HOUR * 24; -const WEEK: i64 = DAY * 7; - #[derive(TemplateOnce)] #[template(path = "panel/notifications/index.html")] pub struct IndexPage { @@ -64,23 +60,7 @@ impl From for Notification { impl Notification { pub fn print_date(&self) -> String { - let date = self.received; - let timestamp = self.received.unix_timestamp(); - let now = OffsetDateTime::now_utc().unix_timestamp(); - - let difference = now - timestamp; - - if difference >= 3 * WEEK { - date.format("%d-%m-%y") - } else if (DAY..(3 * WEEK)).contains(&difference) { - format!("{} days ago", date.hour()) - } else if (HOUR..DAY).contains(&difference) { - format!("{} hours ago", date.hour()) - } else if (MINUTE..HOUR).contains(&difference) { - format!("{} minutes ago", date.minute()) - } else { - format!("{} seconds ago", date.second()) - } + Date::format(&self.received) } } @@ -103,6 +83,7 @@ pub async fn notifications(data: AppData, id: Identity) -> PageResult -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see . -*/ - + * Copyright (C) 2021 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use actix_web::{web, HttpResponse, Responder}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; +use crate::date::Date; use crate::errors::*; +use crate::AppData; #[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Stats { +pub struct StatsUnixTimestamp { pub config_fetches: Vec, pub solves: Vec, pub confirms: Vec, } +pub struct Stats { + pub config_fetches: Vec, + pub solves: Vec, + pub confirms: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct StatsPayload { + pub key: String, +} + +#[my_codegen::post(path = "crate::V1_API_ROUTES.auth.login", wrap = "crate::CheckLogin")] +async fn get_stats( + payload: web::Json, + data: AppData, +) -> ServiceResult { + let stats = Stats::new(&payload.key, &data.db).await?; + let stats = StatsUnixTimestamp::from_stats(&stats); + Ok(HttpResponse::Ok().json(&stats)) +} + impl Stats { pub async fn new(key: &str, db: &PgPool) -> ServiceResult { - let config_fetches_fut = Self::fetch_config_fetched(key, db); - let solves_fut = Self::fetch_solve(key, db); - let confirms_fut = Self::fetch_confirm(key, db); + let config_fetches_fut = runners::fetch_config_fetched(key, db); + let solves_fut = runners::fetch_solve(key, db); + let confirms_fut = runners::fetch_confirm(key, db); let (config_fetches, solves, confirms) = futures::try_join!(config_fetches_fut, solves_fut, confirms_fut)?; @@ -44,70 +67,81 @@ impl Stats { Ok(res) } +} +impl StatsUnixTimestamp { + pub fn from_stats(stats: &Stats) -> Self { + let config_fetches = Self::unix_timestamp(&stats.config_fetches); + let solves = Self::unix_timestamp(&stats.solves); + let confirms = Self::unix_timestamp(&stats.confirms); + Self { + config_fetches, + solves, + confirms, + } + } + + /// featch PoWConfig confirms + #[inline] + fn unix_timestamp(dates: &Vec) -> Vec { + let mut res: Vec = Vec::with_capacity(dates.len()); + + dates + .iter() + .for_each(|record| res.push(record.time.unix_timestamp())); + + res + } +} + +pub mod runners { + use super::*; /// featch PoWConfig fetches #[inline] pub async fn fetch_config_fetched( key: &str, db: &PgPool, - ) -> ServiceResult> { - let records = sqlx::query!( - "SELECT fetched_at FROM mcaptcha_pow_fetched_stats WHERE config_id = + ) -> ServiceResult> { + let records = sqlx::query_as!( + Date, + "SELECT time FROM mcaptcha_pow_fetched_stats WHERE config_id = (SELECT config_id FROM mcaptcha_config where key = $1)", &key, ) .fetch_all(db) .await?; - let mut res: Vec = Vec::with_capacity(records.len()); - - records - .iter() - .for_each(|record| res.push(record.fetched_at.unix_timestamp())); - - Ok(res) + Ok(records) } /// featch PoWConfig solves #[inline] - pub async fn fetch_solve(key: &str, db: &PgPool) -> ServiceResult> { - // "SELECT solved_at FROM mcaptcha_pow_solved_stats WHERE config_id = - // (SELECT config_id FROM mcaptcha_config where key = $1)" - let records = sqlx::query!( - "SELECT solved_at FROM mcaptcha_pow_solved_stats WHERE config_id = + pub async fn fetch_solve(key: &str, db: &PgPool) -> ServiceResult> { + let records = sqlx::query_as!( + Date, + "SELECT time FROM mcaptcha_pow_solved_stats WHERE config_id = (SELECT config_id FROM mcaptcha_config where key = $1)", &key, ) .fetch_all(db) .await?; - let mut res: Vec = Vec::with_capacity(records.len()); - - records - .iter() - .for_each(|record| res.push(record.solved_at.unix_timestamp())); - - Ok(res) + Ok(records) } /// featch PoWConfig confirms #[inline] - pub async fn fetch_confirm(key: &str, db: &PgPool) -> ServiceResult> { - let records = sqlx::query!( - "SELECT confirmed_at FROM mcaptcha_pow_confirmed_stats WHERE config_id = ( + pub async fn fetch_confirm(key: &str, db: &PgPool) -> ServiceResult> { + let records = sqlx::query_as!( + Date, + "SELECT time FROM mcaptcha_pow_confirmed_stats WHERE config_id = ( SELECT config_id FROM mcaptcha_config where key = $1)", &key ) .fetch_all(db) .await?; - let mut res: Vec = Vec::with_capacity(records.len()); - - records - .iter() - .for_each(|record| res.push(record.confirmed_at.unix_timestamp())); - - Ok(res) + Ok(records) } } @@ -148,5 +182,10 @@ mod tests { assert_eq!(stats.config_fetches.len(), 1); assert_eq!(stats.solves.len(), 1); assert_eq!(stats.confirms.len(), 1); + + let ustats = StatsUnixTimestamp::from_stats(&stats); + assert_eq!(ustats.config_fetches.len(), 1); + assert_eq!(ustats.solves.len(), 1); + assert_eq!(ustats.confirms.len(), 1); } }