Skip to content

Commit

Permalink
feat: refactor bot metadata handling and add votes table (#113)
Browse files Browse the repository at this point in the history
  • Loading branch information
chikof authored Nov 20, 2024
2 parents ffa61af + 7647d06 commit 97a62fb
Show file tree
Hide file tree
Showing 15 changed files with 293 additions and 95 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
name = "izumo"
version = "0.1.0"
edition = "2021"
build = "build.rs"

# [workspace]
# members = ["oauth"]
Expand Down
15 changes: 15 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -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();
}
2 changes: 2 additions & 0 deletions migrations/2024-11-15-152616_add_bot_votes/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE IF EXISTS bot_votes;
8 changes: 8 additions & 0 deletions migrations/2024-11-15-152616_add_bot_votes/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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 1 NOT NULL,
PRIMARY KEY (bot_id, date)
);

1 change: 1 addition & 0 deletions src/controllers/bot.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod manage;
pub mod metadata;
pub mod owners;
pub mod votes;
84 changes: 39 additions & 45 deletions src/controllers/bot/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ use crate::app::AppState;
use crate::models::category::{BotCategory, Category};
use crate::models::Bot;
use crate::schema::categories;
use crate::task::spawn_blocking;
use crate::util::errors::{bad_request, bot_not_found, AppResult, BoxedAppError};
use crate::util::errors::{bad_request, AppResult, BoxedAppError};
use crate::util::request_helper::RequestUtils;
use crate::views::{EncodableBot, EncodableCategory};

Expand All @@ -12,54 +11,49 @@ use axum::http::request::Parts;
use axum::Json;

use diesel::prelude::*;
use diesel_async::async_connection_wrapper::AsyncConnectionWrapper;
use diesel_async::RunQueryDsl;
use serde_json::Value;
use std::str::FromStr;

/// Handles the `GET /bots/:bot_id`
pub async fn show(app: AppState, Path(id): Path<String>, req: Parts) -> AppResult<Json<Value>> {
let conn = app.db_read().await?;

spawn_blocking(move || {
let conn: &mut AsyncConnectionWrapper<_> = &mut conn.into();
let id = id.as_str();

let include = req
.query()
.get("include")
.map(|mode| ShowIncludeMode::from_str(mode))
.transpose()?
.unwrap_or_default();

let bot = Bot::find(conn, id)
.optional()?
.ok_or_else(|| bot_not_found(id))?;

let cats = if include.categories {
Some(
BotCategory::belonging_to(&bot)
.inner_join(categories::table)
.select(categories::all_columns)
.load(conn)?,
)
} else {
None
};

let encodable_bot = EncodableBot::from(bot.clone(), cats.as_deref());

let encodable_cats = cats.map(|cats| {
cats.into_iter()
.map(Category::into)
.collect::<Vec<EncodableCategory>>()
});

Ok(Json(serde_json::json!({
"bot": encodable_bot,
"categories": encodable_cats,
})))
})
.await
let mut conn = app.db_read().await?;

let id = id.as_str();

let include = req
.query()
.get("include")
.map(|mode| ShowIncludeMode::from_str(mode))
.transpose()?
.unwrap_or_default();

let bot = Bot::find(&mut conn, id).await?;

let cats = if include.categories {
Some(
BotCategory::belonging_to(&bot)
.inner_join(categories::table)
.select(categories::all_columns)
.load(&mut conn)
.await?,
)
} else {
None
};

let encodable_bot = EncodableBot::from(bot.clone(), cats.as_deref());

let encodable_cats = cats.map(|cats| {
cats.into_iter()
.map(Category::into)
.collect::<Vec<EncodableCategory>>()
});

Ok(Json(serde_json::json!({
"bot": encodable_bot,
"categories": encodable_cats,
})))
}

#[derive(Debug)]
Expand Down
35 changes: 11 additions & 24 deletions src/controllers/bot/owners.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,20 @@
use crate::{
app::AppState,
models::Bot,
task::spawn_blocking,
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 diesel_async::async_connection_wrapper::AsyncConnectionWrapper;
use serde_json::Value;

/// Handles `GET /bots/:bot_id/owners` requests.
pub async fn owners(state: AppState, Path(id): Path<String>) -> AppResult<Json<Value>> {
let conn = state.db_read().await?;
spawn_blocking(move || {
let conn: &mut AsyncConnectionWrapper<_> = &mut conn.into();
let id = id.as_str();
let mut conn = state.db_read().await?;
let id = id.as_str();

let bot: Bot = Bot::find(conn, id)
.optional()?
.ok_or_else(|| bot_not_found(id))?;
let bot: Bot = Bot::find(&mut conn, id).await?;

let owners = bot
.owners(conn)?
.into_iter()
.map(|owner| owner.into())
.collect::<Vec<EncodableBotOwner>>();
let owners = bot
.owners(&mut conn)
.await?
.into_iter()
.map(|owner| owner.into())
.collect::<Vec<EncodableBotOwner>>();

Ok(Json(json!({ "users": owners })))
})
.await
Ok(Json(json!({ "users": owners })))
}
83 changes: 83 additions & 0 deletions src/controllers/bot/votes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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,
models::{Bot, BotVote},
schema::bot_votes,
util::errors::AppResult,
};
use axum::extract::Path;
use axum::http::request::Parts;
use axum::Json;
use diesel_async::async_connection_wrapper::AsyncConnectionWrapper;
use serde_json::Value;

