diff --git a/src/panelapi/server.rs b/src/panelapi/server.rs index 5d34b277..374bd58a 100644 --- a/src/panelapi/server.rs +++ b/src/panelapi/server.rs @@ -19,6 +19,7 @@ use crate::panelapi::types::{ partners::{CreatePartner, Partner, PartnerAction, PartnerType, Partners}, rpc::RPCWebAction, rpclogs::RPCLogEntry, + shop_items::{ShopItem, ShopItemAction}, staff_disciplinary::StaffDisciplinaryTypeAction, staff_positions::StaffPosition, vote_credit_tiers::{VoteCreditTier, VoteCreditTierAction}, @@ -103,6 +104,8 @@ pub async fn init_panelapi(pool: PgPool, cache_http: botox::cache::CacheHttpImpl StaffMemberAction, StaffDisciplinaryTypeAction, VoteCreditTierAction, + ShopItem, + ShopItemAction, BotWhitelistAction, Link, )) @@ -346,6 +349,13 @@ pub enum PanelQuery { /// Action action: VoteCreditTierAction, }, + /// Fetch and update/modify shop items + UpdateShopItems { + /// Login token + login_token: String, + /// Action + action: ShopItemAction, + }, /// Fetch and update/modify bot whitelist UpdateBotWhitelist { /// Login token @@ -3843,6 +3853,204 @@ async fn query( } } } + PanelQuery::UpdateShopItems { + login_token, + action, + } => { + let auth_data = super::auth::check_auth(&state.pool, &login_token) + .await + .map_err(Error::new)?; + + let user_perms = get_user_perms(&state.pool, &auth_data.user_id) + .await + .map_err(Error::new)? + .resolve(); + + match action { + ShopItemAction::List => { + let rows = sqlx::query!( + "SELECT id, name, cents, target_types, benefits, created_at, last_updated, created_by, updated_by, duration, description FROM shop_items ORDER BY created_at DESC" + ) + .fetch_all(&state.pool) + .await + .map_err(Error::new)?; + + let mut entries = Vec::new(); + + for row in rows { + entries.push(ShopItem { + id: row.id, + name: row.name, + cents: row.cents, + target_types: row.target_types, + benefits: row.benefits, + created_at: row.created_at, + last_updated: row.last_updated, + created_by: row.created_by, + updated_by: row.updated_by, + duration: row.duration, + description: row.description, + }); + } + + Ok((StatusCode::OK, Json(entries)).into_response()) + } + ShopItemAction::Create { + id, + name, + cents, + target_types, + benefits, + duration, + description, + } => { + if !perms::has_perm(&user_perms, &"shop_items.create".into()) { + return Ok(( + StatusCode::FORBIDDEN, + "You do not have permission to create shop items [shop_items.create]" + .to_string(), + ) + .into_response()); + } + + if cents < 0.0 { + return Ok(( + StatusCode::BAD_REQUEST, + "Cents cannot be lower than 0".to_string(), + ) + .into_response()); + } + + if duration < 0 { + return Ok(( + StatusCode::BAD_REQUEST, + "Duration cannot be lower than 0".to_string(), + ) + .into_response()); + } + + // Insert entry + sqlx::query!( + "INSERT INTO shop_items (id, name, cents, target_types, benefits, created_by, updated_by, duration, description) VALUES ($1, $2, $3, $4, $5, $6, $6, $7, $8)", + id, + name, + cents, + &target_types, + &benefits, + &auth_data.user_id, + duration, + description, + ) + .execute(&state.pool) + .await + .map_err(Error::new)?; + + Ok((StatusCode::NO_CONTENT, "").into_response()) + } + ShopItemAction::Edit { + id, + name, + cents, + target_types, + benefits, + duration, + description, + } => { + if !perms::has_perm(&user_perms, &"shop_items.update".into()) { + return Ok(( + StatusCode::FORBIDDEN, + "You do not have permission to update shop items [shop_items.update]" + .to_string(), + ) + .into_response()); + } + + if cents < 0.0 { + return Ok(( + StatusCode::BAD_REQUEST, + "Cents cannot be lower than 0".to_string(), + ) + .into_response()); + } + + if duration < 0 { + return Ok(( + StatusCode::BAD_REQUEST, + "Duration cannot be lower than 0".to_string(), + ) + .into_response()); + } + + // Check if entry already exists with same id + if sqlx::query!("SELECT COUNT(*) FROM shop_items WHERE id = $1", id) + .fetch_one(&state.pool) + .await + .map_err(Error::new)? + .count + .unwrap_or(0) + == 0 + { + return Ok(( + StatusCode::BAD_REQUEST, + "Entry with same id does not already exist".to_string(), + ) + .into_response()); + } + + // Update entry + sqlx::query!( + "UPDATE shop_items SET name = $1, cents = $2, target_types = $3, benefits = $4, last_updated = NOW(), updated_by = $5, duration = $6, description = $7 WHERE id = $8", + name, + cents, + &target_types, + &benefits, + &auth_data.user_id, + duration, + description, + id, + ) + .execute(&state.pool) + .await + .map_err(Error::new)?; + + Ok((StatusCode::NO_CONTENT, "").into_response()) + } + ShopItemAction::Delete { id } => { + if !perms::has_perm(&user_perms, &"shop_items.delete".into()) { + return Ok(( + StatusCode::FORBIDDEN, + "You do not have permission to delete shop items [shop_items.delete]" + .to_string(), + ) + .into_response()); + } + + // Check if entry already exists with same vesion + if sqlx::query!("SELECT COUNT(*) FROM shop_items WHERE id = $1", id) + .fetch_one(&state.pool) + .await + .map_err(Error::new)? + .count + .unwrap_or(0) + == 0 + { + return Ok(( + StatusCode::BAD_REQUEST, + "Entry with same id does not already exist".to_string(), + ) + .into_response()); + } + + // Delete entry + sqlx::query!("DELETE FROM shop_items WHERE id = $1", id) + .execute(&state.pool) + .await + .map_err(Error::new)?; + + Ok((StatusCode::NO_CONTENT, "").into_response()) + } + } + } PanelQuery::UpdateBotWhitelist { login_token, action, diff --git a/src/panelapi/types/mod.rs b/src/panelapi/types/mod.rs index 115c709e..f33e2fa0 100644 --- a/src/panelapi/types/mod.rs +++ b/src/panelapi/types/mod.rs @@ -8,6 +8,7 @@ pub mod entity; pub mod partners; pub mod rpc; pub mod rpclogs; +pub mod shop_items; pub mod staff_disciplinary; pub mod staff_members; pub mod staff_positions; diff --git a/src/panelapi/types/shop_items.rs b/src/panelapi/types/shop_items.rs new file mode 100644 index 00000000..e7441d3c --- /dev/null +++ b/src/panelapi/types/shop_items.rs @@ -0,0 +1,90 @@ +use serde::{Deserialize, Serialize}; +use strum_macros::{Display, EnumString, EnumVariantNames}; +use ts_rs::TS; +use utoipa::ToSchema; + +/// Shop items are items that can be purchased by users on the shop +#[derive(Serialize, Deserialize, TS, Clone, ToSchema)] +#[ts(export, export_to = ".generated/ShopItem.ts")] +pub struct ShopItem { + /// The ID of the shop item + pub id: String, + /// The friendly name of the shop item + pub name: String, + /// The description of the shop item + pub description: String, + /// The cents the shop item costs + pub cents: f64, + /// The target type + pub target_types: Vec, + /// The benefits of the shop item + pub benefits: Vec, + /// The number of hours the shop item lasts for + pub duration: i32, + /// The time the shop item was created + pub created_at: chrono::DateTime, + /// The time the shop item was last updated + pub last_updated: chrono::DateTime, + /// Who created the shop item + pub created_by: String, + /// Who last updated the shop item + pub updated_by: String, +} + +#[derive( + Serialize, + Deserialize, + ToSchema, + TS, + EnumString, + EnumVariantNames, + Display, + Clone, + PartialEq, + Default, +)] +#[ts(export, export_to = ".generated/ShopItemAction.ts")] +pub enum ShopItemAction { + /// List all current shop items + #[default] + List, + /// Create a new shop item + Create { + /// The ID of the shop item + id: String, + /// The friendly name of the shop item + name: String, + /// The description of the shop item + description: String, + /// The cents the shop item costs + cents: f64, + /// The target type + target_types: Vec, + /// The benefits of the shop item + benefits: Vec, + /// The number of hours the shop item lasts for + duration: i32, + }, + /// Edit a shop item + Edit { + /// The ID of the shop item + id: String, + /// The friendly name of the shop item + name: String, + /// The description of the shop item + description: String, + /// The cents the shop item costs + cents: f64, + /// The target type + target_types: Vec, + /// The benefits of the shop item + benefits: Vec, + /// The number of hours the shop item lasts for + duration: i32, + }, + /// Deletes a shop item + Delete { + /// The ID of the shop item + id: String, + }, +} diff --git a/src/panelapi/types/vote_credit_tiers.rs b/src/panelapi/types/vote_credit_tiers.rs index f24537c2..d7b40b2f 100644 --- a/src/panelapi/types/vote_credit_tiers.rs +++ b/src/panelapi/types/vote_credit_tiers.rs @@ -64,8 +64,6 @@ pub enum VoteCreditTierAction { votes: i32, }, /// Edit vote credit tier - /// - /// To edit index, use the `SwapIndex` action EditTier { /// The ID of the tier id: String, @@ -83,20 +81,4 @@ pub enum VoteCreditTierAction { /// The ID of the tier id: String, }, - /* - /// Swap the index of two vote credit tiers (A and B) such that the indexes change from (Ia, Ib) -> (Ib, Ia) - SwapIndex { - /// Vote Credit Tier A - a: String, - /// Vote Credit Tier B - b: String, - }, - /// Sets the new index of a vote credit tier - SetIndex { - /// The ID of the tier - id: String, - /// The new index of the tier - index: i32, - }, - */ } diff --git a/src/rpc/core.rs b/src/rpc/core.rs index 2efe97d9..669a7872 100644 --- a/src/rpc/core.rs +++ b/src/rpc/core.rs @@ -998,44 +998,10 @@ impl RPCMethod { return Err("Reason must be lower than/equal to 300 characters".into()); } - let mut tx = state.pool.begin().await?; - - // Clear any entity specific caches - match state.target_type { - TargetType::Bot => { - sqlx::query!("UPDATE bots SET votes = 0 WHERE bot_id = $1", target_id) - .execute(&mut *tx) - .await?; - } - TargetType::Server => { - sqlx::query!( - "UPDATE servers SET votes = 0 WHERE server_id = $1", - target_id - ) - .execute(&mut *tx) - .await?; - } - TargetType::Team => { - sqlx::query!( - "UPDATE teams SET votes = 0 WHERE id = $1", - sqlx::types::Uuid::parse_str(target_id)? - ) - .execute(&mut *tx) - .await?; - } - TargetType::Pack => { - sqlx::query!("UPDATE packs SET votes = 0 WHERE url = $1", target_id) - .execute(&mut *tx) - .await?; - } - }; - sqlx::query!("UPDATE entity_votes SET void = TRUE, void_reason = 'Votes (single entity) reset', voided_at = NOW() WHERE target_type = $1 AND target_id = $2 AND void = FALSE", state.target_type.to_string(), target_id) - .execute(&mut *tx) + .execute(&state.pool) .await?; - tx.commit().await?; - let msg = CreateMessage::default().embed( CreateEmbed::default() .title("__Entity Vote Reset!__") @@ -1062,31 +1028,7 @@ impl RPCMethod { let mut tx = state.pool.begin().await?; - // Clear any entity specific caches - match state.target_type { - TargetType::Bot => { - sqlx::query!("UPDATE bots SET votes = 0") - .execute(&mut *tx) - .await?; - } - TargetType::Server => { - sqlx::query!("UPDATE servers SET votes = 0") - .execute(&mut *tx) - .await?; - } - TargetType::Team => { - sqlx::query!("UPDATE teams SET votes = 0") - .execute(&mut *tx) - .await?; - } - TargetType::Pack => { - sqlx::query!("UPDATE packs SET votes = 0") - .execute(&mut *tx) - .await?; - } - }; - - sqlx::query!("UPDATE entity_votes SET void = TRUE, void_reason = 'Votes (all entities) reset', voided_at = NOW() WHERE target_type = $1", state.target_type.to_string()) + sqlx::query!("UPDATE entity_votes SET void = TRUE, void_reason = 'Votes (all entities) reset', voided_at = NOW() WHERE target_type = $1 AND immutable = false", state.target_type.to_string()) .execute(&mut *tx) .await?; diff --git a/src/tasks/voterestter.rs b/src/tasks/voterestter.rs index b02bf186..cb88d826 100644 --- a/src/tasks/voterestter.rs +++ b/src/tasks/voterestter.rs @@ -1,7 +1,5 @@ use serenity::builder::{CreateEmbed, CreateEmbedFooter, CreateMessage}; -const ENTITY_TYPES: [&str; 4] = ["bots", "servers", "teams", "packs"]; - pub async fn vote_resetter(ctx: &serenity::client::Context) -> Result<(), crate::Error> { let data = ctx.data::(); let pool = &data.pool; @@ -26,18 +24,11 @@ pub async fn vote_resetter(ctx: &serenity::client::Context) -> Result<(), crate: // Set voided to true sqlx::query!( - "UPDATE entity_votes SET void = TRUE, void_reason = 'Automated votes reset', voided_at = NOW() WHERE void = false" + "UPDATE entity_votes SET void = TRUE, void_reason = 'Automated votes reset', voided_at = NOW() WHERE void = false AND immutable = false" ) .execute(&mut *tx) .await?; - // Clear entity-specific tables - for entity_type in ENTITY_TYPES.iter() { - sqlx::query(&format!("UPDATE {} SET votes = 0", entity_type)) - .execute(&mut *tx) - .await?; - } - // Insert into automated_vote_resets sqlx::query!("INSERT INTO automated_vote_resets (created_at) VALUES (NOW())") .execute(&mut *tx)