From 7647d0663b7aa923c046c918660470ff7fe9c774 Mon Sep 17 00:00:00 2001 From: Chiko Date: Wed, 20 Nov 2024 14:00:54 +0000 Subject: [PATCH] feat: refactor bot metadata handling and add votes table Refactor the bot metadata handling to use async/await for improved asynchronous performance when fetching data. This change simplifies the code by removing blocking calls and enhances the readability of the `show` function in `metadata.rs`. Additionally, introduce a new `bot_votes` table in the database to track votes associated with bots. This includes creating a new `BotVote` model to manage vote data effectively. The changes are made to support future features related to bot voting. --- Cargo.toml | 1 + build.rs | 15 ++++++ .../2024-11-15-152616_add_bot_votes/up.sql | 2 +- src/controllers/bot/owners.rs | 12 +---- src/controllers/bot/votes.rs | 51 +++++++++++++++++-- src/controllers/summary.rs | 1 - src/models/bot.rs | 2 +- src/models/vote.rs | 37 +++++++++++++- src/router.rs | 37 +++++++------- 9 files changed, 119 insertions(+), 39 deletions(-) create mode 100644 build.rs diff --git a/Cargo.toml b/Cargo.toml index c79c82a..bb0d200 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ name = "izumo" version = "0.1.0" edition = "2021" +build = "build.rs" # [workspace] # members = ["oauth"] diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..d61b541 --- /dev/null +++ b/build.rs @@ -0,0 +1,15 @@ +// build.rs +use std::env; +use std::fs; +use std::path::Path; + +fn main() { + let version = env::var("CARGO_PKG_VERSION").unwrap(); + let out_dir = env::var("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("version.rs"); + fs::write( + dest_path, + format!("pub const VERSION: &str = \"{}\";", version), + ) + .unwrap(); +} diff --git a/migrations/2024-11-15-152616_add_bot_votes/up.sql b/migrations/2024-11-15-152616_add_bot_votes/up.sql index caa6ae5..a8307ca 100644 --- a/migrations/2024-11-15-152616_add_bot_votes/up.sql +++ b/migrations/2024-11-15-152616_add_bot_votes/up.sql @@ -2,7 +2,7 @@ CREATE TABLE bot_votes ( bot_id VARCHAR NOT NULL REFERENCES bots (id) ON DELETE CASCADE, date DATE DEFAULT CURRENT_DATE NOT NULL, - votes INTEGER DEFAULT 0 NOT NULL, + votes INTEGER DEFAULT 1 NOT NULL, PRIMARY KEY (bot_id, date) ); diff --git a/src/controllers/bot/owners.rs b/src/controllers/bot/owners.rs index f834f7c..5bdbc23 100644 --- a/src/controllers/bot/owners.rs +++ b/src/controllers/bot/owners.rs @@ -1,18 +1,10 @@ -use crate::{ - app::AppState, - models::Bot, - util::errors::{bot_not_found, AppResult}, - views::EncodableBotOwner, -}; +use crate::{app::AppState, models::Bot, util::errors::AppResult, views::EncodableBotOwner}; use axum::{extract::Path, Json}; -use diesel::OptionalExtension; use serde_json::Value; /// Handles `GET /bots/:bot_id/owners` requests. pub async fn owners(state: AppState, Path(id): Path) -> AppResult> { let mut conn = state.db_read().await?; - // spawn_blocking(move || { - // let conn: &mut AsyncConnectionWrapper<_> = &mut conn.into(); let id = id.as_str(); let bot: Bot = Bot::find(&mut conn, id).await?; @@ -25,6 +17,4 @@ pub async fn owners(state: AppState, Path(id): Path) -> AppResult>(); Ok(Json(json!({ "users": owners }))) - // }) - // .await } diff --git a/src/controllers/bot/votes.rs b/src/controllers/bot/votes.rs index f2366c9..6ec1e80 100644 --- a/src/controllers/bot/votes.rs +++ b/src/controllers/bot/votes.rs @@ -1,3 +1,7 @@ +use crate::auth::AuthCheck; +use crate::models::vote::NewBotVote; +use crate::task::spawn_blocking; +use crate::util::errors::bot_not_found; use crate::views::EncodableBotVote; use crate::{ app::AppState, @@ -6,9 +10,9 @@ use crate::{ util::errors::AppResult, }; use axum::extract::Path; +use axum::http::request::Parts; use axum::Json; -use diesel::prelude::*; -use diesel_async::RunQueryDsl; +use diesel_async::async_connection_wrapper::AsyncConnectionWrapper; use serde_json::Value; /// Handles the `GET /bots/:bot_id/votes` route. @@ -16,12 +20,15 @@ pub async fn votes(app: AppState, Path(bot_id): Path) -> AppResult) -> AppResult>(); - let total_votes: i64 = bot_votes::table.count().get_result(&mut conn).await?; + let sum_votes = sql::("SUM(bot_votes.votes)"); + let total_votes: i64 = BotVote::belonging_to(&bot) + .select(sum_votes) + .get_result(&mut conn) + .await?; Ok(Json(json!({ "bot_votes": votes, @@ -38,3 +49,35 @@ pub async fn votes(app: AppState, Path(bot_id): Path) -> AppResult, + req: Parts, +) -> AppResult> { + let conn = app.db_write().await?; + use diesel::OptionalExtension; + use diesel::RunQueryDsl; + + spawn_blocking(move || { + let conn: &mut AsyncConnectionWrapper<_> = &mut conn.into(); + + // Make sure user is logged in + let _ = AuthCheck::only_cookie().check(&req, conn)?.user_id(); + let bot_id = bot_id.as_str(); + + // Check if bot exists + let _: Bot = Bot::by_id(bot_id) + .first(conn) + .optional()? + .ok_or_else(|| bot_not_found(bot_id))?; + + let new_vote = NewBotVote::new(bot_id); + let vote = new_vote.create(conn)?; + + Ok(Json(EncodableBotVote::from(vote))) + }) + .await +} diff --git a/src/controllers/summary.rs b/src/controllers/summary.rs index 6cfb4ba..a089d3e 100644 --- a/src/controllers/summary.rs +++ b/src/controllers/summary.rs @@ -4,7 +4,6 @@ use crate::schema::{bots, bots_categories, categories}; use crate::util::errors::AppResult; use crate::views::{EncodableBot, EncodableCategory}; use axum::Json; -use diesel::prelude::*; use diesel::{BelongingToDsl, ExpressionMethods, QueryDsl, SelectableHelper}; use diesel_async::AsyncPgConnection; diff --git a/src/models/bot.rs b/src/models/bot.rs index 6abc783..acb08ed 100644 --- a/src/models/bot.rs +++ b/src/models/bot.rs @@ -129,7 +129,7 @@ impl Bot { #[dsl::auto_type(no_type_alias)] pub fn by_id(id: &str) -> _ { - bots::table.find(id).select(bots::id) + bots::table.find(id) } pub async fn owners(&self, conn: &mut AsyncPgConnection) -> AppResult> { diff --git a/src/models/vote.rs b/src/models/vote.rs index e260ec3..8b767bc 100644 --- a/src/models/vote.rs +++ b/src/models/vote.rs @@ -1,11 +1,46 @@ +use crate::diesel::ExpressionMethods; +use crate::models::util::diesel::Conn; use crate::models::Bot; use crate::schema::bot_votes; use chrono::NaiveDate; +use diesel::RunQueryDsl; +use diesel::{QueryResult, SelectableHelper}; -#[derive(Queryable, Identifiable, Associations, Debug, Clone)] +#[derive(Queryable, Identifiable, Associations, Selectable, Debug, Clone)] #[diesel(primary_key(bot_id, date), belongs_to(Bot))] pub struct BotVote { pub bot_id: String, pub date: NaiveDate, pub votes: i32, } + +#[derive(Insertable, Debug, Clone)] +#[diesel( + table_name = bot_votes, + check_for_backend(diesel::pg::Pg), +)] +pub struct NewBotVote<'a> { + pub bot_id: &'a str, +} + +impl<'a> NewBotVote<'a> { + pub fn new(bot_id: &'a str) -> NewBotVote { + Self { bot_id } + } + + pub fn create(&self, conn: &mut impl Conn) -> QueryResult { + conn.transaction(|conn| { + use crate::schema::bot_votes::dsl::*; + + let vote: BotVote = diesel::insert_into(bot_votes) + .values(self) + .on_conflict((bot_id, date)) + .do_update() + .set(votes.eq(votes + 1)) + .returning(BotVote::as_returning()) + .get_result(conn)?; + + Ok(vote) + }) + } +} diff --git a/src/router.rs b/src/router.rs index 263ccbb..b3e47ab 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,21 +1,14 @@ -use std::sync::Arc; - use super::controllers::*; -use crate::{ - middleware, - util::errors::{not_found, AppResult}, -}; +use crate::app::{App, AppState}; +use crate::middleware; +use crate::util::errors::{not_found, AppResult}; +use axum::response::IntoResponse; use axum::routing::post; -use axum::{ - response::IntoResponse, - routing::{delete, get}, - Json, Router, -}; -use crates_io_env_vars::required_var; +use axum::routing::{delete, get}; +use axum::{Json, Router}; use reqwest::{Method, StatusCode}; use serde_json::Value; - -use crate::app::{App, AppState}; +use std::sync::Arc; pub fn build_handler(app: Arc) -> axum::Router { let state = AppState(app); @@ -34,9 +27,12 @@ pub fn build_axum_router(state: AppState) -> Router<()> { .route("/private/session", delete(user::session::logout)) // Bots .route("/bots/:bot_id", get(bot::metadata::show)) - .route("/bots/total_votesnew", post(bot::manage::publish)) + .route("/bots/new", post(bot::manage::publish)) .route("/bots/:bot_id/owners", get(bot::owners::owners)) - .route("/bots/:bot_id/votes", get(bot::votes::votes)) + .route( + "/bots/:bot_id/votes", + get(bot::votes::votes).post(bot::votes::vote), + ) // Categories .route("/categories", get(category::index)) .route("/categories/:category_id", get(category::show)) @@ -57,11 +53,12 @@ pub fn build_axum_router(state: AppState) -> Router<()> { .with_state(state) } -async fn handler() -> AppResult> { - let version = required_var("CARGO_PKG_VERSION"); +// imports `VERSION` constant +include!(concat!(env!("OUT_DIR"), "/version.rs")); +async fn handler() -> AppResult> { Ok(Json(serde_json::json!({ - "name": "izumo (api)", - "version": version.unwrap_or("unknown".to_string()), + "name": "Izumo (api)", + "version": VERSION }))) }