/// Handles the `GET /bots/:bot_id/votes` route.
pub async fn votes(app: AppState, Path(bot_id): Path<String>) -> AppResult<Json<Value>> {
let mut conn = app.db_read().await?;

use diesel::dsl::*;
use diesel::prelude::*;
use diesel::sql_types::BigInt;
use diesel_async::RunQueryDsl;

let bot = Bot::find(&mut conn, bot_id.as_str()).await?;

// last 90 votes
let votes = BotVote::belonging_to(&bot)
.filter(bot_votes::date.gt(date(now - 90.days())))
.order((bot_votes::date.asc(), bot_votes::bot_id.desc()))
.load(&mut conn)
.await?
.into_iter()
.map(BotVote::into)
.collect::<Vec<EncodableBotVote>>();

let sum_votes = sql::<BigInt>("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,
"extra": {
"total_votes": total_votes,
}
})))
}

// TODO: 12h user ratelimit
/// Handles the `POST /bots/:bot_id/votes` route.
pub async fn vote(
app: AppState,
Path(bot_id): Path<String>,
req: Parts,
) -> AppResult<Json<EncodableBotVote>> {
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
}
1 change: 0 additions & 1 deletion src/controllers/summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub use self::owners::BotOwner;
pub use self::review::BotReview;
pub use self::token::{ApiToken, CreatedApiToken};
pub use self::user::User;
pub use self::vote::BotVote;

pub mod bot;
pub mod category;
Expand All @@ -14,3 +15,4 @@ pub mod token;
pub mod user;
pub mod util;
pub mod vanity;
pub mod vote;
29 changes: 23 additions & 6 deletions src/models/bot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ use crate::models::util::diesel::Conn;
use crate::models::{BotOwner, User};
use crate::schema::{bot_owners, bots, users};
use crate::sql::pg_enum;
use crate::util::errors::AppResult;
use crate::util::errors::{bot_not_found, AppResult};
use derivative::Derivative;
use diesel::prelude::*;
use diesel::{deserialize::FromSqlRow, expression::AsExpression};
use diesel::{dsl, ExpressionMethods, QueryDsl, QueryResult, SelectableHelper};
use diesel_async::AsyncPgConnection;
use serde::{Deserialize, Serialize};

pg_enum! {
Expand Down Expand Up @@ -114,15 +115,30 @@ pub struct Bot {
}

impl Bot {
pub fn find(conn: &mut impl Conn, id: &str) -> QueryResult<Bot> {
bots::table.find(id).first(conn)
pub async fn find(conn: &mut AsyncPgConnection, id: &str) -> AppResult<Bot> {
use diesel::OptionalExtension;
use diesel_async::RunQueryDsl;

bots::table
.find(id)
.first::<Bot>(conn)
.await
.optional()?
.ok_or_else(|| bot_not_found(id))
}

#[dsl::auto_type(no_type_alias)]
pub fn by_id(id: &str) -> _ {
bots::table.find(id)
}

pub fn owners(&self, conn: &mut impl Conn) -> AppResult<Vec<User>> {
pub async fn owners(&self, conn: &mut AsyncPgConnection) -> AppResult<Vec<User>> {
use diesel_async::RunQueryDsl;
let owners = BotOwner::by_bot_id(&self.id)
.inner_join(users::table)
.select(users::all_columns)
.load(conn)?;
.load(conn)
.await?;

Ok(owners)
}
Expand Down Expand Up @@ -224,6 +240,7 @@ impl<'a> NewBot<'a> {
}

pub fn create(&self, conn: &mut impl Conn, user_id: String) -> QueryResult<Bot> {
use diesel::RunQueryDsl;
conn.transaction(|conn| {
let bot: Bot = diesel::insert_into(bots::table)
.values(self)
Expand Down
46 changes: 46 additions & 0 deletions src/models/vote.rs
Original file line number Diff line number Diff line change
@@ -0,0 +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, 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<BotVote> {
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)
})
}
}
Loading

0 comments on commit 97a62fb

Please sign in to comment.