Skip to content

Commit

Permalink
Botstats (#114)
Browse files Browse the repository at this point in the history
* wipfeat: format + botstats query

* feat: bot statistics on home page
  • Loading branch information
VanillaViking authored Jan 15, 2024
1 parent 79abdb8 commit dde69ad
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 38 deletions.
26 changes: 14 additions & 12 deletions zyenyo-backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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()
Expand Down
19 changes: 17 additions & 2 deletions zyenyo-backend/src/models.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -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
}
39 changes: 39 additions & 0 deletions zyenyo-backend/src/routes/botstats.rs
Original file line number Diff line number Diff line change
@@ -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<Context>) -> 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<Context>) -> Result<BotStats, Box<dyn Error>> {
let tests: Collection<Test> = context.db.collection("testsv2");
let users: Collection<User> = context.db.collection("usersv2");
let prompts: Collection<Prompt> = 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,
})
}
4 changes: 3 additions & 1 deletion zyenyo-backend/src/routes/mod.rs
Original file line number Diff line number Diff line change
@@ -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);
}
62 changes: 41 additions & 21 deletions zyenyo-backend/src/routes/prompts.rs
Original file line number Diff line number Diff line change
@@ -1,54 +1,71 @@
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")]
sort_by: String,
#[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<Context>, controls: web::Query<PromptsConfig>) -> impl Responder {
async fn prompts(
context: web::Data<Context>,
controls: web::Query<PromptsConfig>,
) -> 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<Context>, controls: PromptsConfig) -> Result<Vec<Prompt>, Box<dyn Error>> {
async fn prompt_query(
context: web::Data<Context>,
controls: PromptsConfig,
) -> Result<Vec<Prompt>, Box<dyn Error>> {
let collection: Collection<Prompt> = 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"}}
Expand All @@ -57,10 +74,13 @@ async fn prompt_query(context: web::Data<Context>, 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<Document> = results.try_collect().await?;
Ok(prompts_vec.iter().filter_map(|doc| bson::from_document::<Prompt>(doc.to_owned()).ok()).collect())
Ok(prompts_vec
.iter()
.filter_map(|doc| bson::from_document::<Prompt>(doc.to_owned()).ok())
.collect())
}
47 changes: 47 additions & 0 deletions zyenyo-frontend/src/components/HomePage/BotStats.tsx
Original file line number Diff line number Diff line change
@@ -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<BotStats>()
useEffect(() => {
async function query() {
const res = await axios.get("/api/botstats")
setBotStats(res.data as BotStats)

}
query()
}, [])

return (
<div className={className}>
<div className="border border-zinc-500 my-3 hidden md:block md:w-[600px]" />
<div className={`flex flex-col items-center gap-6 md:flex-row md:gap-[100px] justify-center`}>
<div className="flex flex-col gap-2 items-start">
<div className="text-[#F51A1F] text-3xl">{botStats?.total_tests}+</div>
<div className="text-zinc-300 text-md">Tests Served</div>
</div>

<div className="flex flex-col gap-2 items-start">
<div className="text-orange-400 text-3xl">{botStats?.total_users}</div>
<div className="text-zinc-300 text-md">Active Users</div>
</div>

<div className="flex flex-col gap-2 items-start">
<div className="text-amber-400 text-3xl">{botStats?.total_prompts}</div>
<div className="text-zinc-300 text-md">Typing Prompts</div>
</div>
</div>
<div className="border border-zinc-500 my-3 hidden md:block md:w-[600px]" />
</div>
)
}

export default BotStats
6 changes: 4 additions & 2 deletions zyenyo-frontend/src/components/HomePage/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Link from "next/link";
<StarIcon className="h-6 w-6 text-gray-500" />

import {useEffect, useState} from "react"
import BotStats from "./BotStats";

const Dashboard = () => {

Expand All @@ -17,15 +18,16 @@ const Dashboard = () => {
<div className='flex flex-row gap-0 text-6xl md:text-9xl mb-12'>
<div className='text-white underline underline-offset-8'>Zyenyo</div><div className='text-[#F51A1F]'>Bot</div>
</div>
<BotStats className="my-5 hidden md:block" />
<div className="grid grid-cols-1 md:grid-cols-2 mt-auto mb-auto gap-3">

<a className="flex flex-col justify-evenly hover:bg-zinc-800 ease-in p-6 gap-3 max-w-[350px] h-[180px] text-left border-2 border-white rounded-3xl " href="https://discord.com/api/oauth2/authorize?client_id=696614233944752130&permissions=137439283200&scope=bot" target="_blank">
<Link className="flex flex-col justify-evenly hover:bg-zinc-800 ease-in p-6 gap-3 max-w-[350px] h-[180px] text-left border-2 border-white rounded-3xl " href="https://discord.com/api/oauth2/authorize?client_id=696614233944752130&permissions=137439283200&scope=bot" target="_blank">
<StarIcon className="h-8 w-8 text-[#F51A1F]" />
<div>
<p className="text-lg mb-1 font-bold">Get Zyenyo</p>
<p className="text-sm text-gray-50">Add Zyenyo to your server.</p>
</div>
</a>
</Link>

<Link className="flex flex-col justify-evenly hover:bg-zinc-800 ease-in p-6 gap-3 max-w-[350px] h-[180px] text-left border-2 border-white rounded-3xl " href="/" >
<CommandLineIcon className="h-8 w-8 text-[#F51A1F]" />
Expand Down

0 comments on commit dde69ad

Please sign in to comment.