From dde69adac956f9b3b06c01e83c96b3f6844ba1db Mon Sep 17 00:00:00 2001 From: Ashwin Rajesh <46510831+VanillaViking@users.noreply.github.com> Date: Mon, 15 Jan 2024 15:45:58 +1100 Subject: [PATCH] Botstats (#114) * wipfeat: format + botstats query * feat: bot statistics on home page --- zyenyo-backend/src/main.rs | 26 ++++---- zyenyo-backend/src/models.rs | 19 +++++- zyenyo-backend/src/routes/botstats.rs | 39 ++++++++++++ zyenyo-backend/src/routes/mod.rs | 4 +- zyenyo-backend/src/routes/prompts.rs | 62 ++++++++++++------- .../src/components/HomePage/BotStats.tsx | 47 ++++++++++++++ .../src/components/HomePage/Dashboard.tsx | 6 +- 7 files changed, 165 insertions(+), 38 deletions(-) create mode 100644 zyenyo-backend/src/routes/botstats.rs create mode 100644 zyenyo-frontend/src/components/HomePage/BotStats.tsx diff --git a/zyenyo-backend/src/main.rs b/zyenyo-backend/src/main.rs index 3335776..5989053 100644 --- a/zyenyo-backend/src/main.rs +++ b/zyenyo-backend/src/main.rs @@ -3,8 +3,8 @@ use std::env; mod models; mod routes; -use actix_web::{web, App, HttpServer}; use actix_cors::Cors; +use actix_web::{web, App, HttpServer}; use mongodb::{Client, Database}; use routes::api_config; @@ -18,26 +18,28 @@ pub struct Context { async fn main() -> std::io::Result<()> { env::set_var("RUST_LOG", "debug"); let uri = env::var("MONGO_URI").expect("database URI not provided"); - let client = Client::with_uri_str(uri).await.expect("failed to connect to database"); + let client = Client::with_uri_str(uri) + .await + .expect("failed to connect to database"); let environment = env::var("ZYENYO_ENVIRONMENT").expect("ENVIRONMENT not provided"); - + HttpServer::new(move || { let mut cors = Cors::permissive(); let context = match environment.as_str() { - "development" => { - Context { - db: client.database("ZyenyoStaging"), - environment: environment.to_owned() - } + "development" => Context { + db: client.database("ZyenyoStaging"), + environment: environment.to_owned(), }, "production" => { - cors = Cors::default().allowed_origin("https://zyenyobot.com").allowed_methods(vec!["GET", "POST"]); + cors = Cors::default() + .allowed_origin("https://zyenyobot.com") + .allowed_methods(vec!["GET", "POST"]); Context { db: client.database("MyDatabase"), - environment: environment.to_owned() + environment: environment.to_owned(), } - }, - _ => panic!() + } + _ => panic!(), }; App::new() diff --git a/zyenyo-backend/src/models.rs b/zyenyo-backend/src/models.rs index 55b6f80..90e9a9b 100644 --- a/zyenyo-backend/src/models.rs +++ b/zyenyo-backend/src/models.rs @@ -1,10 +1,12 @@ +use mongodb::bson::DateTime; +use mongodb::bson::serde_helpers::bson_datetime_as_rfc3339_string; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct Prompt { pub title: String, pub text: String, - pub rating: f64 + pub rating: f64, } #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] @@ -21,5 +23,18 @@ pub struct Daily { pub currentStreak: u32, pub maxStreak: u32, //rfc3339 - pub updatedAt: String, + #[serde(with = "bson_datetime_as_rfc3339_string")] + pub updatedAt: DateTime, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct Test { + pub discordId: String, + pub wpm: f64, + pub accuracy: f64, + pub tp: f64, + pub timeTaken: u64, + pub prompt: String, + pub submittedText: String, + pub date: DateTime } diff --git a/zyenyo-backend/src/routes/botstats.rs b/zyenyo-backend/src/routes/botstats.rs new file mode 100644 index 0000000..cc244cd --- /dev/null +++ b/zyenyo-backend/src/routes/botstats.rs @@ -0,0 +1,39 @@ +use std::error::Error; + +use crate::{models::{Test, User, Prompt}, Context}; +use actix_web::{get, web, HttpResponse, Responder}; +use mongodb::{bson::doc, Collection}; +use serde::Serialize; + +#[derive(Serialize)] +struct BotStats { + total_tests: u64, + total_users: u64, + total_prompts: u64, +} + +#[get("/botstats")] +async fn botstats(context: web::Data) -> impl Responder { + + match botstats_query(context).await { + Ok(stats) => HttpResponse::Ok().json(stats), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } + +} + +async fn botstats_query(context: web::Data) -> Result> { + let tests: Collection = context.db.collection("testsv2"); + let users: Collection = context.db.collection("usersv2"); + let prompts: Collection = context.db.collection("prompts"); + + let total_tests = tests.count_documents(doc! {}, None).await?; + let total_users = users.count_documents(doc! {}, None).await?; + let total_prompts = prompts.count_documents(doc! {}, None).await?; + + Ok(BotStats { + total_tests, + total_users, + total_prompts, + }) +} diff --git a/zyenyo-backend/src/routes/mod.rs b/zyenyo-backend/src/routes/mod.rs index 2297d48..7bf4fa1 100644 --- a/zyenyo-backend/src/routes/mod.rs +++ b/zyenyo-backend/src/routes/mod.rs @@ -1,8 +1,10 @@ mod prompts; +mod botstats; use actix_web::web; pub fn api_config(cfg: &mut web::ServiceConfig) { cfg - .service(prompts::prompts); + .service(prompts::prompts) + .service(botstats::botstats); } diff --git a/zyenyo-backend/src/routes/prompts.rs b/zyenyo-backend/src/routes/prompts.rs index 66b9a2c..55bebbe 100644 --- a/zyenyo-backend/src/routes/prompts.rs +++ b/zyenyo-backend/src/routes/prompts.rs @@ -1,26 +1,39 @@ use std::error::Error; -use actix_web::{get, Responder, HttpResponse, web}; -use mongodb::{Collection, bson::{doc, Document, self}}; -use serde::Deserialize; -use crate::{Context, models::Prompt}; +use crate::{models::Prompt, Context}; +use actix_web::{get, web, HttpResponse, Responder}; use futures::stream::TryStreamExt; +use mongodb::{ + bson::{self, doc, Document}, + Collection, +}; +use serde::Deserialize; const PAGE_DEFAULT: u32 = 1; const PAGE_SIZE_DEFAULT: u32 = 20; const SORT_BY_DEFAULT: &str = "title"; const SORT_ORDER_DEFAULT: i32 = 1; const SEARCH_QUERY_DEFAULT: &str = ""; -fn page_default() -> u32 { PAGE_DEFAULT } -fn page_size_default() -> u32 { PAGE_SIZE_DEFAULT } -fn sort_by_default() -> String { SORT_BY_DEFAULT.to_owned() } -fn sort_order_default() -> i32 { SORT_ORDER_DEFAULT } -fn search_query_default() -> String { SEARCH_QUERY_DEFAULT.to_owned() } +fn page_default() -> u32 { + PAGE_DEFAULT +} +fn page_size_default() -> u32 { + PAGE_SIZE_DEFAULT +} +fn sort_by_default() -> String { + SORT_BY_DEFAULT.to_owned() +} +fn sort_order_default() -> i32 { + SORT_ORDER_DEFAULT +} +fn search_query_default() -> String { + SEARCH_QUERY_DEFAULT.to_owned() +} #[derive(Deserialize)] struct PromptsConfig { #[serde(default = "page_default")] - page: u32, + page: u32, #[serde(default = "page_size_default")] page_size: u32, #[serde(default = "sort_by_default")] @@ -28,27 +41,31 @@ struct PromptsConfig { #[serde(default = "sort_order_default")] sort_order: i32, #[serde(default = "search_query_default")] - search_query: String - + search_query: String, } - #[get("/prompts")] -async fn prompts(context: web::Data, controls: web::Query) -> impl Responder { +async fn prompts( + context: web::Data, + controls: web::Query, +) -> impl Responder { let info = controls.into_inner(); match prompt_query(context, info).await { Ok(prompts_vec) => HttpResponse::Ok().json(prompts_vec), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()) + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), } } -async fn prompt_query(context: web::Data, controls: PromptsConfig) -> Result, Box> { +async fn prompt_query( + context: web::Data, + controls: PromptsConfig, +) -> Result, Box> { let collection: Collection = context.db.collection("prompts"); - + let pipeline = vec![ - doc! {"$match": - doc! {"$expr": + doc! {"$match": + doc! {"$expr": doc! {"$or": [ doc! { "$regexMatch": doc! {"input": "$title", "regex": &controls.search_query, "options": "i"}}, doc! { "$regexMatch": doc! {"input": "$text", "regex": &controls.search_query, "options": "i"}} @@ -57,10 +74,13 @@ async fn prompt_query(context: web::Data, controls: PromptsConfig) -> R }, doc! {"$sort": doc! {&controls.sort_by: &controls.sort_order}}, doc! {"$skip": (&controls.page-1)*&controls.page_size}, - doc! {"$limit": &controls.page_size} + doc! {"$limit": &controls.page_size}, ]; let results = collection.aggregate(pipeline, None).await?; let prompts_vec: Vec = results.try_collect().await?; - Ok(prompts_vec.iter().filter_map(|doc| bson::from_document::(doc.to_owned()).ok()).collect()) + Ok(prompts_vec + .iter() + .filter_map(|doc| bson::from_document::(doc.to_owned()).ok()) + .collect()) } diff --git a/zyenyo-frontend/src/components/HomePage/BotStats.tsx b/zyenyo-frontend/src/components/HomePage/BotStats.tsx new file mode 100644 index 0000000..10ffb3c --- /dev/null +++ b/zyenyo-frontend/src/components/HomePage/BotStats.tsx @@ -0,0 +1,47 @@ +"use client" + +import axios from "axios" +import {useEffect, useState} from "react" + +type BotStats = { + total_tests: number, + total_users: number, + total_prompts: number +} + +const BotStats = ({className}: {className: string}) => { + const [botStats, setBotStats] = useState() + useEffect(() => { + async function query() { + const res = await axios.get("/api/botstats") + setBotStats(res.data as BotStats) + + } + query() + }, []) + + return ( +
+
+
+
+
{botStats?.total_tests}+
+
Tests Served
+
+ +
+
{botStats?.total_users}
+
Active Users
+
+ +
+
{botStats?.total_prompts}
+
Typing Prompts
+
+
+
+
+ ) +} + +export default BotStats diff --git a/zyenyo-frontend/src/components/HomePage/Dashboard.tsx b/zyenyo-frontend/src/components/HomePage/Dashboard.tsx index 297124a..a421b93 100644 --- a/zyenyo-frontend/src/components/HomePage/Dashboard.tsx +++ b/zyenyo-frontend/src/components/HomePage/Dashboard.tsx @@ -9,6 +9,7 @@ import Link from "next/link"; import {useEffect, useState} from "react" +import BotStats from "./BotStats"; const Dashboard = () => { @@ -17,15 +18,16 @@ const Dashboard = () => {
Zyenyo
Bot
+