diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 000000000..407798792 --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/migrations/20230104214503_random-projects.sql b/migrations/20230104214503_random-projects.sql new file mode 100644 index 000000000..b99325a22 --- /dev/null +++ b/migrations/20230104214503_random-projects.sql @@ -0,0 +1,4 @@ +-- Add migration script here +DROP EXTENSION IF EXISTS tsm_system_rows; + +CREATE EXTENSION tsm_system_rows; diff --git a/sqlx-data.json b/sqlx-data.json index 5a8ae78df..aa1d174a6 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -992,6 +992,27 @@ }, "query": "\n DELETE FROM donation_platforms\n WHERE short = $1\n " }, + "1cefe4924d3c1f491739858ce844a22903d2dbe26f255219299f1833a10ce3d7": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Int8", + "TextArray" + ] + } + }, + "query": "\n SELECT id FROM mods TABLESAMPLE SYSTEM_ROWS($1) WHERE status = ANY($2)\n " + }, "1d1fe6f0c03a63b1c6bd5ffbddfd82aa7d24e1db3f3137ed046724cb78929f88": { "describe": { "columns": [], @@ -1085,19 +1106,6 @@ }, "query": "SELECT EXISTS(SELECT 1 FROM versions WHERE id=$1)" }, - "20061dd085656ec83b63d893c60bdd3dcbf5b8c78a725358aa8e3312a7571e5c": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - } - }, - "query": "\n UPDATE mods\n SET flame_anvil_user = $1\n WHERE (id = $2)\n " - }, "20413fce27fe9c1dec71900f9563e787acc11e7789b5294786e0ea6f20d7d958": { "describe": { "columns": [], @@ -1416,6 +1424,18 @@ }, "query": "\n SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,\n v.changelog changelog, v.date_published date_published, v.downloads downloads,\n v.version_type version_type, v.featured featured, v.status status, v.requested_status requested_status,\n JSONB_AGG(DISTINCT jsonb_build_object('version', gv.version, 'created', gv.created)) filter (where gv.version is not null) game_versions,\n ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders,\n JSONB_AGG(DISTINCT jsonb_build_object('id', f.id, 'url', f.url, 'filename', f.filename, 'primary', f.is_primary, 'size', f.size, 'file_type', f.file_type)) filter (where f.id is not null) files,\n JSONB_AGG(DISTINCT jsonb_build_object('algorithm', h.algorithm, 'hash', encode(h.hash, 'escape'), 'file_id', h.file_id)) filter (where h.hash is not null) hashes,\n JSONB_AGG(DISTINCT jsonb_build_object('project_id', d.mod_dependency_id, 'version_id', d.dependency_id, 'dependency_type', d.dependency_type,'file_name', dependency_file_name)) filter (where d.dependency_type is not null) dependencies\n FROM versions v\n LEFT OUTER JOIN game_versions_versions gvv on v.id = gvv.joining_version_id\n LEFT OUTER JOIN game_versions gv on gvv.game_version_id = gv.id\n LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id\n LEFT OUTER JOIN loaders l on lv.loader_id = l.id\n LEFT OUTER JOIN files f on v.id = f.version_id\n LEFT OUTER JOIN hashes h on f.id = h.file_id\n LEFT OUTER JOIN dependencies d on v.id = d.dependent_id\n WHERE v.id = $1\n GROUP BY v.id;\n " }, + "299b8ea6e7a0048fa389cc4432715dc2a09e227d2f08e91167a43372a7ac6e35": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1 AND is_additional = FALSE\n " + }, "29e657d26f0fb24a766f5b5eb6a94d01d1616884d8ca10e91536e974d5b585a6": { "describe": { "columns": [], @@ -1887,6 +1907,18 @@ }, "query": "\n INSERT INTO game_versions_versions (game_version_id, joining_version_id)\n VALUES ($1, $2)\n " }, + "3fcfed18cbfb37866e0fa57a4e95efb326864f8219941d1b696add39ed333ad1": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1 AND is_additional = TRUE\n " + }, "40f7c5bec98fe3503d6bd6db2eae5a4edb8d5d6efda9b9dc124f344ae5c60e08": { "describe": { "columns": [], @@ -2151,6 +2183,18 @@ }, "query": "\n INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id)\n VALUES ($1, $2)\n " }, + "4567790f0dc98ff20b596a33161d1f6ac8af73da67fe8c54192724626c6bf670": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM mods_donations\n WHERE joining_mod_id = $1\n " + }, "4778d2f5994fda2f978fa53e0840c1a9a2582ef0434a5ff7f21706f1dc4edcf4": { "describe": { "columns": [], @@ -3751,6 +3795,19 @@ }, "query": "\n INSERT INTO notifications_actions (\n notification_id, title, action_route, action_route_method\n )\n VALUES (\n $1, $2, $3, $4\n )\n " }, + "83d428e1c07d16e356ef26bdf1d707940b1683b5f631ded1f6674a081453d67b": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + } + }, + "query": "\n UPDATE mods\n SET source_url = $1\n WHERE (id = $2)\n " + }, "85b40877c48fc4f23039c1b556007f92056a015f160fe1059b0d3b13615af0fb": { "describe": { "columns": [], @@ -3974,6 +4031,19 @@ }, "query": "\n DELETE FROM payouts_values\n WHERE user_id = $1\n " }, + "8abb317c85f48c7dd9ccf4a7b8fbc0b58ac73f7ae87ff2dfe67009a51089f784": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + } + }, + "query": "\n UPDATE mods\n SET wiki_url = $1\n WHERE (id = $2)\n " + }, "8ba2b2c38958f1c542e514fc62ab4682f58b0b442ac1842d20625420698e34ec": { "describe": { "columns": [], @@ -4138,6 +4208,20 @@ }, "query": "\n UPDATE mods\n SET wiki_url = $1\n WHERE (id = $2)\n " }, + "9aab2350d576fd934b0541d1f71f320ac939b44a179fee3d1638113cdb3ddfe7": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Int4", + "Varchar" + ] + } + }, + "query": "\n INSERT INTO mods_donations (joining_mod_id, joining_platform_id, url)\n VALUES ($1, $2, $3)\n " + }, "9c8f3f9503b5bb52e05bbc8a8eee7f640ab7d6b04a59ec111ce8b23e886911de": { "describe": { "columns": [], @@ -4965,19 +5049,6 @@ }, "query": "\n DELETE FROM users\n WHERE id = $1\n " }, - "b69b18b3451762fc24a1390dd537f612ed066bd285e8237a99fb998ff9d066e9": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int4", - "Int8" - ] - } - }, - "query": "\n UPDATE mods\n SET flame_anvil_project = $1\n WHERE (id = $2)\n " - }, "b7b2b5b99340c7601de53cc33dc56af054b50b2fe4d1d212901c958115a42baa": { "describe": { "columns": [], @@ -5076,6 +5147,19 @@ }, "query": "\n DELETE FROM notifications_actions\n WHERE notification_id = ANY($1)\n " }, + "bad7cae347771e801976c26f2afaf33bda371051923b8f74a2f32a0ef5c65e57": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + } + }, + "query": "\n UPDATE mods\n SET discord_url = $1\n WHERE (id = $2)\n " + }, "bbfb47ae2c972734785df6b7c3e62077dc544ef4ccf8bb89e9c22c2f50a933c1": { "describe": { "columns": [], @@ -5825,6 +5909,19 @@ }, "query": "\n DELETE FROM hashes\n WHERE file_id = $1\n " }, + "cdf20036b29b61da40bf990c9ab04c509297a4d65bc9b136c9fb20f1e97e1149": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Int4" + ] + } + }, + "query": "\n INSERT INTO mods_categories (joining_mod_id, joining_category_id, is_additional)\n VALUES ($1, $2, FALSE)\n " + }, "ce46915d4ce10f3fc2d4328157b06016da838672c8336b3b8d27e09eeec979d3": { "describe": { "columns": [ @@ -6796,6 +6893,19 @@ }, "query": "\n DELETE FROM files\n WHERE files.id = $1\n " }, + "e3fb74a94a6a78b1007dd99ad11bdcfaa0957ed7d1683997aef7301e0f15baba": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + } + }, + "query": "\n UPDATE mods\n SET issues_url = $1\n WHERE (id = $2)\n " + }, "e42d3a64ae4d88b73136a319fe79a8b070c193707e3560d18deca478662d8d90": { "describe": { "columns": [], @@ -6967,27 +7077,6 @@ }, "query": "\n SELECT f.url url, h.hash hash, h.algorithm algorithm, f.version_id version_id, v.mod_id project_id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n INNER JOIN versions v ON v.id = f.version_id AND v.status != ANY($1)\n INNER JOIN mods m on v.mod_id = m.id\n WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ANY($4)\n " }, - "e5de3b33893b6b48a7fee0e3f20e371e56fdfd71640662aacc15fe3bf747b3a1": { - "describe": { - "columns": [ - { - "name": "exists", - "ordinal": 0, - "type_info": "Bool" - } - ], - "nullable": [ - null - ], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - } - }, - "query": "\n SELECT EXISTS(\n SELECT 1 FROM team_members\n INNER JOIN users u on team_members.user_id = u.id AND u.flame_anvil_key IS NOT NULL\n WHERE team_id = $1 AND user_id = $2 AND accepted = TRUE\n )\n " - }, "e673006d1355fa91ba5739d7cf569eec5e1ec501f7b1dc2b431f0b1c25ac07d5": { "describe": { "columns": [], diff --git a/src/database/models/ids.rs b/src/database/models/ids.rs index d90d7a810..c94c3a660 100644 --- a/src/database/models/ids.rs +++ b/src/database/models/ids.rs @@ -110,7 +110,7 @@ generate_ids!( #[sqlx(transparent)] pub struct UserId(pub i64); -#[derive(Copy, Clone, Debug, Type)] +#[derive(Copy, Clone, Debug, Type, Eq, PartialEq)] #[sqlx(transparent)] pub struct TeamId(pub i64); #[derive(Copy, Clone, Debug, Type)] diff --git a/src/database/models/project_item.rs b/src/database/models/project_item.rs index 36c57ba2e..6e2e3b4a7 100644 --- a/src/database/models/project_item.rs +++ b/src/database/models/project_item.rs @@ -784,7 +784,7 @@ impl Project { } pub async fn get_many_full<'a, E>( - project_ids: Vec, + project_ids: &[ProjectId], exec: E, ) -> Result, sqlx::Error> where @@ -793,7 +793,7 @@ impl Project { use futures::TryStreamExt; let project_ids_parsed: Vec = - project_ids.into_iter().map(|x| x.0).collect(); + project_ids.iter().map(|x| x.0).collect(); sqlx::query!( " SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows, diff --git a/src/models/projects.rs b/src/models/projects.rs index 6b819006e..0a0a829d1 100644 --- a/src/models/projects.rs +++ b/src/models/projects.rs @@ -250,7 +250,7 @@ pub struct License { pub url: Option, } -#[derive(Serialize, Deserialize, Validate, Clone)] +#[derive(Serialize, Deserialize, Validate, Clone, Eq, PartialEq)] pub struct DonationLink { pub id: String, pub platform: String, diff --git a/src/routes/mod.rs b/src/routes/mod.rs index d9021358d..46e6c7e8c 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -53,6 +53,8 @@ pub fn v2_config(cfg: &mut web::ServiceConfig) { pub fn projects_config(cfg: &mut web::ServiceConfig) { cfg.service(projects::project_search); cfg.service(projects::projects_get); + cfg.service(projects::projects_edit); + cfg.service(projects::random_projects_get); cfg.service(project_creation::project_create); cfg.service( diff --git a/src/routes/moderation.rs b/src/routes/moderation.rs index d800824a5..fdc086ef3 100644 --- a/src/routes/moderation.rs +++ b/src/routes/moderation.rs @@ -44,7 +44,7 @@ pub async fn get_projects( .await?; let projects: Vec<_> = - database::Project::get_many_full(project_ids, &**pool) + database::Project::get_many_full(&project_ids, &**pool) .await? .into_iter() .map(crate::models::projects::Project::from) diff --git a/src/routes/projects.rs b/src/routes/projects.rs index 1afd91192..10616ebf6 100644 --- a/src/routes/projects.rs +++ b/src/routes/projects.rs @@ -2,7 +2,6 @@ use crate::database; use crate::database::models::notification_item::NotificationBuilder; use crate::file_hosting::FileHost; use crate::models; -use crate::models::ids::UserId; use crate::models::projects::{ DonationLink, Project, ProjectId, ProjectStatus, SearchRequest, SideType, }; @@ -30,6 +29,45 @@ pub async fn project_search( Ok(HttpResponse::Ok().json(results)) } +#[derive(Deserialize, Validate)] +pub struct RandomProjects { + #[validate(range(min = 1, max = 100))] + pub count: u32, +} + +#[get("projects_random")] +pub async fn random_projects_get( + web::Query(count): web::Query, + pool: web::Data, +) -> Result { + count.validate().map_err(|err| { + ApiError::Validation(validation_errors_to_string(err, None)) + })?; + + let project_ids = sqlx::query!( + " + SELECT id FROM mods TABLESAMPLE SYSTEM_ROWS($1) WHERE status = ANY($2) + ", + count.count as i32, + &*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_searchable()).map(|x| x.to_string()).collect::>(), + ) + .fetch_many(&**pool) + .try_filter_map(|e| async { + Ok(e.right().map(|m| database::models::ids::ProjectId(m.id))) + }) + .try_collect::>() + .await?; + + let projects_data = + database::models::Project::get_many_full(&project_ids, &**pool) + .await? + .into_iter() + .map(Project::from) + .collect::>(); + + Ok(HttpResponse::Ok().json(projects_data)) +} + #[derive(Serialize, Deserialize)] pub struct ProjectIds { pub ids: String, @@ -41,13 +79,14 @@ pub async fn projects_get( web::Query(ids): web::Query, pool: web::Data, ) -> Result { - let project_ids = serde_json::from_str::>(&ids.ids)? - .into_iter() - .map(|x| x.into()) - .collect(); + let project_ids: Vec = + serde_json::from_str::>(&ids.ids)? + .into_iter() + .map(|x| x.into()) + .collect(); let projects_data = - database::models::Project::get_many_full(project_ids, &**pool).await?; + database::models::Project::get_many_full(&project_ids, &**pool).await?; let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); @@ -207,22 +246,23 @@ pub async fn dependency_list( )>>() .await?; - let (projects_result, versions_result) = futures::join!( - database::Project::get_many_full( - dependencies - .iter() - .filter_map(|x| if x.0.is_none() { - if let Some(mod_dependency_id) = x.2 { - Some(mod_dependency_id) - } else { - x.1 - } + let project_ids = dependencies + .iter() + .filter_map(|x| { + if x.0.is_none() { + if let Some(mod_dependency_id) = x.2 { + Some(mod_dependency_id) } else { x.1 - }) - .collect(), - &**pool, - ), + } + } else { + x.1 + } + }) + .collect::>(); + + let (projects_result, versions_result) = futures::join!( + database::Project::get_many_full(&project_ids, &**pool,), database::Version::get_many_full( dependencies.iter().filter_map(|x| x.0).collect(), &**pool, @@ -344,18 +384,6 @@ pub struct EditProject { )] #[validate(length(max = 65536))] pub moderation_message_body: Option>, - #[serde( - default, - skip_serializing_if = "Option::is_none", - with = "::serde_with::rust::double_option" - )] - pub flame_anvil_user: Option>, - #[serde( - default, - skip_serializing_if = "Option::is_none", - with = "::serde_with::rust::double_option" - )] - pub flame_anvil_project: Option>, } #[patch("{id}")] @@ -1085,92 +1113,6 @@ pub async fn project_edit( .await?; } - if let Some(project) = &new_project.flame_anvil_project { - if !perms.contains(Permissions::EDIT_DETAILS) { - return Err(ApiError::CustomAuthentication( - "You do not have the permissions to edit the external syncing project!" - .to_string(), - )); - } - - if project_item.project_type == "modpack" { - return Err(ApiError::InvalidInput( - "This project syncing feature is not available for modpacks!" - .to_string(), - )); - } - - sqlx::query!( - " - UPDATE mods - SET flame_anvil_project = $1 - WHERE (id = $2) - ", - *project, - id as database::models::ids::ProjectId, - ) - .execute(&mut *transaction) - .await?; - } - - if let Some(user_id) = &new_project.flame_anvil_user { - if !perms.contains(Permissions::EDIT_DETAILS) { - return Err(ApiError::CustomAuthentication( - "You do not have the permissions to edit the syncing user for this project!" - .to_string(), - )); - } - - if project_item.project_type == "modpack" { - return Err(ApiError::InvalidInput( - "This project syncing feature is not available for modpacks!" - .to_string(), - )); - } - - if let Some(user_id) = user_id { - if user_id != &user.id && !user.role.is_admin() { - return Err(ApiError::InvalidInput( - "You may only set yourself as the syncing user!" - .to_string(), - )); - } - - let results = sqlx::query!( - " - SELECT EXISTS( - SELECT 1 FROM team_members - INNER JOIN users u on team_members.user_id = u.id AND u.flame_anvil_key IS NOT NULL - WHERE team_id = $1 AND user_id = $2 AND accepted = TRUE - ) - ", - project_item.inner.team_id as database::models::ids::TeamId, - database::models::ids::UserId::from(*user_id) as database::models::ids::UserId, - ) - .fetch_one(&mut *transaction) - .await?; - - if !results.exists.unwrap_or(true) { - return Err(ApiError::InvalidInput( - "The given user is not part of your team or does not have a syncing key added to their account!" - .to_string(), - )); - } - } - - sqlx::query!( - " - UPDATE mods - SET flame_anvil_user = $1 - WHERE (id = $2) - ", - user_id.map(|x| x.0 as i64), - id as database::models::ids::ProjectId, - ) - .execute(&mut *transaction) - .await?; - } - transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { @@ -1183,6 +1125,398 @@ pub async fn project_edit( } } +#[derive(Deserialize, Validate)] +pub struct BulkEditProject { + #[validate(length(max = 3))] + pub categories: Option>, + #[validate(length(max = 3))] + pub add_categories: Option>, + pub remove_categories: Option>, + + #[validate(length(max = 256))] + pub additional_categories: Option>, + #[validate(length(max = 3))] + pub add_additional_categories: Option>, + pub remove_additional_categories: Option>, + + #[validate] + pub donation_urls: Option>, + #[validate] + pub add_donation_urls: Option>, + #[validate] + pub remove_donation_urls: Option>, + + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "::serde_with::rust::double_option" + )] + #[validate( + custom(function = "crate::util::validate::validate_url"), + length(max = 2048) + )] + pub issues_url: Option>, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "::serde_with::rust::double_option" + )] + #[validate( + custom(function = "crate::util::validate::validate_url"), + length(max = 2048) + )] + pub source_url: Option>, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "::serde_with::rust::double_option" + )] + #[validate( + custom(function = "crate::util::validate::validate_url"), + length(max = 2048) + )] + pub wiki_url: Option>, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "::serde_with::rust::double_option" + )] + #[validate( + custom(function = "crate::util::validate::validate_url"), + length(max = 2048) + )] + pub discord_url: Option>, +} + +#[patch("projects")] +pub async fn projects_edit( + req: HttpRequest, + web::Query(ids): web::Query, + pool: web::Data, + bulk_edit_project: web::Json, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + + bulk_edit_project.validate().map_err(|err| { + ApiError::Validation(validation_errors_to_string(err, None)) + })?; + + let project_ids: Vec = + serde_json::from_str::>(&ids.ids)? + .into_iter() + .map(|x| x.into()) + .collect(); + + let projects_data = + database::models::Project::get_many_full(&project_ids, &**pool).await?; + + if let Some(id) = project_ids + .iter() + .find(|x| !projects_data.iter().any(|y| x == &&y.inner.id)) + { + return Err(ApiError::InvalidInput(format!( + "Project {} not found", + ProjectId(id.0 as u64) + ))); + } + + let team_members = database::models::TeamMember::get_from_team_full_many( + projects_data.iter().map(|x| x.inner.team_id).collect(), + &**pool, + ) + .await?; + + let categories = + database::models::categories::Category::list(&**pool).await?; + let donation_platforms = + database::models::categories::DonationPlatform::list(&**pool).await?; + + let mut transaction = pool.begin().await?; + + for project in projects_data { + if !user.role.is_mod() { + if let Some(member) = team_members + .iter() + .find(|x| x.team_id == project.inner.team_id) + { + if !member.permissions.contains(Permissions::EDIT_DETAILS) { + return Err(ApiError::CustomAuthentication( + format!("You do not have the permissions to bulk edit project {}!", project.inner.title), + )); + } + } else if project.inner.status.is_hidden() { + return Ok(HttpResponse::NotFound().body("")); + } else { + return Err(ApiError::CustomAuthentication(format!( + "You are not a member of project {}!", + project.inner.title + ))); + }; + } + + let mut set_categories = + if let Some(categories) = bulk_edit_project.categories.clone() { + categories + } else { + project.categories.clone() + }; + + if let Some(delete_categories) = &bulk_edit_project.remove_categories { + for category in delete_categories { + if let Some(pos) = + set_categories.iter().position(|x| x == category) + { + set_categories.remove(pos); + } + } + } + + if let Some(add_categories) = &bulk_edit_project.add_categories { + for category in add_categories { + if set_categories.len() < 3 { + set_categories.push(category.clone()); + } else { + break; + } + } + } + + if set_categories != project.categories { + sqlx::query!( + " + DELETE FROM mods_categories + WHERE joining_mod_id = $1 AND is_additional = FALSE + ", + project.inner.id as database::models::ids::ProjectId, + ) + .execute(&mut *transaction) + .await?; + + for category in set_categories { + let category_id = categories + .iter() + .find(|x| x.category == category) + .ok_or_else(|| { + ApiError::InvalidInput(format!( + "Category {} does not exist.", + category.clone() + )) + })? + .id; + + sqlx::query!( + " + INSERT INTO mods_categories (joining_mod_id, joining_category_id, is_additional) + VALUES ($1, $2, FALSE) + ", + project.inner.id as database::models::ids::ProjectId, + category_id as database::models::ids::CategoryId, + ) + .execute(&mut *transaction) + .await?; + } + } + + let mut set_additional_categories = if let Some(categories) = + bulk_edit_project.additional_categories.clone() + { + categories + } else { + project.additional_categories.clone() + }; + + if let Some(delete_categories) = + &bulk_edit_project.remove_additional_categories + { + for category in delete_categories { + if let Some(pos) = + set_additional_categories.iter().position(|x| x == category) + { + set_additional_categories.remove(pos); + } + } + } + + if let Some(add_categories) = + &bulk_edit_project.add_additional_categories + { + for category in add_categories { + if set_additional_categories.len() < 256 { + set_additional_categories.push(category.clone()); + } else { + break; + } + } + } + + if set_additional_categories != project.additional_categories { + sqlx::query!( + " + DELETE FROM mods_categories + WHERE joining_mod_id = $1 AND is_additional = TRUE + ", + project.inner.id as database::models::ids::ProjectId, + ) + .execute(&mut *transaction) + .await?; + + for category in set_additional_categories { + let category_id = categories + .iter() + .find(|x| x.category == category) + .ok_or_else(|| { + ApiError::InvalidInput(format!( + "Category {} does not exist.", + category.clone() + )) + })? + .id; + + sqlx::query!( + " + INSERT INTO mods_categories (joining_mod_id, joining_category_id, is_additional) + VALUES ($1, $2, TRUE) + ", + project.inner.id as database::models::ids::ProjectId, + category_id as database::models::ids::CategoryId, + ) + .execute(&mut *transaction) + .await?; + } + } + + let project_donations: Vec = project + .donation_urls + .into_iter() + .map(|d| DonationLink { + id: d.platform_short, + platform: d.platform_name, + url: d.url, + }) + .collect(); + let mut set_donation_links = if let Some(donation_links) = + bulk_edit_project.donation_urls.clone() + { + donation_links + } else { + project_donations.clone() + }; + + if let Some(delete_donations) = &bulk_edit_project.remove_donation_urls + { + for donation in delete_donations { + if let Some(pos) = set_donation_links + .iter() + .position(|x| donation.url == x.url && donation.id == x.id) + { + set_donation_links.remove(pos); + } + } + } + + if let Some(add_donations) = &bulk_edit_project.add_donation_urls { + set_donation_links.append(&mut add_donations.clone()); + } + + if set_donation_links != project_donations { + sqlx::query!( + " + DELETE FROM mods_donations + WHERE joining_mod_id = $1 + ", + project.inner.id as database::models::ids::ProjectId, + ) + .execute(&mut *transaction) + .await?; + + for donation in set_donation_links { + let platform_id = donation_platforms + .iter() + .find(|x| x.short == donation.id) + .ok_or_else(|| { + ApiError::InvalidInput(format!( + "Platform {} does not exist.", + donation.id.clone() + )) + })? + .id; + + sqlx::query!( + " + INSERT INTO mods_donations (joining_mod_id, joining_platform_id, url) + VALUES ($1, $2, $3) + ", + project.inner.id as database::models::ids::ProjectId, + platform_id as database::models::ids::DonationPlatformId, + donation.url + ) + .execute(&mut *transaction) + .await?; + } + } + + if let Some(issues_url) = &bulk_edit_project.issues_url { + sqlx::query!( + " + UPDATE mods + SET issues_url = $1 + WHERE (id = $2) + ", + issues_url.as_deref(), + project.inner.id as database::models::ids::ProjectId, + ) + .execute(&mut *transaction) + .await?; + } + + if let Some(source_url) = &bulk_edit_project.source_url { + sqlx::query!( + " + UPDATE mods + SET source_url = $1 + WHERE (id = $2) + ", + source_url.as_deref(), + project.inner.id as database::models::ids::ProjectId, + ) + .execute(&mut *transaction) + .await?; + } + + if let Some(wiki_url) = &bulk_edit_project.wiki_url { + sqlx::query!( + " + UPDATE mods + SET wiki_url = $1 + WHERE (id = $2) + ", + wiki_url.as_deref(), + project.inner.id as database::models::ids::ProjectId, + ) + .execute(&mut *transaction) + .await?; + } + + if let Some(discord_url) = &bulk_edit_project.discord_url { + sqlx::query!( + " + UPDATE mods + SET discord_url = $1 + WHERE (id = $2) + ", + discord_url.as_deref(), + project.inner.id as database::models::ids::ProjectId, + ) + .execute(&mut *transaction) + .await?; + } + } + + transaction.commit().await?; + + Ok(HttpResponse::NoContent().body("")) +} + #[derive(Deserialize)] pub struct SchedulingData { pub time: DateTime, diff --git a/src/routes/users.rs b/src/routes/users.rs index 8da3f7668..045ff4dfd 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -104,7 +104,7 @@ pub async fn projects_list( let project_data = User::get_projects(id, &**pool).await?; let response: Vec<_> = - crate::database::Project::get_many_full(project_data, &**pool) + crate::database::Project::get_many_full(&project_data, &**pool) .await? .into_iter() .filter(|x| can_view_private || x.inner.status.is_approved()) @@ -600,7 +600,7 @@ pub async fn user_follows( .await?; let projects: Vec<_> = - crate::database::Project::get_many_full(project_ids, &**pool) + crate::database::Project::get_many_full(&project_ids, &**pool) .await? .into_iter() .map(Project::from) diff --git a/src/routes/v1/mods.rs b/src/routes/v1/mods.rs index e003a7d23..c933870a0 100644 --- a/src/routes/v1/mods.rs +++ b/src/routes/v1/mods.rs @@ -91,14 +91,14 @@ pub async fn mods_get( ids: web::Query, pool: web::Data, ) -> Result { - let project_ids = + let project_ids: Vec = serde_json::from_str::>(&ids.ids)? .into_iter() .map(|x| x.into()) .collect(); let projects_data = - database::models::Project::get_many_full(project_ids, &**pool).await?; + database::models::Project::get_many_full(&project_ids, &**pool).await?; let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();