From 8b3b0ef07bf6133cf9e83634f311351e6490d11f Mon Sep 17 00:00:00 2001 From: Orion Railean Date: Sat, 24 Aug 2024 08:47:14 +1000 Subject: [PATCH 01/13] add feedback --- .gitignore | 2 +- ...4c1ce5a6220c6d13e4194f22d82bf4f344f8e.json | 58 +++++++ ...347be0211fe7f9da5fb975b68be987559da96.json | 18 +++ ...4b0f64fb752167ece74de940c6838283df569.json | 23 --- .../20240823213817_add_feedback.down.sql | 5 + migrations/20240823213817_add_feedback.up.sql | 24 +++ openapi.yml | 2 +- src/endpoints/mod.rs | 1 + src/endpoints/mod_feedback.rs | 153 ++++++++++++++++++ src/endpoints/mod_versions.rs | 40 ++--- src/main.rs | 2 + src/types/models/mod.rs | 1 + src/types/models/mod_entity.rs | 10 +- src/types/models/mod_feedback.rs | 109 +++++++++++++ 14 files changed, 400 insertions(+), 48 deletions(-) create mode 100644 .sqlx/query-52cff6768eb7b6e4a6401e703474c1ce5a6220c6d13e4194f22d82bf4f344f8e.json create mode 100644 .sqlx/query-5de44a1d52bf36c4b4cf0c72af0347be0211fe7f9da5fb975b68be987559da96.json delete mode 100644 .sqlx/query-8357be940f9bf889c74240f31454b0f64fb752167ece74de940c6838283df569.json create mode 100644 migrations/20240823213817_add_feedback.down.sql create mode 100644 migrations/20240823213817_add_feedback.up.sql create mode 100644 src/endpoints/mod_feedback.rs create mode 100644 src/types/models/mod_feedback.rs diff --git a/.gitignore b/.gitignore index 009b8f3..1537625 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ /target *.db .env - +.idea/ .vscode/settings.json \ No newline at end of file diff --git a/.sqlx/query-52cff6768eb7b6e4a6401e703474c1ce5a6220c6d13e4194f22d82bf4f344f8e.json b/.sqlx/query-52cff6768eb7b6e4a6401e703474c1ce5a6220c6d13e4194f22d82bf4f344f8e.json new file mode 100644 index 0000000..244559c --- /dev/null +++ b/.sqlx/query-52cff6768eb7b6e4a6401e703474c1ce5a6220c6d13e4194f22d82bf4f344f8e.json @@ -0,0 +1,58 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT mf.id, mf.reviewer_id, dev.display_name AS reviewer_name, dev.admin AS reviewer_admin, mf.positive, mf.feedback, mf.decision\n FROM mod_feedback mf\n\t\t\tINNER JOIN developers dev ON dev.id = mf.reviewer_id\n WHERE mf.mod_version_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "reviewer_id", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "reviewer_name", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "reviewer_admin", + "type_info": "Bool" + }, + { + "ordinal": 4, + "name": "positive", + "type_info": "Bool" + }, + { + "ordinal": 5, + "name": "feedback", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "decision", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "52cff6768eb7b6e4a6401e703474c1ce5a6220c6d13e4194f22d82bf4f344f8e" +} diff --git a/.sqlx/query-5de44a1d52bf36c4b4cf0c72af0347be0211fe7f9da5fb975b68be987559da96.json b/.sqlx/query-5de44a1d52bf36c4b4cf0c72af0347be0211fe7f9da5fb975b68be987559da96.json new file mode 100644 index 0000000..c9134c6 --- /dev/null +++ b/.sqlx/query-5de44a1d52bf36c4b4cf0c72af0347be0211fe7f9da5fb975b68be987559da96.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO mod_feedback (mod_version_id, reviewer_id, positive, feedback, decision)\n VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (mod_version_id, reviewer_id)\n DO UPDATE SET positive = EXCLUDED.positive, feedback = EXCLUDED.feedback, decision = EXCLUDED.decision", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int4", + "Bool", + "Text", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "5de44a1d52bf36c4b4cf0c72af0347be0211fe7f9da5fb975b68be987559da96" +} diff --git a/.sqlx/query-8357be940f9bf889c74240f31454b0f64fb752167ece74de940c6838283df569.json b/.sqlx/query-8357be940f9bf889c74240f31454b0f64fb752167ece74de940c6838283df569.json deleted file mode 100644 index 37e16d1..0000000 --- a/.sqlx/query-8357be940f9bf889c74240f31454b0f64fb752167ece74de940c6838283df569.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "select id from mod_versions where mod_id = $1 and version = $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false - ] - }, - "hash": "8357be940f9bf889c74240f31454b0f64fb752167ece74de940c6838283df569" -} diff --git a/migrations/20240823213817_add_feedback.down.sql b/migrations/20240823213817_add_feedback.down.sql new file mode 100644 index 0000000..607f802 --- /dev/null +++ b/migrations/20240823213817_add_feedback.down.sql @@ -0,0 +1,5 @@ +-- Add down migration script here + +DROP TABLE IF EXISTS mod_feedback; +DROP INDEX IF EXISTS public.idx_mod_feedback_mod_version_id; +DROP INDEX IF EXISTS public.idx_mod_feedback_reviewer_id; \ No newline at end of file diff --git a/migrations/20240823213817_add_feedback.up.sql b/migrations/20240823213817_add_feedback.up.sql new file mode 100644 index 0000000..2698d2f --- /dev/null +++ b/migrations/20240823213817_add_feedback.up.sql @@ -0,0 +1,24 @@ +-- Add up migration script here + +CREATE TABLE mod_feedback +( + id SERIAL PRIMARY KEY NOT NULL, + mod_version_id INTEGER NOT NULL, + reviewer_id INTEGER NOT NULL, + positive BOOLEAN NOT NULL, + feedback TEXT COLLATE pg_catalog."default" NOT NULL DEFAULT 'No feedback provided.'::text, + decision BOOLEAN NOT NULL DEFAULT false, + CONSTRAINT mod_feedback_mod_id_reviewer_id_key UNIQUE (mod_version_id, reviewer_id), + CONSTRAINT mod_feedback_mod_version_id_fkey FOREIGN KEY (mod_version_id) + REFERENCES public.mod_versions (id) + ON DELETE CASCADE, + CONSTRAINT mod_feedback_reviewer_id_fkey FOREIGN KEY (reviewer_id) + REFERENCES public.developers (id) + ON DELETE CASCADE +); + +CREATE INDEX idx_mod_feedback_mod_version_id + ON public.mod_feedback (mod_version_id); + +CREATE INDEX idx_mod_feedback_reviewer_id + ON public.mod_feedback (reviewer_id); \ No newline at end of file diff --git a/openapi.yml b/openapi.yml index 0145531..c4edf85 100644 --- a/openapi.yml +++ b/openapi.yml @@ -465,7 +465,7 @@ paths: tags: - mods summary: Add a developer to a mod - description: This endpoint is only used for adding a developer to a mod. Must be the owner the mod to access this endpoint. + description: This endpoint is only used for adding a developer to a mod. Must be the owner of the mod to access this endpoint. security: - bearerAuth: [] diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 8a59284..5d99867 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -4,3 +4,4 @@ pub mod mod_versions; pub mod mods; pub mod tags; pub mod stats; +pub(crate) mod mod_feedback; \ No newline at end of file diff --git a/src/endpoints/mod_feedback.rs b/src/endpoints/mod_feedback.rs new file mode 100644 index 0000000..e769dbd --- /dev/null +++ b/src/endpoints/mod_feedback.rs @@ -0,0 +1,153 @@ +use actix_web::{get, post, web, HttpResponse, Responder}; +use serde::{Deserialize}; +use sqlx::Acquire; + +use crate::{ + extractors::auth::Auth, + types::{ + api::{ApiError, ApiResponse}, + models::{ + developer::{Developer}, + }, + }, + AppData, + webhook::send_webhook +}; +use crate::types::models::mod_version::ModVersion; +use crate::types::models::mod_feedback::ModFeedback; +use crate::types::models::mod_version_status::ModVersionStatusEnum; + +#[derive(Deserialize)] +pub struct GetModFeedbackPath { + id: String, + version: String +} + +#[derive(Deserialize)] +pub struct PostModFeedbackPayload { + positive: bool, + feedback: String, + decision: Option, // Admin Only: Setting this will turn the request into a decision, like PUT v1/mods/{id}/versions/{version} +} + +#[get("/v1/mods/{id}/versions/{version}/feedback")] +pub async fn get_mod_feedback( + data: web::Data, + path: web::Path, + auth: Auth, +) -> Result { + let dev = auth.developer()?; + let mut pool = data.db.acquire().await.or(Err(ApiError::DbAcquireError))?; + + if !Developer::has_access_to_mod(dev.id, &path.id, &mut pool).await? && !dev.admin { + return Err(ApiError::Forbidden); + } + + let mod_version = { + if path.version == "latest" { + ModVersion::get_latest_for_mod(&path.id, None, vec![], None, &mut pool).await? + } else { + ModVersion::get_one(&path.id, &path.version, false, false, &mut pool).await? + } + }; + + let feedback = ModFeedback::get_for_mod_version_id(&mod_version, &mut pool).await?; + + Ok(web::Json(ApiResponse { + error: "".to_string(), + payload: feedback, + })) +} + +#[post("/v1/mods/{id}/versions/{version}/feedback")] +pub async fn post_mod_feedback( + data: web::Data, + path: web::Path, + payload: web::Json, + auth: Auth, +) -> Result { + let dev = auth.developer()?; + let mut pool = data.db.acquire().await.or(Err(ApiError::DbAcquireError))?; + let mut transaction = pool.begin().await.or(Err(ApiError::TransactionError))?; + + if (!dev.verified && !dev.admin) || Developer::has_access_to_mod(dev.id, &path.id, &mut transaction).await? { + return Err(ApiError::Forbidden); + } + + let decision = payload.decision.unwrap_or(false); + let mut status = None; + if decision { + if !dev.admin { + return Err(ApiError::Forbidden); + } + status = Some(match payload.positive { + true => ModVersionStatusEnum::Accepted, + false => ModVersionStatusEnum::Rejected, + }); + } + + let mod_version = { + if path.version == "latest" { + ModVersion::get_latest_for_mod(&path.id, None, vec![], None, &mut transaction).await? + } else { + ModVersion::get_one(&path.id, &path.version, false, false, &mut transaction).await? + } + }; + + let result = ModFeedback::set(&mod_version, dev.id, payload.positive, &payload.feedback, decision, &mut transaction).await; + + if result.is_err() { + transaction + .rollback() + .await + .or(Err(ApiError::TransactionError))?; + return Err(result.err().unwrap()); + } + + if let Some(status) = status { + if let Err(e) = ModVersion::update_version( + mod_version.id, + status, + payload.feedback.clone().into(), + dev.id, + &mut transaction, + ) + .await + { + transaction + .rollback() + .await + .or(Err(ApiError::TransactionError))?; + return Err(e); + } + + if status == ModVersionStatusEnum::Accepted { + let approved_count = ModVersion::get_accepted_count(mod_version.mod_id.as_str(), &mut transaction).await?; + + let is_update = approved_count > 0; + + let owner = Developer::fetch_for_mod(path.id.as_str(), &mut transaction) + .await? + .into_iter() + .find(|dev| dev.is_owner); + + send_webhook( + mod_version.mod_id, + mod_version.name.clone(), + mod_version.version.clone(), + is_update, + owner.as_ref().unwrap().clone(), + dev.clone(), + data.webhook_url.clone(), + data.app_url.clone() + ).await; + } + } + + transaction + .commit() + .await + .or(Err(ApiError::TransactionError))?; + + Ok(HttpResponse::NoContent()) +} \ No newline at end of file diff --git a/src/endpoints/mod_versions.rs b/src/endpoints/mod_versions.rs index b5d962b..9d58dc3 100644 --- a/src/endpoints/mod_versions.rs +++ b/src/endpoints/mod_versions.rs @@ -18,6 +18,7 @@ use crate::{ }, }, webhook::send_webhook, AppData }; +use crate::types::models::mod_feedback::ModFeedback; #[derive(Deserialize)] struct IndexPath { @@ -286,6 +287,8 @@ pub async fn create_version( Ok(HttpResponse::NoContent()) } +// POST /v1/mods/{id}/versions/{version}/feedback should supersede this endpoint +// adding compatibility for now #[put("v1/mods/{id}/versions/{version}")] pub async fn update_version( path: web::Path, @@ -307,26 +310,9 @@ pub async fn update_version( ).await?; let approved_count = ModVersion::get_accepted_count(version.mod_id.as_str(), &mut pool).await?; let mut transaction = pool.begin().await.or(Err(ApiError::TransactionError))?; - let id = match sqlx::query!( - "select id from mod_versions where mod_id = $1 and version = $2", - &path.id, - path.version.trim_start_matches('v') - ) - .fetch_optional(&mut *transaction) - .await - { - Ok(Some(id)) => id.id, - Ok(None) => { - return Err(ApiError::NotFound(String::from("Not Found"))); - } - Err(e) => { - log::error!("{}", e); - return Err(ApiError::DbError); - } - }; if let Err(e) = ModVersion::update_version( - id, + version.id, payload.status, payload.info.clone(), dev.id, @@ -340,10 +326,28 @@ pub async fn update_version( .or(Err(ApiError::TransactionError))?; return Err(e); } + + if let Err(e) = ModFeedback::set( + &version, + dev.id, + payload.status == ModVersionStatusEnum::Accepted, + payload.info.as_deref().unwrap_or_default(), + true, + &mut transaction + ).await { + transaction + .rollback() + .await + .or(Err(ApiError::TransactionError))?; + return Err(e); + } + transaction .commit() .await .or(Err(ApiError::TransactionError))?; + + if payload.status == ModVersionStatusEnum::Accepted { let is_update = approved_count > 0; diff --git a/src/main.rs b/src/main.rs index c808c91..53a1a3f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -122,6 +122,8 @@ async fn main() -> anyhow::Result<()> { .service(endpoints::developers::update_developer) .service(endpoints::tags::index) .service(endpoints::stats::get_stats) + .service(endpoints::mod_feedback::get_mod_feedback) + .service(endpoints::mod_feedback::post_mod_feedback) .service(health) }) .bind((addr, port))?; diff --git a/src/types/models/mod.rs b/src/types/models/mod.rs index 142755b..f4d37c0 100644 --- a/src/types/models/mod.rs +++ b/src/types/models/mod.rs @@ -10,3 +10,4 @@ pub mod mod_version; pub mod mod_version_status; pub mod stats; pub mod tag; +pub mod mod_feedback; diff --git a/src/types/models/mod_entity.rs b/src/types/models/mod_entity.rs index c6a903b..e1555bc 100644 --- a/src/types/models/mod_entity.rs +++ b/src/types/models/mod_entity.rs @@ -664,14 +664,14 @@ impl Mod { developer: FetchedDeveloper, pool: &mut PgConnection, ) -> Result<(), ApiError> { - if semver::Version::parse(json.version.trim_start_matches('v')).is_err() { + if Version::parse(json.version.trim_start_matches('v')).is_err() { return Err(ApiError::BadRequest(format!( "Invalid mod version semver {}", json.version ))); }; - if semver::Version::parse(json.geode.trim_start_matches('v')).is_err() { + if Version::parse(json.geode.trim_start_matches('v')).is_err() { return Err(ApiError::BadRequest(format!( "Invalid geode version semver {}", json.geode @@ -736,8 +736,8 @@ impl Mod { } }; - let version = semver::Version::parse(latest.version.trim_start_matches('v')).unwrap(); - let new_version = match semver::Version::parse(json.version.trim_start_matches('v')) { + let version = Version::parse(latest.version.trim_start_matches('v')).unwrap(); + let new_version = match Version::parse(json.version.trim_start_matches('v')) { Ok(v) => v, Err(_) => { return Err(ApiError::BadRequest(format!( @@ -1232,7 +1232,7 @@ impl Mod { pub async fn get_updates( ids: &[String], platforms: VerPlatform, - geode: &semver::Version, + geode: &Version, gd: GDVersionEnum, pool: &mut PgConnection, ) -> Result, ApiError> { diff --git a/src/types/models/mod_feedback.rs b/src/types/models/mod_feedback.rs new file mode 100644 index 0000000..4c04aab --- /dev/null +++ b/src/types/models/mod_feedback.rs @@ -0,0 +1,109 @@ +use serde::Serialize; +use sqlx::{PgConnection}; + +use crate::types::api::ApiError; +use crate::types::models::mod_version::ModVersion; + +#[derive(Serialize)] +pub struct ModFeedback { + pub score: i32, + pub mod_id: String, + pub mod_version: String, + pub feedback: Vec, +} + +#[derive(Serialize)] +pub struct Reviewer { + pub id: i32, + pub display_name: String, + pub admin: bool, +} + +#[derive(Serialize)] +pub struct ModFeedbackOne { + #[serde(skip_serializing)] + pub id: i32, + pub reviewer: Reviewer, + pub positive: bool, + pub feedback: String, + pub decision: bool, +} + +impl ModFeedback { + pub async fn get_for_mod_version_id( + version: &ModVersion, + pool: &mut PgConnection, + )-> Result { + let result = match sqlx::query!( + r#"SELECT mf.id, mf.reviewer_id, dev.display_name AS reviewer_name, dev.admin AS reviewer_admin, mf.positive, mf.feedback, mf.decision + FROM mod_feedback mf + INNER JOIN developers dev ON dev.id = mf.reviewer_id + WHERE mf.mod_version_id = $1"#, + version.id + ) + .fetch_all(&mut *pool) + .await + { + Err(e) => { + log::error!("{}", e); + return Err(ApiError::DbError); + } + Ok(r) => r, + }; + + let feedback: Vec = result.iter().map(|row| { + ModFeedbackOne { + id: row.id, + reviewer: Reviewer { + id: row.reviewer_id, + display_name: row.reviewer_name.clone(), + admin: row.reviewer_admin, + }, + positive: row.positive, + feedback: row.feedback.clone(), + decision: row.decision, + } + }).collect(); + + let positive = result.iter().filter(|r| r.positive).count() as i32; + let negative = result.iter().filter(|r| !r.positive).count() as i32; + let return_res = + ModFeedback { + score: positive - negative, + mod_id: version.mod_id.clone(), + mod_version: version.version.clone(), + feedback, + }; + + Ok(return_res) + } + + pub async fn set( + version: &ModVersion, + reviewer_id: i32, + positive: bool, + feedback: &str, + decision: bool, + pool: &mut PgConnection + ) -> Result<(), ApiError> { + sqlx::query!( + r#"INSERT INTO mod_feedback (mod_version_id, reviewer_id, positive, feedback, decision) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (mod_version_id, reviewer_id) + DO UPDATE SET positive = EXCLUDED.positive, feedback = EXCLUDED.feedback, decision = EXCLUDED.decision"#, + version.id, + reviewer_id, + positive, + feedback, + decision + ) + .execute(&mut *pool) + .await + .map_err(|e| { + log::error!("{}", e); + ApiError::DbError + })?; + + Ok(()) + } +} \ No newline at end of file From d3a8b4a8eb9fe4ded182f365efb1a105c1c5f973 Mon Sep 17 00:00:00 2001 From: Orion Railean Date: Sat, 24 Aug 2024 10:50:06 +1000 Subject: [PATCH 02/13] fix docs --- openapi.yml | 108 +++++++++++++++++++++++++++++++++- src/endpoints/mod_feedback.rs | 34 +++++++---- 2 files changed, 128 insertions(+), 14 deletions(-) diff --git a/openapi.yml b/openapi.yml index c4edf85..53c3dff 100644 --- a/openapi.yml +++ b/openapi.yml @@ -628,6 +628,70 @@ paths: "500": $ref: "#/components/responses/InternalServerError" + /v1/mods/{id}/versions/{version}/feedback: + get: + tags: + - mods + summary: Get feedback for a specific version of a mod + security: + - bearerAuth: [] + parameters: + - $ref: "#/components/parameters/ModID" + - $ref: "#/components/parameters/ModVersion" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/ModFeedback" + "401": + $ref: "#/components/responses/Unauthorized" + "404": + $ref: "#/components/responses/NotFoundError" + "500": + $ref: "#/components/responses/InternalServerError" + + post: + tags: + - mods + summary: Add feedback for a specific version of a mod + security: + - bearerAuth: [] + parameters: + - $ref: "#/components/parameters/ModID" + - $ref: "#/components/parameters/ModVersion" + requestBody: + content: + application/json: + schema: + type: object + properties: + positive: + type: boolean + description: Type of feedback - positive or negative + example: true + feedback: + type: string + description: The feedback given by the reviewer + example: "This mod is great!" + decision: + type: boolean + description: Whether or not this feedback was used to make a decision (admin only) + example: false + default: false + responses: + "204": + description: No Content (Feedback added) + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "404": + $ref: "#/components/responses/NotFoundError" + "500": + $ref: "#/components/responses/InternalServerError" + components: securitySchemes: bearerAuth: @@ -973,6 +1037,48 @@ components: - android32 - android64 - ios + + Reviewer: + type: object + properties: + id: + type: integer + display_name: + type: string + admin: + type: boolean + + ModFeedbackOne: + type: object + properties: + reviewer: + $ref: "#/components/schemas/Reviewer" + description: The reviewer that gave the feedback + positive: + type: boolean + description: Type of feedback - positive or negative + feedback: + type: string + description: The feedback given by the reviewer + decision: + type: boolean + description: Whether or not this feedback was used to make a decision + + ModFeedback: + type: object + properties: + score: + type: integer + description: The score of the mod, calculated as a sum of all feedback where positive is 1 and negative is -1 + mod_id: + $ref: "#/components/schemas/ModID" + mod_version: + $ref: "#/components/schemas/ModVersionString" + feedback: + type: array + items: + $ref: "#/components/schemas/ModFeedbackOne" + parameters: ModID: name: id @@ -1079,4 +1185,4 @@ components: error: type: string payload: - type: "null" + type: "null" \ No newline at end of file diff --git a/src/endpoints/mod_feedback.rs b/src/endpoints/mod_feedback.rs index e769dbd..0948b5e 100644 --- a/src/endpoints/mod_feedback.rs +++ b/src/endpoints/mod_feedback.rs @@ -44,11 +44,13 @@ pub async fn get_mod_feedback( } let mod_version = { - if path.version == "latest" { - ModVersion::get_latest_for_mod(&path.id, None, vec![], None, &mut pool).await? - } else { - ModVersion::get_one(&path.id, &path.version, false, false, &mut pool).await? - } + // latest bugs for some reason + + //if path.version == "latest" { + // ModVersion::get_latest_for_mod(&path.id, None, vec![], None, &mut pool).await? + //} else { + ModVersion::get_one(path.id.strip_prefix('v').unwrap_or(&path.id), &path.version, false, false, &mut pool).await? + //} }; let feedback = ModFeedback::get_for_mod_version_id(&mod_version, &mut pool).await?; @@ -74,6 +76,20 @@ pub async fn post_mod_feedback( return Err(ApiError::Forbidden); } + let mod_version = { + // latest bugs for some reason + + //if path.version == "latest" { + // ModVersion::get_latest_for_mod(&path.id, None, vec![], None, &mut transaction).await? + //} else { + ModVersion::get_one(path.id.strip_prefix('v').unwrap_or(&path.id), &path.version, false, false, &mut transaction).await? + //} + }; + + if mod_version.status != ModVersionStatusEnum::Pending { + return Err(ApiError::BadRequest("Mod version is not pending".to_string())); + } + let decision = payload.decision.unwrap_or(false); let mut status = None; if decision { @@ -86,14 +102,6 @@ pub async fn post_mod_feedback( }); } - let mod_version = { - if path.version == "latest" { - ModVersion::get_latest_for_mod(&path.id, None, vec![], None, &mut transaction).await? - } else { - ModVersion::get_one(&path.id, &path.version, false, false, &mut transaction).await? - } - }; - let result = ModFeedback::set(&mod_version, dev.id, payload.positive, &payload.feedback, decision, &mut transaction).await; if result.is_err() { From 2bf8d11ce7481dfaa7357aace9fa3ef4c828c480 Mon Sep 17 00:00:00 2001 From: Orion Railean Date: Tue, 27 Aug 2024 06:37:32 +1000 Subject: [PATCH 03/13] feedback v2 --- ...347be0211fe7f9da5fb975b68be987559da96.json | 18 ---- ...b511848845b04460426bababfca7bc164b1bb.json | 30 +++++++ ...9188954aaf9632a417f7ba9362e218fcc021.json} | 20 ++++- .../20240826202953_feedback_v2.down.sql | 7 ++ migrations/20240826202953_feedback_v2.up.sql | 8 ++ src/endpoints/mod_feedback.rs | 86 +++++-------------- src/endpoints/mod_versions.rs | 36 +++++--- src/types/models/mod_feedback.rs | 74 +++++++++++----- 8 files changed, 154 insertions(+), 125 deletions(-) delete mode 100644 .sqlx/query-5de44a1d52bf36c4b4cf0c72af0347be0211fe7f9da5fb975b68be987559da96.json create mode 100644 .sqlx/query-65406a8107f5b918cb90946352ab511848845b04460426bababfca7bc164b1bb.json rename .sqlx/{query-52cff6768eb7b6e4a6401e703474c1ce5a6220c6d13e4194f22d82bf4f344f8e.json => query-9b7acc44bb2484b620efd73816879188954aaf9632a417f7ba9362e218fcc021.json} (59%) create mode 100644 migrations/20240826202953_feedback_v2.down.sql create mode 100644 migrations/20240826202953_feedback_v2.up.sql diff --git a/.sqlx/query-5de44a1d52bf36c4b4cf0c72af0347be0211fe7f9da5fb975b68be987559da96.json b/.sqlx/query-5de44a1d52bf36c4b4cf0c72af0347be0211fe7f9da5fb975b68be987559da96.json deleted file mode 100644 index c9134c6..0000000 --- a/.sqlx/query-5de44a1d52bf36c4b4cf0c72af0347be0211fe7f9da5fb975b68be987559da96.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO mod_feedback (mod_version_id, reviewer_id, positive, feedback, decision)\n VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (mod_version_id, reviewer_id)\n DO UPDATE SET positive = EXCLUDED.positive, feedback = EXCLUDED.feedback, decision = EXCLUDED.decision", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int4", - "Int4", - "Bool", - "Text", - "Bool" - ] - }, - "nullable": [] - }, - "hash": "5de44a1d52bf36c4b4cf0c72af0347be0211fe7f9da5fb975b68be987559da96" -} diff --git a/.sqlx/query-65406a8107f5b918cb90946352ab511848845b04460426bababfca7bc164b1bb.json b/.sqlx/query-65406a8107f5b918cb90946352ab511848845b04460426bababfca7bc164b1bb.json new file mode 100644 index 0000000..da43624 --- /dev/null +++ b/.sqlx/query-65406a8107f5b918cb90946352ab511848845b04460426bababfca7bc164b1bb.json @@ -0,0 +1,30 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO mod_feedback (mod_version_id, reviewer_id, type, feedback, decision)\n VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (mod_version_id, reviewer_id)\n DO UPDATE SET type = EXCLUDED.type, feedback = EXCLUDED.feedback, decision = EXCLUDED.decision", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int4", + { + "Custom": { + "name": "feedback_type", + "kind": { + "Enum": [ + "Positive", + "Negative", + "Suggestion", + "Note" + ] + } + } + }, + "Text", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "65406a8107f5b918cb90946352ab511848845b04460426bababfca7bc164b1bb" +} diff --git a/.sqlx/query-52cff6768eb7b6e4a6401e703474c1ce5a6220c6d13e4194f22d82bf4f344f8e.json b/.sqlx/query-9b7acc44bb2484b620efd73816879188954aaf9632a417f7ba9362e218fcc021.json similarity index 59% rename from .sqlx/query-52cff6768eb7b6e4a6401e703474c1ce5a6220c6d13e4194f22d82bf4f344f8e.json rename to .sqlx/query-9b7acc44bb2484b620efd73816879188954aaf9632a417f7ba9362e218fcc021.json index 244559c..d622408 100644 --- a/.sqlx/query-52cff6768eb7b6e4a6401e703474c1ce5a6220c6d13e4194f22d82bf4f344f8e.json +++ b/.sqlx/query-9b7acc44bb2484b620efd73816879188954aaf9632a417f7ba9362e218fcc021.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT mf.id, mf.reviewer_id, dev.display_name AS reviewer_name, dev.admin AS reviewer_admin, mf.positive, mf.feedback, mf.decision\n FROM mod_feedback mf\n\t\t\tINNER JOIN developers dev ON dev.id = mf.reviewer_id\n WHERE mf.mod_version_id = $1", + "query": "SELECT mf.id, mf.reviewer_id, dev.display_name AS reviewer_name, dev.admin AS reviewer_admin, mf.type AS \"feedback_type: _\", mf.feedback, mf.decision\n FROM mod_feedback mf\n\t\t\tINNER JOIN developers dev ON dev.id = mf.reviewer_id\n WHERE mf.mod_version_id = $1", "describe": { "columns": [ { @@ -25,8 +25,20 @@ }, { "ordinal": 4, - "name": "positive", - "type_info": "Bool" + "name": "feedback_type: _", + "type_info": { + "Custom": { + "name": "feedback_type", + "kind": { + "Enum": [ + "Positive", + "Negative", + "Suggestion", + "Note" + ] + } + } + } }, { "ordinal": 5, @@ -54,5 +66,5 @@ false ] }, - "hash": "52cff6768eb7b6e4a6401e703474c1ce5a6220c6d13e4194f22d82bf4f344f8e" + "hash": "9b7acc44bb2484b620efd73816879188954aaf9632a417f7ba9362e218fcc021" } diff --git a/migrations/20240826202953_feedback_v2.down.sql b/migrations/20240826202953_feedback_v2.down.sql new file mode 100644 index 0000000..bbc6226 --- /dev/null +++ b/migrations/20240826202953_feedback_v2.down.sql @@ -0,0 +1,7 @@ +-- Add down migration script here + +DROP TYPE feedback_type; + +ALTER TABLE mod_feedback + ADD COLUMN positive BOOLEAN NOT NULL, + DROP COLUMN type; \ No newline at end of file diff --git a/migrations/20240826202953_feedback_v2.up.sql b/migrations/20240826202953_feedback_v2.up.sql new file mode 100644 index 0000000..47543c2 --- /dev/null +++ b/migrations/20240826202953_feedback_v2.up.sql @@ -0,0 +1,8 @@ +-- Add up migration script here + +CREATE TYPE feedback_type AS ENUM + ('Positive', 'Negative', 'Suggestion', 'Note'); + +ALTER TABLE mod_feedback + DROP COLUMN positive, + ADD COLUMN type feedback_type NOT NULL; \ No newline at end of file diff --git a/src/endpoints/mod_feedback.rs b/src/endpoints/mod_feedback.rs index 0948b5e..5f6e363 100644 --- a/src/endpoints/mod_feedback.rs +++ b/src/endpoints/mod_feedback.rs @@ -10,12 +10,10 @@ use crate::{ developer::{Developer}, }, }, - AppData, - webhook::send_webhook + AppData }; use crate::types::models::mod_version::ModVersion; -use crate::types::models::mod_feedback::ModFeedback; -use crate::types::models::mod_version_status::ModVersionStatusEnum; +use crate::types::models::mod_feedback::{ModFeedback,FeedbackTypeEnum}; #[derive(Deserialize)] pub struct GetModFeedbackPath { @@ -25,9 +23,8 @@ pub struct GetModFeedbackPath { #[derive(Deserialize)] pub struct PostModFeedbackPayload { - positive: bool, + feedback_type: FeedbackTypeEnum, feedback: String, - decision: Option, // Admin Only: Setting this will turn the request into a decision, like PUT v1/mods/{id}/versions/{version} } #[get("/v1/mods/{id}/versions/{version}/feedback")] @@ -39,10 +36,15 @@ pub async fn get_mod_feedback( let dev = auth.developer()?; let mut pool = data.db.acquire().await.or(Err(ApiError::DbAcquireError))?; - if !Developer::has_access_to_mod(dev.id, &path.id, &mut pool).await? && !dev.admin { + if !Developer::has_access_to_mod(dev.id, &path.id, &mut pool).await? && !dev.admin && !dev.verified { return Err(ApiError::Forbidden); } + let mut note_only = false; + if !Developer::has_access_to_mod(dev.id, &path.id, &mut pool).await? && !dev.admin { + note_only = true; + } + let mod_version = { // latest bugs for some reason @@ -53,7 +55,7 @@ pub async fn get_mod_feedback( //} }; - let feedback = ModFeedback::get_for_mod_version_id(&mod_version, &mut pool).await?; + let feedback = ModFeedback::get_for_mod_version_id(&mod_version, note_only, &mut pool).await?; Ok(web::Json(ApiResponse { error: "".to_string(), @@ -72,10 +74,18 @@ pub async fn post_mod_feedback( let mut pool = data.db.acquire().await.or(Err(ApiError::DbAcquireError))?; let mut transaction = pool.begin().await.or(Err(ApiError::TransactionError))?; - if (!dev.verified && !dev.admin) || Developer::has_access_to_mod(dev.id, &path.id, &mut transaction).await? { + if (!dev.verified && !dev.admin) { return Err(ApiError::Forbidden); } + if Developer::has_access_to_mod(dev.id, &path.id, &mut transaction).await? && payload.feedback_type != FeedbackTypeEnum::Note { + return Err(ApiError::Forbidden); + } + + if !Developer::has_access_to_mod(dev.id, &path.id, &mut transaction).await? && payload.feedback_type == FeedbackTypeEnum::Note { + return Err(ApiError::BadRequest("Only mod owners can leave notes".to_string())); + } + let mod_version = { // latest bugs for some reason @@ -86,23 +96,7 @@ pub async fn post_mod_feedback( //} }; - if mod_version.status != ModVersionStatusEnum::Pending { - return Err(ApiError::BadRequest("Mod version is not pending".to_string())); - } - - let decision = payload.decision.unwrap_or(false); - let mut status = None; - if decision { - if !dev.admin { - return Err(ApiError::Forbidden); - } - status = Some(match payload.positive { - true => ModVersionStatusEnum::Accepted, - false => ModVersionStatusEnum::Rejected, - }); - } - - let result = ModFeedback::set(&mod_version, dev.id, payload.positive, &payload.feedback, decision, &mut transaction).await; + let result = ModFeedback::set(&mod_version, dev.id, payload.feedback_type.clone(), &payload.feedback, false, &mut transaction).await; if result.is_err() { transaction @@ -112,46 +106,6 @@ pub async fn post_mod_feedback( return Err(result.err().unwrap()); } - if let Some(status) = status { - if let Err(e) = ModVersion::update_version( - mod_version.id, - status, - payload.feedback.clone().into(), - dev.id, - &mut transaction, - ) - .await - { - transaction - .rollback() - .await - .or(Err(ApiError::TransactionError))?; - return Err(e); - } - - if status == ModVersionStatusEnum::Accepted { - let approved_count = ModVersion::get_accepted_count(mod_version.mod_id.as_str(), &mut transaction).await?; - - let is_update = approved_count > 0; - - let owner = Developer::fetch_for_mod(path.id.as_str(), &mut transaction) - .await? - .into_iter() - .find(|dev| dev.is_owner); - - send_webhook( - mod_version.mod_id, - mod_version.name.clone(), - mod_version.version.clone(), - is_update, - owner.as_ref().unwrap().clone(), - dev.clone(), - data.webhook_url.clone(), - data.app_url.clone() - ).await; - } - } - transaction .commit() .await diff --git a/src/endpoints/mod_versions.rs b/src/endpoints/mod_versions.rs index 9d58dc3..9f5713c 100644 --- a/src/endpoints/mod_versions.rs +++ b/src/endpoints/mod_versions.rs @@ -18,7 +18,7 @@ use crate::{ }, }, webhook::send_webhook, AppData }; -use crate::types::models::mod_feedback::ModFeedback; +use crate::types::models::mod_feedback::{ModFeedback,FeedbackTypeEnum}; #[derive(Deserialize)] struct IndexPath { @@ -327,19 +327,27 @@ pub async fn update_version( return Err(e); } - if let Err(e) = ModFeedback::set( - &version, - dev.id, - payload.status == ModVersionStatusEnum::Accepted, - payload.info.as_deref().unwrap_or_default(), - true, - &mut transaction - ).await { - transaction - .rollback() - .await - .or(Err(ApiError::TransactionError))?; - return Err(e); + let feedback_type = match payload.status { + ModVersionStatusEnum::Accepted => FeedbackTypeEnum::Positive, + ModVersionStatusEnum::Rejected => FeedbackTypeEnum::Negative, + _ => FeedbackTypeEnum::Note, + }; + + if feedback_type != FeedbackTypeEnum::Note { + if let Err(e) = ModFeedback::set( + &version, + dev.id, + feedback_type, + payload.info.as_deref().unwrap_or_default(), + true, + &mut transaction + ).await { + transaction + .rollback() + .await + .or(Err(ApiError::TransactionError))?; + return Err(e); + } } transaction diff --git a/src/types/models/mod_feedback.rs b/src/types/models/mod_feedback.rs index 4c04aab..3a199d4 100644 --- a/src/types/models/mod_feedback.rs +++ b/src/types/models/mod_feedback.rs @@ -1,9 +1,22 @@ -use serde::Serialize; -use sqlx::{PgConnection}; +use std::cmp::PartialEq; +use std::str::FromStr; +use serde::{Serialize, Deserialize}; +use sqlx::{PgConnection,FromRow}; use crate::types::api::ApiError; use crate::types::models::mod_version::ModVersion; +#[derive(FromRow)] +struct ModFeedbackRow { + id: i32, + reviewer_id: i32, + reviewer_name: String, + reviewer_admin: bool, + feedback_type: FeedbackTypeEnum, + feedback: String, + decision: bool, +} + #[derive(Serialize)] pub struct ModFeedback { pub score: i32, @@ -24,18 +37,29 @@ pub struct ModFeedbackOne { #[serde(skip_serializing)] pub id: i32, pub reviewer: Reviewer, - pub positive: bool, + pub feedback_type: FeedbackTypeEnum, pub feedback: String, pub decision: bool, } +#[derive(sqlx::Type, Serialize, Deserialize, Clone, PartialEq)] +#[sqlx(type_name = "feedback_type")] +pub enum FeedbackTypeEnum { + Positive, + Negative, + Suggestion, + Note +} + impl ModFeedback { pub async fn get_for_mod_version_id( version: &ModVersion, + note_only: bool, pool: &mut PgConnection, - )-> Result { - let result = match sqlx::query!( - r#"SELECT mf.id, mf.reviewer_id, dev.display_name AS reviewer_name, dev.admin AS reviewer_admin, mf.positive, mf.feedback, mf.decision + ) -> Result { + let result = match sqlx::query_as!( + ModFeedbackRow, + r#"SELECT mf.id, mf.reviewer_id, dev.display_name AS reviewer_name, dev.admin AS reviewer_admin, mf.type AS "feedback_type: _", mf.feedback, mf.decision FROM mod_feedback mf INNER JOIN developers dev ON dev.id = mf.reviewer_id WHERE mf.mod_version_id = $1"#, @@ -51,22 +75,26 @@ impl ModFeedback { Ok(r) => r, }; - let feedback: Vec = result.iter().map(|row| { - ModFeedbackOne { - id: row.id, - reviewer: Reviewer { - id: row.reviewer_id, - display_name: row.reviewer_name.clone(), - admin: row.reviewer_admin, - }, - positive: row.positive, - feedback: row.feedback.clone(), - decision: row.decision, + let feedback: Vec = result.into_iter().filter_map(|row| { + if note_only && row.feedback_type != FeedbackTypeEnum::Note { + None + } else { + Some(ModFeedbackOne { + id: row.id, + reviewer: Reviewer { + id: row.reviewer_id, + display_name: row.reviewer_name, + admin: row.reviewer_admin, + }, + feedback_type: row.feedback_type, + feedback: row.feedback, + decision: row.decision, + }) } }).collect(); - let positive = result.iter().filter(|r| r.positive).count() as i32; - let negative = result.iter().filter(|r| !r.positive).count() as i32; + let positive = feedback.iter().filter(|r| r.feedback_type == FeedbackTypeEnum::Positive).count() as i32; + let negative = feedback.iter().filter(|r| r.feedback_type == FeedbackTypeEnum::Negative).count() as i32; let return_res = ModFeedback { score: positive - negative, @@ -81,19 +109,19 @@ impl ModFeedback { pub async fn set( version: &ModVersion, reviewer_id: i32, - positive: bool, + feedback_type: FeedbackTypeEnum, feedback: &str, decision: bool, pool: &mut PgConnection ) -> Result<(), ApiError> { sqlx::query!( - r#"INSERT INTO mod_feedback (mod_version_id, reviewer_id, positive, feedback, decision) + r#"INSERT INTO mod_feedback (mod_version_id, reviewer_id, type, feedback, decision) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (mod_version_id, reviewer_id) - DO UPDATE SET positive = EXCLUDED.positive, feedback = EXCLUDED.feedback, decision = EXCLUDED.decision"#, + DO UPDATE SET type = EXCLUDED.type, feedback = EXCLUDED.feedback, decision = EXCLUDED.decision"#, version.id, reviewer_id, - positive, + feedback_type as _, feedback, decision ) From 4f5d50ec90de44f42a5de60881010a55d46826eb Mon Sep 17 00:00:00 2001 From: Orion Railean Date: Tue, 27 Aug 2024 06:40:20 +1000 Subject: [PATCH 04/13] remove old coment --- src/endpoints/mod_versions.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/endpoints/mod_versions.rs b/src/endpoints/mod_versions.rs index 9f5713c..6c9cd77 100644 --- a/src/endpoints/mod_versions.rs +++ b/src/endpoints/mod_versions.rs @@ -287,8 +287,6 @@ pub async fn create_version( Ok(HttpResponse::NoContent()) } -// POST /v1/mods/{id}/versions/{version}/feedback should supersede this endpoint -// adding compatibility for now #[put("v1/mods/{id}/versions/{version}")] pub async fn update_version( path: web::Path, From f6955bdc2cb3f94915bafc3930f7cb202ba3408e Mon Sep 17 00:00:00 2001 From: Orion Railean Date: Tue, 27 Aug 2024 06:47:33 +1000 Subject: [PATCH 05/13] bump api spec --- openapi.yml | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/openapi.yml b/openapi.yml index 53c3dff..4bdb286 100644 --- a/openapi.yml +++ b/openapi.yml @@ -667,19 +667,19 @@ paths: schema: type: object properties: - positive: - type: boolean - description: Type of feedback - positive or negative - example: true + feedback_type: + type: string + enum: + - Positive + - Negative + - Suggestion + - Note + description: Type of feedback - positive/negative modifies score, note is mod dev only + example: Positive feedback: type: string description: The feedback given by the reviewer example: "This mod is great!" - decision: - type: boolean - description: Whether or not this feedback was used to make a decision (admin only) - example: false - default: false responses: "204": description: No Content (Feedback added) @@ -1043,10 +1043,13 @@ components: properties: id: type: integer + description: The developer ID of the reviewer display_name: type: string + description: The display name of the reviewer admin: type: boolean + description: Whether the reviewer is an admin ModFeedbackOne: type: object @@ -1054,9 +1057,14 @@ components: reviewer: $ref: "#/components/schemas/Reviewer" description: The reviewer that gave the feedback - positive: - type: boolean - description: Type of feedback - positive or negative + feedback_type: + type: string + enum: + - Positive + - Negative + - Suggestion + - Note + description: Type of feedback - positive/negative modifies score, note is mod dev only feedback: type: string description: The feedback given by the reviewer From bbfa27369dba077e6d100d941c5add7596d4d444 Mon Sep 17 00:00:00 2001 From: Orion Railean Date: Wed, 28 Aug 2024 05:56:14 +1000 Subject: [PATCH 06/13] merge migrations --- migrations/20240823213817_add_feedback.down.sql | 5 +++-- migrations/20240823213817_add_feedback.up.sql | 5 ++++- migrations/20240826202953_feedback_v2.down.sql | 7 ------- migrations/20240826202953_feedback_v2.up.sql | 8 -------- 4 files changed, 7 insertions(+), 18 deletions(-) delete mode 100644 migrations/20240826202953_feedback_v2.down.sql delete mode 100644 migrations/20240826202953_feedback_v2.up.sql diff --git a/migrations/20240823213817_add_feedback.down.sql b/migrations/20240823213817_add_feedback.down.sql index 607f802..99ad4a6 100644 --- a/migrations/20240823213817_add_feedback.down.sql +++ b/migrations/20240823213817_add_feedback.down.sql @@ -1,5 +1,6 @@ -- Add down migration script here DROP TABLE IF EXISTS mod_feedback; -DROP INDEX IF EXISTS public.idx_mod_feedback_mod_version_id; -DROP INDEX IF EXISTS public.idx_mod_feedback_reviewer_id; \ No newline at end of file +DROP TYPE IF EXISTS feedback_type; +DROP INDEX IF EXISTS idx_mod_feedback_mod_version_id; +DROP INDEX IF EXISTS idx_mod_feedback_reviewer_id; \ No newline at end of file diff --git a/migrations/20240823213817_add_feedback.up.sql b/migrations/20240823213817_add_feedback.up.sql index 2698d2f..9b77a3e 100644 --- a/migrations/20240823213817_add_feedback.up.sql +++ b/migrations/20240823213817_add_feedback.up.sql @@ -1,13 +1,16 @@ -- Add up migration script here +CREATE TYPE feedback_type AS ENUM + ('Positive', 'Negative', 'Suggestion', 'Note'); + CREATE TABLE mod_feedback ( id SERIAL PRIMARY KEY NOT NULL, mod_version_id INTEGER NOT NULL, reviewer_id INTEGER NOT NULL, - positive BOOLEAN NOT NULL, feedback TEXT COLLATE pg_catalog."default" NOT NULL DEFAULT 'No feedback provided.'::text, decision BOOLEAN NOT NULL DEFAULT false, + type feedback_type NOT NULL, CONSTRAINT mod_feedback_mod_id_reviewer_id_key UNIQUE (mod_version_id, reviewer_id), CONSTRAINT mod_feedback_mod_version_id_fkey FOREIGN KEY (mod_version_id) REFERENCES public.mod_versions (id) diff --git a/migrations/20240826202953_feedback_v2.down.sql b/migrations/20240826202953_feedback_v2.down.sql deleted file mode 100644 index bbc6226..0000000 --- a/migrations/20240826202953_feedback_v2.down.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Add down migration script here - -DROP TYPE feedback_type; - -ALTER TABLE mod_feedback - ADD COLUMN positive BOOLEAN NOT NULL, - DROP COLUMN type; \ No newline at end of file diff --git a/migrations/20240826202953_feedback_v2.up.sql b/migrations/20240826202953_feedback_v2.up.sql deleted file mode 100644 index 47543c2..0000000 --- a/migrations/20240826202953_feedback_v2.up.sql +++ /dev/null @@ -1,8 +0,0 @@ --- Add up migration script here - -CREATE TYPE feedback_type AS ENUM - ('Positive', 'Negative', 'Suggestion', 'Note'); - -ALTER TABLE mod_feedback - DROP COLUMN positive, - ADD COLUMN type feedback_type NOT NULL; \ No newline at end of file From 6484a9fe44dc5277d0a2e89698d4534b9622b99d Mon Sep 17 00:00:00 2001 From: Orion Railean Date: Wed, 28 Aug 2024 05:58:01 +1000 Subject: [PATCH 07/13] remove redundant db calls --- src/endpoints/mod_feedback.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/endpoints/mod_feedback.rs b/src/endpoints/mod_feedback.rs index 5f6e363..6afa27c 100644 --- a/src/endpoints/mod_feedback.rs +++ b/src/endpoints/mod_feedback.rs @@ -36,12 +36,14 @@ pub async fn get_mod_feedback( let dev = auth.developer()?; let mut pool = data.db.acquire().await.or(Err(ApiError::DbAcquireError))?; - if !Developer::has_access_to_mod(dev.id, &path.id, &mut pool).await? && !dev.admin && !dev.verified { + let access = Developer::has_access_to_mod(dev.id, &path.id, &mut pool).await?; + + if !access && !dev.admin && !dev.verified { return Err(ApiError::Forbidden); } let mut note_only = false; - if !Developer::has_access_to_mod(dev.id, &path.id, &mut pool).await? && !dev.admin { + if !access && !dev.admin { note_only = true; } @@ -78,11 +80,13 @@ pub async fn post_mod_feedback( return Err(ApiError::Forbidden); } - if Developer::has_access_to_mod(dev.id, &path.id, &mut transaction).await? && payload.feedback_type != FeedbackTypeEnum::Note { + let access = Developer::has_access_to_mod(dev.id, &path.id, &mut transaction).await?; + + if access && payload.feedback_type != FeedbackTypeEnum::Note { return Err(ApiError::Forbidden); } - if !Developer::has_access_to_mod(dev.id, &path.id, &mut transaction).await? && payload.feedback_type == FeedbackTypeEnum::Note { + if !access && payload.feedback_type == FeedbackTypeEnum::Note { return Err(ApiError::BadRequest("Only mod owners can leave notes".to_string())); } From 40f72952b79c33517ab5209f64a00d731775c509 Mon Sep 17 00:00:00 2001 From: Orion Railean Date: Wed, 28 Aug 2024 06:38:04 +1000 Subject: [PATCH 08/13] remove redundant import --- src/types/models/mod_feedback.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/types/models/mod_feedback.rs b/src/types/models/mod_feedback.rs index 3a199d4..e65425a 100644 --- a/src/types/models/mod_feedback.rs +++ b/src/types/models/mod_feedback.rs @@ -1,5 +1,4 @@ use std::cmp::PartialEq; -use std::str::FromStr; use serde::{Serialize, Deserialize}; use sqlx::{PgConnection,FromRow}; From 7bd426ca11908a2b5b243ccb6fa1b92df868ad4c Mon Sep 17 00:00:00 2001 From: Orion Railean Date: Wed, 28 Aug 2024 16:29:52 +1000 Subject: [PATCH 09/13] fix latest --- src/endpoints/mod_feedback.rs | 27 ++++++++++++--------------- src/endpoints/mod_versions.rs | 4 ++-- src/types/models/mod_version.rs | 15 +++++++++++++-- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/endpoints/mod_feedback.rs b/src/endpoints/mod_feedback.rs index 6afa27c..1e90c37 100644 --- a/src/endpoints/mod_feedback.rs +++ b/src/endpoints/mod_feedback.rs @@ -14,6 +14,7 @@ use crate::{ }; use crate::types::models::mod_version::ModVersion; use crate::types::models::mod_feedback::{ModFeedback,FeedbackTypeEnum}; +use crate::types::models::mod_version_status::ModVersionStatusEnum; #[derive(Deserialize)] pub struct GetModFeedbackPath { @@ -48,13 +49,11 @@ pub async fn get_mod_feedback( } let mod_version = { - // latest bugs for some reason - - //if path.version == "latest" { - // ModVersion::get_latest_for_mod(&path.id, None, vec![], None, &mut pool).await? - //} else { - ModVersion::get_one(path.id.strip_prefix('v').unwrap_or(&path.id), &path.version, false, false, &mut pool).await? - //} + if path.version == "latest" { + ModVersion::get_latest_for_mod(&path.id, None, vec![], None, vec![ModVersionStatusEnum::Accepted, ModVersionStatusEnum::Pending, ModVersionStatusEnum::Rejected, ModVersionStatusEnum::Unlisted], &mut pool).await? + } else { + ModVersion::get_one(path.id.strip_prefix('v').unwrap_or(&path.id), &path.version, false, false, &mut pool).await? + } }; let feedback = ModFeedback::get_for_mod_version_id(&mod_version, note_only, &mut pool).await?; @@ -76,7 +75,7 @@ pub async fn post_mod_feedback( let mut pool = data.db.acquire().await.or(Err(ApiError::DbAcquireError))?; let mut transaction = pool.begin().await.or(Err(ApiError::TransactionError))?; - if (!dev.verified && !dev.admin) { + if !dev.verified && !dev.admin { return Err(ApiError::Forbidden); } @@ -91,13 +90,11 @@ pub async fn post_mod_feedback( } let mod_version = { - // latest bugs for some reason - - //if path.version == "latest" { - // ModVersion::get_latest_for_mod(&path.id, None, vec![], None, &mut transaction).await? - //} else { - ModVersion::get_one(path.id.strip_prefix('v').unwrap_or(&path.id), &path.version, false, false, &mut transaction).await? - //} + if path.version == "latest" { + ModVersion::get_latest_for_mod(&path.id, None, vec![], None, vec![ModVersionStatusEnum::Accepted, ModVersionStatusEnum::Pending, ModVersionStatusEnum::Rejected, ModVersionStatusEnum::Unlisted], &mut transaction).await? + } else { + ModVersion::get_one(path.id.strip_prefix('v').unwrap_or(&path.id), &path.version, false, false, &mut transaction).await? + } }; let result = ModFeedback::set(&mod_version, dev.id, payload.feedback_type.clone(), &payload.feedback, false, &mut transaction).await; diff --git a/src/endpoints/mod_versions.rs b/src/endpoints/mod_versions.rs index 6c9cd77..9d1445b 100644 --- a/src/endpoints/mod_versions.rs +++ b/src/endpoints/mod_versions.rs @@ -149,7 +149,7 @@ pub async fn get_one( let platform_string = query.platforms.clone().unwrap_or_default(); let platforms = VerPlatform::parse_query_string(&platform_string); - ModVersion::get_latest_for_mod(&path.id, gd, platforms, query.major, &mut pool).await? + ModVersion::get_latest_for_mod(&path.id, gd, platforms, query.major, vec![ModVersionStatusEnum::Accepted], &mut pool).await? } else { ModVersion::get_one(&path.id, &path.version, true, false, &mut pool).await? } @@ -182,7 +182,7 @@ pub async fn download_version( if path.version == "latest" { let platform_str = query.platforms.clone().unwrap_or_default(); let platforms = VerPlatform::parse_query_string(&platform_str); - ModVersion::get_latest_for_mod(&path.id, query.gd, platforms, query.major, &mut pool) + ModVersion::get_latest_for_mod(&path.id, query.gd, platforms, query.major, vec![ModVersionStatusEnum::Accepted], &mut pool) .await? } else { ModVersion::get_one(&path.id, &path.version, false, false, &mut pool).await? diff --git a/src/types/models/mod_version.rs b/src/types/models/mod_version.rs index 9b5514e..47d8c70 100644 --- a/src/types/models/mod_version.rs +++ b/src/types/models/mod_version.rs @@ -436,6 +436,7 @@ impl ModVersion { gd: Option, platforms: Vec, major: Option, + statuses: Vec, pool: &mut PgConnection, ) -> Result { let mut query_builder: QueryBuilder = QueryBuilder::new( @@ -450,9 +451,19 @@ impl ModVersion { FROM mods m INNER JOIN mod_versions mv ON m.id = mv.mod_id INNER JOIN mod_gd_versions mgv ON mgv.mod_id = mv.id - INNER JOIN mod_version_statuses mvs ON mvs.mod_version_id = mv.id - WHERE mvs.status = 'accepted'"#, + INNER JOIN mod_version_statuses mvs ON mvs.mod_version_id = mv.id"#, ); + for (i, status) in statuses.iter().enumerate() { + if i == 0 { + query_builder.push(" WHERE mvs.status IN ("); + } + query_builder.push_bind(*status); + if i == statuses.len() - 1 { + query_builder.push(")"); + } else { + query_builder.push(", "); + } + } if let Some(m) = major { let major_ver = format!("{}.%", m); query_builder.push(" AND mv.version LIKE "); From 35943d2c29dad8690ed712b9c960c9997a571d2a Mon Sep 17 00:00:00 2001 From: Orion Railean Date: Thu, 29 Aug 2024 06:09:27 +1000 Subject: [PATCH 10/13] add delete endpoint --- ...9504c20a6b9e85057e12ef0bdf31c58a5e372.json | 15 ++++++ openapi.yml | 19 +++++++ src/endpoints/mod_feedback.rs | 50 +++++++++++++++++-- src/main.rs | 1 + src/types/models/mod_feedback.rs | 21 ++++++++ 5 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 .sqlx/query-e6d5a9f773b845d8e95f2f604149504c20a6b9e85057e12ef0bdf31c58a5e372.json diff --git a/.sqlx/query-e6d5a9f773b845d8e95f2f604149504c20a6b9e85057e12ef0bdf31c58a5e372.json b/.sqlx/query-e6d5a9f773b845d8e95f2f604149504c20a6b9e85057e12ef0bdf31c58a5e372.json new file mode 100644 index 0000000..7ec9812 --- /dev/null +++ b/.sqlx/query-e6d5a9f773b845d8e95f2f604149504c20a6b9e85057e12ef0bdf31c58a5e372.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM mod_feedback\n WHERE mod_version_id = $1 AND reviewer_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "e6d5a9f773b845d8e95f2f604149504c20a6b9e85057e12ef0bdf31c58a5e372" +} diff --git a/openapi.yml b/openapi.yml index 4bdb286..98639b2 100644 --- a/openapi.yml +++ b/openapi.yml @@ -692,6 +692,25 @@ paths: "500": $ref: "#/components/responses/InternalServerError" + delete: + tags: + - mods + summary: Delete feedback for a specific version of a mod + security: + - bearerAuth: [] + parameters: + - $ref: "#/components/parameters/ModID" + - $ref: "#/components/parameters/ModVersion" + responses: + "204": + description: No Content (Feedback deleted) + "401": + $ref: "#/components/responses/Unauthorized" + "404": + $ref: "#/components/responses/NotFoundError" + "500": + $ref: "#/components/responses/InternalServerError" + components: securitySchemes: bearerAuth: diff --git a/src/endpoints/mod_feedback.rs b/src/endpoints/mod_feedback.rs index 1e90c37..6fc3c45 100644 --- a/src/endpoints/mod_feedback.rs +++ b/src/endpoints/mod_feedback.rs @@ -1,4 +1,4 @@ -use actix_web::{get, post, web, HttpResponse, Responder}; +use actix_web::{get, post, delete, web, HttpResponse, Responder}; use serde::{Deserialize}; use sqlx::Acquire; @@ -75,12 +75,12 @@ pub async fn post_mod_feedback( let mut pool = data.db.acquire().await.or(Err(ApiError::DbAcquireError))?; let mut transaction = pool.begin().await.or(Err(ApiError::TransactionError))?; - if !dev.verified && !dev.admin { + let access = Developer::has_access_to_mod(dev.id, &path.id, &mut transaction).await?; + + if !access && !dev.verified && !dev.admin { return Err(ApiError::Forbidden); } - let access = Developer::has_access_to_mod(dev.id, &path.id, &mut transaction).await?; - if access && payload.feedback_type != FeedbackTypeEnum::Note { return Err(ApiError::Forbidden); } @@ -107,6 +107,48 @@ pub async fn post_mod_feedback( return Err(result.err().unwrap()); } + transaction + .commit() + .await + .or(Err(ApiError::TransactionError))?; + + Ok(HttpResponse::NoContent()) +} + +#[delete("/v1/mods/{id}/versions/{version}/feedback")] +pub async fn delete_mod_feedback( + data: web::Data, + path: web::Path, + auth: Auth, +) -> Result { + let dev = auth.developer()?; + let mut pool = data.db.acquire().await.or(Err(ApiError::DbAcquireError))?; + let mut transaction = pool.begin().await.or(Err(ApiError::TransactionError))?; + + let access = Developer::has_access_to_mod(dev.id, &path.id, &mut transaction).await?; + + if !access && !dev.verified && !dev.admin { + return Err(ApiError::Forbidden); + } + + let mod_version = { + if path.version == "latest" { + ModVersion::get_latest_for_mod(&path.id, None, vec![], None, vec![ModVersionStatusEnum::Accepted, ModVersionStatusEnum::Pending, ModVersionStatusEnum::Rejected, ModVersionStatusEnum::Unlisted], &mut transaction).await? + } else { + ModVersion::get_one(path.id.strip_prefix('v').unwrap_or(&path.id), &path.version, false, false, &mut transaction).await? + } + }; + + let result = ModFeedback::remove(&mod_version, dev.id, &mut transaction).await; + + if result.is_err() { + transaction + .rollback() + .await + .or(Err(ApiError::TransactionError))?; + return Err(result.err().unwrap()); + } + transaction .commit() .await diff --git a/src/main.rs b/src/main.rs index 53a1a3f..0761121 100644 --- a/src/main.rs +++ b/src/main.rs @@ -124,6 +124,7 @@ async fn main() -> anyhow::Result<()> { .service(endpoints::stats::get_stats) .service(endpoints::mod_feedback::get_mod_feedback) .service(endpoints::mod_feedback::post_mod_feedback) + .service(endpoints::mod_feedback::delete_mod_feedback) .service(health) }) .bind((addr, port))?; diff --git a/src/types/models/mod_feedback.rs b/src/types/models/mod_feedback.rs index e65425a..ce626fc 100644 --- a/src/types/models/mod_feedback.rs +++ b/src/types/models/mod_feedback.rs @@ -133,4 +133,25 @@ impl ModFeedback { Ok(()) } + + pub async fn remove( + version: &ModVersion, + reviewer_id: i32, + pool: &mut PgConnection + ) -> Result<(), ApiError> { + sqlx::query!( + r#"DELETE FROM mod_feedback + WHERE mod_version_id = $1 AND reviewer_id = $2"#, + version.id, + reviewer_id + ) + .execute(&mut *pool) + .await + .map_err(|e| { + log::error!("{}", e); + ApiError::DbError + })?; + + Ok(()) + } } \ No newline at end of file From 4f46629604d17f82454a433a8afe6006508c1579 Mon Sep 17 00:00:00 2001 From: Orion Railean Date: Tue, 3 Sep 2024 06:43:28 +1000 Subject: [PATCH 11/13] add zmx's changes --- ...83cc592f0306be4ed72cbea482d08eab8d79.json} | 17 ++- ...5ebd88fda942ab6a7ed34e3f0b230e6152c0.json} | 10 +- ...148d0ad5c8ade32bbfe94788059322d988729.json | 14 ++ ...9504c20a6b9e85057e12ef0bdf31c58a5e372.json | 15 -- migrations/20240823213817_add_feedback.up.sql | 5 +- src/endpoints/mod_feedback.rs | 46 +++--- src/endpoints/mod_versions.rs | 5 +- src/types/models/mod_feedback.rs | 133 ++++++++++++------ src/types/models/mod_version.rs | 10 ++ 9 files changed, 164 insertions(+), 91 deletions(-) rename .sqlx/{query-65406a8107f5b918cb90946352ab511848845b04460426bababfca7bc164b1bb.json => query-296eb7105188cc1accddc18c33e383cc592f0306be4ed72cbea482d08eab8d79.json} (59%) rename .sqlx/{query-9b7acc44bb2484b620efd73816879188954aaf9632a417f7ba9362e218fcc021.json => query-aca5f555d538070d553442baff1b5ebd88fda942ab6a7ed34e3f0b230e6152c0.json} (79%) create mode 100644 .sqlx/query-dd99975f5da65151e1b973da542148d0ad5c8ade32bbfe94788059322d988729.json delete mode 100644 .sqlx/query-e6d5a9f773b845d8e95f2f604149504c20a6b9e85057e12ef0bdf31c58a5e372.json diff --git a/.sqlx/query-65406a8107f5b918cb90946352ab511848845b04460426bababfca7bc164b1bb.json b/.sqlx/query-296eb7105188cc1accddc18c33e383cc592f0306be4ed72cbea482d08eab8d79.json similarity index 59% rename from .sqlx/query-65406a8107f5b918cb90946352ab511848845b04460426bababfca7bc164b1bb.json rename to .sqlx/query-296eb7105188cc1accddc18c33e383cc592f0306be4ed72cbea482d08eab8d79.json index da43624..d5a68d7 100644 --- a/.sqlx/query-65406a8107f5b918cb90946352ab511848845b04460426bababfca7bc164b1bb.json +++ b/.sqlx/query-296eb7105188cc1accddc18c33e383cc592f0306be4ed72cbea482d08eab8d79.json @@ -1,8 +1,14 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO mod_feedback (mod_version_id, reviewer_id, type, feedback, decision)\n VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (mod_version_id, reviewer_id)\n DO UPDATE SET type = EXCLUDED.type, feedback = EXCLUDED.feedback, decision = EXCLUDED.decision", + "query": "INSERT INTO mod_feedback (mod_version_id, reviewer_id, type, feedback, decision, dev)\n VALUES ($1, $2, $3, $4, $5, $6)\n RETURNING id", "describe": { - "columns": [], + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], "parameters": { "Left": [ "Int4", @@ -21,10 +27,13 @@ } }, "Text", + "Bool", "Bool" ] }, - "nullable": [] + "nullable": [ + false + ] }, - "hash": "65406a8107f5b918cb90946352ab511848845b04460426bababfca7bc164b1bb" + "hash": "296eb7105188cc1accddc18c33e383cc592f0306be4ed72cbea482d08eab8d79" } diff --git a/.sqlx/query-9b7acc44bb2484b620efd73816879188954aaf9632a417f7ba9362e218fcc021.json b/.sqlx/query-aca5f555d538070d553442baff1b5ebd88fda942ab6a7ed34e3f0b230e6152c0.json similarity index 79% rename from .sqlx/query-9b7acc44bb2484b620efd73816879188954aaf9632a417f7ba9362e218fcc021.json rename to .sqlx/query-aca5f555d538070d553442baff1b5ebd88fda942ab6a7ed34e3f0b230e6152c0.json index d622408..57852b3 100644 --- a/.sqlx/query-9b7acc44bb2484b620efd73816879188954aaf9632a417f7ba9362e218fcc021.json +++ b/.sqlx/query-aca5f555d538070d553442baff1b5ebd88fda942ab6a7ed34e3f0b230e6152c0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT mf.id, mf.reviewer_id, dev.display_name AS reviewer_name, dev.admin AS reviewer_admin, mf.type AS \"feedback_type: _\", mf.feedback, mf.decision\n FROM mod_feedback mf\n\t\t\tINNER JOIN developers dev ON dev.id = mf.reviewer_id\n WHERE mf.mod_version_id = $1", + "query": "SELECT mf.id, mf.reviewer_id, dev.display_name AS reviewer_name, dev.admin AS reviewer_admin, mf.type AS \"feedback_type: _\", mf.feedback, mf.decision, mf.dev\n FROM mod_feedback mf\n\t\t\tINNER JOIN developers dev ON dev.id = mf.reviewer_id\n WHERE mf.id = $1", "describe": { "columns": [ { @@ -49,6 +49,11 @@ "ordinal": 6, "name": "decision", "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "dev", + "type_info": "Bool" } ], "parameters": { @@ -63,8 +68,9 @@ false, false, false, + false, false ] }, - "hash": "9b7acc44bb2484b620efd73816879188954aaf9632a417f7ba9362e218fcc021" + "hash": "aca5f555d538070d553442baff1b5ebd88fda942ab6a7ed34e3f0b230e6152c0" } diff --git a/.sqlx/query-dd99975f5da65151e1b973da542148d0ad5c8ade32bbfe94788059322d988729.json b/.sqlx/query-dd99975f5da65151e1b973da542148d0ad5c8ade32bbfe94788059322d988729.json new file mode 100644 index 0000000..4bfa301 --- /dev/null +++ b/.sqlx/query-dd99975f5da65151e1b973da542148d0ad5c8ade32bbfe94788059322d988729.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM mod_feedback\n WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [] + }, + "hash": "dd99975f5da65151e1b973da542148d0ad5c8ade32bbfe94788059322d988729" +} diff --git a/.sqlx/query-e6d5a9f773b845d8e95f2f604149504c20a6b9e85057e12ef0bdf31c58a5e372.json b/.sqlx/query-e6d5a9f773b845d8e95f2f604149504c20a6b9e85057e12ef0bdf31c58a5e372.json deleted file mode 100644 index 7ec9812..0000000 --- a/.sqlx/query-e6d5a9f773b845d8e95f2f604149504c20a6b9e85057e12ef0bdf31c58a5e372.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM mod_feedback\n WHERE mod_version_id = $1 AND reviewer_id = $2", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int4", - "Int4" - ] - }, - "nullable": [] - }, - "hash": "e6d5a9f773b845d8e95f2f604149504c20a6b9e85057e12ef0bdf31c58a5e372" -} diff --git a/migrations/20240823213817_add_feedback.up.sql b/migrations/20240823213817_add_feedback.up.sql index 9b77a3e..e3d5550 100644 --- a/migrations/20240823213817_add_feedback.up.sql +++ b/migrations/20240823213817_add_feedback.up.sql @@ -8,10 +8,11 @@ CREATE TABLE mod_feedback id SERIAL PRIMARY KEY NOT NULL, mod_version_id INTEGER NOT NULL, reviewer_id INTEGER NOT NULL, - feedback TEXT COLLATE pg_catalog."default" NOT NULL DEFAULT 'No feedback provided.'::text, + feedback TEXT COLLATE pg_catalog."default" NOT NULL, decision BOOLEAN NOT NULL DEFAULT false, type feedback_type NOT NULL, - CONSTRAINT mod_feedback_mod_id_reviewer_id_key UNIQUE (mod_version_id, reviewer_id), + dev bool NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT mod_feedback_mod_version_id_fkey FOREIGN KEY (mod_version_id) REFERENCES public.mod_versions (id) ON DELETE CASCADE, diff --git a/src/endpoints/mod_feedback.rs b/src/endpoints/mod_feedback.rs index 6fc3c45..d90889b 100644 --- a/src/endpoints/mod_feedback.rs +++ b/src/endpoints/mod_feedback.rs @@ -14,7 +14,6 @@ use crate::{ }; use crate::types::models::mod_version::ModVersion; use crate::types::models::mod_feedback::{ModFeedback,FeedbackTypeEnum}; -use crate::types::models::mod_version_status::ModVersionStatusEnum; #[derive(Deserialize)] pub struct GetModFeedbackPath { @@ -28,6 +27,11 @@ pub struct PostModFeedbackPayload { feedback: String, } +#[derive(Deserialize)] +pub struct DeleteModFeedbackPayload { + id: i32 +} + #[get("/v1/mods/{id}/versions/{version}/feedback")] pub async fn get_mod_feedback( data: web::Data, @@ -43,14 +47,11 @@ pub async fn get_mod_feedback( return Err(ApiError::Forbidden); } - let mut note_only = false; - if !access && !dev.admin { - note_only = true; - } + let note_only = !access && !dev.admin; let mod_version = { if path.version == "latest" { - ModVersion::get_latest_for_mod(&path.id, None, vec![], None, vec![ModVersionStatusEnum::Accepted, ModVersionStatusEnum::Pending, ModVersionStatusEnum::Rejected, ModVersionStatusEnum::Unlisted], &mut pool).await? + ModVersion::get_latest_for_mod(&path.id, None, vec![], None, &mut pool).await? } else { ModVersion::get_one(path.id.strip_prefix('v').unwrap_or(&path.id), &path.version, false, false, &mut pool).await? } @@ -85,19 +86,15 @@ pub async fn post_mod_feedback( return Err(ApiError::Forbidden); } - if !access && payload.feedback_type == FeedbackTypeEnum::Note { - return Err(ApiError::BadRequest("Only mod owners can leave notes".to_string())); - } - let mod_version = { if path.version == "latest" { - ModVersion::get_latest_for_mod(&path.id, None, vec![], None, vec![ModVersionStatusEnum::Accepted, ModVersionStatusEnum::Pending, ModVersionStatusEnum::Rejected, ModVersionStatusEnum::Unlisted], &mut transaction).await? + ModVersion::get_latest_for_mod(&path.id, None, vec![], None, &mut transaction).await? } else { ModVersion::get_one(path.id.strip_prefix('v').unwrap_or(&path.id), &path.version, false, false, &mut transaction).await? } }; - let result = ModFeedback::set(&mod_version, dev.id, payload.feedback_type.clone(), &payload.feedback, false, &mut transaction).await; + let result = ModFeedback::set(&mod_version, dev.id, payload.feedback_type.clone(), &payload.feedback, false, access, &mut transaction).await; if result.is_err() { transaction @@ -112,34 +109,31 @@ pub async fn post_mod_feedback( .await .or(Err(ApiError::TransactionError))?; - Ok(HttpResponse::NoContent()) + Ok(web::Json(ApiResponse { + error: "".to_string(), + payload: result?, + })) } #[delete("/v1/mods/{id}/versions/{version}/feedback")] pub async fn delete_mod_feedback( data: web::Data, path: web::Path, + payload: web::Json, auth: Auth, ) -> Result { let dev = auth.developer()?; let mut pool = data.db.acquire().await.or(Err(ApiError::DbAcquireError))?; let mut transaction = pool.begin().await.or(Err(ApiError::TransactionError))?; - let access = Developer::has_access_to_mod(dev.id, &path.id, &mut transaction).await?; - - if !access && !dev.verified && !dev.admin { - return Err(ApiError::Forbidden); - } - - let mod_version = { - if path.version == "latest" { - ModVersion::get_latest_for_mod(&path.id, None, vec![], None, vec![ModVersionStatusEnum::Accepted, ModVersionStatusEnum::Pending, ModVersionStatusEnum::Rejected, ModVersionStatusEnum::Unlisted], &mut transaction).await? - } else { - ModVersion::get_one(path.id.strip_prefix('v').unwrap_or(&path.id), &path.version, false, false, &mut transaction).await? + if !dev.admin { + let feedback = ModFeedback::get_feedback_by_id(payload.id, &mut transaction).await?; + if feedback.reviewer.id != dev.id { + return Err(ApiError::Forbidden); } - }; + } - let result = ModFeedback::remove(&mod_version, dev.id, &mut transaction).await; + let result = ModFeedback::remove(payload.id, &mut transaction).await; if result.is_err() { transaction diff --git a/src/endpoints/mod_versions.rs b/src/endpoints/mod_versions.rs index 9d1445b..b861a10 100644 --- a/src/endpoints/mod_versions.rs +++ b/src/endpoints/mod_versions.rs @@ -149,7 +149,7 @@ pub async fn get_one( let platform_string = query.platforms.clone().unwrap_or_default(); let platforms = VerPlatform::parse_query_string(&platform_string); - ModVersion::get_latest_for_mod(&path.id, gd, platforms, query.major, vec![ModVersionStatusEnum::Accepted], &mut pool).await? + ModVersion::get_latest_for_mod_statuses(&path.id, gd, platforms, query.major, vec![ModVersionStatusEnum::Accepted], &mut pool).await? } else { ModVersion::get_one(&path.id, &path.version, true, false, &mut pool).await? } @@ -182,7 +182,7 @@ pub async fn download_version( if path.version == "latest" { let platform_str = query.platforms.clone().unwrap_or_default(); let platforms = VerPlatform::parse_query_string(&platform_str); - ModVersion::get_latest_for_mod(&path.id, query.gd, platforms, query.major, vec![ModVersionStatusEnum::Accepted], &mut pool) + ModVersion::get_latest_for_mod_statuses(&path.id, query.gd, platforms, query.major, vec![ModVersionStatusEnum::Accepted], &mut pool) .await? } else { ModVersion::get_one(&path.id, &path.version, false, false, &mut pool).await? @@ -338,6 +338,7 @@ pub async fn update_version( feedback_type, payload.info.as_deref().unwrap_or_default(), true, + Developer::has_access_to_mod(dev.id, &version.mod_id, &mut transaction).await?, &mut transaction ).await { transaction diff --git a/src/types/models/mod_feedback.rs b/src/types/models/mod_feedback.rs index ce626fc..fdfdc73 100644 --- a/src/types/models/mod_feedback.rs +++ b/src/types/models/mod_feedback.rs @@ -1,8 +1,9 @@ use std::cmp::PartialEq; use serde::{Serialize, Deserialize}; -use sqlx::{PgConnection,FromRow}; +use sqlx::{PgConnection, FromRow, Postgres, QueryBuilder}; use crate::types::api::ApiError; +use crate::types::models::developer::Developer; use crate::types::models::mod_version::ModVersion; #[derive(FromRow)] @@ -14,11 +15,12 @@ struct ModFeedbackRow { feedback_type: FeedbackTypeEnum, feedback: String, decision: bool, + dev: bool } #[derive(Serialize)] pub struct ModFeedback { - pub score: i32, + pub score: Score, pub mod_id: String, pub mod_version: String, pub feedback: Vec, @@ -27,13 +29,13 @@ pub struct ModFeedback { #[derive(Serialize)] pub struct Reviewer { pub id: i32, + pub dev: bool, pub display_name: String, pub admin: bool, } #[derive(Serialize)] pub struct ModFeedbackOne { - #[serde(skip_serializing)] pub id: i32, pub reviewer: Reviewer, pub feedback_type: FeedbackTypeEnum, @@ -41,6 +43,13 @@ pub struct ModFeedbackOne { pub decision: bool, } +#[derive(Serialize)] +pub struct Score { + pub score: i32, + pub positive: i32, + pub negative: i32, +} + #[derive(sqlx::Type, Serialize, Deserialize, Clone, PartialEq)] #[sqlx(type_name = "feedback_type")] pub enum FeedbackTypeEnum { @@ -53,17 +62,22 @@ pub enum FeedbackTypeEnum { impl ModFeedback { pub async fn get_for_mod_version_id( version: &ModVersion, - note_only: bool, + dev_only: bool, pool: &mut PgConnection, ) -> Result { - let result = match sqlx::query_as!( - ModFeedbackRow, - r#"SELECT mf.id, mf.reviewer_id, dev.display_name AS reviewer_name, dev.admin AS reviewer_admin, mf.type AS "feedback_type: _", mf.feedback, mf.decision + let mut query_builder: QueryBuilder = QueryBuilder::new( + r#"SELECT mf.id, mf.reviewer_id, dev.display_name AS reviewer_name, dev.admin AS reviewer_admin, mf.type AS "feedback_type: _", mf.feedback, mf.decision, mf.dev FROM mod_feedback mf INNER JOIN developers dev ON dev.id = mf.reviewer_id - WHERE mf.mod_version_id = $1"#, - version.id - ) + WHERE mf.mod_version_id = "# + ); + query_builder.push_bind(version.id); + if dev_only { + query_builder.push(" AND mf.dev = true"); + } + query_builder.push(" ORDER BY created_at DESC"); + let result = match query_builder + .build_query_as::() .fetch_all(&mut *pool) .await { @@ -75,28 +89,29 @@ impl ModFeedback { }; let feedback: Vec = result.into_iter().filter_map(|row| { - if note_only && row.feedback_type != FeedbackTypeEnum::Note { - None - } else { - Some(ModFeedbackOne { - id: row.id, - reviewer: Reviewer { - id: row.reviewer_id, - display_name: row.reviewer_name, - admin: row.reviewer_admin, - }, - feedback_type: row.feedback_type, - feedback: row.feedback, - decision: row.decision, - }) - } + Some(ModFeedbackOne { + id: row.id, + reviewer: Reviewer { + id: row.reviewer_id, + display_name: row.reviewer_name, + admin: row.reviewer_admin, + dev: row.dev, + }, + feedback_type: row.feedback_type, + feedback: row.feedback, + decision: row.decision, + }) }).collect(); let positive = feedback.iter().filter(|r| r.feedback_type == FeedbackTypeEnum::Positive).count() as i32; let negative = feedback.iter().filter(|r| r.feedback_type == FeedbackTypeEnum::Negative).count() as i32; let return_res = ModFeedback { - score: positive - negative, + score: Score { + score: positive - negative, + positive, + negative, + }, mod_id: version.mod_id.clone(), mod_version: version.version.clone(), feedback, @@ -111,39 +126,38 @@ impl ModFeedback { feedback_type: FeedbackTypeEnum, feedback: &str, decision: bool, + dev: bool, pool: &mut PgConnection - ) -> Result<(), ApiError> { - sqlx::query!( - r#"INSERT INTO mod_feedback (mod_version_id, reviewer_id, type, feedback, decision) - VALUES ($1, $2, $3, $4, $5) - ON CONFLICT (mod_version_id, reviewer_id) - DO UPDATE SET type = EXCLUDED.type, feedback = EXCLUDED.feedback, decision = EXCLUDED.decision"#, + ) -> Result { + let result = sqlx::query!( + r#"INSERT INTO mod_feedback (mod_version_id, reviewer_id, type, feedback, decision, dev) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING id"#, version.id, reviewer_id, feedback_type as _, feedback, - decision + decision, + dev ) - .execute(&mut *pool) + .fetch_one(&mut *pool) .await .map_err(|e| { log::error!("{}", e); ApiError::DbError })?; - Ok(()) + Ok(result.id) } pub async fn remove( - version: &ModVersion, - reviewer_id: i32, + feedback_id: i32, pool: &mut PgConnection ) -> Result<(), ApiError> { sqlx::query!( r#"DELETE FROM mod_feedback - WHERE mod_version_id = $1 AND reviewer_id = $2"#, - version.id, - reviewer_id + WHERE id = $1"#, + feedback_id, ) .execute(&mut *pool) .await @@ -154,4 +168,43 @@ impl ModFeedback { Ok(()) } + + pub async fn get_feedback_by_id( + feedback_id: i32, + pool: &mut PgConnection + ) -> Result { + let result = match sqlx::query_as!( + ModFeedbackRow, + r#"SELECT mf.id, mf.reviewer_id, dev.display_name AS reviewer_name, dev.admin AS reviewer_admin, mf.type AS "feedback_type: _", mf.feedback, mf.decision, mf.dev + FROM mod_feedback mf + INNER JOIN developers dev ON dev.id = mf.reviewer_id + WHERE mf.id = $1"#, + feedback_id + ) + .fetch_optional(&mut *pool) + .await + { + Err(e) => { + log::error!("{}", e); + return Err(ApiError::DbError); + } + Ok(None) => { + return Err(ApiError::NotFound("Feedback not found".to_string())); + } + Ok(Some(r)) => r, + }; + + Ok(ModFeedbackOne { + id: result.id, + reviewer: Reviewer { + id: result.reviewer_id, + display_name: result.reviewer_name, + admin: result.reviewer_admin, + dev: result.dev, + }, + feedback_type: result.feedback_type, + feedback: result.feedback, + decision: result.decision, + }) + } } \ No newline at end of file diff --git a/src/types/models/mod_version.rs b/src/types/models/mod_version.rs index 47d8c70..7022e80 100644 --- a/src/types/models/mod_version.rs +++ b/src/types/models/mod_version.rs @@ -432,6 +432,16 @@ impl ModVersion { } pub async fn get_latest_for_mod( + id: &str, + gd: Option, + platforms: Vec, + major: Option, + pool: &mut PgConnection, + ) -> Result { + Self::get_latest_for_mod_statuses(id, gd, platforms, major, vec![ModVersionStatusEnum::Accepted, ModVersionStatusEnum::Rejected, ModVersionStatusEnum::Unlisted, ModVersionStatusEnum::Pending], pool).await + } + + pub async fn get_latest_for_mod_statuses( id: &str, gd: Option, platforms: Vec, From 84a6534eb701cc0b445791ca34dc2bbda1000cb1 Mon Sep 17 00:00:00 2001 From: Orion Railean Date: Tue, 3 Sep 2024 06:51:42 +1000 Subject: [PATCH 12/13] update api spec --- openapi.yml | 44 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/openapi.yml b/openapi.yml index 98639b2..b23298d 100644 --- a/openapi.yml +++ b/openapi.yml @@ -681,8 +681,18 @@ paths: description: The feedback given by the reviewer example: "This mod is great!" responses: - "204": - description: No Content (Feedback added) + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + error: + type: string + payload: + description: The ID of the new feedback (for use in deleting) + type: integer "400": $ref: "#/components/responses/BadRequest" "401": @@ -701,6 +711,16 @@ paths: parameters: - $ref: "#/components/parameters/ModID" - $ref: "#/components/parameters/ModVersion" + requestBody: + content: + application/json: + schema: + type: object + properties: + id: + type: integer + description: The ID of the feedback to delete + example: 1 responses: "204": description: No Content (Feedback deleted) @@ -1069,6 +1089,22 @@ components: admin: type: boolean description: Whether the reviewer is an admin + dev: + type: boolean + description: Whether the reviewer is a developer of the mod + + Score: + type: object + properties: + score: + type: integer + description: The score of the mod, calculated as a sum of all feedback where positive is 1 and negative is -1 + positive: + type: integer + description: The number of positive feedback + negative: + type: integer + description: The number of negative feedback ModFeedbackOne: type: object @@ -1095,8 +1131,8 @@ components: type: object properties: score: - type: integer - description: The score of the mod, calculated as a sum of all feedback where positive is 1 and negative is -1 + $ref: "#/components/schemas/Score" + description: The mod's score mod_id: $ref: "#/components/schemas/ModID" mod_version: From a7643a5bdc08873c0adff18bd5910beae508f210 Mon Sep 17 00:00:00 2001 From: SorkoPiko Date: Tue, 10 Sep 2024 15:44:12 +1000 Subject: [PATCH 13/13] replace dev_only with filter_user (unfinished) --- src/types/models/mod_feedback.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/types/models/mod_feedback.rs b/src/types/models/mod_feedback.rs index fdfdc73..6998bf4 100644 --- a/src/types/models/mod_feedback.rs +++ b/src/types/models/mod_feedback.rs @@ -62,7 +62,7 @@ pub enum FeedbackTypeEnum { impl ModFeedback { pub async fn get_for_mod_version_id( version: &ModVersion, - dev_only: bool, + filter_user: Option, pool: &mut PgConnection, ) -> Result { let mut query_builder: QueryBuilder = QueryBuilder::new( @@ -72,8 +72,10 @@ impl ModFeedback { WHERE mf.mod_version_id = "# ); query_builder.push_bind(version.id); - if dev_only { - query_builder.push(" AND mf.dev = true"); + if let Some(user_id) = filter_user { + query_builder.push(" AND (mf.dev = true OR mf.reviewer_id = "); + query_builder.push_bind(user_id); + query_builder.push(")"); } query_builder.push(" ORDER BY created_at DESC"); let result = match query_builder