diff --git a/migrations/20210201001429_reports.sql b/migrations/20210201001429_reports.sql new file mode 100644 index 000000000..1d539d872 --- /dev/null +++ b/migrations/20210201001429_reports.sql @@ -0,0 +1,24 @@ +CREATE TABLE report_types ( + id serial PRIMARY KEY, + name varchar(64) UNIQUE NOT NULL +); + +INSERT INTO report_types (name) VALUES ('spam'); +INSERT INTO report_types (name) VALUES ('copyright'); +INSERT INTO report_types (name) VALUES ('inappropriate'); +INSERT INTO report_types (name) VALUES ('malicious'); +INSERT INTO report_types (name) VALUES ('name-squatting'); + +CREATE TABLE reports ( + id bigint PRIMARY KEY, + report_type_id int REFERENCES report_types ON UPDATE CASCADE NOT NULL, + mod_id bigint REFERENCES mods ON UPDATE CASCADE, + version_id bigint REFERENCES versions ON UPDATE CASCADE, + user_id bigint REFERENCES users ON UPDATE CASCADE, + body varchar(65536) NOT NULL, + reporter bigint REFERENCES users ON UPDATE CASCADE NOT NULL, + created timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL +); + +ALTER TABLE game_versions + ADD COLUMN major boolean NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/sqlx-data.json b/sqlx-data.json index 81222d988..baa30d69d 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -75,6 +75,18 @@ "nullable": [] } }, + "041f499f542ddab1b81bd445d6cabe225b1b2ad3ec7bbc1f755346c016ae06e6": { + "query": "\n DELETE FROM reports\n WHERE user_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, "04345d9c23430267f755b1420520df91bd403524fd60ba1a94e3a239ea70cae7": { "query": "\n UPDATE mods\n SET source_url = $1\n WHERE (id = $2)\n ", "describe": { @@ -203,6 +215,26 @@ ] } }, + "0dbd0fa9a25416716a047184944d243ed5cb55808c6f300d7335c887f02a7f6e": { + "query": "\n INSERT INTO report_types (name)\n VALUES ($1)\n ON CONFLICT (name) DO NOTHING\n RETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Varchar" + ] + }, + "nullable": [ + false + ] + } + }, "1220d15a56dbf823eaa452fbafa17442ab0568bc81a31fa38e16e3df3278e5f9": { "query": "SELECT EXISTS(SELECT 1 FROM users WHERE id = $1)", "describe": { @@ -634,6 +666,26 @@ "nullable": [] } }, + "1d3b582e6765e1ae578039e44b5dc9be6f3f845c96ffd43b7ba83f9eab816f93": { + "query": "\n SELECT name FROM report_types\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false + ] + } + }, "1db6be78a74ff04c52ee105e0df30acf5bbf18f1de328980bb7f3da7f5f6569e": { "query": "\n SELECT id FROM side_types\n WHERE name = $1\n ", "describe": { @@ -654,6 +706,68 @@ ] } }, + "1f24988f92819272c10a45fecd7eb96cc901c2f7f4ec191bc1c1cf4982bf1b38": { + "query": "\n SELECT r.id, rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created\n FROM reports r\n INNER JOIN report_types rt ON rt.id = r.report_type_id\n WHERE r.id IN (SELECT * FROM UNNEST($1::bigint[]))\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "mod_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "version_id", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 5, + "name": "body", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "reporter", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "created", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + true, + true, + true, + false, + false, + false + ] + } + }, "1ffce9b2d5c9fa6c8b9abce4bad9f9419c44ad6367b7463b979c91b9b5b4fea1": { "query": "SELECT EXISTS(SELECT 1 FROM versions WHERE id=$1)", "describe": { @@ -1619,6 +1733,26 @@ "nullable": [] } }, + "56578cb820533fbc17599ee8744b8f563cf4852e7c62a6a935765d3c60235e7b": { + "query": "\n SELECT version FROM game_versions\n WHERE type = $1\n ORDER BY created DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "version", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + } + }, "56fc196cbe33032b699348d7a2f3366100bc54decb1d18bb6aad865a88096c67": { "query": "\n SELECT id FROM mods\n WHERE slug = $1\n ", "describe": { @@ -1639,6 +1773,94 @@ ] } }, + "57bb3db92e6a8fb8606005be955e2379f13a04f101f91358322a591a860a7f9e": { + "query": "\n SELECT id FROM reports\n ORDER BY created ASC\n LIMIT $1;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + } + }, + "5a13a79ebb1ab975f88b58e6deaba9685fe16e242c0fa4a5eea54f12f9448e6b": { + "query": "\n DELETE FROM reports\n WHERE version_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, + "5ad1f23da1b6f0f613de3412b928d2677a0359111dab4174e69ef6b0ef78202b": { + "query": "\n SELECT rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created\n FROM reports r\n INNER JOIN report_types rt ON rt.id = r.report_type_id\n WHERE r.id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "mod_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "version_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "body", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "reporter", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "created", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + true, + true, + true, + false, + false, + false + ] + } + }, "5c4262689205aafdd97a74bee0003f39eef0a34c97f97a939c14fb8fe349f7eb": { "query": "\n UPDATE files\n SET is_primary = TRUE\n WHERE (id = $1)\n ", "describe": { @@ -1741,137 +1963,22 @@ ] } }, - "637fd5f9564a79b625e00a705b3c9fe70ba3cba9050c0993557ca46f50d89623": { - "query": "\n SELECT * FROM mods\n WHERE status = (\n SELECT id FROM statuses WHERE status = $1\n )\n ORDER BY updated ASC\n LIMIT $2;\n ", + "67d021f0776276081d3c50ca97afa6b78b98860bf929009e845e9c00a192e3b5": { + "query": "\n SELECT id FROM report_types\n WHERE name = $1\n ", "describe": { "columns": [ { "ordinal": 0, "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "team_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "description", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "body_url", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "published", - "type_info": "Timestamptz" - }, - { - "ordinal": 6, - "name": "downloads", "type_info": "Int4" - }, - { - "ordinal": 7, - "name": "icon_url", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "issues_url", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "source_url", - "type_info": "Varchar" - }, - { - "ordinal": 10, - "name": "wiki_url", - "type_info": "Varchar" - }, - { - "ordinal": 11, - "name": "status", - "type_info": "Int4" - }, - { - "ordinal": 12, - "name": "updated", - "type_info": "Timestamptz" - }, - { - "ordinal": 13, - "name": "license", - "type_info": "Int4" - }, - { - "ordinal": 14, - "name": "license_url", - "type_info": "Varchar" - }, - { - "ordinal": 15, - "name": "client_side", - "type_info": "Int4" - }, - { - "ordinal": 16, - "name": "server_side", - "type_info": "Int4" - }, - { - "ordinal": 17, - "name": "discord_url", - "type_info": "Varchar" - }, - { - "ordinal": 18, - "name": "slug", - "type_info": "Varchar" - }, - { - "ordinal": 19, - "name": "body", - "type_info": "Varchar" } ], "parameters": { "Left": [ - "Text", - "Int8" + "Text" ] }, "nullable": [ - false, - false, - false, - false, - true, - false, - false, - true, - true, - true, - true, - false, - false, - false, - true, - false, - false, - true, - true, false ] } @@ -2131,6 +2238,26 @@ ] } }, + "7826a4f18add285afe556335581474071e601361775ef85c91074d70287392b9": { + "query": "\n SELECT version FROM game_versions\n WHERE major = $1\n ORDER BY created DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "version", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Bool" + ] + }, + "nullable": [ + false + ] + } + }, "796f057ea8eb5b01d3eedeee9840fb37464ea567f32871953fb07e14ed86af1c": { "query": "SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)", "describe": { @@ -2608,6 +2735,26 @@ ] } }, + "97690dda7edea8c985891cae5ad405f628ed81e333bc88df5493c928a4324d43": { + "query": "SELECT EXISTS(SELECT 1 FROM reports WHERE id=$1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + null + ] + } + }, "99a1eac69d7f5a5139703df431e6a5c3012a90143a8c635f93632f04d0bc41d4": { "query": "\n UPDATE mods\n SET wiki_url = $1\n WHERE (id = $2)\n ", "describe": { @@ -3111,6 +3258,27 @@ ] } }, + "b3c1b38d2e72c5ec9e6f34d497fb6eb5d01d6cdd07f38ee4a2bbae3b92911df7": { + "query": "\n SELECT version FROM game_versions\n WHERE major = $1 AND type = $2\n ORDER BY created DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "version", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Bool", + "Text" + ] + }, + "nullable": [ + false + ] + } + }, "b69a6f42965b3e7103fcbf46e39528466926789ff31e9ed2591bb175527ec169": { "query": "\n DELETE FROM users\n WHERE id = $1\n ", "describe": { @@ -3173,24 +3341,28 @@ "nullable": [] } }, - "ba2d5d676aca425a61243a9d8d3b5745c5550aa934df087aac1c9c2b5e49a243": { - "query": "\n SELECT version FROM game_versions\n WHERE type = $1\n ORDER BY created DESC\n ", + "b99e906aa6ca18b9f3f111eae7bf0d360f42385ca99228a844387bf9456a6a31": { + "query": "\n DELETE FROM reports WHERE id = $1\n ", "describe": { - "columns": [ - { - "ordinal": 0, - "name": "version", - "type_info": "Varchar" - } - ], + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, + "bbfb47ae2c972734785df6b7c3e62077dc544ef4ccf8bb89e9c22c2f50a933c1": { + "query": "\n DELETE FROM report_types\n WHERE name = $1\n ", + "describe": { + "columns": [], "parameters": { "Left": [ "Text" ] }, - "nullable": [ - false - ] + "nullable": [] } }, "bc91841f9672608a28bd45a862919f2bd34fac0b3479e3b4b67a9f6bea2a562a": { @@ -3328,6 +3500,26 @@ "nullable": [] } }, + "c1a3f6dcef6110d6ea884670fb82bac14b98e922bb5673c048ccce7b7300539b": { + "query": "\n SELECT EXISTS(SELECT 1 FROM reports WHERE id = $1)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + null + ] + } + }, "c1fddbf97350871b79cb0c235b1f7488c6616b7c1dfbde76a712fd57e91ba158": { "query": "\n SELECT id FROM game_versions\n WHERE version = $1\n ", "describe": { @@ -3348,6 +3540,45 @@ ] } }, + "c3dcb5a8b798ea6c0922698a007dbc8ab549f5f85bad780da59163f4d6371238": { + "query": "\n SELECT id FROM mods\n WHERE status = (\n SELECT id FROM statuses WHERE status = $1\n )\n ORDER BY updated ASC\n LIMIT $2;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Int8" + ] + }, + "nullable": [ + false + ] + } + }, + "c3f594d8d0ffcf5df1b36759cf3088bfaec496c5dfdbf496d3b05f0b122a5d0c": { + "query": "\n INSERT INTO reports (\n id, report_type_id, mod_id, version_id, user_id,\n body, reporter\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int4", + "Int8", + "Int8", + "Int8", + "Varchar", + "Int8" + ] + }, + "nullable": [] + } + }, "c545a74e902c5c63bca1057b76e94b9547ee21fadbc61964f45837915d5f4608": { "query": "\n INSERT INTO mods_donations (\n joining_mod_id, joining_platform_id, url\n )\n VALUES (\n $1, $2, $3\n )\n ", "describe": { @@ -3622,6 +3853,18 @@ "nullable": [] } }, + "cb597bf191d1ffe14634a9e7dc5089262497862eb4ee02091ee27c7a7606417a": { + "query": "\n DELETE FROM reports\n WHERE mod_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, "cc8b672c2733bfd110ed3361c6f477b185b530228c7206cb641dbaa40e41ea9f": { "query": "\n SELECT loader FROM loaders\n ", "describe": { @@ -3858,6 +4101,24 @@ "nullable": [] } }, + "e3235e872f98eb85d3eb4a2518fb9dc88049ce62362bfd02623e9b49ac2e9fed": { + "query": "\n SELECT name FROM report_types\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + } + }, "e35fa345b43725309b976efffbc8f9e20a62a5e90a86a82a77b55c39c168d2de": { "query": "\n SELECT id FROM versions\n WHERE mod_id = $1\n ", "describe": { diff --git a/src/database/models/categories.rs b/src/database/models/categories.rs index 658f4f968..54c114d39 100644 --- a/src/database/models/categories.rs +++ b/src/database/models/categories.rs @@ -17,6 +17,11 @@ pub struct Category { pub category: String, } +pub struct ReportType { + pub id: ReportTypeId, + pub report_type: String, +} + pub struct License { pub id: LicenseId, pub short: String, @@ -354,22 +359,61 @@ impl GameVersion { Ok(result) } - pub async fn list_type<'a, E>(version_type: &str, exec: E) -> Result, DatabaseError> + pub async fn list_filter<'a, E>( + version_type_option: Option<&str>, + major_option: Option, + exec: E, + ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { - let result = sqlx::query!( - " - SELECT version FROM game_versions - WHERE type = $1 - ORDER BY created DESC - ", - version_type - ) - .fetch_many(exec) - .try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) }) - .try_collect::>() - .await?; + let result; + + if let Some(version_type) = version_type_option { + if let Some(major) = major_option { + result = sqlx::query!( + " + SELECT version FROM game_versions + WHERE major = $1 AND type = $2 + ORDER BY created DESC + ", + major, + version_type + ) + .fetch_many(exec) + .try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) }) + .try_collect::>() + .await?; + } else { + result = sqlx::query!( + " + SELECT version FROM game_versions + WHERE type = $1 + ORDER BY created DESC + ", + version_type + ) + .fetch_many(exec) + .try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) }) + .try_collect::>() + .await?; + } + } else if let Some(major) = major_option { + result = sqlx::query!( + " + SELECT version FROM game_versions + WHERE major = $1 + ORDER BY created DESC + ", + major + ) + .fetch_many(exec) + .try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) }) + .try_collect::>() + .await?; + } else { + result = Vec::new(); + } Ok(result) } @@ -755,3 +799,129 @@ impl<'a> DonationPlatformBuilder<'a> { Ok(DonationPlatformId(result.id)) } } + +pub struct ReportTypeBuilder<'a> { + pub name: Option<&'a str>, +} + +impl ReportType { + pub fn builder() -> ReportTypeBuilder<'static> { + ReportTypeBuilder { name: None } + } + + pub async fn get_id<'a, E>(name: &str, exec: E) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + if !name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') + { + return Err(DatabaseError::InvalidIdentifier(name.to_string())); + } + + let result = sqlx::query!( + " + SELECT id FROM report_types + WHERE name = $1 + ", + name + ) + .fetch_optional(exec) + .await?; + + Ok(result.map(|r| ReportTypeId(r.id))) + } + + pub async fn get_name<'a, E>(id: ReportTypeId, exec: E) -> Result + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + SELECT name FROM report_types + WHERE id = $1 + ", + id as ReportTypeId + ) + .fetch_one(exec) + .await?; + + Ok(result.name) + } + + pub async fn list<'a, E>(exec: E) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + SELECT name FROM report_types + " + ) + .fetch_many(exec) + .try_filter_map(|e| async { Ok(e.right().map(|c| c.name)) }) + .try_collect::>() + .await?; + + Ok(result) + } + + // TODO: remove loaders with mods using them + pub async fn remove<'a, E>(name: &str, exec: E) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + use sqlx::Done; + + let result = sqlx::query!( + " + DELETE FROM report_types + WHERE name = $1 + ", + name + ) + .execute(exec) + .await?; + + if result.rows_affected() == 0 { + // Nothing was deleted + Ok(None) + } else { + Ok(Some(())) + } + } +} + +impl<'a> ReportTypeBuilder<'a> { + /// The name of the report type. Must be ASCII alphanumeric or `-`/`_` + pub fn name(self, name: &'a str) -> Result, DatabaseError> { + if name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') + { + Ok(Self { name: Some(name) }) + } else { + Err(DatabaseError::InvalidIdentifier(name.to_string())) + } + } + + pub async fn insert<'b, E>(self, exec: E) -> Result + where + E: sqlx::Executor<'b, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + INSERT INTO report_types (name) + VALUES ($1) + ON CONFLICT (name) DO NOTHING + RETURNING id + ", + self.name + ) + .fetch_one(exec) + .await?; + + Ok(ReportTypeId(result.id)) + } +} diff --git a/src/database/models/ids.rs b/src/database/models/ids.rs index d6aa2a6c1..f61c3c191 100644 --- a/src/database/models/ids.rs +++ b/src/database/models/ids.rs @@ -86,6 +86,13 @@ generate_ids!( "SELECT EXISTS(SELECT 1 FROM users WHERE id=$1)", UserId ); +generate_ids!( + pub generate_report_id, + ReportId, + 8, + "SELECT EXISTS(SELECT 1 FROM reports WHERE id=$1)", + ReportId +); #[derive(Copy, Clone, Debug, PartialEq, Eq, Type)] #[sqlx(transparent)] @@ -130,6 +137,13 @@ pub struct LoaderId(pub i32); #[sqlx(transparent)] pub struct CategoryId(pub i32); +#[derive(Copy, Clone, Debug, Type)] +#[sqlx(transparent)] +pub struct ReportId(pub i64); +#[derive(Copy, Clone, Debug, Type)] +#[sqlx(transparent)] +pub struct ReportTypeId(pub i32); + #[derive(Copy, Clone, Debug, Type)] #[sqlx(transparent)] pub struct FileId(pub i64); @@ -180,3 +194,13 @@ impl From for ids::VersionId { ids::VersionId(id.0 as u64) } } +impl From for ReportId { + fn from(id: ids::ReportId) -> Self { + ReportId(id.0 as i64) + } +} +impl From for ids::ReportId { + fn from(id: ReportId) -> Self { + ids::ReportId(id.0 as u64) + } +} diff --git a/src/database/models/mod.rs b/src/database/models/mod.rs index ec8328ddd..2dd2e8c3e 100644 --- a/src/database/models/mod.rs +++ b/src/database/models/mod.rs @@ -6,6 +6,7 @@ use thiserror::Error; pub mod categories; pub mod ids; pub mod mod_item; +pub mod report_item; pub mod team_item; pub mod user_item; pub mod version_item; diff --git a/src/database/models/mod_item.rs b/src/database/models/mod_item.rs index 53a4f99ae..793635261 100644 --- a/src/database/models/mod_item.rs +++ b/src/database/models/mod_item.rs @@ -300,6 +300,16 @@ impl Mod { return Ok(None); }; + sqlx::query!( + " + DELETE FROM reports + WHERE mod_id = $1 + ", + id as ModId, + ) + .execute(exec) + .await?; + sqlx::query!( " DELETE FROM mods_categories @@ -453,13 +463,13 @@ impl Mod { categories: m .categories .unwrap_or_default() - .split(",") + .split(',') .map(|x| x.to_string()) .collect(), versions: m .versions .unwrap_or_default() - .split(",") + .split(',') .map(|x| VersionId(x.parse().unwrap_or_default())) .collect(), donation_urls: vec![], @@ -531,8 +541,8 @@ impl Mod { slug: m.slug.clone(), body: m.body.clone(), }, - categories: m.categories.unwrap_or_default().split(",").map(|x| x.to_string()).collect(), - versions: m.versions.unwrap_or_default().split(",").map(|x| VersionId(x.parse().unwrap_or_default())).collect(), + categories: m.categories.unwrap_or_default().split(',').map(|x| x.to_string()).collect(), + versions: m.versions.unwrap_or_default().split(',').map(|x| VersionId(x.parse().unwrap_or_default())).collect(), donation_urls: vec![], status: crate::models::mods::ModStatus::from_str(&m.status_name), license_id: m.short, diff --git a/src/database/models/report_item.rs b/src/database/models/report_item.rs new file mode 100644 index 000000000..1034b3e17 --- /dev/null +++ b/src/database/models/report_item.rs @@ -0,0 +1,153 @@ +use super::ids::*; + +pub struct Report { + pub id: ReportId, + pub report_type_id: ReportTypeId, + pub mod_id: Option, + pub version_id: Option, + pub user_id: Option, + pub body: String, + pub reporter: UserId, + pub created: chrono::DateTime, +} + +pub struct QueryReport { + pub id: ReportId, + pub report_type: String, + pub mod_id: Option, + pub version_id: Option, + pub user_id: Option, + pub body: String, + pub reporter: UserId, + pub created: chrono::DateTime, +} + +impl Report { + pub async fn insert( + &self, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result<(), sqlx::error::Error> { + sqlx::query!( + " + INSERT INTO reports ( + id, report_type_id, mod_id, version_id, user_id, + body, reporter + ) + VALUES ( + $1, $2, $3, $4, $5, + $6, $7 + ) + ", + self.id as ReportId, + self.report_type_id as ReportTypeId, + self.mod_id.map(|x| x.0 as i64), + self.version_id.map(|x| x.0 as i64), + self.user_id.map(|x| x.0 as i64), + self.body, + self.reporter as UserId + ) + .execute(&mut *transaction) + .await?; + + Ok(()) + } + + pub async fn get<'a, E>(id: ReportId, exec: E) -> Result, sqlx::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + let result = sqlx::query!( + " + SELECT rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created + FROM reports r + INNER JOIN report_types rt ON rt.id = r.report_type_id + WHERE r.id = $1 + ", + id as ReportId, + ) + .fetch_optional(exec) + .await?; + + if let Some(row) = result { + Ok(Some(QueryReport { + id, + report_type: row.name, + mod_id: row.mod_id.map(ModId), + version_id: row.version_id.map(VersionId), + user_id: row.user_id.map(UserId), + body: row.body, + reporter: UserId(row.reporter), + created: row.created, + })) + } else { + Ok(None) + } + } + + pub async fn get_many<'a, E>( + version_ids: Vec, + exec: E, + ) -> Result, sqlx::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + use futures::stream::TryStreamExt; + + let version_ids_parsed: Vec = version_ids.into_iter().map(|x| x.0).collect(); + let versions = sqlx::query!( + " + SELECT r.id, rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created + FROM reports r + INNER JOIN report_types rt ON rt.id = r.report_type_id + WHERE r.id IN (SELECT * FROM UNNEST($1::bigint[])) + ", + &version_ids_parsed + ) + .fetch_many(exec) + .try_filter_map(|e| async { + Ok(e.right().map(|row| QueryReport { + id: ReportId(row.id), + report_type: row.name, + mod_id: row.mod_id.map(ModId), + version_id: row.version_id.map(VersionId), + user_id: row.user_id.map(UserId), + body: row.body, + reporter: UserId(row.reporter), + created: row.created, + })) + }) + .try_collect::>() + .await?; + + Ok(versions) + } + + pub async fn remove_full<'a, E>(id: ReportId, exec: E) -> Result, sqlx::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + let result = sqlx::query!( + " + SELECT EXISTS(SELECT 1 FROM reports WHERE id = $1) + ", + id as ReportId + ) + .fetch_one(exec) + .await?; + + if !result.exists.unwrap_or(false) { + return Ok(None); + } + + sqlx::query!( + " + DELETE FROM reports WHERE id = $1 + ", + id as ReportId, + ) + .execute(exec) + .await?; + + Ok(Some(())) + } +} diff --git a/src/database/models/user_item.rs b/src/database/models/user_item.rs index cb96a9f3a..e4b0a6d9c 100644 --- a/src/database/models/user_item.rs +++ b/src/database/models/user_item.rs @@ -239,6 +239,16 @@ impl User { .execute(exec) .await?; + sqlx::query!( + " + DELETE FROM reports + WHERE user_id = $1 + ", + id as UserId, + ) + .execute(exec) + .await?; + sqlx::query!( " DELETE FROM team_members diff --git a/src/database/models/version_item.rs b/src/database/models/version_item.rs index 05fd99354..5ccce5353 100644 --- a/src/database/models/version_item.rs +++ b/src/database/models/version_item.rs @@ -217,6 +217,16 @@ impl Version { return Ok(None); } + sqlx::query!( + " + DELETE FROM reports + WHERE version_id = $1 + ", + id as VersionId, + ) + .execute(exec) + .await?; + sqlx::query!( " DELETE FROM game_versions_versions gvv @@ -569,13 +579,13 @@ impl Version { game_versions: v .game_versions .unwrap_or_default() - .split(",") + .split(',') .map(|x| x.to_string()) .collect(), loaders: v .loaders .unwrap_or_default() - .split(",") + .split(',') .map(|x| x.to_string()) .collect(), featured: v.featured, @@ -683,8 +693,8 @@ impl Version { downloads: v.downloads, release_channel: v.release_channel, files, - game_versions: v.game_versions.unwrap_or_default().split(",").map(|x| x.to_string()).collect(), - loaders: v.loaders.unwrap_or_default().split(",").map(|x| x.to_string()).collect(), + game_versions: v.game_versions.unwrap_or_default().split(',').map(|x| x.to_string()).collect(), + loaders: v.loaders.unwrap_or_default().split(',').map(|x| x.to_string()).collect(), featured: v.featured, dependencies, } @@ -714,6 +724,7 @@ pub struct FileHash { pub hash: Vec, } +#[derive(Clone)] pub struct QueryVersion { pub id: VersionId, pub mod_id: ModId, @@ -733,6 +744,7 @@ pub struct QueryVersion { pub dependencies: Vec<(VersionId, String)>, } +#[derive(Clone)] pub struct QueryFile { pub id: FileId, pub url: String, diff --git a/src/main.rs b/src/main.rs index 7cf4a32d6..d411c2614 100644 --- a/src/main.rs +++ b/src/main.rs @@ -306,7 +306,8 @@ async fn main() -> std::io::Result<()> { .configure(routes::versions_config) .configure(routes::teams_config) .configure(routes::users_config) - .configure(routes::moderation_config), + .configure(routes::moderation_config) + .configure(routes::reports_config), ) .default_service(web::get().to(routes::not_found)) }) diff --git a/src/models/ids.rs b/src/models/ids.rs index b59dfd303..c74d7249f 100644 --- a/src/models/ids.rs +++ b/src/models/ids.rs @@ -1,6 +1,7 @@ use thiserror::Error; pub use super::mods::{ModId, VersionId}; +pub use super::reports::ReportId; pub use super::teams::TeamId; pub use super::users::UserId; @@ -107,6 +108,7 @@ base62_id_impl!(ModId, ModId); base62_id_impl!(UserId, UserId); base62_id_impl!(VersionId, VersionId); base62_id_impl!(TeamId, TeamId); +base62_id_impl!(ReportId, ReportId); pub mod base62_impl { use serde::de::{self, Deserializer, Visitor}; diff --git a/src/models/mod.rs b/src/models/mod.rs index cf01da93f..238294dcf 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,5 +1,6 @@ pub mod error; pub mod ids; pub mod mods; +pub mod reports; pub mod teams; pub mod users; diff --git a/src/models/reports.rs b/src/models/reports.rs new file mode 100644 index 000000000..e0932a5be --- /dev/null +++ b/src/models/reports.rs @@ -0,0 +1,39 @@ +use super::ids::Base62Id; +use crate::models::ids::UserId; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(from = "Base62Id")] +#[serde(into = "Base62Id")] +pub struct ReportId(pub u64); + +#[derive(Serialize, Deserialize)] +pub struct Report { + pub id: ReportId, + pub report_type: String, + pub item_id: String, + pub item_type: ItemType, + pub reporter: UserId, + pub body: String, + pub created: DateTime, +} + +#[derive(Serialize, Deserialize, Clone)] +pub enum ItemType { + Mod, + Version, + User, + Unknown, +} + +impl ItemType { + pub fn as_str(&self) -> &'static str { + match self { + ItemType::Mod => "mod", + ItemType::Version => "version", + ItemType::User => "user", + ItemType::Unknown => "unknown", + } + } +} diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 4114c218f..c59cf99e2 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -21,7 +21,7 @@ pub fn config(cfg: &mut ServiceConfig) { pub enum AuthorizationError { #[error("Environment Error")] EnvError(#[from] dotenv::Error), - #[error("An unknown database error occured")] + #[error("An unknown database error occured: {0}")] SqlxDatabaseError(#[from] sqlx::Error), #[error("Database Error: {0}")] DatabaseError(#[from] crate::database::models::DatabaseError), diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 813b89730..ce4751dec 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -6,6 +6,7 @@ mod mod_creation; mod moderation; mod mods; mod not_found; +mod reports; mod tags; mod teams; mod users; @@ -84,6 +85,12 @@ pub fn moderation_config(cfg: &mut web::ServiceConfig) { cfg.service(web::scope("moderation").service(moderation::mods)); } +pub fn reports_config(cfg: &mut web::ServiceConfig) { + cfg.service(reports::reports); + cfg.service(reports::report_create); + cfg.service(reports::delete_report); +} + #[derive(thiserror::Error, Debug)] pub enum ApiError { #[error("Environment Error")] diff --git a/src/routes/moderation.rs b/src/routes/moderation.rs index 075e1d741..8604ea5d6 100644 --- a/src/routes/moderation.rs +++ b/src/routes/moderation.rs @@ -1,11 +1,9 @@ use super::ApiError; use crate::auth::check_is_moderator_from_headers; use crate::database; -use crate::models::mods::{ModId, ModStatus}; -use crate::models::teams::TeamId; +use crate::models::mods::{Mod, ModStatus}; use actix_web::{get, web, HttpRequest, HttpResponse}; -use serde::{Deserialize, Serialize}; -use sqlx::types::chrono::{DateTime, Utc}; +use serde::Deserialize; use sqlx::PgPool; #[derive(Deserialize)] @@ -18,42 +16,6 @@ fn default_count() -> i16 { 100 } -/// A mod returned from the API moderation routes -#[derive(Serialize)] -pub struct ModerationMod { - /// The ID of the mod, encoded as a base62 string. - pub id: ModId, - /// The slug of a mod, used for vanity URLs - pub slug: Option, - /// The team of people that has ownership of this mod. - pub team: TeamId, - /// The title or name of the mod. - pub title: String, - /// A short description of the mod. - pub description: String, - /// The long description of the mod. - pub body: String, - /// The date at which the mod was first published. - pub published: DateTime, - /// The date at which the mod was first published. - pub updated: DateTime, - /// The status of the mod - pub status: ModStatus, - - /// The total number of downloads the mod has had. - pub downloads: u32, - /// The URL of the icon of the mod - pub icon_url: Option, - /// An optional link to where to submit bugs or issues with the mod. - pub issues_url: Option, - /// An optional link to the source code for the mod. - pub source_url: Option, - /// An optional link to the mod's wiki page or other relevant information. - pub wiki_url: Option, - /// An optional link to the mod's discord - pub discord_url: Option, -} - #[get("mods")] pub async fn mods( req: HttpRequest, @@ -64,9 +26,9 @@ pub async fn mods( use futures::stream::TryStreamExt; - let mods = sqlx::query!( + let mod_ids = sqlx::query!( " - SELECT * FROM mods + SELECT id FROM mods WHERE status = ( SELECT id FROM statuses WHERE status = $1 ) @@ -77,28 +39,17 @@ pub async fn mods( count.count as i64 ) .fetch_many(&**pool) - .try_filter_map(|e| async { - Ok(e.right().map(|m| ModerationMod { - id: database::models::ids::ModId(m.id).into(), - slug: m.slug, - team: database::models::ids::TeamId(m.team_id).into(), - title: m.title, - description: m.description, - body: m.body, - published: m.published, - icon_url: m.icon_url, - issues_url: m.issues_url, - source_url: m.source_url, - status: ModStatus::Processing, - updated: m.updated, - downloads: m.downloads as u32, - wiki_url: m.wiki_url, - discord_url: m.discord_url, - })) - }) - .try_collect::>() + .try_filter_map(|e| async { Ok(e.right().map(|m| database::models::ids::ModId(m.id))) }) + .try_collect::>() .await .map_err(|e| ApiError::DatabaseError(e.into()))?; + let mods: Vec = database::models::mod_item::Mod::get_many_full(mod_ids, &**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))? + .into_iter() + .map(super::mods::convert_mod) + .collect(); + Ok(HttpResponse::Ok().json(mods)) } diff --git a/src/routes/mods.rs b/src/routes/mods.rs index 1107ef548..d9add07a9 100644 --- a/src/routes/mods.rs +++ b/src/routes/mods.rs @@ -192,7 +192,7 @@ pub async fn mod_get( } } -fn convert_mod(data: database::models::mod_item::QueryMod) -> models::mods::Mod { +pub fn convert_mod(data: database::models::mod_item::QueryMod) -> models::mods::Mod { let m = data.inner; models::mods::Mod { @@ -392,6 +392,7 @@ pub async fn mod_edit( "No database entry for status provided.".to_string(), ) })?; + sqlx::query!( " UPDATE mods @@ -406,7 +407,7 @@ pub async fn mod_edit( .map_err(|e| ApiError::DatabaseError(e.into()))?; if mod_item.status.is_searchable() && !status.is_searchable() { - delete_from_index(id.into(), config).await?; + delete_from_index(mod_id, config).await?; } else if !mod_item.status.is_searchable() && status.is_searchable() { let index_mod = crate::search::indexing::local_import::query_one( mod_id.into(), diff --git a/src/routes/reports.rs b/src/routes/reports.rs new file mode 100644 index 000000000..14019492d --- /dev/null +++ b/src/routes/reports.rs @@ -0,0 +1,181 @@ +use crate::auth::{check_is_moderator_from_headers, get_user_from_headers}; +use crate::models::ids::{ModId, UserId, VersionId}; +use crate::models::reports::{ItemType, Report}; +use crate::routes::ApiError; +use actix_web::{delete, get, post, web, HttpRequest, HttpResponse}; +use serde::Deserialize; +use sqlx::PgPool; + +#[derive(Deserialize)] +pub struct CreateReport { + pub report_type: String, + pub item_id: String, + pub item_type: ItemType, + pub body: String, +} + +#[post("report")] +pub async fn report_create( + req: HttpRequest, + pool: web::Data, + new_report: web::Json, +) -> Result { + let mut transaction = pool + .begin() + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + let current_user = get_user_from_headers(req.headers(), &mut *transaction).await?; + + let id = crate::database::models::generate_report_id(&mut transaction).await?; + let report_type = crate::database::models::categories::ReportType::get_id( + &*new_report.report_type, + &mut *transaction, + ) + .await? + .ok_or_else(|| { + ApiError::InvalidInputError(format!("Invalid report type: {}", new_report.report_type)) + })?; + let mut report = crate::database::models::report_item::Report { + id, + report_type_id: report_type, + mod_id: None, + version_id: None, + user_id: None, + body: new_report.body.clone(), + reporter: current_user.id.into(), + created: chrono::Utc::now(), + }; + + match new_report.item_type { + ItemType::Mod => { + report.mod_id = Some(serde_json::from_str::(&*new_report.item_id)?.into()) + } + ItemType::Version => { + report.version_id = + Some(serde_json::from_str::(&*new_report.item_id)?.into()) + } + ItemType::User => { + report.user_id = Some(serde_json::from_str::(&*new_report.item_id)?.into()) + } + ItemType::Unknown => { + return Err(ApiError::InvalidInputError(format!( + "Invalid report item type: {}", + new_report.item_type.as_str() + ))) + } + } + + report + .insert(&mut transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + transaction + .commit() + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + Ok(HttpResponse::Ok().json(Report { + id: id.into(), + report_type: new_report.report_type.clone(), + item_id: new_report.item_id.clone(), + item_type: new_report.item_type.clone(), + reporter: current_user.id, + body: new_report.body.clone(), + created: chrono::Utc::now(), + })) +} + +#[derive(Deserialize)] +pub struct ResultCount { + #[serde(default = "default_count")] + count: i16, +} + +fn default_count() -> i16 { + 100 +} + +#[get("report")] +pub async fn reports( + req: HttpRequest, + pool: web::Data, + count: web::Query, +) -> Result { + check_is_moderator_from_headers(req.headers(), &**pool).await?; + + use futures::stream::TryStreamExt; + + let report_ids = sqlx::query!( + " + SELECT id FROM reports + ORDER BY created ASC + LIMIT $1; + ", + count.count as i64 + ) + .fetch_many(&**pool) + .try_filter_map(|e| async { + Ok(e.right() + .map(|m| crate::database::models::ids::ReportId(m.id))) + }) + .try_collect::>() + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + let query_reports = crate::database::models::report_item::Report::get_many(report_ids, &**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + let mut reports = Vec::new(); + + for x in query_reports { + let mut item_id = "".to_string(); + let mut item_type = ItemType::Unknown; + + if let Some(mod_id) = x.mod_id { + item_id = serde_json::to_string::(&mod_id.into())?; + item_type = ItemType::Mod; + } else if let Some(version_id) = x.version_id { + item_id = serde_json::to_string::(&version_id.into())?; + item_type = ItemType::Version; + } else if let Some(user_id) = x.user_id { + item_id = serde_json::to_string::(&user_id.into())?; + item_type = ItemType::User; + } + + reports.push(Report { + id: x.id.into(), + report_type: x.report_type, + item_id, + item_type, + reporter: x.reporter.into(), + body: x.body, + created: x.created, + }) + } + + Ok(HttpResponse::Ok().json(reports)) +} + +#[delete("report/{id}")] +pub async fn delete_report( + req: HttpRequest, + pool: web::Data, + info: web::Path<(crate::models::reports::ReportId,)>, +) -> Result { + check_is_moderator_from_headers(req.headers(), &**pool).await?; + + let result = crate::database::models::report_item::Report::remove_full( + info.into_inner().0.into(), + &**pool, + ) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + if result.is_some() { + Ok(HttpResponse::Ok().body("")) + } else { + Ok(HttpResponse::NotFound().body("")) + } +} diff --git a/src/routes/tags.rs b/src/routes/tags.rs index 4cbb857df..13d132803 100644 --- a/src/routes/tags.rs +++ b/src/routes/tags.rs @@ -1,7 +1,7 @@ use super::ApiError; use crate::auth::check_is_admin_from_headers; use crate::database::models; -use crate::database::models::categories::{DonationPlatform, License}; +use crate::database::models::categories::{DonationPlatform, License, ReportType}; use actix_web::{delete, get, put, web, HttpRequest, HttpResponse}; use models::categories::{Category, GameVersion, Loader}; use sqlx::PgPool; @@ -125,6 +125,7 @@ pub async fn loader_delete( pub struct GameVersionQueryData { #[serde(rename = "type")] type_: Option, + major: Option, } #[get("game_version")] @@ -132,8 +133,9 @@ pub async fn game_version_list( pool: web::Data, query: web::Query, ) -> Result { - if let Some(type_) = &query.type_ { - let results = GameVersion::list_type(type_, &**pool).await?; + if query.type_.is_some() || query.major.is_some() { + let results = + GameVersion::list_filter(query.type_.as_deref(), query.major, &**pool).await?; Ok(HttpResponse::Ok().json(results)) } else { let results = GameVersion::list(&**pool).await?; @@ -337,3 +339,49 @@ pub async fn donation_platform_delete( Ok(HttpResponse::NotFound().body("")) } } + +#[get("report_type")] +pub async fn report_type_list(pool: web::Data) -> Result { + let results = ReportType::list(&**pool).await?; + Ok(HttpResponse::Ok().json(results)) +} + +#[put("report_type/{name}")] +pub async fn report_type_create( + req: HttpRequest, + pool: web::Data, + loader: web::Path<(String,)>, +) -> Result { + check_is_admin_from_headers(req.headers(), &**pool).await?; + + let name = loader.into_inner().0; + + let _id = ReportType::builder().name(&name)?.insert(&**pool).await?; + + Ok(HttpResponse::Ok().body("")) +} + +#[delete("report_type/{name}")] +pub async fn report_type_delete( + req: HttpRequest, + pool: web::Data, + report_type: web::Path<(String,)>, +) -> Result { + check_is_admin_from_headers(req.headers(), &**pool).await?; + + let name = report_type.into_inner().0; + let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?; + + let result = ReportType::remove(&name, &mut transaction).await?; + + transaction + .commit() + .await + .map_err(models::DatabaseError::from)?; + + if result.is_some() { + Ok(HttpResponse::Ok().body("")) + } else { + Ok(HttpResponse::NotFound().body("")) + } +} diff --git a/src/routes/versions.rs b/src/routes/versions.rs index 0c0fe49a4..9bd403d6b 100644 --- a/src/routes/versions.rs +++ b/src/routes/versions.rs @@ -11,10 +11,10 @@ use sqlx::PgPool; use std::borrow::Borrow; use std::sync::Arc; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct VersionListFilters { - pub game_versions: Option>, - pub loaders: Option>, + pub game_versions: Option, + pub loaders: Option, pub featured: Option, } @@ -38,8 +38,14 @@ pub async fn version_list( if mod_exists.unwrap_or(false) { let version_ids = database::models::Version::get_mod_versions( id, - filters.game_versions, - filters.loaders, + filters + .game_versions + .as_ref() + .map(|x| serde_json::from_str(x).unwrap_or_default()), + filters + .loaders + .as_ref() + .map(|x| serde_json::from_str(x).unwrap_or_default()), &**pool, ) .await @@ -49,17 +55,35 @@ pub async fn version_list( .await .map_err(|e| ApiError::DatabaseError(e.into()))?; - let mut response = Vec::new(); - for version in versions { - if let Some(featured) = filters.featured { - if featured { - response.push(convert_version(version)) - } - } else { - response.push(convert_version(version)) - } + let mut response = versions + .iter() + .cloned() + .filter(|version| { + filters + .featured + .map(|featured| featured == version.featured) + .unwrap_or(true) + }) + .map(convert_version) + .collect::>(); + + // Attempt to populate versions with "auto featured" versions + if response.is_empty() && !versions.is_empty() && filters.featured.unwrap_or(false) { + database::models::categories::GameVersion::list_filter(None, Some(true), &**pool) + .await? + .into_iter() + .for_each(|major_version| { + versions + .iter() + .find(|version| version.game_versions.contains(&major_version)) + .map(|version| response.push(convert_version(version.clone()))) + .unwrap_or(()); + }); } + response.sort_by(|a, b| b.date_published.cmp(&a.date_published)); + response.dedup_by(|a, b| a.id == b.id); + Ok(HttpResponse::Ok().json(response)) } else { Ok(HttpResponse::NotFound().body("")) diff --git a/src/search/mod.rs b/src/search/mod.rs index 87bef0060..f52cb6be5 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -14,8 +14,8 @@ pub mod indexing; #[derive(Error, Debug)] pub enum SearchError { - #[error("Error while connecting to the MeiliSearch database: {0}")] - IndexDBError(#[from] meilisearch_sdk::errors::Error), + #[error("MeiliSearch Error: {0}")] + MeiliSearchError(#[from] meilisearch_sdk::errors::Error), #[error("Error while serializing or deserializing JSON: {0}")] SerDeError(#[from] serde_json::Error), #[error("Error while parsing an integer: {0}")] @@ -30,7 +30,7 @@ impl actix_web::ResponseError for SearchError { fn status_code(&self) -> StatusCode { match self { SearchError::EnvError(..) => StatusCode::INTERNAL_SERVER_ERROR, - SearchError::IndexDBError(..) => StatusCode::INTERNAL_SERVER_ERROR, + SearchError::MeiliSearchError(..) => StatusCode::BAD_REQUEST, SearchError::SerDeError(..) => StatusCode::BAD_REQUEST, SearchError::IntParsingError(..) => StatusCode::BAD_REQUEST, SearchError::InvalidIndex(..) => StatusCode::BAD_REQUEST, @@ -41,7 +41,7 @@ impl actix_web::ResponseError for SearchError { HttpResponse::build(self.status_code()).json(ApiError { error: match self { SearchError::EnvError(..) => "environment_error", - SearchError::IndexDBError(..) => "indexdb_error", + SearchError::MeiliSearchError(..) => "meilisearch_error", SearchError::SerDeError(..) => "invalid_input", SearchError::IntParsingError(..) => "invalid_input", SearchError::InvalidIndex(..) => "invalid_input",