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-296eb7105188cc1accddc18c33e383cc592f0306be4ed72cbea482d08eab8d79.json b/.sqlx/query-296eb7105188cc1accddc18c33e383cc592f0306be4ed72cbea482d08eab8d79.json new file mode 100644 index 0000000..d5a68d7 --- /dev/null +++ b/.sqlx/query-296eb7105188cc1accddc18c33e383cc592f0306be4ed72cbea482d08eab8d79.json @@ -0,0 +1,39 @@ +{ + "db_name": "PostgreSQL", + "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": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int4", + "Int4", + { + "Custom": { + "name": "feedback_type", + "kind": { + "Enum": [ + "Positive", + "Negative", + "Suggestion", + "Note" + ] + } + } + }, + "Text", + "Bool", + "Bool" + ] + }, + "nullable": [ + false + ] + }, + "hash": "296eb7105188cc1accddc18c33e383cc592f0306be4ed72cbea482d08eab8d79" +} 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/.sqlx/query-aca5f555d538070d553442baff1b5ebd88fda942ab6a7ed34e3f0b230e6152c0.json b/.sqlx/query-aca5f555d538070d553442baff1b5ebd88fda942ab6a7ed34e3f0b230e6152c0.json new file mode 100644 index 0000000..57852b3 --- /dev/null +++ b/.sqlx/query-aca5f555d538070d553442baff1b5ebd88fda942ab6a7ed34e3f0b230e6152c0.json @@ -0,0 +1,76 @@ +{ + "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, 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": [ + { + "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": "feedback_type: _", + "type_info": { + "Custom": { + "name": "feedback_type", + "kind": { + "Enum": [ + "Positive", + "Negative", + "Suggestion", + "Note" + ] + } + } + } + }, + { + "ordinal": 5, + "name": "feedback", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "decision", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "dev", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "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/migrations/20240823213817_add_feedback.down.sql b/migrations/20240823213817_add_feedback.down.sql new file mode 100644 index 0000000..99ad4a6 --- /dev/null +++ b/migrations/20240823213817_add_feedback.down.sql @@ -0,0 +1,6 @@ +-- Add down migration script here + +DROP TABLE IF EXISTS mod_feedback; +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 new file mode 100644 index 0000000..e3d5550 --- /dev/null +++ b/migrations/20240823213817_add_feedback.up.sql @@ -0,0 +1,28 @@ +-- 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, + feedback TEXT COLLATE pg_catalog."default" NOT NULL, + decision BOOLEAN NOT NULL DEFAULT false, + type feedback_type NOT NULL, + 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, + 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..b23298d 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: [] @@ -628,6 +628,109 @@ 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: + 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!" + responses: + "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": + $ref: "#/components/responses/Unauthorized" + "404": + $ref: "#/components/responses/NotFoundError" + "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" + 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) + "401": + $ref: "#/components/responses/Unauthorized" + "404": + $ref: "#/components/responses/NotFoundError" + "500": + $ref: "#/components/responses/InternalServerError" + components: securitySchemes: bearerAuth: @@ -973,6 +1076,72 @@ components: - android32 - android64 - ios + + Reviewer: + type: object + 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 + 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 + properties: + reviewer: + $ref: "#/components/schemas/Reviewer" + description: The reviewer that gave the feedback + 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 + decision: + type: boolean + description: Whether or not this feedback was used to make a decision + + ModFeedback: + type: object + properties: + score: + $ref: "#/components/schemas/Score" + description: The mod's score + 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 +1248,4 @@ components: error: type: string payload: - type: "null" + type: "null" \ No newline at end of file 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..d90889b --- /dev/null +++ b/src/endpoints/mod_feedback.rs @@ -0,0 +1,152 @@ +use actix_web::{get, post, delete, web, HttpResponse, Responder}; +use serde::{Deserialize}; +use sqlx::Acquire; + +use crate::{ + extractors::auth::Auth, + types::{ + api::{ApiError, ApiResponse}, + models::{ + developer::{Developer}, + }, + }, + AppData +}; +use crate::types::models::mod_version::ModVersion; +use crate::types::models::mod_feedback::{ModFeedback,FeedbackTypeEnum}; + +#[derive(Deserialize)] +pub struct GetModFeedbackPath { + id: String, + version: String +} + +#[derive(Deserialize)] +pub struct PostModFeedbackPayload { + feedback_type: FeedbackTypeEnum, + 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, + path: web::Path, + auth: Auth, +) -> Result { + let dev = auth.developer()?; + let mut pool = data.db.acquire().await.or(Err(ApiError::DbAcquireError))?; + + 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 note_only = !access && !dev.admin; + + 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.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?; + + 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))?; + + let access = Developer::has_access_to_mod(dev.id, &path.id, &mut transaction).await?; + + if !access && !dev.verified && !dev.admin { + return Err(ApiError::Forbidden); + } + + if access && payload.feedback_type != FeedbackTypeEnum::Note { + return Err(ApiError::Forbidden); + } + + 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.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, access, &mut transaction).await; + + if result.is_err() { + transaction + .rollback() + .await + .or(Err(ApiError::TransactionError))?; + return Err(result.err().unwrap()); + } + + transaction + .commit() + .await + .or(Err(ApiError::TransactionError))?; + + 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))?; + + 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(payload.id, &mut transaction).await; + + if result.is_err() { + transaction + .rollback() + .await + .or(Err(ApiError::TransactionError))?; + return Err(result.err().unwrap()); + } + + 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..b861a10 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,FeedbackTypeEnum}; #[derive(Deserialize)] struct IndexPath { @@ -148,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_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? } @@ -181,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_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? @@ -307,26 +308,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 +324,37 @@ pub async fn update_version( .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, + Developer::has_access_to_mod(dev.id, &version.mod_id, &mut transaction).await?, + &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..0761121 100644 --- a/src/main.rs +++ b/src/main.rs @@ -122,6 +122,9 @@ 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(endpoints::mod_feedback::delete_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..6998bf4 --- /dev/null +++ b/src/types/models/mod_feedback.rs @@ -0,0 +1,212 @@ +use std::cmp::PartialEq; +use serde::{Serialize, Deserialize}; +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)] +struct ModFeedbackRow { + id: i32, + reviewer_id: i32, + reviewer_name: String, + reviewer_admin: bool, + feedback_type: FeedbackTypeEnum, + feedback: String, + decision: bool, + dev: bool +} + +#[derive(Serialize)] +pub struct ModFeedback { + pub score: Score, + pub mod_id: String, + pub mod_version: String, + pub feedback: Vec, +} + +#[derive(Serialize)] +pub struct Reviewer { + pub id: i32, + pub dev: bool, + pub display_name: String, + pub admin: bool, +} + +#[derive(Serialize)] +pub struct ModFeedbackOne { + pub id: i32, + pub reviewer: Reviewer, + pub feedback_type: FeedbackTypeEnum, + pub feedback: String, + 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 { + Positive, + Negative, + Suggestion, + Note +} + +impl ModFeedback { + pub async fn get_for_mod_version_id( + version: &ModVersion, + filter_user: Option, + pool: &mut PgConnection, + ) -> Result { + 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 = "# + ); + query_builder.push_bind(version.id); + 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 + .build_query_as::() + .fetch_all(&mut *pool) + .await + { + Err(e) => { + log::error!("{}", e); + return Err(ApiError::DbError); + } + Ok(r) => r, + }; + + let feedback: Vec = result.into_iter().filter_map(|row| { + 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: Score { + score: positive - negative, + 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, + feedback_type: FeedbackTypeEnum, + feedback: &str, + decision: bool, + dev: bool, + pool: &mut PgConnection + ) -> 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, + dev + ) + .fetch_one(&mut *pool) + .await + .map_err(|e| { + log::error!("{}", e); + ApiError::DbError + })?; + + Ok(result.id) + } + + pub async fn remove( + feedback_id: i32, + pool: &mut PgConnection + ) -> Result<(), ApiError> { + sqlx::query!( + r#"DELETE FROM mod_feedback + WHERE id = $1"#, + feedback_id, + ) + .execute(&mut *pool) + .await + .map_err(|e| { + log::error!("{}", e); + ApiError::DbError + })?; + + 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 9b5514e..7022e80 100644 --- a/src/types/models/mod_version.rs +++ b/src/types/models/mod_version.rs @@ -437,6 +437,17 @@ impl ModVersion { 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, + major: Option, + statuses: Vec, + pool: &mut PgConnection, ) -> Result { let mut query_builder: QueryBuilder = QueryBuilder::new( r#"SELECT q.name, q.id, q.description, q.version, q.download_link, @@ -450,9 +461,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 ");