diff --git a/apps/frontend/src/composables/project-nag-validation.ts b/apps/frontend/src/composables/project-nag-validation.ts index 08f111dad99..d3bb16d6ddb 100644 --- a/apps/frontend/src/composables/project-nag-validation.ts +++ b/apps/frontend/src/composables/project-nag-validation.ts @@ -75,7 +75,7 @@ export const projectNagFields = { 'server-region': ['select-country'], 'server-languages': ['all-languages', 'too-many-languages', 'select-language'], 'java-address': ['add-java-address'], - 'server-compatibility': ['select-compatibility'], + 'server-compatibility': ['select-compatibility', 'modpack-version-not-public'], tags: [ 'select-tags', 'too-many-tags', diff --git a/apps/labrinth/.sqlx/query-052a70e917cdfea5d29785ec8607a3fa040e842dfbcce1e3420d9986bb12017d.json b/apps/labrinth/.sqlx/query-052a70e917cdfea5d29785ec8607a3fa040e842dfbcce1e3420d9986bb12017d.json index 6ac03479064..88b76f06484 100644 --- a/apps/labrinth/.sqlx/query-052a70e917cdfea5d29785ec8607a3fa040e842dfbcce1e3420d9986bb12017d.json +++ b/apps/labrinth/.sqlx/query-052a70e917cdfea5d29785ec8607a3fa040e842dfbcce1e3420d9986bb12017d.json @@ -6,7 +6,13 @@ { "ordinal": 0, "name": "user_id", - "type_info": "Int8" + "type_info": "Int8", + "origin": { + "Table": { + "table": "team_members", + "name": "user_id" + } + } } ], "parameters": { diff --git a/apps/labrinth/.sqlx/query-32b564ef51064665b5ba3320f685cfe1c7f370a7de5d5d731201612e38fab606.json b/apps/labrinth/.sqlx/query-32b564ef51064665b5ba3320f685cfe1c7f370a7de5d5d731201612e38fab606.json deleted file mode 100644 index 763fce19387..00000000000 --- a/apps/labrinth/.sqlx/query-32b564ef51064665b5ba3320f685cfe1c7f370a7de5d5d731201612e38fab606.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE mods\n SET status = $1,\n approved = CASE WHEN $3 THEN COALESCE(approved, NOW()) ELSE approved END\n WHERE (id = $2)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Int8", - "Bool" - ] - }, - "nullable": [] - }, - "hash": "32b564ef51064665b5ba3320f685cfe1c7f370a7de5d5d731201612e38fab606" -} diff --git a/apps/labrinth/.sqlx/query-56a9276c3bb5d826fc5d791fccb5badea62230cf9447d490463196adf5d04da8.json b/apps/labrinth/.sqlx/query-56a9276c3bb5d826fc5d791fccb5badea62230cf9447d490463196adf5d04da8.json index db2746ad6a5..f8dcf0d933f 100644 --- a/apps/labrinth/.sqlx/query-56a9276c3bb5d826fc5d791fccb5badea62230cf9447d490463196adf5d04da8.json +++ b/apps/labrinth/.sqlx/query-56a9276c3bb5d826fc5d791fccb5badea62230cf9447d490463196adf5d04da8.json @@ -6,7 +6,13 @@ { "ordinal": 0, "name": "id", - "type_info": "Int8" + "type_info": "Int8", + "origin": { + "Table": { + "table": "mods", + "name": "id" + } + } } ], "parameters": { diff --git a/apps/labrinth/.sqlx/query-a14621ef6ca02af546ae5d2319956c6910d380eb1996c8692bc771a1a67b5937.json b/apps/labrinth/.sqlx/query-a14621ef6ca02af546ae5d2319956c6910d380eb1996c8692bc771a1a67b5937.json index ee6ca543be8..4d14b3f7f7d 100644 --- a/apps/labrinth/.sqlx/query-a14621ef6ca02af546ae5d2319956c6910d380eb1996c8692bc771a1a67b5937.json +++ b/apps/labrinth/.sqlx/query-a14621ef6ca02af546ae5d2319956c6910d380eb1996c8692bc771a1a67b5937.json @@ -6,7 +6,13 @@ { "ordinal": 0, "name": "project_id!: DBProjectId", - "type_info": "Int8" + "type_info": "Int8", + "origin": { + "Table": { + "table": "delphi_issue_details_with_statuses", + "name": "project_id" + } + } } ], "parameters": { diff --git a/apps/labrinth/AGENTS.md b/apps/labrinth/AGENTS.md index 29cb2d53d43..6d9c01e545d 100644 --- a/apps/labrinth/AGENTS.md +++ b/apps/labrinth/AGENTS.md @@ -1,4 +1,5 @@ - Use `ApiError` as the error type for API routes +- Always use spaces instead of tabs, and run `cargo fmt` before finishing a turn - Always use `cargo clippy` instead of `cargo check` - The return type of an HTTP route should not be `HttpResponse` if possible; always prefer more specific types - Use `web::Json` for JSON-encoded response diff --git a/apps/labrinth/src/routes/internal/delphi/mod.rs b/apps/labrinth/src/routes/internal/delphi/mod.rs index 082873d2c42..885bc443ee2 100644 --- a/apps/labrinth/src/routes/internal/delphi/mod.rs +++ b/apps/labrinth/src/routes/internal/delphi/mod.rs @@ -323,6 +323,7 @@ async fn ingest_report_deserialized( crate::routes::v3::projects::mutation::prepare_technical_review_exits( &exited_projects, &mut transaction, + &pool, &redis, ) .await?; diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 4fb0efd6898..8780a4a4943 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -1267,6 +1267,7 @@ pub async fn submit_report( crate::routes::v3::projects::mutation::prepare_mutations( &[project_id], &mut txn, + &pool, &redis, Some(&user), ) @@ -1291,7 +1292,7 @@ pub async fn submit_report( if prepared.is_none() && removed.rows_affected() > 0 { prepared = Some(crate::routes::v3::projects::mutation::prepare_technical_review_exits( - &[project_id], &mut txn, &redis, + &[project_id], &mut txn, &pool, &redis, ).await?); } @@ -1483,6 +1484,7 @@ pub async fn update_issue_details( crate::routes::v3::projects::mutation::prepare_technical_review_exits( &exited_projects, &mut txn, + &pool, &redis, ) .await?; @@ -1623,6 +1625,7 @@ pub async fn update_global_issue_details( crate::routes::v3::projects::mutation::prepare_technical_review_exits( &exited_projects, &mut txn, + &pool, &redis, ) .await?; diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review/rules_scan.rs b/apps/labrinth/src/routes/internal/moderation/tech_review/rules_scan.rs index 5bc21af7e32..2b1da2ab1e3 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review/rules_scan.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review/rules_scan.rs @@ -401,7 +401,7 @@ pub async fn scan_rules( let _ = sender.send(Bytes::from_static(b": connected\n\n")); actix_web::rt::spawn(async move { - match run_scan(transaction, &sender, &redis).await { + match run_scan(transaction, &sender, &pool, &redis).await { Ok(summary) => { let event = RuleScanEvent { phase: "complete", @@ -461,6 +461,7 @@ pub async fn scan_rules( async fn run_scan( mut transaction: PgTransaction<'static>, sender: &mpsc::UnboundedSender, + pool: &PgPool, redis: &RedisPool, ) -> Result { let project_ids = sqlx::query!( @@ -797,6 +798,7 @@ async fn run_scan( crate::routes::v3::projects::mutation::prepare_technical_review_exits( &exited_projects, &mut transaction, + pool, redis, ) .await?; diff --git a/apps/labrinth/src/routes/v3/disclosures.rs b/apps/labrinth/src/routes/v3/disclosures.rs index 8d295e2e0ad..c46d5d4f272 100644 --- a/apps/labrinth/src/routes/v3/disclosures.rs +++ b/apps/labrinth/src/routes/v3/disclosures.rs @@ -309,6 +309,7 @@ pub async fn modify_project_disclosures( super::projects::mutation::finalize_mutation( project.inner.id, transaction, + &pool, &redis, Some(&user), ) diff --git a/apps/labrinth/src/routes/v3/organizations.rs b/apps/labrinth/src/routes/v3/organizations.rs index 81c0f0df4ce..8e8a35376df 100644 --- a/apps/labrinth/src/routes/v3/organizations.rs +++ b/apps/labrinth/src/routes/v3/organizations.rs @@ -866,6 +866,7 @@ pub async fn organization_delete( super::projects::mutation::finalize_mutations( &organization_project_ids, transaction, + &pool, &redis, Some(&user), ) @@ -1059,6 +1060,7 @@ pub async fn organization_projects_add( super::projects::mutation::finalize_mutation( project_item.inner.id, transaction, + &pool, &redis, Some(¤t_user), ) @@ -1278,6 +1280,7 @@ pub async fn organization_projects_remove( super::projects::mutation::finalize_mutation( project_item.inner.id, transaction, + &pool, &redis, Some(¤t_user), ) diff --git a/apps/labrinth/src/routes/v3/project_creation.rs b/apps/labrinth/src/routes/v3/project_creation.rs index 3eaacab9ab8..0325d9a8545 100644 --- a/apps/labrinth/src/routes/v3/project_creation.rs +++ b/apps/labrinth/src/routes/v3/project_creation.rs @@ -381,6 +381,7 @@ pub async fn project_create_internal( let prepared = super::projects::mutation::prepare_mutations( &[project_id.into()], &mut transaction, + &client, &redis, Some(¤t_user), ) @@ -466,6 +467,7 @@ pub async fn project_create_with_id( let prepared = super::projects::mutation::prepare_mutations( &[project_id.into()], &mut transaction, + &client, &redis, Some(¤t_user), ) diff --git a/apps/labrinth/src/routes/v3/project_creation/new.rs b/apps/labrinth/src/routes/v3/project_creation/new.rs index b26229edbba..ab0b583ed9c 100644 --- a/apps/labrinth/src/routes/v3/project_creation/new.rs +++ b/apps/labrinth/src/routes/v3/project_creation/new.rs @@ -375,6 +375,7 @@ pub async fn create( super::super::projects::mutation::finalize_mutation( project_id.into(), txn, + &db, &redis, Some(&user), ) diff --git a/apps/labrinth/src/routes/v3/projects/mod.rs b/apps/labrinth/src/routes/v3/projects/mod.rs index 00f9dafa791..2e2f57bbee0 100644 --- a/apps/labrinth/src/routes/v3/projects/mod.rs +++ b/apps/labrinth/src/routes/v3/projects/mod.rs @@ -1473,6 +1473,7 @@ pub async fn project_edit_internal( team_member.as_ref().is_none_or(|member| !member.accepted), sync_archival_disclosure, &mut transaction, + &pool, &redis, ) .await?; @@ -1484,6 +1485,7 @@ pub async fn project_edit_internal( project_item.inner.slug.clone(), submit_for_review, transaction, + &pool, &redis, Some(&user), ) @@ -1542,6 +1544,7 @@ async fn submit_project_for_review( notify_team_members: bool, sync_archival_disclosure: bool, transaction: &mut PgTransaction<'_>, + pool: &PgPool, redis: &RedisPool, ) -> Result { let archival_disclosure = @@ -1572,9 +1575,13 @@ async fn submit_project_for_review( .wrap_internal_err("failed to remove archival disclosure")?; } - let mut state = - mutation::sync_project_state(project.inner.id, transaction, redis) - .await?; + let mut state = mutation::sync_project_state( + project.inner.id, + transaction, + pool, + redis, + ) + .await?; sqlx::query!( " @@ -2322,6 +2329,7 @@ pub async fn projects_edit( mutation::finalize_mutations( &changed_project_ids, transaction, + &pool, &redis, Some(&user), ) @@ -2577,6 +2585,7 @@ pub async fn project_icon_edit_internal( mutation::finalize_mutation( project_item.inner.id, transaction, + &pool, &redis, Some(&user), ) @@ -2727,6 +2736,7 @@ pub async fn delete_project_icon_internal( mutation::finalize_mutation( project_item.inner.id, transaction, + &pool, &redis, Some(&user), ) @@ -2986,6 +2996,7 @@ pub async fn add_gallery_item_internal( mutation::finalize_mutation( project_item.inner.id, transaction, + &pool, &redis, Some(&user), ) @@ -3221,6 +3232,7 @@ pub async fn edit_gallery_item_internal( mutation::finalize_mutation( project_item.inner.id, transaction, + &pool, &redis, Some(&user), ) @@ -3368,6 +3380,7 @@ pub async fn delete_gallery_item_internal( mutation::finalize_mutation( project_item.inner.id, transaction, + &pool, &redis, Some(&user), ) @@ -3658,6 +3671,7 @@ pub async fn project_delete_internal( original_slug, false, transaction, + &pool, &redis, Some(&user), ) diff --git a/apps/labrinth/src/routes/v3/projects/mutation.rs b/apps/labrinth/src/routes/v3/projects/mutation.rs index a361d1d39fe..98022895d10 100644 --- a/apps/labrinth/src/routes/v3/projects/mutation.rs +++ b/apps/labrinth/src/routes/v3/projects/mutation.rs @@ -8,7 +8,7 @@ use crate::database::models::{ DBProjectId, DBTeamId, DBThreadIssue, DBThreadIssueFacetId, DBUserId, DBVersionId, }; -use crate::database::{PgTransaction, models as db_models}; +use crate::database::{PgPool, PgTransaction, models as db_models}; use crate::models::ids::ProjectId; use crate::models::notifications::NotificationBody; use crate::models::projects::{Project, ProjectStatus, Version}; @@ -182,17 +182,19 @@ impl SyncedProjectState { pub(crate) async fn sync_project_state( project_id: DBProjectId, transaction: &mut PgTransaction<'_>, + pool: &PgPool, redis: &RedisPool, ) -> Result { - let exited_projects = delphi::tech_review_queue::sync_projects( - &[project_id], - delphi::tech_review_queue::TechReviewRemovalReason::FileDeleted, + let (exited_projects, queued_projects) = + sync_technical_review(&[project_id], transaction).await?; + let mut state = load_project_state_with_membership( + project_id, + queued_projects.contains(&project_id), transaction, + pool, + redis, ) - .await - .wrap_api_err("synchronizing project technical review state")?; - - let mut state = load_project_state(project_id, transaction, redis).await?; + .await?; if exited_projects.contains(&project_id) && state.status() == ProjectStatus::Processing { @@ -204,7 +206,7 @@ pub(crate) async fn sync_project_state( async fn sync_technical_review( project_ids: &[DBProjectId], transaction: &mut PgTransaction<'_>, -) -> Result, ApiError> { +) -> Result<(Vec, Vec), ApiError> { delphi::tech_review_queue::lock_projects(project_ids, transaction).await?; for project_id in project_ids { sqlx::query!("SELECT pg_advisory_xact_lock($1)", project_id.0) @@ -213,13 +215,25 @@ async fn sync_technical_review( .wrap_internal_err("locking project state synchronization")?; } - delphi::tech_review_queue::sync_projects_with_membership( + let exited_projects = delphi::tech_review_queue::sync_projects( project_ids, delphi::tech_review_queue::TechReviewRemovalReason::FileDeleted, transaction, ) .await - .wrap_api_err("synchronizing project technical review state") + .wrap_api_err("synchronizing project technical review state")?; + let queued_projects = sqlx::query_scalar!( + r#" + SELECT project_id AS "project_id!: DBProjectId" + FROM delphi_tech_review_queue + WHERE project_id = ANY($1::bigint[]) + "#, + &project_ids.iter().map(|id| id.0).collect::>(), + ) + .fetch_all(&mut *transaction) + .await + .wrap_internal_err("fetching synchronized technical review membership")?; + Ok((exited_projects, queued_projects)) } /// Loads project state without reconciling technical review membership. @@ -227,6 +241,7 @@ async fn sync_technical_review( pub(crate) async fn load_project_state( project_id: DBProjectId, transaction: &mut PgTransaction<'_>, + pool: &PgPool, redis: &RedisPool, ) -> Result { delphi::tech_review_queue::lock_projects(&[project_id], transaction) @@ -252,6 +267,7 @@ pub(crate) async fn load_project_state( project_id, queued_projects.contains(&project_id), transaction, + pool, redis, ) .await @@ -261,6 +277,7 @@ async fn load_project_state_with_membership( project_id: DBProjectId, has_outstanding_technical_review: bool, transaction: &mut PgTransaction<'_>, + pool: &PgPool, redis: &RedisPool, ) -> Result { let mut projects = db_models::DBProject::get_many_uncached( @@ -329,11 +346,14 @@ async fn load_project_state_with_membership( }) .collect::>(); let has_required_nags = validate_project( - &project, - &versions, - &available_categories, - &disclosures, + project.clone(), + versions.clone(), + available_categories, + disclosures.clone(), + pool, + redis, ) + .await? .iter() .any(|nag| nag.severity == ProjectNagSeverity::Required); @@ -363,21 +383,29 @@ async fn load_project_state_with_membership( pub(crate) async fn finalize_mutation( project_id: DBProjectId, transaction: PgTransaction<'_>, + pool: &PgPool, redis: &RedisPool, project_editor: Option<&User>, ) -> Result<(), ApiError> { - finalize_mutations(&[project_id], transaction, redis, project_editor).await + finalize_mutations(&[project_id], transaction, pool, redis, project_editor) + .await } pub(crate) async fn finalize_mutations( project_ids: &[DBProjectId], mut transaction: PgTransaction<'_>, + pool: &PgPool, redis: &RedisPool, project_editor: Option<&User>, ) -> Result<(), ApiError> { - let prepared = - prepare_mutations(project_ids, &mut transaction, redis, project_editor) - .await?; + let prepared = prepare_mutations( + project_ids, + &mut transaction, + pool, + redis, + project_editor, + ) + .await?; prepared.commit(transaction, redis).await } @@ -388,6 +416,7 @@ pub(crate) struct PreparedMutations { pub(crate) async fn prepare_technical_review_exits( project_ids: &[DBProjectId], transaction: &mut PgTransaction<'_>, + pool: &PgPool, redis: &RedisPool, ) -> Result { let mut project_ids = project_ids.to_vec(); @@ -397,7 +426,7 @@ pub(crate) async fn prepare_technical_review_exits( let mut states = Vec::with_capacity(project_ids.len()); for project_id in project_ids { let mut state = - load_project_state(project_id, transaction, redis).await?; + load_project_state(project_id, transaction, pool, redis).await?; if state.status() == ProjectStatus::Processing { state.try_auto_approve(transaction, redis).await?; } @@ -410,6 +439,7 @@ pub(crate) async fn prepare_technical_review_exits( pub(crate) async fn prepare_mutations( project_ids: &[DBProjectId], transaction: &mut PgTransaction<'_>, + pool: &PgPool, redis: &RedisPool, project_editor: Option<&User>, ) -> Result { @@ -417,21 +447,27 @@ pub(crate) async fn prepare_mutations( project_ids.sort_by_key(|project_id| project_id.0); project_ids.dedup(); - let queued_projects = sync_technical_review(&project_ids, transaction) - .await? + let (exited_projects, queued_projects) = + sync_technical_review(&project_ids, transaction).await?; + let queued_projects = queued_projects .into_iter() .collect::>(); let mut states = Vec::with_capacity(project_ids.len()); for project_id in project_ids { - states.push( - load_project_state_with_membership( - project_id, - queued_projects.contains(&project_id), - transaction, - redis, - ) - .await?, - ); + let mut state = load_project_state_with_membership( + project_id, + queued_projects.contains(&project_id), + transaction, + pool, + redis, + ) + .await?; + if exited_projects.contains(&project_id) + && state.status() == ProjectStatus::Processing + { + state.try_auto_approve(transaction, redis).await?; + } + states.push(state); } if project_editor.is_some_and(|user| !user.role.is_mod()) { @@ -481,10 +517,12 @@ pub(crate) async fn finalize_project_edit( original_slug: Option, submit_for_review: bool, mut transaction: PgTransaction<'_>, + pool: &PgPool, redis: &RedisPool, project_editor: Option<&User>, ) -> Result<(), ApiError> { - let state = sync_project_state(project_id, &mut transaction, redis).await?; + let state = + sync_project_state(project_id, &mut transaction, pool, redis).await?; let entered_review = state.project.status == ProjectStatus::Processing && original_status != ProjectStatus::Processing; diff --git a/apps/labrinth/src/routes/v3/projects/validate.rs b/apps/labrinth/src/routes/v3/projects/validate.rs index 6bb2108beff..cb2b59a3dea 100644 --- a/apps/labrinth/src/routes/v3/projects/validate.rs +++ b/apps/labrinth/src/routes/v3/projects/validate.rs @@ -105,17 +105,15 @@ pub(crate) async fn ensure_project_is_valid_for_review( .map(|disclosure| disclosure.disclosure) .collect::>(); let project = Project::from(reloaded_project.clone()); - - let nags = web::block(move || { - validate_project( - &project, - &versions, - &available_categories, - &disclosures, - ) - }) - .await - .wrap_internal_err("validating project for review")?; + let nags = validate_project( + project, + versions, + available_categories, + disclosures, + pool, + redis, + ) + .await?; require_valid_project(nags)?; Ok(reloaded_project) @@ -209,16 +207,14 @@ pub async fn validate( .map(|disclosure| disclosure.disclosure) .collect::>(); let project = Project::from(project); - - let nags = web::block(move || { - validate_project( - &project, - &versions, - &available_categories, - &disclosures, - ) - }) - .await - .wrap_internal_err("validating project")?; + let nags = validate_project( + project, + versions, + available_categories, + disclosures, + &ro_pool, + &redis, + ) + .await?; Ok(HttpResponse::Ok().json(ProjectValidationResponse { nags })) } diff --git a/apps/labrinth/src/routes/v3/teams.rs b/apps/labrinth/src/routes/v3/teams.rs index 45d8b57c034..a6c44e87bb5 100644 --- a/apps/labrinth/src/routes/v3/teams.rs +++ b/apps/labrinth/src/routes/v3/teams.rs @@ -808,6 +808,7 @@ pub async fn add_team_member( super::projects::mutation::finalize_mutations( &affected_project_ids, transaction, + &pool, &redis, Some(¤t_user), ) @@ -1040,6 +1041,7 @@ pub async fn edit_team_member( super::projects::mutation::finalize_mutations( &affected_project_ids, transaction, + &pool, &redis, Some(¤t_user), ) @@ -1260,6 +1262,7 @@ pub async fn transfer_ownership( super::projects::mutation::finalize_mutations( &affected_project_ids, transaction, + &pool, &redis, Some(¤t_user), ) @@ -1460,6 +1463,7 @@ pub async fn remove_team_member( super::projects::mutation::finalize_mutations( &affected_project_ids, transaction, + &pool, &redis, Some(¤t_user), ) diff --git a/apps/labrinth/src/routes/v3/threads.rs b/apps/labrinth/src/routes/v3/threads.rs index 8c8acc57e57..7bd266022dd 100644 --- a/apps/labrinth/src/routes/v3/threads.rs +++ b/apps/labrinth/src/routes/v3/threads.rs @@ -646,6 +646,7 @@ pub async fn thread_issues_create( super::projects::mutation::finalize_mutation( project_id, transaction, + &pool, &redis, None, ) @@ -740,6 +741,7 @@ pub async fn thread_issue_facet_edit( let state = super::projects::mutation::load_project_state( project_id, &mut transaction, + &pool, &redis, ) .await?; @@ -770,6 +772,7 @@ pub async fn thread_issue_facet_edit( super::projects::mutation::finalize_mutation( project_id, transaction, + &pool, &redis, project_editor, ) @@ -825,6 +828,7 @@ pub async fn thread_issue_delete( super::projects::mutation::finalize_mutation( project_id, transaction, + &pool, &redis, None, ) diff --git a/apps/labrinth/src/routes/v3/version_creation.rs b/apps/labrinth/src/routes/v3/version_creation.rs index 4bcff5f6bcc..e88eff9184d 100644 --- a/apps/labrinth/src/routes/v3/version_creation.rs +++ b/apps/labrinth/src/routes/v3/version_creation.rs @@ -188,6 +188,7 @@ pub async fn version_create( let prepared = super::projects::mutation::prepare_mutations( &[project_id], &mut transaction, + &client, &redis, Some(&user), ) @@ -709,6 +710,7 @@ pub async fn upload_file_to_version( let prepared = super::projects::mutation::prepare_mutations( &[project_id], &mut transaction, + &client, &redis, Some(&user), ) diff --git a/apps/labrinth/src/routes/v3/version_file.rs b/apps/labrinth/src/routes/v3/version_file.rs index 5718164ff13..8d61102d755 100644 --- a/apps/labrinth/src/routes/v3/version_file.rs +++ b/apps/labrinth/src/routes/v3/version_file.rs @@ -1116,6 +1116,7 @@ pub async fn delete_file( super::projects::mutation::finalize_mutation( row.project_id, transaction, + &pool, &redis, Some(&user), ) diff --git a/apps/labrinth/src/routes/v3/versions.rs b/apps/labrinth/src/routes/v3/versions.rs index 0c66d67b841..5f423b9214b 100644 --- a/apps/labrinth/src/routes/v3/versions.rs +++ b/apps/labrinth/src/routes/v3/versions.rs @@ -491,7 +491,7 @@ pub async fn version_edit_helper( let (version_item, user) = version_edit_in_transaction( req, info, - pool, + pool.clone(), redis.clone(), new_version, session_queue, @@ -502,6 +502,7 @@ pub async fn version_edit_helper( super::projects::mutation::finalize_mutation( version_item.inner.project_id, transaction, + &pool, &redis, Some(&user), ) @@ -1450,6 +1451,7 @@ pub async fn version_delete( super::projects::mutation::finalize_mutation( version.inner.project_id, transaction, + &pool, &redis, Some(&user), ) diff --git a/apps/labrinth/src/validate/project/mod.rs b/apps/labrinth/src/validate/project/mod.rs index 6ce56c145b0..b23755bff49 100644 --- a/apps/labrinth/src/validate/project/mod.rs +++ b/apps/labrinth/src/validate/project/mod.rs @@ -96,6 +96,7 @@ pub enum ProjectNagKind { AllLanguages, AddJavaAddress, SelectCompatibility, + ModpackVersionNotPublic, TooManyLanguages, SelectLanguage, @@ -169,18 +170,30 @@ pub fn is_project_description_non_english(project: &Project) -> bool { description::is_non_english(project) } -pub fn validate_with_context( - project: &Project, - versions: &[Version], - available_categories: &[Category], - disclosures: &[ProjectDisclosure], -) -> Vec { - validate_inner( - project, - versions, - Some(available_categories), - Some(disclosures), - ) +pub async fn validate_with_context( + project: Project, + versions: Vec, + available_categories: Vec, + disclosures: Vec, + pool: &crate::database::PgPool, + redis: &xredis::RedisPool, +) -> Result, crate::routes::ApiError> { + use crate::util::error::Context as _; + + let server_nags = + server_settings::validate_server_modpack(&project, pool, redis).await?; + let mut nags = actix_web::web::block(move || { + validate_inner( + &project, + &versions, + Some(&available_categories), + Some(&disclosures), + ) + }) + .await + .wrap_internal_err("validating project")?; + nags.extend(server_nags); + Ok(nags) } fn validate_inner( diff --git a/apps/labrinth/src/validate/project/server_settings.rs b/apps/labrinth/src/validate/project/server_settings.rs index cd9a0841ed6..7f765ae146f 100644 --- a/apps/labrinth/src/validate/project/server_settings.rs +++ b/apps/labrinth/src/validate/project/server_settings.rs @@ -1,4 +1,10 @@ +use xredis::RedisPool; + +use crate::auth::checks::is_visible_version; +use crate::database::{PgPool, models::DBVersion}; use crate::models::{exp::minecraft::ServerContentQuery, projects::Project}; +use crate::routes::ApiError; +use crate::util::error::Context as _; use super::{ProjectNag, ProjectNagKind, ProjectNagSeverity}; @@ -74,3 +80,44 @@ pub(super) fn validate(project: &Project) -> Vec { nags } + +pub(super) async fn validate_server_modpack( + project: &Project, + pool: &PgPool, + redis: &RedisPool, +) -> Result, ApiError> { + let Some(server) = &project.components.minecraft_java_server else { + return Ok(Vec::new()); + }; + let ServerContentQuery::Modpack { + version_id, + project_id, + .. + } = &server.content + else { + return Ok(Vec::new()); + }; + if *project_id == project.id { + return Ok(Vec::new()); + } + let version = DBVersion::get((*version_id).into(), pool, redis) + .await + .wrap_internal_err("fetching server modpack version")?; + let visible = if let Some(version) = version { + is_visible_version(&version.inner, &None, pool, redis).await? + } else { + false + }; + Ok(if visible { + Vec::new() + } else { + vec![ProjectNag { + kind: ProjectNagKind::ModpackVersionNotPublic, + severity: ProjectNagSeverity::Required, + details: serde_json::json!({ + + "version_id": version_id, + }), + }] + }) +} diff --git a/packages/api-client/src/modules/labrinth/types.ts b/packages/api-client/src/modules/labrinth/types.ts index 3c110afdf48..90800c7448f 100644 --- a/packages/api-client/src/modules/labrinth/types.ts +++ b/packages/api-client/src/modules/labrinth/types.ts @@ -1111,6 +1111,7 @@ export namespace Labrinth { | 'add-java-address' | 'all-languages' | 'select-compatibility' + | 'modpack-version-not-public' | 'select-country' | 'select-language' | 'too-many-languages' diff --git a/packages/moderation/src/data/nags/server-settings.ts b/packages/moderation/src/data/nags/server-settings.ts index af4592327d3..dad68f69d50 100644 --- a/packages/moderation/src/data/nags/server-settings.ts +++ b/packages/moderation/src/data/nags/server-settings.ts @@ -29,6 +29,15 @@ const messages = defineMessages({ defaultMessage: 'Select what versions your server supports, choose a Modpack, or upload your own.', }, + modpackVersionNotPublicTitle: { + id: 'nags.modpack-version-not-public.title', + defaultMessage: 'Select a publicly available modpack version', + }, + modpackVersionNotPublic: { + id: 'nags.modpack-version-not-public.description', + defaultMessage: + 'The selected modpack version is not publicly available. Choose a version that all players can access.', + }, countryTitle: { id: 'nags.select-country.title', defaultMessage: 'Select a region' }, country: { id: 'nags.select-country.description', @@ -61,6 +70,11 @@ export const serverSettingNags = { description: messages.compatibility, destination: 'server', }, + 'modpack-version-not-public': { + title: messages.modpackVersionNotPublicTitle, + description: messages.modpackVersionNotPublic, + destination: 'server', + }, 'select-country': { title: messages.countryTitle, description: messages.country, diff --git a/packages/moderation/src/locales/en-US/index.json b/packages/moderation/src/locales/en-US/index.json index 0bd3143bbfd..0739ee9b715 100644 --- a/packages/moderation/src/locales/en-US/index.json +++ b/packages/moderation/src/locales/en-US/index.json @@ -236,6 +236,12 @@ "nags.moderator-feedback.title": { "defaultMessage": "Review feedback" }, + "nags.modpack-version-not-public.description": { + "defaultMessage": "The selected modpack version is not publicly available. Choose a version that all players can access." + }, + "nags.modpack-version-not-public.title": { + "defaultMessage": "Select a publicly available modpack version" + }, "nags.multiple-resolution-tags.description": { "defaultMessage": "You've selected {count, plural, one {# resolution tag} other {# resolution tags}} ({tags}). Resource packs should typically only have the tag that matches the primary resolution." },