From a6ccb8ef073fbaacfda6cc4f425607b54da989f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= Date: Tue, 19 Aug 2025 01:04:42 +0200 Subject: [PATCH 01/65] chore: fix typo in status message --- apps/frontend/nuxt.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/nuxt.config.ts b/apps/frontend/nuxt.config.ts index beaf5a9982..fb136f6676 100644 --- a/apps/frontend/nuxt.config.ts +++ b/apps/frontend/nuxt.config.ts @@ -154,7 +154,7 @@ export default defineNuxtConfig({ (state.errors ?? []).length === 0 ) { console.log( - 'Tags already recently generated. Delete apps/frontend/generated/state.json to force regeneration.', + 'Tags already recently generated. Delete apps/src/frontend/generated/state.json to force regeneration.', ) return } From 47f3481a2566a3c9fad07814d6c2b30529ed982f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= Date: Mon, 18 Aug 2025 23:25:24 +0200 Subject: [PATCH 02/65] feat(labrinth): overhaul malware scanner report storage and routes --- ...bc457a08e70dcde320c6852074819e41f8ad9.json | 24 ++ ...f9530c311eef084abb6fce35de5f37d79bcea.json | 34 ++ ...724e9a4d5b9765d52305f99f859f939c2e854.json | 63 ++++ ...3153f5e9796b55ae753ab57b14f37708b400d.json | 24 ++ ...d0a1658c6ddf7a486082cdb847fab06150328.json | 164 +++++++++ ...5d818fde0499d8e5a08e9e22bee42014877f3.json | 20 ++ .../20250810155316_delphi-reports.sql | 64 ++++ .../src/database/models/delphi_report_item.rs | 334 ++++++++++++++++++ apps/labrinth/src/database/models/ids.rs | 31 +- apps/labrinth/src/database/models/mod.rs | 1 + .../src/database/models/version_item.rs | 10 + apps/labrinth/src/routes/internal/admin.rs | 103 +----- apps/labrinth/src/routes/internal/delphi.rs | 265 ++++++++++++++ apps/labrinth/src/routes/internal/mod.rs | 4 +- apps/labrinth/src/routes/mod.rs | 4 + .../src/routes/v3/project_creation.rs | 4 - .../src/routes/v3/version_creation.rs | 44 +-- 17 files changed, 1032 insertions(+), 161 deletions(-) create mode 100644 apps/labrinth/.sqlx/query-0080a101c9ae040adbaadf9e46fbc457a08e70dcde320c6852074819e41f8ad9.json create mode 100644 apps/labrinth/.sqlx/query-0ed2e6e3149352d12a673fddc50f9530c311eef084abb6fce35de5f37d79bcea.json create mode 100644 apps/labrinth/.sqlx/query-10a332091be118f580d50ceb7a8724e9a4d5b9765d52305f99f859f939c2e854.json create mode 100644 apps/labrinth/.sqlx/query-8f1f75d9c52a5a340aae2b3fd863153f5e9796b55ae753ab57b14f37708b400d.json create mode 100644 apps/labrinth/.sqlx/query-c1cd83ddcd112e46477a195e8bed0a1658c6ddf7a486082cdb847fab06150328.json create mode 100644 apps/labrinth/.sqlx/query-fe571872262fe7d119b4b6eb1e55d818fde0499d8e5a08e9e22bee42014877f3.json create mode 100644 apps/labrinth/migrations/20250810155316_delphi-reports.sql create mode 100644 apps/labrinth/src/database/models/delphi_report_item.rs create mode 100644 apps/labrinth/src/routes/internal/delphi.rs diff --git a/apps/labrinth/.sqlx/query-0080a101c9ae040adbaadf9e46fbc457a08e70dcde320c6852074819e41f8ad9.json b/apps/labrinth/.sqlx/query-0080a101c9ae040adbaadf9e46fbc457a08e70dcde320c6852074819e41f8ad9.json new file mode 100644 index 0000000000..37dcad2943 --- /dev/null +++ b/apps/labrinth/.sqlx/query-0080a101c9ae040adbaadf9e46fbc457a08e70dcde320c6852074819e41f8ad9.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO delphi_report_issue_java_classes (issue_id, internal_class_name, decompiled_source)\n VALUES ($1, $2, $3)\n ON CONFLICT (issue_id, internal_class_name) DO UPDATE SET decompiled_source = $3\n RETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "0080a101c9ae040adbaadf9e46fbc457a08e70dcde320c6852074819e41f8ad9" +} diff --git a/apps/labrinth/.sqlx/query-0ed2e6e3149352d12a673fddc50f9530c311eef084abb6fce35de5f37d79bcea.json b/apps/labrinth/.sqlx/query-0ed2e6e3149352d12a673fddc50f9530c311eef084abb6fce35de5f37d79bcea.json new file mode 100644 index 0000000000..6f7b991949 --- /dev/null +++ b/apps/labrinth/.sqlx/query-0ed2e6e3149352d12a673fddc50f9530c311eef084abb6fce35de5f37d79bcea.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n version_id AS \"version_id: crate::database::models::DBVersionId\",\n versions.mod_id AS \"project_id: crate::database::models::DBProjectId\",\n files.url AS \"url\"\n FROM files INNER JOIN versions ON files.version_id = versions.id\n WHERE files.id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "version_id: crate::database::models::DBVersionId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "project_id: crate::database::models::DBProjectId", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "url", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "0ed2e6e3149352d12a673fddc50f9530c311eef084abb6fce35de5f37d79bcea" +} diff --git a/apps/labrinth/.sqlx/query-10a332091be118f580d50ceb7a8724e9a4d5b9765d52305f99f859f939c2e854.json b/apps/labrinth/.sqlx/query-10a332091be118f580d50ceb7a8724e9a4d5b9765d52305f99f859f939c2e854.json new file mode 100644 index 0000000000..963ea430b4 --- /dev/null +++ b/apps/labrinth/.sqlx/query-10a332091be118f580d50ceb7a8724e9a4d5b9765d52305f99f859f939c2e854.json @@ -0,0 +1,63 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO delphi_report_issues (report_id, issue_type, status)\n VALUES ($1, $2, $3)\n ON CONFLICT (report_id, issue_type) DO UPDATE SET status = $3\n RETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + { + "Custom": { + "name": "delphi_report_issue_type", + "kind": { + "Enum": [ + "reflection_indirection", + "xor_obfuscation", + "included_libraries", + "suspicious_binaries", + "corrupt_classes", + "suspicious_classes", + "url_usage", + "classloader_usage", + "processbuilder_usage", + "runtime_exec_usage", + "jni_usage", + "main_method", + "native_loading", + "malformed_jar", + "nested_jar_too_deep", + "failed_decompilation", + "analysis_failure", + "malware_easyforme", + "malware_simplyloader", + "unknown" + ] + } + } + }, + { + "Custom": { + "name": "delphi_report_issue_status", + "kind": { + "Enum": [ + "pending", + "approved", + "rejected" + ] + } + } + } + ] + }, + "nullable": [ + false + ] + }, + "hash": "10a332091be118f580d50ceb7a8724e9a4d5b9765d52305f99f859f939c2e854" +} diff --git a/apps/labrinth/.sqlx/query-8f1f75d9c52a5a340aae2b3fd863153f5e9796b55ae753ab57b14f37708b400d.json b/apps/labrinth/.sqlx/query-8f1f75d9c52a5a340aae2b3fd863153f5e9796b55ae753ab57b14f37708b400d.json new file mode 100644 index 0000000000..de31a078f0 --- /dev/null +++ b/apps/labrinth/.sqlx/query-8f1f75d9c52a5a340aae2b3fd863153f5e9796b55ae753ab57b14f37708b400d.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO delphi_reports (file_id, delphi_version, artifact_url)\n VALUES ($1, $2, $3)\n ON CONFLICT (file_id, delphi_version) DO UPDATE SET\n delphi_version = $2, artifact_url = $3, created = CURRENT_TIMESTAMP\n RETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int4", + "Varchar" + ] + }, + "nullable": [ + false + ] + }, + "hash": "8f1f75d9c52a5a340aae2b3fd863153f5e9796b55ae753ab57b14f37708b400d" +} diff --git a/apps/labrinth/.sqlx/query-c1cd83ddcd112e46477a195e8bed0a1658c6ddf7a486082cdb847fab06150328.json b/apps/labrinth/.sqlx/query-c1cd83ddcd112e46477a195e8bed0a1658c6ddf7a486082cdb847fab06150328.json new file mode 100644 index 0000000000..54969cea41 --- /dev/null +++ b/apps/labrinth/.sqlx/query-c1cd83ddcd112e46477a195e8bed0a1658c6ddf7a486082cdb847fab06150328.json @@ -0,0 +1,164 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n delphi_report_issues.id AS \"id\", report_id,\n issue_type AS \"issue_type: DelphiReportIssueType\",\n delphi_report_issues.status as \"status: DelphiReportIssueStatus\",\n\n file_id, delphi_version, artifact_url, created,\n json_array(SELECT to_jsonb(delphi_report_issue_java_classes)\n FROM delphi_report_issue_java_classes\n WHERE issue_id = delphi_report_issues.id\n ) AS \"classes: sqlx::types::Json>\",\n versions.mod_id AS \"project_id?\", mods.published AS \"project_published?\"\n FROM delphi_report_issues\n INNER JOIN delphi_reports ON delphi_reports.id = report_id\n LEFT OUTER JOIN files ON files.id = file_id\n LEFT OUTER JOIN versions ON versions.id = files.version_id\n LEFT OUTER JOIN mods ON mods.id = versions.mod_id\n WHERE\n (issue_type = $1 OR $1 IS NULL)\n AND (delphi_report_issues.status = $2 OR $2 IS NULL)\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'pending_status_first' THEN delphi_report_issues.status ELSE 'pending'::delphi_report_issue_status END ASC\n OFFSET $5\n LIMIT $4\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "report_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "issue_type: DelphiReportIssueType", + "type_info": { + "Custom": { + "name": "delphi_report_issue_type", + "kind": { + "Enum": [ + "reflection_indirection", + "xor_obfuscation", + "included_libraries", + "suspicious_binaries", + "corrupt_classes", + "suspicious_classes", + "url_usage", + "classloader_usage", + "processbuilder_usage", + "runtime_exec_usage", + "jni_usage", + "main_method", + "native_loading", + "malformed_jar", + "nested_jar_too_deep", + "failed_decompilation", + "analysis_failure", + "malware_easyforme", + "malware_simplyloader", + "unknown" + ] + } + } + } + }, + { + "ordinal": 3, + "name": "status: DelphiReportIssueStatus", + "type_info": { + "Custom": { + "name": "delphi_report_issue_status", + "kind": { + "Enum": [ + "pending", + "approved", + "rejected" + ] + } + } + } + }, + { + "ordinal": 4, + "name": "file_id", + "type_info": "Int8" + }, + { + "ordinal": 5, + "name": "delphi_version", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "artifact_url", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "created", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "classes: sqlx::types::Json>", + "type_info": "Jsonb" + }, + { + "ordinal": 9, + "name": "project_id?", + "type_info": "Int8" + }, + { + "ordinal": 10, + "name": "project_published?", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + { + "Custom": { + "name": "delphi_report_issue_type", + "kind": { + "Enum": [ + "reflection_indirection", + "xor_obfuscation", + "included_libraries", + "suspicious_binaries", + "corrupt_classes", + "suspicious_classes", + "url_usage", + "classloader_usage", + "processbuilder_usage", + "runtime_exec_usage", + "jni_usage", + "main_method", + "native_loading", + "malformed_jar", + "nested_jar_too_deep", + "failed_decompilation", + "analysis_failure", + "malware_easyforme", + "malware_simplyloader", + "unknown" + ] + } + } + }, + { + "Custom": { + "name": "delphi_report_issue_status", + "kind": { + "Enum": [ + "pending", + "approved", + "rejected" + ] + } + } + }, + "Text", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + false, + null, + true, + true + ] + }, + "hash": "c1cd83ddcd112e46477a195e8bed0a1658c6ddf7a486082cdb847fab06150328" +} diff --git a/apps/labrinth/.sqlx/query-fe571872262fe7d119b4b6eb1e55d818fde0499d8e5a08e9e22bee42014877f3.json b/apps/labrinth/.sqlx/query-fe571872262fe7d119b4b6eb1e55d818fde0499d8e5a08e9e22bee42014877f3.json new file mode 100644 index 0000000000..38db606828 --- /dev/null +++ b/apps/labrinth/.sqlx/query-fe571872262fe7d119b4b6eb1e55d818fde0499d8e5a08e9e22bee42014877f3.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT MAX(delphi_version) FROM delphi_reports", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "max", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "fe571872262fe7d119b4b6eb1e55d818fde0499d8e5a08e9e22bee42014877f3" +} diff --git a/apps/labrinth/migrations/20250810155316_delphi-reports.sql b/apps/labrinth/migrations/20250810155316_delphi-reports.sql new file mode 100644 index 0000000000..4bc15e705b --- /dev/null +++ b/apps/labrinth/migrations/20250810155316_delphi-reports.sql @@ -0,0 +1,64 @@ +CREATE TYPE delphi_report_issue_status AS ENUM ('pending', 'approved', 'rejected'); + +CREATE TYPE delphi_report_issue_type AS ENUM ( + 'reflection_indirection', + 'xor_obfuscation', + 'included_libraries', + 'suspicious_binaries', + 'corrupt_classes', + 'suspicious_classes', + 'url_usage', + 'classloader_usage', + 'processbuilder_usage', + 'runtime_exec_usage', + 'jni_usage', + 'main_method', + 'native_loading', + 'malformed_jar', + 'nested_jar_too_deep', + 'failed_decompilation', + 'analysis_failure', + 'malware_easyforme', + 'malware_simplyloader', + 'unknown' +); + +-- A Delphi analysis report for a project version +CREATE TABLE delphi_reports ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + file_id BIGINT REFERENCES files (id) + ON DELETE SET NULL + ON UPDATE CASCADE, + delphi_version INTEGER NOT NULL, + artifact_url VARCHAR(2048) NOT NULL, + created TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, + UNIQUE (file_id, delphi_version) +); +CREATE INDEX delphi_version ON delphi_reports (delphi_version); + +-- An issue found in a Delphi report. Every issue belongs to a report, +-- and a report can have zero, one, or more issues attached to it +CREATE TABLE delphi_report_issues ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + report_id BIGINT NOT NULL REFERENCES delphi_reports (id) + ON DELETE CASCADE + ON UPDATE CASCADE, + issue_type DELPHI_REPORT_ISSUE_TYPE NOT NULL, + status DELPHI_REPORT_ISSUE_STATUS NOT NULL, + UNIQUE (report_id, issue_type) +); +CREATE INDEX delphi_report_issue_by_status_and_type ON delphi_report_issues (status, issue_type); + +-- A Java class affected by a Delphi report issue. Every affected +-- Java class belongs to a specific issue, and an issue can have zero, +-- one, or more affected classes. (Some issues may be artifact-wide, +-- or otherwise not really specific to any particular class.) +CREATE TABLE delphi_report_issue_java_classes ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + issue_id BIGINT NOT NULL REFERENCES delphi_report_issues (id) + ON DELETE CASCADE + ON UPDATE CASCADE, + internal_class_name TEXT NOT NULL, + decompiled_source TEXT NOT NULL, + UNIQUE (issue_id, internal_class_name) +); diff --git a/apps/labrinth/src/database/models/delphi_report_item.rs b/apps/labrinth/src/database/models/delphi_report_item.rs new file mode 100644 index 0000000000..8d83bc7dd2 --- /dev/null +++ b/apps/labrinth/src/database/models/delphi_report_item.rs @@ -0,0 +1,334 @@ +use std::{ + fmt::{self, Display, Formatter}, + ops::Deref, +}; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::database::models::{ + DBFileId, DBProjectId, DatabaseError, DelphiReportId, DelphiReportIssueId, + DelphiReportIssueJavaClassId, +}; + +/// A Delphi malware analysis report for a project version file. +/// +/// Malware analysis reports usually belong to a specific project file, +/// but they can get orphaned if the versions they belong to are deleted. +/// Thus, deleting versions does not delete these reports. +#[derive(Serialize)] +pub struct DBDelphiReport { + pub id: DelphiReportId, + pub file_id: Option, + /// A sequential, monotonically increasing version number for the + /// Delphi version that generated this report. + pub delphi_version: i32, + pub artifact_url: String, + pub created: DateTime, +} + +impl DBDelphiReport { + pub async fn upsert( + &self, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + Ok(DelphiReportId(sqlx::query_scalar!( + " + INSERT INTO delphi_reports (file_id, delphi_version, artifact_url) + VALUES ($1, $2, $3) + ON CONFLICT (file_id, delphi_version) DO UPDATE SET + delphi_version = $2, artifact_url = $3, created = CURRENT_TIMESTAMP + RETURNING id + ", + self.file_id as Option, + self.delphi_version, + self.artifact_url, + ) + .fetch_one(&mut **transaction) + .await?)) + } +} + +/// An issue found in a Delphi report. Every issue belongs to a report, +/// and a report can have zero, one, or more issues attached to it. +#[derive(Deserialize, Serialize)] +pub struct DBDelphiReportIssue { + pub id: DelphiReportIssueId, + pub report_id: DelphiReportId, + pub issue_type: DelphiReportIssueType, + pub status: DelphiReportIssueStatus, +} + +/// An status a Delphi report issue can have. +#[derive( + Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash, sqlx::Type, +)] +#[serde(rename_all = "snake_case")] +#[sqlx(type_name = "delphi_report_issue_status")] +#[sqlx(rename_all = "snake_case")] +pub enum DelphiReportIssueStatus { + /// The issue is pending review by the moderation team. + Pending, + /// The issue has been approved (i.e., reviewed as a valid, true positive). + /// The affected artifact has thus been verified to be potentially malicious. + Approved, + /// The issue has been rejected (i.e., reviewed as a false positive). + /// The affected artifact has thus been verified to be clean, other issues + /// with it notwithstanding. + Rejected, +} + +impl Display for DelphiReportIssueStatus { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.serialize(f) + } +} + +/// An order in which Delphi report issues can be sorted during queries. +#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[serde(rename_all = "snake_case")] +pub enum DelphiReportListOrder { + CreatedAsc, + CreatedDesc, + PendingStatusFirst, +} + +impl Display for DelphiReportListOrder { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.serialize(f) + } +} + +/// A result returned from a Delphi report issue query, slightly +/// denormalized with related entity information for ease of +/// consumption by clients. +#[derive(Serialize)] +pub struct DelphiReportIssueResult { + pub issue: DBDelphiReportIssue, + pub report: DBDelphiReport, + pub java_classes: Vec, + pub project_id: Option, + pub project_published: Option>, +} + +impl DBDelphiReportIssue { + pub async fn upsert( + &self, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + Ok(DelphiReportIssueId( + sqlx::query_scalar!( + " + INSERT INTO delphi_report_issues (report_id, issue_type, status) + VALUES ($1, $2, $3) + ON CONFLICT (report_id, issue_type) DO UPDATE SET status = $3 + RETURNING id + ", + self.report_id as DelphiReportId, + self.issue_type as DelphiReportIssueType, + self.status as DelphiReportIssueStatus, + ) + .fetch_one(&mut **transaction) + .await?, + )) + } + + pub async fn find_all_by( + ty: Option, + status: Option, + order_by: Option, + count: Option, + offset: Option, + exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>, + ) -> Result, DatabaseError> { + Ok(sqlx::query!( + r#" + SELECT + delphi_report_issues.id AS "id", report_id, + issue_type AS "issue_type: DelphiReportIssueType", + delphi_report_issues.status as "status: DelphiReportIssueStatus", + + file_id, delphi_version, artifact_url, created, + json_array(SELECT to_jsonb(delphi_report_issue_java_classes) + FROM delphi_report_issue_java_classes + WHERE issue_id = delphi_report_issues.id + ) AS "classes: sqlx::types::Json>", + versions.mod_id AS "project_id?", mods.published AS "project_published?" + FROM delphi_report_issues + INNER JOIN delphi_reports ON delphi_reports.id = report_id + LEFT OUTER JOIN files ON files.id = file_id + LEFT OUTER JOIN versions ON versions.id = files.version_id + LEFT OUTER JOIN mods ON mods.id = versions.mod_id + WHERE + (issue_type = $1 OR $1 IS NULL) + AND (delphi_report_issues.status = $2 OR $2 IS NULL) + ORDER BY + CASE WHEN $3 = 'created_asc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END ASC, + CASE WHEN $3 = 'created_desc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END DESC, + CASE WHEN $3 = 'pending_status_first' THEN delphi_report_issues.status ELSE 'pending'::delphi_report_issue_status END ASC + OFFSET $5 + LIMIT $4 + "#, + ty as Option, + status as Option, + order_by.map(|order_by| order_by.to_string()), + count.map(|count| count as i64), + offset, + ) + .map(|row| DelphiReportIssueResult { + issue: DBDelphiReportIssue { + id: DelphiReportIssueId(row.id), + report_id: DelphiReportId(row.report_id), + issue_type: row.issue_type, + status: row.status, + }, + report: DBDelphiReport { + id: DelphiReportId(row.report_id), + file_id: row.file_id.map(DBFileId), + delphi_version: row.delphi_version, + artifact_url: row.artifact_url, + created: row.created, + }, + java_classes: row + .classes + .into_iter() + .flat_map(|class_list| class_list.0) + .collect(), + project_id: row.project_id.map(DBProjectId), + project_published: row.project_published, + }) + .fetch_all(exec) + .await?) + } +} + +/// A type of issue found by Delphi for an artifact. +#[derive( + Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash, sqlx::Type, +)] +#[serde(rename_all = "snake_case")] +#[sqlx(type_name = "delphi_report_issue_type")] +#[sqlx(rename_all = "snake_case")] +pub enum DelphiReportIssueType { + ReflectionIndirection, + XorObfuscation, + IncludedLibraries, + SuspiciousBinaries, + CorruptClasses, + SuspiciousClasses, + + UrlUsage, + ClassloaderUsage, + ProcessbuilderUsage, + RuntimeExecUsage, + #[serde(rename = "jni_usage")] + #[sqlx(rename = "jni_usage")] + JNIUsage, + + MainMethod, + NativeLoading, + + MalformedJar, + NestedJarTooDeep, + FailedDecompilation, + #[serde(alias = "ANALYSIS FAILURE!")] + AnalysisFailure, + + MalwareEasyforme, + MalwareSimplyloader, + + /// An issue reported by Delphi but not known by labrinth yet. + #[serde(other)] + Unknown, +} + +impl Display for DelphiReportIssueType { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.serialize(f) + } +} + +/// A Java class affected by a Delphi report issue. Every affected +/// Java class belongs to a specific issue, and an issue can have zero, +/// one, or more affected classes. (Some issues may be artifact-wide, +/// or otherwise not really specific to any particular class.) +#[derive(Debug, Deserialize, Serialize)] +pub struct DBDelphiReportIssueJavaClass { + pub id: DelphiReportIssueJavaClassId, + pub issue_id: DelphiReportIssueId, + pub internal_class_name: InternalJavaClassName, + pub decompiled_source: DecompiledJavaClassSource, +} + +impl DBDelphiReportIssueJavaClass { + pub async fn upsert( + &self, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + Ok(DelphiReportIssueJavaClassId(sqlx::query_scalar!( + " + INSERT INTO delphi_report_issue_java_classes (issue_id, internal_class_name, decompiled_source) + VALUES ($1, $2, $3) + ON CONFLICT (issue_id, internal_class_name) DO UPDATE SET decompiled_source = $3 + RETURNING id + ", + self.issue_id as DelphiReportIssueId, + self.internal_class_name.0, + self.decompiled_source.0, + ) + .fetch_one(&mut **transaction) + .await?)) + } +} + +/// A [Java class name] with dots replaced by forward slashes (/). +/// +/// Because class names are usually the [binary names] passed to a classloader, top level interfaces and classes +/// have a binary name that matches its canonical, fully qualified name, such canonical names are prefixed by the +/// package path the class is in, and packages usually match the directory structure within a JAR for typical +/// classloaders, this usually (but not necessarily) corresponds to the path to the class file within its JAR. +/// +/// [Java class name]: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Class.html#getName() +/// [binary names]: https://docs.oracle.com/javase/specs/jls/se21/html/jls-13.html#jls-13.1 +#[derive( + Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash, sqlx::Type, +)] +#[serde(transparent)] +#[sqlx(transparent)] +pub struct InternalJavaClassName(String); + +impl Deref for InternalJavaClassName { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Display for InternalJavaClassName { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// The decompiled source code of a Java class. +#[derive( + Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash, sqlx::Type, +)] +#[serde(transparent)] +#[sqlx(transparent)] +pub struct DecompiledJavaClassSource(String); + +impl Deref for DecompiledJavaClassSource { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Display for DecompiledJavaClassSource { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/apps/labrinth/src/database/models/ids.rs b/apps/labrinth/src/database/models/ids.rs index 483f6841e5..6ec97476fe 100644 --- a/apps/labrinth/src/database/models/ids.rs +++ b/apps/labrinth/src/database/models/ids.rs @@ -140,8 +140,8 @@ macro_rules! db_id_interface { }; } -macro_rules! short_id_type { - ($name:ident) => { +macro_rules! id_type { + ($name:ident as $type:ty) => { #[derive( Copy, Clone, @@ -154,7 +154,7 @@ macro_rules! short_id_type { Hash, )] #[sqlx(transparent)] - pub struct $name(pub i32); + pub struct $name(pub $type); }; } @@ -268,14 +268,17 @@ db_id_interface!( generator: generate_affiliate_code_id @ "affiliate_codes", ); -short_id_type!(CategoryId); -short_id_type!(GameId); -short_id_type!(LinkPlatformId); -short_id_type!(LoaderFieldEnumId); -short_id_type!(LoaderFieldEnumValueId); -short_id_type!(LoaderFieldId); -short_id_type!(LoaderId); -short_id_type!(NotificationActionId); -short_id_type!(ProjectTypeId); -short_id_type!(ReportTypeId); -short_id_type!(StatusId); +id_type!(CategoryId as i32); +id_type!(GameId as i32); +id_type!(LinkPlatformId as i32); +id_type!(LoaderFieldEnumId as i32); +id_type!(LoaderFieldEnumValueId as i32); +id_type!(LoaderFieldId as i32); +id_type!(LoaderId as i32); +id_type!(NotificationActionId as i32); +id_type!(ProjectTypeId as i32); +id_type!(ReportTypeId as i32); +id_type!(StatusId as i32); +id_type!(DelphiReportId as i64); +id_type!(DelphiReportIssueId as i64); +id_type!(DelphiReportIssueJavaClassId as i64); diff --git a/apps/labrinth/src/database/models/mod.rs b/apps/labrinth/src/database/models/mod.rs index e3f42b518a..6315fc45cf 100644 --- a/apps/labrinth/src/database/models/mod.rs +++ b/apps/labrinth/src/database/models/mod.rs @@ -4,6 +4,7 @@ pub mod affiliate_code_item; pub mod categories; pub mod charge_item; pub mod collection_item; +pub mod delphi_report_item; pub mod flow_item; pub mod friend_item; pub mod ids; diff --git a/apps/labrinth/src/database/models/version_item.rs b/apps/labrinth/src/database/models/version_item.rs index c97a43e50c..d69ac2e531 100644 --- a/apps/labrinth/src/database/models/version_item.rs +++ b/apps/labrinth/src/database/models/version_item.rs @@ -6,6 +6,7 @@ use crate::database::models::loader_fields::{ }; use crate::database::redis::RedisPool; use crate::models::projects::{FileType, VersionStatus}; +use crate::routes::internal::delphi::DelphiRunParameters; use chrono::{DateTime, Utc}; use dashmap::{DashMap, DashSet}; use futures::TryStreamExt; @@ -164,6 +165,15 @@ impl VersionFileBuilder { .await?; } + if let Err(err) = crate::routes::internal::delphi::run( + &mut **transaction, + DelphiRunParameters { file_id }, + ) + .await + { + tracing::error!("Error submitting new file to Delphi: {err}"); + } + Ok(file_id) } } diff --git a/apps/labrinth/src/routes/internal/admin.rs b/apps/labrinth/src/routes/internal/admin.rs index 518a315aa8..c4c64b5bf6 100644 --- a/apps/labrinth/src/routes/internal/admin.rs +++ b/apps/labrinth/src/routes/internal/admin.rs @@ -1,12 +1,9 @@ use crate::auth::validate::get_user_record_from_bearer_token; -use crate::database::models::thread_item::ThreadMessageBuilder; use crate::database::redis::RedisPool; use crate::models::analytics::Download; use crate::models::ids::ProjectId; use crate::models::pats::Scopes; -use crate::models::threads::MessageBody; use crate::queue::analytics::AnalyticsQueue; -use crate::queue::moderation::AUTOMOD_ID; use crate::queue::session::AuthQueue; use crate::routes::ApiError; use crate::search::SearchConfig; @@ -17,17 +14,14 @@ use modrinth_maxmind::MaxMind; use serde::Deserialize; use sqlx::PgPool; use std::collections::HashMap; -use std::fmt::Write; use std::net::Ipv4Addr; use std::sync::Arc; -use tracing::info; pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("admin") .service(count_download) - .service(force_reindex) - .service(delphi_result_ingest), + .service(force_reindex), ); } @@ -163,98 +157,3 @@ pub async fn force_reindex( index_projects(pool.as_ref().clone(), redis.clone(), &config).await?; Ok(HttpResponse::NoContent().finish()) } - -#[derive(Deserialize)] -pub struct DelphiIngest { - pub url: String, - pub project_id: crate::models::ids::ProjectId, - pub version_id: crate::models::ids::VersionId, - pub issues: HashMap>, -} - -#[post("/_delphi", guard = "admin_key_guard")] -pub async fn delphi_result_ingest( - pool: web::Data, - redis: web::Data, - body: web::Json, -) -> Result { - if body.issues.is_empty() { - info!("No issues found for file {}", body.url); - return Ok(HttpResponse::NoContent().finish()); - } - - let webhook_url = dotenvy::var("DELPHI_SLACK_WEBHOOK")?; - - let project = crate::database::models::DBProject::get_id( - body.project_id.into(), - &**pool, - &redis, - ) - .await? - .ok_or_else(|| { - ApiError::InvalidInput(format!( - "Project {} does not exist", - body.project_id - )) - })?; - - let mut header = format!("Suspicious traces found at {}", body.url); - - for (issue, trace) in &body.issues { - for (path, code) in trace { - write!( - &mut header, - "\n issue {issue} found at file {path}: \n ```\n{code}\n```" - ) - .unwrap(); - } - } - - crate::util::webhook::send_slack_project_webhook( - body.project_id, - &pool, - &redis, - webhook_url, - Some(header), - ) - .await - .ok(); - - let mut thread_header = format!( - "Suspicious traces found at [version {}](https://modrinth.com/project/{}/version/{})", - body.version_id, body.project_id, body.version_id - ); - - for (issue, trace) in &body.issues { - for path in trace.keys() { - write!( - &mut thread_header, - "\n\n- issue {issue} found at file {path}" - ) - .unwrap(); - } - - if trace.is_empty() { - write!(&mut thread_header, "\n\n- issue {issue} found").unwrap(); - } - } - - let mut transaction = pool.begin().await?; - ThreadMessageBuilder { - author_id: Some(crate::database::models::DBUserId(AUTOMOD_ID)), - body: MessageBody::Text { - body: thread_header, - private: true, - replying_to: None, - associated_images: vec![], - }, - thread_id: project.thread_id, - hide_identity: false, - } - .insert(&mut transaction) - .await?; - - transaction.commit().await?; - - Ok(HttpResponse::NoContent().finish()) -} diff --git a/apps/labrinth/src/routes/internal/delphi.rs b/apps/labrinth/src/routes/internal/delphi.rs new file mode 100644 index 0000000000..9e88d19d01 --- /dev/null +++ b/apps/labrinth/src/routes/internal/delphi.rs @@ -0,0 +1,265 @@ +use std::{collections::HashMap, fmt::Write, io, sync::LazyLock}; + +use actix_web::{HttpResponse, get, post, put, web}; +use chrono::{DateTime, Utc}; +use serde::Deserialize; +use sqlx::PgPool; +use tracing::info; + +use crate::{ + database::{ + models::{ + DBFileId, DelphiReportId, DelphiReportIssueId, + DelphiReportIssueJavaClassId, + delphi_report_item::{ + DBDelphiReport, DBDelphiReportIssue, + DBDelphiReportIssueJavaClass, DecompiledJavaClassSource, + DelphiReportIssueStatus, DelphiReportIssueType, + DelphiReportListOrder, InternalJavaClassName, + }, + }, + redis::RedisPool, + }, + routes::ApiError, + util::guards::admin_key_guard, +}; + +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("delphi") + .service(ingest_report) + .service(_run) + .service(version) + .service(issues) + .service(update_issue), + ); +} + +#[derive(Deserialize)] +struct DelphiReport { + pub url: String, + pub project_id: crate::models::ids::ProjectId, + #[serde(rename = "version_id")] + pub _version_id: crate::models::ids::VersionId, + pub file_id: crate::models::ids::FileId, + /// A sequential, monotonically increasing version number for the + /// Delphi version that generated this report. + pub delphi_version: i32, + pub issues: HashMap< + DelphiReportIssueType, + HashMap, + >, +} + +impl DelphiReport { + async fn send_to_slack( + &self, + pool: &PgPool, + redis: &RedisPool, + ) -> Result<(), ApiError> { + let webhook_url = dotenvy::var("DELPHI_SLACK_WEBHOOK")?; + + let mut message_header = + format!("⚠️ Suspicious traces found at {}", self.url); + + for (issue, trace) in &self.issues { + for (path, code) in trace { + write!( + &mut message_header, + "\n issue {issue} found at file {path}:\n```\n{code}\n```" + ) + .ok(); + } + } + + crate::util::webhook::send_slack_project_webhook( + self.project_id, + pool, + redis, + webhook_url, + Some(message_header), + ) + .await + } +} + +#[derive(Deserialize)] +pub struct DelphiRunParameters { + pub file_id: crate::database::models::ids::DBFileId, +} + +#[post("ingest", guard = "admin_key_guard")] +async fn ingest_report( + pool: web::Data, + redis: web::Data, + web::Json(report): web::Json, +) -> Result { + if report.issues.is_empty() { + info!("No issues found for file {}", report.url); + return Ok(HttpResponse::NoContent().finish()); + } + + report.send_to_slack(&pool, &redis).await.ok(); + + let mut transaction = pool.begin().await?; + + let report_id = DBDelphiReport { + id: DelphiReportId(0), // This will be set by the database + file_id: Some(DBFileId(report.file_id.0 as i64)), + delphi_version: report.delphi_version, + artifact_url: report.url.clone(), + created: DateTime::::MIN_UTC, // This will be set by the database + } + .upsert(&mut transaction) + .await?; + + for (issue_type, issue_java_classes) in report.issues { + let issue_id = DBDelphiReportIssue { + id: DelphiReportIssueId(0), // This will be set by the database + report_id, + issue_type, + status: DelphiReportIssueStatus::Pending, + } + .upsert(&mut transaction) + .await?; + + for (internal_class_name, decompiled_source) in issue_java_classes { + DBDelphiReportIssueJavaClass { + id: DelphiReportIssueJavaClassId(0), // This will be set by the database + issue_id, + internal_class_name, + decompiled_source, + } + .upsert(&mut transaction) + .await?; + } + } + + transaction.commit().await?; + + Ok(HttpResponse::NoContent().finish()) +} + +pub async fn run( + exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>, + run_parameters: DelphiRunParameters, +) -> Result { + let file_data = sqlx::query!( + r#" + SELECT + version_id AS "version_id: crate::database::models::DBVersionId", + versions.mod_id AS "project_id: crate::database::models::DBProjectId", + files.url AS "url" + FROM files INNER JOIN versions ON files.version_id = versions.id + WHERE files.id = $1 + "#, + run_parameters.file_id.0 + ) + .fetch_one(exec) + .await?; + + static DELPHI_CLIENT: LazyLock = + LazyLock::new(reqwest::Client::new); + + tracing::debug!( + "Running Delphi for project {}, version {}, file {}", + file_data.project_id.0, + file_data.version_id.0, + run_parameters.file_id.0 + ); + + DELPHI_CLIENT + .post(dotenvy::var("DELPHI_URL")?) + .json(&serde_json::json!({ + "url": file_data.url, + "project_id": file_data.project_id, + "version_id": file_data.version_id, + "file_id": run_parameters.file_id, + })) + .send() + .await + .and_then(|res| res.error_for_status()) + .map_err(ApiError::Delphi)?; + + Ok(HttpResponse::NoContent().finish()) +} + +#[post("run", guard = "admin_key_guard")] +async fn _run( + pool: web::Data, + run_parameters: web::Query, +) -> Result { + run(&**pool, run_parameters.into_inner()).await +} + +#[get("version", guard = "admin_key_guard")] +async fn version(pool: web::Data) -> Result { + Ok(HttpResponse::Ok().json( + sqlx::query_scalar!("SELECT MAX(delphi_version) FROM delphi_reports") + .fetch_one(&**pool) + .await?, + )) +} + +#[derive(Deserialize)] +struct DelphiIssuesSearchOptions { + #[serde(rename = "type")] + ty: Option, + status: Option, + order_by: Option, + count: Option, + offset: Option, +} + +#[get("issues", guard = "admin_key_guard")] +async fn issues( + pool: web::Data, + search_options: web::Query, +) -> Result { + Ok(HttpResponse::Ok().json( + DBDelphiReportIssue::find_all_by( + search_options.ty, + search_options.status, + search_options.order_by, + search_options.count, + search_options + .offset + .map(|offset| offset.try_into()) + .transpose() + .map_err(|err| { + io::Error::other(format!("Invalid offset: {err}")) + })?, + &**pool, + ) + .await?, + )) +} + +#[put("issue/{issue_id}", guard = "admin_key_guard")] +async fn update_issue( + pool: web::Data, + issue_id: web::Path, + web::Json(update_data): web::Json, +) -> Result { + let new_id = issue_id.into_inner(); + + let mut transaction = pool.begin().await?; + + let modified_same_issue = (DBDelphiReportIssue { + id: new_id, // Doesn't matter, upsert done for values of other fields + report_id: update_data.report_id, + issue_type: update_data.issue_type, + status: update_data.status, + }) + .upsert(&mut transaction) + .await? + == new_id; + + transaction.commit().await?; + + if modified_same_issue { + Ok(HttpResponse::NoContent().finish()) + } else { + Ok(HttpResponse::Created().finish()) + } +} diff --git a/apps/labrinth/src/routes/internal/mod.rs b/apps/labrinth/src/routes/internal/mod.rs index f15da09f0b..af79ed3408 100644 --- a/apps/labrinth/src/routes/internal/mod.rs +++ b/apps/labrinth/src/routes/internal/mod.rs @@ -1,6 +1,7 @@ pub(crate) mod admin; pub mod affiliate; pub mod billing; +pub mod delphi; pub mod external_notifications; pub mod flows; pub mod gdpr; @@ -32,7 +33,8 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) { .configure(medal::config) .configure(external_notifications::config) .configure(affiliate::config) - .configure(mural::config), + .configure(mural::config) + .configure(delphi::config), ); } diff --git a/apps/labrinth/src/routes/mod.rs b/apps/labrinth/src/routes/mod.rs index ca55240b8e..2ba78fa632 100644 --- a/apps/labrinth/src/routes/mod.rs +++ b/apps/labrinth/src/routes/mod.rs @@ -161,6 +161,8 @@ pub enum ApiError { RateLimitError(u128, u32), #[error("Error while interacting with payment processor: {0}")] Stripe(#[from] stripe::StripeError), + #[error("Error while interacting with Delphi: {0}")] + Delphi(reqwest::Error), } impl ApiError { @@ -201,6 +203,7 @@ impl ApiError { Self::Stripe(..) => "stripe_error", Self::TaxProcessor(..) => "tax_processor_error", Self::Slack(..) => "slack_error", + Self::Delphi(..) => "delphi_error", }, description: match self { Self::Internal(e) => format!("{e:#?}"), @@ -249,6 +252,7 @@ impl actix_web::ResponseError for ApiError { Self::Stripe(..) => StatusCode::FAILED_DEPENDENCY, Self::TaxProcessor(..) => StatusCode::INTERNAL_SERVER_ERROR, Self::Slack(..) => StatusCode::INTERNAL_SERVER_ERROR, + Self::Delphi(..) => StatusCode::INTERNAL_SERVER_ERROR, } } diff --git a/apps/labrinth/src/routes/v3/project_creation.rs b/apps/labrinth/src/routes/v3/project_creation.rs index 4984adbd57..0cc1dcab38 100644 --- a/apps/labrinth/src/routes/v3/project_creation.rs +++ b/apps/labrinth/src/routes/v3/project_creation.rs @@ -412,9 +412,6 @@ async fn project_create_inner( session_queue: &AuthQueue, project_id: ProjectId, ) -> Result { - // The base URL for files uploaded to S3 - let cdn_url = dotenvy::var("CDN_URL")?; - // The currently logged in user let (_, current_user) = get_user_from_headers( &req, @@ -650,7 +647,6 @@ async fn project_create_inner( uploaded_files, &mut created_version.files, &mut created_version.dependencies, - &cdn_url, &content_disposition, project_id, created_version.version_id.into(), diff --git a/apps/labrinth/src/routes/v3/version_creation.rs b/apps/labrinth/src/routes/v3/version_creation.rs index 396395e69c..e70ffd7c79 100644 --- a/apps/labrinth/src/routes/v3/version_creation.rs +++ b/apps/labrinth/src/routes/v3/version_creation.rs @@ -38,7 +38,6 @@ use sha1::Digest; use sqlx::postgres::PgPool; use std::collections::{HashMap, HashSet}; use std::sync::Arc; -use tracing::error; use validator::Validate; fn default_requested_status() -> VersionStatus { @@ -158,8 +157,6 @@ async fn version_create_inner( session_queue: &AuthQueue, moderation_queue: &AutomatedModerationQueue, ) -> Result { - let cdn_url = dotenvy::var("CDN_URL")?; - let mut initial_version_data = None; let mut version_builder = None; let mut selected_loaders = None; @@ -355,7 +352,6 @@ async fn version_create_inner( uploaded_files, &mut version.files, &mut version.dependencies, - &cdn_url, &content_disposition, version.project_id.into(), version.version_id.into(), @@ -590,8 +586,6 @@ async fn upload_file_to_version_inner( version_id: models::DBVersionId, session_queue: &AuthQueue, ) -> Result { - let cdn_url = dotenvy::var("CDN_URL")?; - let mut initial_file_data: Option = None; let mut file_builders: Vec = Vec::new(); @@ -741,7 +735,6 @@ async fn upload_file_to_version_inner( uploaded_files, &mut file_builders, &mut dependencies, - &cdn_url, &content_disposition, project_id, version_id.into(), @@ -795,7 +788,6 @@ pub async fn upload_file( uploaded_files: &mut Vec, version_files: &mut Vec, dependencies: &mut Vec, - cdn_url: &str, content_disposition: &actix_web::http::header::ContentDisposition, project_id: ProjectId, version_id: VersionId, @@ -942,21 +934,17 @@ pub async fn upload_file( || force_primary || total_files_len == 1; - let file_path_encode = format!( - "data/{}/versions/{}/{}", - project_id, - version_id, + let file_path = format!( + "data/{project_id}/versions/{version_id}/{}", urlencoding::encode(file_name) ); - let file_path = - format!("data/{}/versions/{}/{}", project_id, version_id, &file_name); let upload_data = file_host .upload_file(content_type, &file_path, FileHostPublicity::Public, data) .await?; uploaded_files.push(UploadedFile { - name: file_path, + name: file_path.clone(), publicity: FileHostPublicity::Public, }); @@ -980,33 +968,9 @@ pub async fn upload_file( return Err(CreateError::InvalidInput(msg.to_string())); } - let url = format!("{cdn_url}/{file_path_encode}"); - - let client = reqwest::Client::new(); - let delphi_url = dotenvy::var("DELPHI_URL")?; - match client - .post(delphi_url) - .json(&serde_json::json!({ - "url": url, - "project_id": project_id, - "version_id": version_id, - })) - .send() - .await - { - Ok(res) => { - if !res.status().is_success() { - error!("Failed to upload file to Delphi: {url}"); - } - } - Err(e) => { - error!("Failed to upload file to Delphi: {url}: {e}"); - } - } - version_files.push(VersionFileBuilder { filename: file_name.to_string(), - url: format!("{cdn_url}/{file_path_encode}"), + url: format!("{}/{file_path}", dotenvy::var("CDN_URL")?), hashes: vec![ models::version_item::HashBuilder { algorithm: "sha1".to_string(), From b4e9fadb7c59017f8adc156e8b98dcab866febc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= Date: Fri, 22 Aug 2025 22:37:04 +0200 Subject: [PATCH 03/65] chore: address some review comments --- apps/labrinth/src/routes/internal/delphi.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/labrinth/src/routes/internal/delphi.rs b/apps/labrinth/src/routes/internal/delphi.rs index 9e88d19d01..4e8f588e98 100644 --- a/apps/labrinth/src/routes/internal/delphi.rs +++ b/apps/labrinth/src/routes/internal/delphi.rs @@ -63,10 +63,10 @@ impl DelphiReport { format!("⚠️ Suspicious traces found at {}", self.url); for (issue, trace) in &self.issues { - for (path, code) in trace { + for (class, code) in trace { write!( &mut message_header, - "\n issue {issue} found at file {path}:\n```\n{code}\n```" + "\n issue {issue} found at class `{class}`:\n```\n{code}\n```" ) .ok(); } @@ -227,7 +227,7 @@ async fn issues( .map(|offset| offset.try_into()) .transpose() .map_err(|err| { - io::Error::other(format!("Invalid offset: {err}")) + ApiError::InvalidInput(format!("Invalid offset: {err}")) })?, &**pool, ) From 17173c70068f51f3d3594f6be6952652522c8185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= Date: Fri, 22 Aug 2025 22:50:05 +0200 Subject: [PATCH 04/65] feat: add Delphi to Docker Compose `with-delphi` profile --- apps/labrinth/.env.docker-compose | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/labrinth/.env.docker-compose b/apps/labrinth/.env.docker-compose index 6102fc99ab..18f62577c6 100644 --- a/apps/labrinth/.env.docker-compose +++ b/apps/labrinth/.env.docker-compose @@ -129,7 +129,7 @@ PYRO_API_KEY=none BREX_API_URL=https://platform.brexapis.com/v2/ BREX_API_KEY=none -DELPHI_URL=none +DELPHI_URL=http://labrinth-delphi:59999 DELPHI_SLACK_WEBHOOK=none AVALARA_1099_API_URL=https://www.track1099.com/api From 2c646eb968e0bf108567b33dcf743a41900fb776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= Date: Fri, 22 Aug 2025 22:52:23 +0200 Subject: [PATCH 05/65] chore: fix unused import Clippy lint --- apps/labrinth/src/routes/internal/delphi.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/labrinth/src/routes/internal/delphi.rs b/apps/labrinth/src/routes/internal/delphi.rs index 4e8f588e98..b5205434f0 100644 --- a/apps/labrinth/src/routes/internal/delphi.rs +++ b/apps/labrinth/src/routes/internal/delphi.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, fmt::Write, io, sync::LazyLock}; +use std::{collections::HashMap, fmt::Write, sync::LazyLock}; use actix_web::{HttpResponse, get, post, put, web}; use chrono::{DateTime, Utc}; From 1ccf17cf41b76bce1a47704d58f0fb3eb6f6b739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= Date: Fri, 22 Aug 2025 23:13:48 +0200 Subject: [PATCH 06/65] feat(labrinth/delphi): use PAT token authorization with project read scopes --- apps/labrinth/src/routes/internal/delphi.rs | 65 +++++++++++++++++++-- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/apps/labrinth/src/routes/internal/delphi.rs b/apps/labrinth/src/routes/internal/delphi.rs index b5205434f0..e9c36ae213 100644 --- a/apps/labrinth/src/routes/internal/delphi.rs +++ b/apps/labrinth/src/routes/internal/delphi.rs @@ -1,12 +1,13 @@ use std::{collections::HashMap, fmt::Write, sync::LazyLock}; -use actix_web::{HttpResponse, get, post, put, web}; +use actix_web::{HttpRequest, HttpResponse, get, post, put, web}; use chrono::{DateTime, Utc}; use serde::Deserialize; use sqlx::PgPool; use tracing::info; use crate::{ + auth::check_is_moderator_from_headers, database::{ models::{ DBFileId, DelphiReportId, DelphiReportIssueId, @@ -20,6 +21,8 @@ use crate::{ }, redis::RedisPool, }, + models::pats::Scopes, + queue::session::AuthQueue, routes::ApiError, util::guards::admin_key_guard, }; @@ -184,16 +187,42 @@ pub async fn run( Ok(HttpResponse::NoContent().finish()) } -#[post("run", guard = "admin_key_guard")] +#[post("run")] async fn _run( + req: HttpRequest, pool: web::Data, + redis: web::Data, + session_queue: web::Data, run_parameters: web::Query, ) -> Result { + check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_READ, + ) + .await?; + run(&**pool, run_parameters.into_inner()).await } -#[get("version", guard = "admin_key_guard")] -async fn version(pool: web::Data) -> Result { +#[get("version")] +async fn version( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_READ, + ) + .await?; + Ok(HttpResponse::Ok().json( sqlx::query_scalar!("SELECT MAX(delphi_version) FROM delphi_reports") .fetch_one(&**pool) @@ -211,11 +240,23 @@ struct DelphiIssuesSearchOptions { offset: Option, } -#[get("issues", guard = "admin_key_guard")] +#[get("issues")] async fn issues( + req: HttpRequest, pool: web::Data, + redis: web::Data, + session_queue: web::Data, search_options: web::Query, ) -> Result { + check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_READ, + ) + .await?; + Ok(HttpResponse::Ok().json( DBDelphiReportIssue::find_all_by( search_options.ty, @@ -235,12 +276,24 @@ async fn issues( )) } -#[put("issue/{issue_id}", guard = "admin_key_guard")] +#[put("issue/{issue_id}")] async fn update_issue( + req: HttpRequest, pool: web::Data, + redis: web::Data, + session_queue: web::Data, issue_id: web::Path, web::Json(update_data): web::Json, ) -> Result { + check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_READ, + ) + .await?; + let new_id = issue_id.into_inner(); let mut transaction = pool.begin().await?; From 71fb6b2e88a3a193ad73d769b0117916f8f53789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= Date: Fri, 22 Aug 2025 23:55:42 +0200 Subject: [PATCH 07/65] chore: expose file IDs in version queries --- apps/labrinth/src/models/v3/projects.rs | 5 +++++ apps/labrinth/src/routes/v3/version_creation.rs | 1 + 2 files changed, 6 insertions(+) diff --git a/apps/labrinth/src/models/v3/projects.rs b/apps/labrinth/src/models/v3/projects.rs index f70d806af1..559e76d002 100644 --- a/apps/labrinth/src/models/v3/projects.rs +++ b/apps/labrinth/src/models/v3/projects.rs @@ -731,6 +731,7 @@ impl From for Version { .files .into_iter() .map(|f| VersionFile { + id: Some(f.id.0), url: f.url, filename: f.filename, hashes: f.hashes, @@ -855,6 +856,10 @@ impl VersionStatus { /// A single project file, with a url for the file and the file's hash #[derive(Serialize, Deserialize, Clone)] pub struct VersionFile { + /// The ID of the file. Every file has an ID once created, but it + /// is not known until it indeed has been created. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub id: Option, /// A map of hashes of the file. The key is the hashing algorithm /// and the value is the string version of the hash. pub hashes: std::collections::HashMap, diff --git a/apps/labrinth/src/routes/v3/version_creation.rs b/apps/labrinth/src/routes/v3/version_creation.rs index e70ffd7c79..03ebb81283 100644 --- a/apps/labrinth/src/routes/v3/version_creation.rs +++ b/apps/labrinth/src/routes/v3/version_creation.rs @@ -447,6 +447,7 @@ async fn version_create_inner( .files .iter() .map(|file| VersionFile { + id: None, hashes: file .hashes .iter() From 8269cc872bef9d983a7dd8c895d96924a42e1f36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= Date: Sat, 23 Aug 2025 22:01:21 +0200 Subject: [PATCH 08/65] fix: accept null decompiled source payloads from Delphi --- apps/labrinth/migrations/20250810155316_delphi-reports.sql | 2 +- apps/labrinth/src/database/models/delphi_report_item.rs | 5 ++--- apps/labrinth/src/routes/internal/delphi.rs | 6 ++++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/labrinth/migrations/20250810155316_delphi-reports.sql b/apps/labrinth/migrations/20250810155316_delphi-reports.sql index 4bc15e705b..003511bf49 100644 --- a/apps/labrinth/migrations/20250810155316_delphi-reports.sql +++ b/apps/labrinth/migrations/20250810155316_delphi-reports.sql @@ -59,6 +59,6 @@ CREATE TABLE delphi_report_issue_java_classes ( ON DELETE CASCADE ON UPDATE CASCADE, internal_class_name TEXT NOT NULL, - decompiled_source TEXT NOT NULL, + decompiled_source TEXT, UNIQUE (issue_id, internal_class_name) ); diff --git a/apps/labrinth/src/database/models/delphi_report_item.rs b/apps/labrinth/src/database/models/delphi_report_item.rs index 8d83bc7dd2..89c1c0c89f 100644 --- a/apps/labrinth/src/database/models/delphi_report_item.rs +++ b/apps/labrinth/src/database/models/delphi_report_item.rs @@ -231,7 +231,6 @@ pub enum DelphiReportIssueType { MalformedJar, NestedJarTooDeep, FailedDecompilation, - #[serde(alias = "ANALYSIS FAILURE!")] AnalysisFailure, MalwareEasyforme, @@ -257,7 +256,7 @@ pub struct DBDelphiReportIssueJavaClass { pub id: DelphiReportIssueJavaClassId, pub issue_id: DelphiReportIssueId, pub internal_class_name: InternalJavaClassName, - pub decompiled_source: DecompiledJavaClassSource, + pub decompiled_source: Option, } impl DBDelphiReportIssueJavaClass { @@ -274,7 +273,7 @@ impl DBDelphiReportIssueJavaClass { ", self.issue_id as DelphiReportIssueId, self.internal_class_name.0, - self.decompiled_source.0, + self.decompiled_source.as_ref().map(|decompiled_source| &decompiled_source.0), ) .fetch_one(&mut **transaction) .await?)) diff --git a/apps/labrinth/src/routes/internal/delphi.rs b/apps/labrinth/src/routes/internal/delphi.rs index e9c36ae213..7276306c51 100644 --- a/apps/labrinth/src/routes/internal/delphi.rs +++ b/apps/labrinth/src/routes/internal/delphi.rs @@ -50,7 +50,7 @@ struct DelphiReport { pub delphi_version: i32, pub issues: HashMap< DelphiReportIssueType, - HashMap, + HashMap>, >, } @@ -67,9 +67,11 @@ impl DelphiReport { for (issue, trace) in &self.issues { for (class, code) in trace { + let code = code.as_deref().map(|code| &**code); write!( &mut message_header, - "\n issue {issue} found at class `{class}`:\n```\n{code}\n```" + "\n issue {issue} found at class `{class}`:\n```\n{}\n```", + code.unwrap_or("No decompiled source available") ) .ok(); } From ce0e26c609ca2578ef5db5755cea6fbff9e1883a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= Date: Sat, 23 Aug 2025 22:02:09 +0200 Subject: [PATCH 09/65] tweak(labrinth): expose base62 file IDs more consistently for Delphi --- apps/labrinth/src/database/models/version_item.rs | 4 +++- apps/labrinth/src/models/v3/projects.rs | 6 +++--- apps/labrinth/src/routes/internal/delphi.rs | 13 ++++++++----- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/apps/labrinth/src/database/models/version_item.rs b/apps/labrinth/src/database/models/version_item.rs index d69ac2e531..d0ba9c0b97 100644 --- a/apps/labrinth/src/database/models/version_item.rs +++ b/apps/labrinth/src/database/models/version_item.rs @@ -167,7 +167,9 @@ impl VersionFileBuilder { if let Err(err) = crate::routes::internal::delphi::run( &mut **transaction, - DelphiRunParameters { file_id }, + DelphiRunParameters { + file_id: file_id.into(), + }, ) .await { diff --git a/apps/labrinth/src/models/v3/projects.rs b/apps/labrinth/src/models/v3/projects.rs index 559e76d002..c6c1101ffe 100644 --- a/apps/labrinth/src/models/v3/projects.rs +++ b/apps/labrinth/src/models/v3/projects.rs @@ -5,7 +5,7 @@ use crate::database::models::loader_fields::VersionField; use crate::database::models::project_item::{LinkUrl, ProjectQueryResult}; use crate::database::models::version_item::VersionQueryResult; use crate::models::ids::{ - OrganizationId, ProjectId, TeamId, ThreadId, VersionId, + FileId, OrganizationId, ProjectId, TeamId, ThreadId, VersionId, }; use ariadne::ids::UserId; use chrono::{DateTime, Utc}; @@ -731,7 +731,7 @@ impl From for Version { .files .into_iter() .map(|f| VersionFile { - id: Some(f.id.0), + id: Some(FileId(f.id.0 as u64)), url: f.url, filename: f.filename, hashes: f.hashes, @@ -859,7 +859,7 @@ pub struct VersionFile { /// The ID of the file. Every file has an ID once created, but it /// is not known until it indeed has been created. #[serde(default, skip_serializing_if = "Option::is_none")] - pub id: Option, + pub id: Option, /// A map of hashes of the file. The key is the hashing algorithm /// and the value is the string version of the hash. pub hashes: std::collections::HashMap, diff --git a/apps/labrinth/src/routes/internal/delphi.rs b/apps/labrinth/src/routes/internal/delphi.rs index 7276306c51..96c55d97ef 100644 --- a/apps/labrinth/src/routes/internal/delphi.rs +++ b/apps/labrinth/src/routes/internal/delphi.rs @@ -21,7 +21,10 @@ use crate::{ }, redis::RedisPool, }, - models::pats::Scopes, + models::{ + ids::{ProjectId, VersionId}, + pats::Scopes, + }, queue::session::AuthQueue, routes::ApiError, util::guards::admin_key_guard, @@ -90,7 +93,7 @@ impl DelphiReport { #[derive(Deserialize)] pub struct DelphiRunParameters { - pub file_id: crate::database::models::ids::DBFileId, + pub file_id: crate::models::ids::FileId, } #[post("ingest", guard = "admin_key_guard")] @@ -158,7 +161,7 @@ pub async fn run( FROM files INNER JOIN versions ON files.version_id = versions.id WHERE files.id = $1 "#, - run_parameters.file_id.0 + run_parameters.file_id.0 as i64 ) .fetch_one(exec) .await?; @@ -177,8 +180,8 @@ pub async fn run( .post(dotenvy::var("DELPHI_URL")?) .json(&serde_json::json!({ "url": file_data.url, - "project_id": file_data.project_id, - "version_id": file_data.version_id, + "project_id": ProjectId(file_data.project_id.0 as u64), + "version_id": VersionId(file_data.version_id.0 as u64), "file_id": run_parameters.file_id, })) .send() From 7152edd518927fed3437df534e65bed564ba893b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= Date: Sat, 23 Aug 2025 22:42:59 +0200 Subject: [PATCH 10/65] feat(labrinth/delphi): support new Delphi report severity field --- .../20250810155316_delphi-reports.sql | 3 ++ .../src/database/models/delphi_report_item.rs | 38 ++++++++++++++----- apps/labrinth/src/routes/internal/delphi.rs | 5 ++- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/apps/labrinth/migrations/20250810155316_delphi-reports.sql b/apps/labrinth/migrations/20250810155316_delphi-reports.sql index 003511bf49..141bdc5fdd 100644 --- a/apps/labrinth/migrations/20250810155316_delphi-reports.sql +++ b/apps/labrinth/migrations/20250810155316_delphi-reports.sql @@ -1,3 +1,5 @@ +CREATE TYPE delphi_report_severity AS ENUM ('low', 'medium', 'high', 'severe'); + CREATE TYPE delphi_report_issue_status AS ENUM ('pending', 'approved', 'rejected'); CREATE TYPE delphi_report_issue_type AS ENUM ( @@ -32,6 +34,7 @@ CREATE TABLE delphi_reports ( delphi_version INTEGER NOT NULL, artifact_url VARCHAR(2048) NOT NULL, created TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, + severity DELPHI_REPORT_SEVERITY NOT NULL, UNIQUE (file_id, delphi_version) ); CREATE INDEX delphi_version ON delphi_reports (delphi_version); diff --git a/apps/labrinth/src/database/models/delphi_report_item.rs b/apps/labrinth/src/database/models/delphi_report_item.rs index 89c1c0c89f..6bc155953f 100644 --- a/apps/labrinth/src/database/models/delphi_report_item.rs +++ b/apps/labrinth/src/database/models/delphi_report_item.rs @@ -25,6 +25,7 @@ pub struct DBDelphiReport { pub delphi_version: i32, pub artifact_url: String, pub created: DateTime, + pub severity: DelphiReportSeverity, } impl DBDelphiReport { @@ -34,21 +35,35 @@ impl DBDelphiReport { ) -> Result { Ok(DelphiReportId(sqlx::query_scalar!( " - INSERT INTO delphi_reports (file_id, delphi_version, artifact_url) - VALUES ($1, $2, $3) + INSERT INTO delphi_reports (file_id, delphi_version, artifact_url, severity) + VALUES ($1, $2, $3, $4) ON CONFLICT (file_id, delphi_version) DO UPDATE SET - delphi_version = $2, artifact_url = $3, created = CURRENT_TIMESTAMP + delphi_version = $2, artifact_url = $3, created = CURRENT_TIMESTAMP, severity = $4 RETURNING id ", self.file_id as Option, self.delphi_version, self.artifact_url, + self.severity as DelphiReportSeverity, ) .fetch_one(&mut **transaction) .await?)) } } +/// A severity level for a Delphi report. +#[derive( + Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash, sqlx::Type, +)] +#[serde(rename_all = "UPPERCASE")] +#[sqlx(type_name = "delphi_report_severity", rename_all = "snake_case")] +pub enum DelphiReportSeverity { + Low, + Medium, + High, + Severe, +} + /// An issue found in a Delphi report. Every issue belongs to a report, /// and a report can have zero, one, or more issues attached to it. #[derive(Deserialize, Serialize)] @@ -64,8 +79,7 @@ pub struct DBDelphiReportIssue { Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash, sqlx::Type, )] #[serde(rename_all = "snake_case")] -#[sqlx(type_name = "delphi_report_issue_status")] -#[sqlx(rename_all = "snake_case")] +#[sqlx(type_name = "delphi_report_issue_status", rename_all = "snake_case")] pub enum DelphiReportIssueStatus { /// The issue is pending review by the moderation team. Pending, @@ -91,6 +105,8 @@ pub enum DelphiReportListOrder { CreatedAsc, CreatedDesc, PendingStatusFirst, + SeverityAsc, + SeverityDesc, } impl Display for DelphiReportListOrder { @@ -146,9 +162,9 @@ impl DBDelphiReportIssue { SELECT delphi_report_issues.id AS "id", report_id, issue_type AS "issue_type: DelphiReportIssueType", - delphi_report_issues.status as "status: DelphiReportIssueStatus", + delphi_report_issues.status AS "status: DelphiReportIssueStatus", - file_id, delphi_version, artifact_url, created, + file_id, delphi_version, artifact_url, created, severity AS "severity: DelphiReportSeverity", json_array(SELECT to_jsonb(delphi_report_issue_java_classes) FROM delphi_report_issue_java_classes WHERE issue_id = delphi_report_issues.id @@ -165,7 +181,9 @@ impl DBDelphiReportIssue { ORDER BY CASE WHEN $3 = 'created_asc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END ASC, CASE WHEN $3 = 'created_desc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END DESC, - CASE WHEN $3 = 'pending_status_first' THEN delphi_report_issues.status ELSE 'pending'::delphi_report_issue_status END ASC + CASE WHEN $3 = 'pending_status_first' THEN delphi_report_issues.status ELSE 'pending'::delphi_report_issue_status END ASC, + CASE WHEN $3 = 'severity_asc' THEN delphi_reports.severity ELSE 'low'::delphi_report_severity END ASC, + CASE WHEN $3 = 'severity_desc' THEN delphi_reports.severity ELSE 'low'::delphi_report_severity END DESC OFFSET $5 LIMIT $4 "#, @@ -188,6 +206,7 @@ impl DBDelphiReportIssue { delphi_version: row.delphi_version, artifact_url: row.artifact_url, created: row.created, + severity: row.severity, }, java_classes: row .classes @@ -207,8 +226,7 @@ impl DBDelphiReportIssue { Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash, sqlx::Type, )] #[serde(rename_all = "snake_case")] -#[sqlx(type_name = "delphi_report_issue_type")] -#[sqlx(rename_all = "snake_case")] +#[sqlx(type_name = "delphi_report_issue_type", rename_all = "snake_case")] pub enum DelphiReportIssueType { ReflectionIndirection, XorObfuscation, diff --git a/apps/labrinth/src/routes/internal/delphi.rs b/apps/labrinth/src/routes/internal/delphi.rs index 96c55d97ef..7297e8579b 100644 --- a/apps/labrinth/src/routes/internal/delphi.rs +++ b/apps/labrinth/src/routes/internal/delphi.rs @@ -16,7 +16,8 @@ use crate::{ DBDelphiReport, DBDelphiReportIssue, DBDelphiReportIssueJavaClass, DecompiledJavaClassSource, DelphiReportIssueStatus, DelphiReportIssueType, - DelphiReportListOrder, InternalJavaClassName, + DelphiReportListOrder, DelphiReportSeverity, + InternalJavaClassName, }, }, redis::RedisPool, @@ -55,6 +56,7 @@ struct DelphiReport { DelphiReportIssueType, HashMap>, >, + pub severity: DelphiReportSeverity, } impl DelphiReport { @@ -117,6 +119,7 @@ async fn ingest_report( delphi_version: report.delphi_version, artifact_url: report.url.clone(), created: DateTime::::MIN_UTC, // This will be set by the database + severity: report.severity, } .upsert(&mut transaction) .await?; From da08a03d1e2e8b804e6e41c726d7b89bbd5eceb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= Date: Mon, 25 Aug 2025 14:41:15 +0200 Subject: [PATCH 11/65] chore(labrinth): run `cargo sqlx prepare` to fix Docker build errors --- ...87c2db6cf840fc9284a8d8b47f42be741b03.json} | 26 +++++++++++-- ...3153f5e9796b55ae753ab57b14f37708b400d.json | 24 ------------ ...de1e7cddd68ac956143bef994104280a8dc07.json | 37 +++++++++++++++++++ 3 files changed, 59 insertions(+), 28 deletions(-) rename apps/labrinth/.sqlx/{query-c1cd83ddcd112e46477a195e8bed0a1658c6ddf7a486082cdb847fab06150328.json => query-2fd9af40467ca5c7ac81d921ca0a87c2db6cf840fc9284a8d8b47f42be741b03.json} (65%) delete mode 100644 apps/labrinth/.sqlx/query-8f1f75d9c52a5a340aae2b3fd863153f5e9796b55ae753ab57b14f37708b400d.json create mode 100644 apps/labrinth/.sqlx/query-f2054ae7dcc89b21ed6b2f04526de1e7cddd68ac956143bef994104280a8dc07.json diff --git a/apps/labrinth/.sqlx/query-c1cd83ddcd112e46477a195e8bed0a1658c6ddf7a486082cdb847fab06150328.json b/apps/labrinth/.sqlx/query-2fd9af40467ca5c7ac81d921ca0a87c2db6cf840fc9284a8d8b47f42be741b03.json similarity index 65% rename from apps/labrinth/.sqlx/query-c1cd83ddcd112e46477a195e8bed0a1658c6ddf7a486082cdb847fab06150328.json rename to apps/labrinth/.sqlx/query-2fd9af40467ca5c7ac81d921ca0a87c2db6cf840fc9284a8d8b47f42be741b03.json index 54969cea41..078e1fce41 100644 --- a/apps/labrinth/.sqlx/query-c1cd83ddcd112e46477a195e8bed0a1658c6ddf7a486082cdb847fab06150328.json +++ b/apps/labrinth/.sqlx/query-2fd9af40467ca5c7ac81d921ca0a87c2db6cf840fc9284a8d8b47f42be741b03.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n delphi_report_issues.id AS \"id\", report_id,\n issue_type AS \"issue_type: DelphiReportIssueType\",\n delphi_report_issues.status as \"status: DelphiReportIssueStatus\",\n\n file_id, delphi_version, artifact_url, created,\n json_array(SELECT to_jsonb(delphi_report_issue_java_classes)\n FROM delphi_report_issue_java_classes\n WHERE issue_id = delphi_report_issues.id\n ) AS \"classes: sqlx::types::Json>\",\n versions.mod_id AS \"project_id?\", mods.published AS \"project_published?\"\n FROM delphi_report_issues\n INNER JOIN delphi_reports ON delphi_reports.id = report_id\n LEFT OUTER JOIN files ON files.id = file_id\n LEFT OUTER JOIN versions ON versions.id = files.version_id\n LEFT OUTER JOIN mods ON mods.id = versions.mod_id\n WHERE\n (issue_type = $1 OR $1 IS NULL)\n AND (delphi_report_issues.status = $2 OR $2 IS NULL)\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'pending_status_first' THEN delphi_report_issues.status ELSE 'pending'::delphi_report_issue_status END ASC\n OFFSET $5\n LIMIT $4\n ", + "query": "\n SELECT\n delphi_report_issues.id AS \"id\", report_id,\n issue_type AS \"issue_type: DelphiReportIssueType\",\n delphi_report_issues.status AS \"status: DelphiReportIssueStatus\",\n\n file_id, delphi_version, artifact_url, created, severity AS \"severity: DelphiReportSeverity\",\n json_array(SELECT to_jsonb(delphi_report_issue_java_classes)\n FROM delphi_report_issue_java_classes\n WHERE issue_id = delphi_report_issues.id\n ) AS \"classes: sqlx::types::Json>\",\n versions.mod_id AS \"project_id?\", mods.published AS \"project_published?\"\n FROM delphi_report_issues\n INNER JOIN delphi_reports ON delphi_reports.id = report_id\n LEFT OUTER JOIN files ON files.id = file_id\n LEFT OUTER JOIN versions ON versions.id = files.version_id\n LEFT OUTER JOIN mods ON mods.id = versions.mod_id\n WHERE\n (issue_type = $1 OR $1 IS NULL)\n AND (delphi_report_issues.status = $2 OR $2 IS NULL)\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'pending_status_first' THEN delphi_report_issues.status ELSE 'pending'::delphi_report_issue_status END ASC,\n CASE WHEN $3 = 'severity_asc' THEN delphi_reports.severity ELSE 'low'::delphi_report_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN delphi_reports.severity ELSE 'low'::delphi_report_severity END DESC\n OFFSET $5\n LIMIT $4\n ", "describe": { "columns": [ { @@ -84,16 +84,33 @@ }, { "ordinal": 8, + "name": "severity: DelphiReportSeverity", + "type_info": { + "Custom": { + "name": "delphi_report_severity", + "kind": { + "Enum": [ + "low", + "medium", + "high", + "severe" + ] + } + } + } + }, + { + "ordinal": 9, "name": "classes: sqlx::types::Json>", "type_info": "Jsonb" }, { - "ordinal": 9, + "ordinal": 10, "name": "project_id?", "type_info": "Int8" }, { - "ordinal": 10, + "ordinal": 11, "name": "project_published?", "type_info": "Timestamptz" } @@ -155,10 +172,11 @@ false, false, false, + false, null, true, true ] }, - "hash": "c1cd83ddcd112e46477a195e8bed0a1658c6ddf7a486082cdb847fab06150328" + "hash": "2fd9af40467ca5c7ac81d921ca0a87c2db6cf840fc9284a8d8b47f42be741b03" } diff --git a/apps/labrinth/.sqlx/query-8f1f75d9c52a5a340aae2b3fd863153f5e9796b55ae753ab57b14f37708b400d.json b/apps/labrinth/.sqlx/query-8f1f75d9c52a5a340aae2b3fd863153f5e9796b55ae753ab57b14f37708b400d.json deleted file mode 100644 index de31a078f0..0000000000 --- a/apps/labrinth/.sqlx/query-8f1f75d9c52a5a340aae2b3fd863153f5e9796b55ae753ab57b14f37708b400d.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO delphi_reports (file_id, delphi_version, artifact_url)\n VALUES ($1, $2, $3)\n ON CONFLICT (file_id, delphi_version) DO UPDATE SET\n delphi_version = $2, artifact_url = $3, created = CURRENT_TIMESTAMP\n RETURNING id\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8", - "Int4", - "Varchar" - ] - }, - "nullable": [ - false - ] - }, - "hash": "8f1f75d9c52a5a340aae2b3fd863153f5e9796b55ae753ab57b14f37708b400d" -} diff --git a/apps/labrinth/.sqlx/query-f2054ae7dcc89b21ed6b2f04526de1e7cddd68ac956143bef994104280a8dc07.json b/apps/labrinth/.sqlx/query-f2054ae7dcc89b21ed6b2f04526de1e7cddd68ac956143bef994104280a8dc07.json new file mode 100644 index 0000000000..cc1c7b84f3 --- /dev/null +++ b/apps/labrinth/.sqlx/query-f2054ae7dcc89b21ed6b2f04526de1e7cddd68ac956143bef994104280a8dc07.json @@ -0,0 +1,37 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO delphi_reports (file_id, delphi_version, artifact_url, severity)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT (file_id, delphi_version) DO UPDATE SET\n delphi_version = $2, artifact_url = $3, created = CURRENT_TIMESTAMP, severity = $4\n RETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int4", + "Varchar", + { + "Custom": { + "name": "delphi_report_severity", + "kind": { + "Enum": [ + "low", + "medium", + "high", + "severe" + ] + } + } + } + ] + }, + "nullable": [ + false + ] + }, + "hash": "f2054ae7dcc89b21ed6b2f04526de1e7cddd68ac956143bef994104280a8dc07" +} From 93cf10603c2a6707c2febfcbe88062629a6d0225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= Date: Sat, 6 Sep 2025 21:28:38 +0200 Subject: [PATCH 12/65] tweak: add route for fetching Delphi issue type schema, abstract Labrinth away from issue types --- .../20250810155316_delphi-reports.sql | 25 +----- .../src/database/models/delphi_report_item.rs | 54 ++---------- apps/labrinth/src/routes/internal/delphi.rs | 83 ++++++++++++++++--- 3 files changed, 78 insertions(+), 84 deletions(-) diff --git a/apps/labrinth/migrations/20250810155316_delphi-reports.sql b/apps/labrinth/migrations/20250810155316_delphi-reports.sql index 141bdc5fdd..d717d09f9d 100644 --- a/apps/labrinth/migrations/20250810155316_delphi-reports.sql +++ b/apps/labrinth/migrations/20250810155316_delphi-reports.sql @@ -2,29 +2,6 @@ CREATE TYPE delphi_report_severity AS ENUM ('low', 'medium', 'high', 'severe'); CREATE TYPE delphi_report_issue_status AS ENUM ('pending', 'approved', 'rejected'); -CREATE TYPE delphi_report_issue_type AS ENUM ( - 'reflection_indirection', - 'xor_obfuscation', - 'included_libraries', - 'suspicious_binaries', - 'corrupt_classes', - 'suspicious_classes', - 'url_usage', - 'classloader_usage', - 'processbuilder_usage', - 'runtime_exec_usage', - 'jni_usage', - 'main_method', - 'native_loading', - 'malformed_jar', - 'nested_jar_too_deep', - 'failed_decompilation', - 'analysis_failure', - 'malware_easyforme', - 'malware_simplyloader', - 'unknown' -); - -- A Delphi analysis report for a project version CREATE TABLE delphi_reports ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, @@ -46,7 +23,7 @@ CREATE TABLE delphi_report_issues ( report_id BIGINT NOT NULL REFERENCES delphi_reports (id) ON DELETE CASCADE ON UPDATE CASCADE, - issue_type DELPHI_REPORT_ISSUE_TYPE NOT NULL, + issue_type TEXT NOT NULL, status DELPHI_REPORT_ISSUE_STATUS NOT NULL, UNIQUE (report_id, issue_type) ); diff --git a/apps/labrinth/src/database/models/delphi_report_item.rs b/apps/labrinth/src/database/models/delphi_report_item.rs index 6bc155953f..d7757be92c 100644 --- a/apps/labrinth/src/database/models/delphi_report_item.rs +++ b/apps/labrinth/src/database/models/delphi_report_item.rs @@ -70,7 +70,7 @@ pub enum DelphiReportSeverity { pub struct DBDelphiReportIssue { pub id: DelphiReportIssueId, pub report_id: DelphiReportId, - pub issue_type: DelphiReportIssueType, + pub issue_type: String, pub status: DelphiReportIssueStatus, } @@ -141,7 +141,7 @@ impl DBDelphiReportIssue { RETURNING id ", self.report_id as DelphiReportId, - self.issue_type as DelphiReportIssueType, + self.issue_type, self.status as DelphiReportIssueStatus, ) .fetch_one(&mut **transaction) @@ -150,7 +150,7 @@ impl DBDelphiReportIssue { } pub async fn find_all_by( - ty: Option, + ty: Option, status: Option, order_by: Option, count: Option, @@ -161,7 +161,7 @@ impl DBDelphiReportIssue { r#" SELECT delphi_report_issues.id AS "id", report_id, - issue_type AS "issue_type: DelphiReportIssueType", + issue_type, delphi_report_issues.status AS "status: DelphiReportIssueStatus", file_id, delphi_version, artifact_url, created, severity AS "severity: DelphiReportSeverity", @@ -187,7 +187,7 @@ impl DBDelphiReportIssue { OFFSET $5 LIMIT $4 "#, - ty as Option, + ty, status as Option, order_by.map(|order_by| order_by.to_string()), count.map(|count| count as i64), @@ -221,50 +221,6 @@ impl DBDelphiReportIssue { } } -/// A type of issue found by Delphi for an artifact. -#[derive( - Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash, sqlx::Type, -)] -#[serde(rename_all = "snake_case")] -#[sqlx(type_name = "delphi_report_issue_type", rename_all = "snake_case")] -pub enum DelphiReportIssueType { - ReflectionIndirection, - XorObfuscation, - IncludedLibraries, - SuspiciousBinaries, - CorruptClasses, - SuspiciousClasses, - - UrlUsage, - ClassloaderUsage, - ProcessbuilderUsage, - RuntimeExecUsage, - #[serde(rename = "jni_usage")] - #[sqlx(rename = "jni_usage")] - JNIUsage, - - MainMethod, - NativeLoading, - - MalformedJar, - NestedJarTooDeep, - FailedDecompilation, - AnalysisFailure, - - MalwareEasyforme, - MalwareSimplyloader, - - /// An issue reported by Delphi but not known by labrinth yet. - #[serde(other)] - Unknown, -} - -impl Display for DelphiReportIssueType { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - self.serialize(f) - } -} - /// A Java class affected by a Delphi report issue. Every affected /// Java class belongs to a specific issue, and an issue can have zero, /// one, or more affected classes. (Some issues may be artifact-wide, diff --git a/apps/labrinth/src/routes/internal/delphi.rs b/apps/labrinth/src/routes/internal/delphi.rs index 7297e8579b..bc5eb64fd4 100644 --- a/apps/labrinth/src/routes/internal/delphi.rs +++ b/apps/labrinth/src/routes/internal/delphi.rs @@ -1,9 +1,11 @@ -use std::{collections::HashMap, fmt::Write, sync::LazyLock}; +use std::{collections::HashMap, fmt::Write, sync::LazyLock, time::Instant}; use actix_web::{HttpRequest, HttpResponse, get, post, put, web}; use chrono::{DateTime, Utc}; +use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT}; use serde::Deserialize; use sqlx::PgPool; +use tokio::sync::Mutex; use tracing::info; use crate::{ @@ -15,9 +17,8 @@ use crate::{ delphi_report_item::{ DBDelphiReport, DBDelphiReportIssue, DBDelphiReportIssueJavaClass, DecompiledJavaClassSource, - DelphiReportIssueStatus, DelphiReportIssueType, - DelphiReportListOrder, DelphiReportSeverity, - InternalJavaClassName, + DelphiReportIssueStatus, DelphiReportListOrder, + DelphiReportSeverity, InternalJavaClassName, }, }, redis::RedisPool, @@ -38,10 +39,26 @@ pub fn config(cfg: &mut web::ServiceConfig) { .service(_run) .service(version) .service(issues) - .service(update_issue), + .service(update_issue) + .service(issue_type_schema), ); } +static DELPHI_CLIENT: LazyLock = LazyLock::new(|| { + reqwest::Client::builder() + .default_headers({ + HeaderMap::from_iter([( + USER_AGENT, + HeaderValue::from_static(concat!( + "Labrinth/", + env!("COMPILATION_DATE") + )), + )]) + }) + .build() + .unwrap() +}); + #[derive(Deserialize)] struct DelphiReport { pub url: String, @@ -53,7 +70,7 @@ struct DelphiReport { /// Delphi version that generated this report. pub delphi_version: i32, pub issues: HashMap< - DelphiReportIssueType, + String, HashMap>, >, pub severity: DelphiReportSeverity, @@ -169,9 +186,6 @@ pub async fn run( .fetch_one(exec) .await?; - static DELPHI_CLIENT: LazyLock = - LazyLock::new(reqwest::Client::new); - tracing::debug!( "Running Delphi for project {}, version {}, file {}", file_data.project_id.0, @@ -241,7 +255,7 @@ async fn version( #[derive(Deserialize)] struct DelphiIssuesSearchOptions { #[serde(rename = "type")] - ty: Option, + ty: Option, status: Option, order_by: Option, count: Option, @@ -254,7 +268,7 @@ async fn issues( pool: web::Data, redis: web::Data, session_queue: web::Data, - search_options: web::Query, + web::Query(search_options): web::Query, ) -> Result { check_is_moderator_from_headers( &req, @@ -324,3 +338,50 @@ async fn update_issue( Ok(HttpResponse::Created().finish()) } } + +#[get("issue_type/schema")] +async fn issue_type_schema( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_READ, + ) + .await?; + + // This route is expected to be called often by the frontend, and Delphi is not necessarily + // built to scale beyond malware analysis, so cache the result of its quasi-constant-valued + // schema route to alleviate the load on it + + static CACHED_ISSUE_TYPE_SCHEMA: Mutex< + Option<(serde_json::Map, Instant)>, + > = Mutex::const_new(None); + + match &mut *CACHED_ISSUE_TYPE_SCHEMA.lock().await { + Some((schema, last_fetch)) if last_fetch.elapsed().as_secs() < 60 => { + Ok(HttpResponse::Ok().json(schema)) + } + cache_entry => Ok(HttpResponse::Ok().json( + &cache_entry + .insert(( + DELPHI_CLIENT + .get(format!("{}/schema", dotenvy::var("DELPHI_URL")?)) + .send() + .await + .and_then(|res| res.error_for_status()) + .map_err(ApiError::Delphi)? + .json::>() + .await + .map_err(ApiError::Delphi)?, + Instant::now(), + )) + .0, + )), + } +} From b4284af2eb1366c75ac6c781970f4345f8403567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= Date: Sat, 6 Sep 2025 21:39:02 +0200 Subject: [PATCH 13/65] chore: run `cargo sqlx prepare` --- ...724e9a4d5b9765d52305f99f859f939c2e854.json | 30 +-- ...a87c2db6cf840fc9284a8d8b47f42be741b03.json | 182 ------------------ ...d299224bea59fb593310cfae59e41cc322da5.json | 126 ++++++++++++ 3 files changed, 127 insertions(+), 211 deletions(-) delete mode 100644 apps/labrinth/.sqlx/query-2fd9af40467ca5c7ac81d921ca0a87c2db6cf840fc9284a8d8b47f42be741b03.json create mode 100644 apps/labrinth/.sqlx/query-54ab8449978fce405ad46a8720ed299224bea59fb593310cfae59e41cc322da5.json diff --git a/apps/labrinth/.sqlx/query-10a332091be118f580d50ceb7a8724e9a4d5b9765d52305f99f859f939c2e854.json b/apps/labrinth/.sqlx/query-10a332091be118f580d50ceb7a8724e9a4d5b9765d52305f99f859f939c2e854.json index 963ea430b4..31ff65e350 100644 --- a/apps/labrinth/.sqlx/query-10a332091be118f580d50ceb7a8724e9a4d5b9765d52305f99f859f939c2e854.json +++ b/apps/labrinth/.sqlx/query-10a332091be118f580d50ceb7a8724e9a4d5b9765d52305f99f859f939c2e854.json @@ -12,35 +12,7 @@ "parameters": { "Left": [ "Int8", - { - "Custom": { - "name": "delphi_report_issue_type", - "kind": { - "Enum": [ - "reflection_indirection", - "xor_obfuscation", - "included_libraries", - "suspicious_binaries", - "corrupt_classes", - "suspicious_classes", - "url_usage", - "classloader_usage", - "processbuilder_usage", - "runtime_exec_usage", - "jni_usage", - "main_method", - "native_loading", - "malformed_jar", - "nested_jar_too_deep", - "failed_decompilation", - "analysis_failure", - "malware_easyforme", - "malware_simplyloader", - "unknown" - ] - } - } - }, + "Text", { "Custom": { "name": "delphi_report_issue_status", diff --git a/apps/labrinth/.sqlx/query-2fd9af40467ca5c7ac81d921ca0a87c2db6cf840fc9284a8d8b47f42be741b03.json b/apps/labrinth/.sqlx/query-2fd9af40467ca5c7ac81d921ca0a87c2db6cf840fc9284a8d8b47f42be741b03.json deleted file mode 100644 index 078e1fce41..0000000000 --- a/apps/labrinth/.sqlx/query-2fd9af40467ca5c7ac81d921ca0a87c2db6cf840fc9284a8d8b47f42be741b03.json +++ /dev/null @@ -1,182 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n delphi_report_issues.id AS \"id\", report_id,\n issue_type AS \"issue_type: DelphiReportIssueType\",\n delphi_report_issues.status AS \"status: DelphiReportIssueStatus\",\n\n file_id, delphi_version, artifact_url, created, severity AS \"severity: DelphiReportSeverity\",\n json_array(SELECT to_jsonb(delphi_report_issue_java_classes)\n FROM delphi_report_issue_java_classes\n WHERE issue_id = delphi_report_issues.id\n ) AS \"classes: sqlx::types::Json>\",\n versions.mod_id AS \"project_id?\", mods.published AS \"project_published?\"\n FROM delphi_report_issues\n INNER JOIN delphi_reports ON delphi_reports.id = report_id\n LEFT OUTER JOIN files ON files.id = file_id\n LEFT OUTER JOIN versions ON versions.id = files.version_id\n LEFT OUTER JOIN mods ON mods.id = versions.mod_id\n WHERE\n (issue_type = $1 OR $1 IS NULL)\n AND (delphi_report_issues.status = $2 OR $2 IS NULL)\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'pending_status_first' THEN delphi_report_issues.status ELSE 'pending'::delphi_report_issue_status END ASC,\n CASE WHEN $3 = 'severity_asc' THEN delphi_reports.severity ELSE 'low'::delphi_report_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN delphi_reports.severity ELSE 'low'::delphi_report_severity END DESC\n OFFSET $5\n LIMIT $4\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "report_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "issue_type: DelphiReportIssueType", - "type_info": { - "Custom": { - "name": "delphi_report_issue_type", - "kind": { - "Enum": [ - "reflection_indirection", - "xor_obfuscation", - "included_libraries", - "suspicious_binaries", - "corrupt_classes", - "suspicious_classes", - "url_usage", - "classloader_usage", - "processbuilder_usage", - "runtime_exec_usage", - "jni_usage", - "main_method", - "native_loading", - "malformed_jar", - "nested_jar_too_deep", - "failed_decompilation", - "analysis_failure", - "malware_easyforme", - "malware_simplyloader", - "unknown" - ] - } - } - } - }, - { - "ordinal": 3, - "name": "status: DelphiReportIssueStatus", - "type_info": { - "Custom": { - "name": "delphi_report_issue_status", - "kind": { - "Enum": [ - "pending", - "approved", - "rejected" - ] - } - } - } - }, - { - "ordinal": 4, - "name": "file_id", - "type_info": "Int8" - }, - { - "ordinal": 5, - "name": "delphi_version", - "type_info": "Int4" - }, - { - "ordinal": 6, - "name": "artifact_url", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "created", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "severity: DelphiReportSeverity", - "type_info": { - "Custom": { - "name": "delphi_report_severity", - "kind": { - "Enum": [ - "low", - "medium", - "high", - "severe" - ] - } - } - } - }, - { - "ordinal": 9, - "name": "classes: sqlx::types::Json>", - "type_info": "Jsonb" - }, - { - "ordinal": 10, - "name": "project_id?", - "type_info": "Int8" - }, - { - "ordinal": 11, - "name": "project_published?", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - { - "Custom": { - "name": "delphi_report_issue_type", - "kind": { - "Enum": [ - "reflection_indirection", - "xor_obfuscation", - "included_libraries", - "suspicious_binaries", - "corrupt_classes", - "suspicious_classes", - "url_usage", - "classloader_usage", - "processbuilder_usage", - "runtime_exec_usage", - "jni_usage", - "main_method", - "native_loading", - "malformed_jar", - "nested_jar_too_deep", - "failed_decompilation", - "analysis_failure", - "malware_easyforme", - "malware_simplyloader", - "unknown" - ] - } - } - }, - { - "Custom": { - "name": "delphi_report_issue_status", - "kind": { - "Enum": [ - "pending", - "approved", - "rejected" - ] - } - } - }, - "Text", - "Int8", - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - true, - false, - false, - false, - false, - null, - true, - true - ] - }, - "hash": "2fd9af40467ca5c7ac81d921ca0a87c2db6cf840fc9284a8d8b47f42be741b03" -} diff --git a/apps/labrinth/.sqlx/query-54ab8449978fce405ad46a8720ed299224bea59fb593310cfae59e41cc322da5.json b/apps/labrinth/.sqlx/query-54ab8449978fce405ad46a8720ed299224bea59fb593310cfae59e41cc322da5.json new file mode 100644 index 0000000000..2349d6c90a --- /dev/null +++ b/apps/labrinth/.sqlx/query-54ab8449978fce405ad46a8720ed299224bea59fb593310cfae59e41cc322da5.json @@ -0,0 +1,126 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n delphi_report_issues.id AS \"id\", report_id,\n issue_type,\n delphi_report_issues.status AS \"status: DelphiReportIssueStatus\",\n\n file_id, delphi_version, artifact_url, created, severity AS \"severity: DelphiReportSeverity\",\n json_array(SELECT to_jsonb(delphi_report_issue_java_classes)\n FROM delphi_report_issue_java_classes\n WHERE issue_id = delphi_report_issues.id\n ) AS \"classes: sqlx::types::Json>\",\n versions.mod_id AS \"project_id?\", mods.published AS \"project_published?\"\n FROM delphi_report_issues\n INNER JOIN delphi_reports ON delphi_reports.id = report_id\n LEFT OUTER JOIN files ON files.id = file_id\n LEFT OUTER JOIN versions ON versions.id = files.version_id\n LEFT OUTER JOIN mods ON mods.id = versions.mod_id\n WHERE\n (issue_type = $1 OR $1 IS NULL)\n AND (delphi_report_issues.status = $2 OR $2 IS NULL)\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'pending_status_first' THEN delphi_report_issues.status ELSE 'pending'::delphi_report_issue_status END ASC,\n CASE WHEN $3 = 'severity_asc' THEN delphi_reports.severity ELSE 'low'::delphi_report_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN delphi_reports.severity ELSE 'low'::delphi_report_severity END DESC\n OFFSET $5\n LIMIT $4\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "report_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "issue_type", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "status: DelphiReportIssueStatus", + "type_info": { + "Custom": { + "name": "delphi_report_issue_status", + "kind": { + "Enum": [ + "pending", + "approved", + "rejected" + ] + } + } + } + }, + { + "ordinal": 4, + "name": "file_id", + "type_info": "Int8" + }, + { + "ordinal": 5, + "name": "delphi_version", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "artifact_url", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "created", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "severity: DelphiReportSeverity", + "type_info": { + "Custom": { + "name": "delphi_report_severity", + "kind": { + "Enum": [ + "low", + "medium", + "high", + "severe" + ] + } + } + } + }, + { + "ordinal": 9, + "name": "classes: sqlx::types::Json>", + "type_info": "Jsonb" + }, + { + "ordinal": 10, + "name": "project_id?", + "type_info": "Int8" + }, + { + "ordinal": 11, + "name": "project_published?", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text", + { + "Custom": { + "name": "delphi_report_issue_status", + "kind": { + "Enum": [ + "pending", + "approved", + "rejected" + ] + } + } + }, + "Text", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + false, + false, + null, + true, + true + ] + }, + "hash": "54ab8449978fce405ad46a8720ed299224bea59fb593310cfae59e41cc322da5" +} From ff5471cb82a9a1c0f3b6dd093a7715799bde4f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= Date: Sun, 5 Oct 2025 13:18:38 +0200 Subject: [PATCH 14/65] chore: fix typo on frontend generated state file message --- apps/frontend/nuxt.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/nuxt.config.ts b/apps/frontend/nuxt.config.ts index fb136f6676..5e2f726cfe 100644 --- a/apps/frontend/nuxt.config.ts +++ b/apps/frontend/nuxt.config.ts @@ -154,7 +154,7 @@ export default defineNuxtConfig({ (state.errors ?? []).length === 0 ) { console.log( - 'Tags already recently generated. Delete apps/src/frontend/generated/state.json to force regeneration.', + 'Tags already recently generated. Delete apps/frontend/src/generated/state.json to force regeneration.', ) return } From ce05e51edfe3ae3e18ad827084eb1f996b3d5ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= Date: Fri, 24 Oct 2025 23:02:26 +0200 Subject: [PATCH 15/65] feat: update to use new Delphi issue schema --- ...bc457a08e70dcde320c6852074819e41f8ad9.json | 24 ------ ...01763de4c51bd16c100549261b11c5a4142b8.json | 38 +++++++++ ...c053676964380451b3f461e3276f3a26bbff.json} | 10 +-- ...adf1de8aaf214b32a2aab299a0d87fd2dc453.json | 14 ++++ ...de1e7cddd68ac956143bef994104280a8dc07.json | 2 +- .../20250810155316_delphi-reports.sql | 16 ++-- .../src/database/models/delphi_report_item.rs | 77 ++++++++++++------- apps/labrinth/src/database/models/ids.rs | 2 +- apps/labrinth/src/routes/internal/delphi.rs | 63 ++++++++++----- 9 files changed, 158 insertions(+), 88 deletions(-) delete mode 100644 apps/labrinth/.sqlx/query-0080a101c9ae040adbaadf9e46fbc457a08e70dcde320c6852074819e41f8ad9.json create mode 100644 apps/labrinth/.sqlx/query-26a43c77d3c1e875889141d209f01763de4c51bd16c100549261b11c5a4142b8.json rename apps/labrinth/.sqlx/{query-54ab8449978fce405ad46a8720ed299224bea59fb593310cfae59e41cc322da5.json => query-c0ef7e1f2ddc02604c14a94235afc053676964380451b3f461e3276f3a26bbff.json} (57%) create mode 100644 apps/labrinth/.sqlx/query-c7c72cf1f98cbc2b647ab840bdfadf1de8aaf214b32a2aab299a0d87fd2dc453.json diff --git a/apps/labrinth/.sqlx/query-0080a101c9ae040adbaadf9e46fbc457a08e70dcde320c6852074819e41f8ad9.json b/apps/labrinth/.sqlx/query-0080a101c9ae040adbaadf9e46fbc457a08e70dcde320c6852074819e41f8ad9.json deleted file mode 100644 index 37dcad2943..0000000000 --- a/apps/labrinth/.sqlx/query-0080a101c9ae040adbaadf9e46fbc457a08e70dcde320c6852074819e41f8ad9.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO delphi_report_issue_java_classes (issue_id, internal_class_name, decompiled_source)\n VALUES ($1, $2, $3)\n ON CONFLICT (issue_id, internal_class_name) DO UPDATE SET decompiled_source = $3\n RETURNING id\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8", - "Text", - "Text" - ] - }, - "nullable": [ - false - ] - }, - "hash": "0080a101c9ae040adbaadf9e46fbc457a08e70dcde320c6852074819e41f8ad9" -} diff --git a/apps/labrinth/.sqlx/query-26a43c77d3c1e875889141d209f01763de4c51bd16c100549261b11c5a4142b8.json b/apps/labrinth/.sqlx/query-26a43c77d3c1e875889141d209f01763de4c51bd16c100549261b11c5a4142b8.json new file mode 100644 index 0000000000..8806320d11 --- /dev/null +++ b/apps/labrinth/.sqlx/query-26a43c77d3c1e875889141d209f01763de4c51bd16c100549261b11c5a4142b8.json @@ -0,0 +1,38 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO delphi_report_issue_details (issue_id, internal_class_name, decompiled_source, data, severity)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text", + "Text", + "Jsonb", + { + "Custom": { + "name": "delphi_severity", + "kind": { + "Enum": [ + "low", + "medium", + "high", + "severe" + ] + } + } + } + ] + }, + "nullable": [ + false + ] + }, + "hash": "26a43c77d3c1e875889141d209f01763de4c51bd16c100549261b11c5a4142b8" +} diff --git a/apps/labrinth/.sqlx/query-54ab8449978fce405ad46a8720ed299224bea59fb593310cfae59e41cc322da5.json b/apps/labrinth/.sqlx/query-c0ef7e1f2ddc02604c14a94235afc053676964380451b3f461e3276f3a26bbff.json similarity index 57% rename from apps/labrinth/.sqlx/query-54ab8449978fce405ad46a8720ed299224bea59fb593310cfae59e41cc322da5.json rename to apps/labrinth/.sqlx/query-c0ef7e1f2ddc02604c14a94235afc053676964380451b3f461e3276f3a26bbff.json index 2349d6c90a..f31751fe42 100644 --- a/apps/labrinth/.sqlx/query-54ab8449978fce405ad46a8720ed299224bea59fb593310cfae59e41cc322da5.json +++ b/apps/labrinth/.sqlx/query-c0ef7e1f2ddc02604c14a94235afc053676964380451b3f461e3276f3a26bbff.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n delphi_report_issues.id AS \"id\", report_id,\n issue_type,\n delphi_report_issues.status AS \"status: DelphiReportIssueStatus\",\n\n file_id, delphi_version, artifact_url, created, severity AS \"severity: DelphiReportSeverity\",\n json_array(SELECT to_jsonb(delphi_report_issue_java_classes)\n FROM delphi_report_issue_java_classes\n WHERE issue_id = delphi_report_issues.id\n ) AS \"classes: sqlx::types::Json>\",\n versions.mod_id AS \"project_id?\", mods.published AS \"project_published?\"\n FROM delphi_report_issues\n INNER JOIN delphi_reports ON delphi_reports.id = report_id\n LEFT OUTER JOIN files ON files.id = file_id\n LEFT OUTER JOIN versions ON versions.id = files.version_id\n LEFT OUTER JOIN mods ON mods.id = versions.mod_id\n WHERE\n (issue_type = $1 OR $1 IS NULL)\n AND (delphi_report_issues.status = $2 OR $2 IS NULL)\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'pending_status_first' THEN delphi_report_issues.status ELSE 'pending'::delphi_report_issue_status END ASC,\n CASE WHEN $3 = 'severity_asc' THEN delphi_reports.severity ELSE 'low'::delphi_report_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN delphi_reports.severity ELSE 'low'::delphi_report_severity END DESC\n OFFSET $5\n LIMIT $4\n ", + "query": "\n SELECT\n delphi_report_issues.id AS \"id\", report_id,\n issue_type,\n delphi_report_issues.status AS \"status: DelphiReportIssueStatus\",\n\n file_id, delphi_version, artifact_url, created, severity AS \"severity: DelphiSeverity\",\n json_array(SELECT to_jsonb(delphi_report_issue_details)\n FROM delphi_report_issue_details\n WHERE issue_id = delphi_report_issues.id\n ) AS \"details: sqlx::types::Json>\",\n versions.mod_id AS \"project_id?\", mods.published AS \"project_published?\"\n FROM delphi_report_issues\n INNER JOIN delphi_reports ON delphi_reports.id = report_id\n LEFT OUTER JOIN files ON files.id = file_id\n LEFT OUTER JOIN versions ON versions.id = files.version_id\n LEFT OUTER JOIN mods ON mods.id = versions.mod_id\n WHERE\n (issue_type = $1 OR $1 IS NULL)\n AND (delphi_report_issues.status = $2 OR $2 IS NULL)\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'pending_status_first' THEN delphi_report_issues.status ELSE 'pending'::delphi_report_issue_status END ASC,\n CASE WHEN $3 = 'severity_asc' THEN delphi_reports.severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN delphi_reports.severity ELSE 'low'::delphi_severity END DESC\n OFFSET $5\n LIMIT $4\n ", "describe": { "columns": [ { @@ -56,10 +56,10 @@ }, { "ordinal": 8, - "name": "severity: DelphiReportSeverity", + "name": "severity: DelphiSeverity", "type_info": { "Custom": { - "name": "delphi_report_severity", + "name": "delphi_severity", "kind": { "Enum": [ "low", @@ -73,7 +73,7 @@ }, { "ordinal": 9, - "name": "classes: sqlx::types::Json>", + "name": "details: sqlx::types::Json>", "type_info": "Jsonb" }, { @@ -122,5 +122,5 @@ true ] }, - "hash": "54ab8449978fce405ad46a8720ed299224bea59fb593310cfae59e41cc322da5" + "hash": "c0ef7e1f2ddc02604c14a94235afc053676964380451b3f461e3276f3a26bbff" } diff --git a/apps/labrinth/.sqlx/query-c7c72cf1f98cbc2b647ab840bdfadf1de8aaf214b32a2aab299a0d87fd2dc453.json b/apps/labrinth/.sqlx/query-c7c72cf1f98cbc2b647ab840bdfadf1de8aaf214b32a2aab299a0d87fd2dc453.json new file mode 100644 index 0000000000..db0d075672 --- /dev/null +++ b/apps/labrinth/.sqlx/query-c7c72cf1f98cbc2b647ab840bdfadf1de8aaf214b32a2aab299a0d87fd2dc453.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM delphi_report_issue_details WHERE issue_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "c7c72cf1f98cbc2b647ab840bdfadf1de8aaf214b32a2aab299a0d87fd2dc453" +} diff --git a/apps/labrinth/.sqlx/query-f2054ae7dcc89b21ed6b2f04526de1e7cddd68ac956143bef994104280a8dc07.json b/apps/labrinth/.sqlx/query-f2054ae7dcc89b21ed6b2f04526de1e7cddd68ac956143bef994104280a8dc07.json index cc1c7b84f3..8cbe94abd5 100644 --- a/apps/labrinth/.sqlx/query-f2054ae7dcc89b21ed6b2f04526de1e7cddd68ac956143bef994104280a8dc07.json +++ b/apps/labrinth/.sqlx/query-f2054ae7dcc89b21ed6b2f04526de1e7cddd68ac956143bef994104280a8dc07.json @@ -16,7 +16,7 @@ "Varchar", { "Custom": { - "name": "delphi_report_severity", + "name": "delphi_severity", "kind": { "Enum": [ "low", diff --git a/apps/labrinth/migrations/20250810155316_delphi-reports.sql b/apps/labrinth/migrations/20250810155316_delphi-reports.sql index d717d09f9d..e2851c18d0 100644 --- a/apps/labrinth/migrations/20250810155316_delphi-reports.sql +++ b/apps/labrinth/migrations/20250810155316_delphi-reports.sql @@ -1,4 +1,4 @@ -CREATE TYPE delphi_report_severity AS ENUM ('low', 'medium', 'high', 'severe'); +CREATE TYPE delphi_severity AS ENUM ('low', 'medium', 'high', 'severe'); CREATE TYPE delphi_report_issue_status AS ENUM ('pending', 'approved', 'rejected'); @@ -11,7 +11,7 @@ CREATE TABLE delphi_reports ( delphi_version INTEGER NOT NULL, artifact_url VARCHAR(2048) NOT NULL, created TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, - severity DELPHI_REPORT_SEVERITY NOT NULL, + severity DELPHI_SEVERITY NOT NULL, UNIQUE (file_id, delphi_version) ); CREATE INDEX delphi_version ON delphi_reports (delphi_version); @@ -29,16 +29,18 @@ CREATE TABLE delphi_report_issues ( ); CREATE INDEX delphi_report_issue_by_status_and_type ON delphi_report_issues (status, issue_type); --- A Java class affected by a Delphi report issue. Every affected --- Java class belongs to a specific issue, and an issue can have zero, --- one, or more affected classes. (Some issues may be artifact-wide, +-- The details of a Delphi report issue, which contain data about a +-- Java class affected by it. Every Delphi report issue details object +-- belongs to a specific issue, and an issue can have zero, one, or +-- more details attached to it. (Some issues may be artifact-wide, -- or otherwise not really specific to any particular class.) -CREATE TABLE delphi_report_issue_java_classes ( +CREATE TABLE delphi_report_issue_details ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, issue_id BIGINT NOT NULL REFERENCES delphi_report_issues (id) ON DELETE CASCADE ON UPDATE CASCADE, internal_class_name TEXT NOT NULL, decompiled_source TEXT, - UNIQUE (issue_id, internal_class_name) + data JSONB NOT NULL, + severity DELPHI_SEVERITY NOT NULL ); diff --git a/apps/labrinth/src/database/models/delphi_report_item.rs b/apps/labrinth/src/database/models/delphi_report_item.rs index d7757be92c..c1bbacd58c 100644 --- a/apps/labrinth/src/database/models/delphi_report_item.rs +++ b/apps/labrinth/src/database/models/delphi_report_item.rs @@ -1,14 +1,16 @@ use std::{ + collections::HashMap, fmt::{self, Display, Formatter}, ops::Deref, }; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use sqlx::types::Json; use crate::database::models::{ - DBFileId, DBProjectId, DatabaseError, DelphiReportId, DelphiReportIssueId, - DelphiReportIssueJavaClassId, + DBFileId, DBProjectId, DatabaseError, DelphiReportId, + DelphiReportIssueDetailsId, DelphiReportIssueId, }; /// A Delphi malware analysis report for a project version file. @@ -25,7 +27,7 @@ pub struct DBDelphiReport { pub delphi_version: i32, pub artifact_url: String, pub created: DateTime, - pub severity: DelphiReportSeverity, + pub severity: DelphiSeverity, } impl DBDelphiReport { @@ -44,20 +46,20 @@ impl DBDelphiReport { self.file_id as Option, self.delphi_version, self.artifact_url, - self.severity as DelphiReportSeverity, + self.severity as DelphiSeverity, ) .fetch_one(&mut **transaction) .await?)) } } -/// A severity level for a Delphi report. +/// A severity level reported by Delphi. #[derive( Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash, sqlx::Type, )] #[serde(rename_all = "UPPERCASE")] -#[sqlx(type_name = "delphi_report_severity", rename_all = "snake_case")] -pub enum DelphiReportSeverity { +#[sqlx(type_name = "delphi_severity", rename_all = "snake_case")] +pub enum DelphiSeverity { Low, Medium, High, @@ -122,7 +124,7 @@ impl Display for DelphiReportListOrder { pub struct DelphiReportIssueResult { pub issue: DBDelphiReportIssue, pub report: DBDelphiReport, - pub java_classes: Vec, + pub details: Vec, pub project_id: Option, pub project_published: Option>, } @@ -164,11 +166,11 @@ impl DBDelphiReportIssue { issue_type, delphi_report_issues.status AS "status: DelphiReportIssueStatus", - file_id, delphi_version, artifact_url, created, severity AS "severity: DelphiReportSeverity", - json_array(SELECT to_jsonb(delphi_report_issue_java_classes) - FROM delphi_report_issue_java_classes + file_id, delphi_version, artifact_url, created, severity AS "severity: DelphiSeverity", + json_array(SELECT to_jsonb(delphi_report_issue_details) + FROM delphi_report_issue_details WHERE issue_id = delphi_report_issues.id - ) AS "classes: sqlx::types::Json>", + ) AS "details: sqlx::types::Json>", versions.mod_id AS "project_id?", mods.published AS "project_published?" FROM delphi_report_issues INNER JOIN delphi_reports ON delphi_reports.id = report_id @@ -182,8 +184,8 @@ impl DBDelphiReportIssue { CASE WHEN $3 = 'created_asc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END ASC, CASE WHEN $3 = 'created_desc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END DESC, CASE WHEN $3 = 'pending_status_first' THEN delphi_report_issues.status ELSE 'pending'::delphi_report_issue_status END ASC, - CASE WHEN $3 = 'severity_asc' THEN delphi_reports.severity ELSE 'low'::delphi_report_severity END ASC, - CASE WHEN $3 = 'severity_desc' THEN delphi_reports.severity ELSE 'low'::delphi_report_severity END DESC + CASE WHEN $3 = 'severity_asc' THEN delphi_reports.severity ELSE 'low'::delphi_severity END ASC, + CASE WHEN $3 = 'severity_desc' THEN delphi_reports.severity ELSE 'low'::delphi_severity END DESC OFFSET $5 LIMIT $4 "#, @@ -208,10 +210,10 @@ impl DBDelphiReportIssue { created: row.created, severity: row.severity, }, - java_classes: row - .classes + details: row + .details .into_iter() - .flat_map(|class_list| class_list.0) + .flat_map(|details_list| details_list.0) .collect(), project_id: row.project_id.map(DBProjectId), project_published: row.project_published, @@ -221,37 +223,54 @@ impl DBDelphiReportIssue { } } -/// A Java class affected by a Delphi report issue. Every affected -/// Java class belongs to a specific issue, and an issue can have zero, -/// one, or more affected classes. (Some issues may be artifact-wide, +/// The details of a Delphi report issue, which contain data about a +/// Java class affected by it. Every Delphi report issue details object +/// belongs to a specific issue, and an issue can have zero, one, or +/// more details attached to it. (Some issues may be artifact-wide, /// or otherwise not really specific to any particular class.) #[derive(Debug, Deserialize, Serialize)] -pub struct DBDelphiReportIssueJavaClass { - pub id: DelphiReportIssueJavaClassId, +pub struct DBDelphiReportIssueDetails { + pub id: DelphiReportIssueDetailsId, pub issue_id: DelphiReportIssueId, pub internal_class_name: InternalJavaClassName, pub decompiled_source: Option, + pub data: Json>, + pub severity: DelphiSeverity, } -impl DBDelphiReportIssueJavaClass { - pub async fn upsert( +impl DBDelphiReportIssueDetails { + pub async fn insert( &self, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, - ) -> Result { - Ok(DelphiReportIssueJavaClassId(sqlx::query_scalar!( + ) -> Result { + Ok(DelphiReportIssueDetailsId(sqlx::query_scalar!( " - INSERT INTO delphi_report_issue_java_classes (issue_id, internal_class_name, decompiled_source) - VALUES ($1, $2, $3) - ON CONFLICT (issue_id, internal_class_name) DO UPDATE SET decompiled_source = $3 + INSERT INTO delphi_report_issue_details (issue_id, internal_class_name, decompiled_source, data, severity) + VALUES ($1, $2, $3, $4, $5) RETURNING id ", self.issue_id as DelphiReportIssueId, self.internal_class_name.0, self.decompiled_source.as_ref().map(|decompiled_source| &decompiled_source.0), + &self.data as &Json>, + self.severity as DelphiSeverity, ) .fetch_one(&mut **transaction) .await?)) } + + pub async fn remove_all_by_issue_id( + issue_id: DelphiReportIssueId, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + Ok(sqlx::query!( + "DELETE FROM delphi_report_issue_details WHERE issue_id = $1", + issue_id as DelphiReportIssueId, + ) + .execute(&mut **transaction) + .await? + .rows_affected()) + } } /// A [Java class name] with dots replaced by forward slashes (/). diff --git a/apps/labrinth/src/database/models/ids.rs b/apps/labrinth/src/database/models/ids.rs index 6ec97476fe..5c2c37cc42 100644 --- a/apps/labrinth/src/database/models/ids.rs +++ b/apps/labrinth/src/database/models/ids.rs @@ -281,4 +281,4 @@ id_type!(ReportTypeId as i32); id_type!(StatusId as i32); id_type!(DelphiReportId as i64); id_type!(DelphiReportIssueId as i64); -id_type!(DelphiReportIssueJavaClassId as i64); +id_type!(DelphiReportIssueDetailsId as i64); diff --git a/apps/labrinth/src/routes/internal/delphi.rs b/apps/labrinth/src/routes/internal/delphi.rs index bc5eb64fd4..6960dfbcd3 100644 --- a/apps/labrinth/src/routes/internal/delphi.rs +++ b/apps/labrinth/src/routes/internal/delphi.rs @@ -12,13 +12,13 @@ use crate::{ auth::check_is_moderator_from_headers, database::{ models::{ - DBFileId, DelphiReportId, DelphiReportIssueId, - DelphiReportIssueJavaClassId, + DBFileId, DelphiReportId, DelphiReportIssueDetailsId, + DelphiReportIssueId, delphi_report_item::{ DBDelphiReport, DBDelphiReportIssue, - DBDelphiReportIssueJavaClass, DecompiledJavaClassSource, - DelphiReportIssueStatus, DelphiReportListOrder, - DelphiReportSeverity, InternalJavaClassName, + DBDelphiReportIssueDetails, DecompiledJavaClassSource, + DelphiReportIssueStatus, DelphiReportListOrder, DelphiSeverity, + InternalJavaClassName, }, }, redis::RedisPool, @@ -59,6 +59,14 @@ static DELPHI_CLIENT: LazyLock = LazyLock::new(|| { .unwrap() }); +#[derive(Deserialize)] +struct DelphiReportIssueDetails { + pub internal_class_name: InternalJavaClassName, + pub decompiled_source: Option, + pub data: HashMap, + pub severity: DelphiSeverity, +} + #[derive(Deserialize)] struct DelphiReport { pub url: String, @@ -69,11 +77,8 @@ struct DelphiReport { /// A sequential, monotonically increasing version number for the /// Delphi version that generated this report. pub delphi_version: i32, - pub issues: HashMap< - String, - HashMap>, - >, - pub severity: DelphiReportSeverity, + pub issues: HashMap>, + pub severity: DelphiSeverity, } impl DelphiReport { @@ -88,12 +93,19 @@ impl DelphiReport { format!("⚠️ Suspicious traces found at {}", self.url); for (issue, trace) in &self.issues { - for (class, code) in trace { - let code = code.as_deref().map(|code| &**code); + for DelphiReportIssueDetails { + internal_class_name, + decompiled_source, + .. + } in trace + { write!( &mut message_header, - "\n issue {issue} found at class `{class}`:\n```\n{}\n```", - code.unwrap_or("No decompiled source available") + "\n issue {issue} found at class `{internal_class_name}`:\n```\n{}\n```", + decompiled_source.as_ref().map_or( + "No decompiled source available", + |decompiled_source| &**decompiled_source + ) ) .ok(); } @@ -141,7 +153,7 @@ async fn ingest_report( .upsert(&mut transaction) .await?; - for (issue_type, issue_java_classes) in report.issues { + for (issue_type, issue_details) in report.issues { let issue_id = DBDelphiReportIssue { id: DelphiReportIssueId(0), // This will be set by the database report_id, @@ -151,14 +163,23 @@ async fn ingest_report( .upsert(&mut transaction) .await?; - for (internal_class_name, decompiled_source) in issue_java_classes { - DBDelphiReportIssueJavaClass { - id: DelphiReportIssueJavaClassId(0), // This will be set by the database + // This is required to handle the case where the same Delphi version is re-run on the same file + DBDelphiReportIssueDetails::remove_all_by_issue_id( + issue_id, + &mut transaction, + ) + .await?; + + for issue_detail in issue_details { + DBDelphiReportIssueDetails { + id: DelphiReportIssueDetailsId(0), // This will be set by the database issue_id, - internal_class_name, - decompiled_source, + internal_class_name: issue_detail.internal_class_name, + decompiled_source: issue_detail.decompiled_source, + data: issue_detail.data.into(), + severity: issue_detail.severity, } - .upsert(&mut transaction) + .insert(&mut transaction) .await?; } } From 45580f7799a50031c03834af01261f0016ff6d05 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Fri, 7 Nov 2025 10:01:39 +0000 Subject: [PATCH 16/65] wip: tech review endpoints --- .../{moderation.rs => moderation/mod.rs} | 8 +++- .../routes/internal/moderation/tech_review.rs | 43 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) rename apps/labrinth/src/routes/internal/{moderation.rs => moderation/mod.rs} (98%) create mode 100644 apps/labrinth/src/routes/internal/moderation/tech_review.rs diff --git a/apps/labrinth/src/routes/internal/moderation.rs b/apps/labrinth/src/routes/internal/moderation/mod.rs similarity index 98% rename from apps/labrinth/src/routes/internal/moderation.rs rename to apps/labrinth/src/routes/internal/moderation/mod.rs index 973183820b..e5968d67e6 100644 --- a/apps/labrinth/src/routes/internal/moderation.rs +++ b/apps/labrinth/src/routes/internal/moderation/mod.rs @@ -15,10 +15,16 @@ use serde::{Deserialize, Serialize}; use sqlx::PgPool; use std::collections::HashMap; +mod tech_review; + pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { cfg.service(get_projects) .service(get_project_meta) - .service(set_project_meta); + .service(set_project_meta) + .service( + utoipa_actix_web::scope("/tech-review") + .configure(tech_review::config), + ); } #[derive(Deserialize, utoipa::ToSchema)] diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs new file mode 100644 index 0000000000..bd9551ed8a --- /dev/null +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -0,0 +1,43 @@ +use actix_web::{HttpRequest, get, web}; +use sqlx::PgPool; + +use crate::{ + auth::check_is_moderator_from_headers, + database::redis::RedisPool, + models::pats::Scopes, + queue::session::AuthQueue, + routes::{ApiError, internal::moderation::ProjectsRequestOptions}, +}; + +pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { + cfg.service(get_projects); +} + +/// Gets all projects which are awaiting technical review. +#[utoipa::path] +#[get("")] +async fn get_projects( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + request_opts: web::Query, + session_queue: web::Data, +) -> Result<(), ApiError> { + check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_READ, + ) + .await?; + + sqlx::query!( + " + SELECT id FROM delphi_reports + ORDER BY created + + " + ) + .fetch(&**pool) +} From 465aa51ec27e19134103c7992349813935efbe6c Mon Sep 17 00:00:00 2001 From: aecsocket Date: Mon, 10 Nov 2025 16:46:54 +0000 Subject: [PATCH 17/65] wip: add ToSchema for dependent types --- CLAUDE.md | 2 +- apps/labrinth/src/auth/mod.rs | 10 +- .../src/database/models/delphi_report_item.rs | 11 +- apps/labrinth/src/database/models/ids.rs | 1 + apps/labrinth/src/models/v3/threads.rs | 10 +- apps/labrinth/src/models/v3/users.rs | 27 ++- .../routes/internal/moderation/tech_review.rs | 225 ++++++++++++++++-- 7 files changed, 260 insertions(+), 26 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2f6a226977..0ab19bc52c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,7 +56,7 @@ Use `docker exec labrinth-clickhouse clickhouse-client` to access the Clickhouse ### Postgres -Use `docker exec labrinth-postgres psql -U postgres` to access the PostgreSQL instance. +Use `docker exec labrinth-postgres psql -U labrinth -d labrinth -c "SELECT 1"` to access the PostgreSQL instance, replacing the `SELECT 1` with your query. # Guidelines diff --git a/apps/labrinth/src/auth/mod.rs b/apps/labrinth/src/auth/mod.rs index 953d978c52..8131c77f5f 100644 --- a/apps/labrinth/src/auth/mod.rs +++ b/apps/labrinth/src/auth/mod.rs @@ -112,7 +112,15 @@ impl AuthenticationError { } #[derive( - Serialize, Deserialize, Default, Eq, PartialEq, Clone, Copy, Debug, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Default, + Serialize, + Deserialize, + utoipa::ToSchema, )] #[serde(rename_all = "lowercase")] pub enum AuthProvider { diff --git a/apps/labrinth/src/database/models/delphi_report_item.rs b/apps/labrinth/src/database/models/delphi_report_item.rs index c1bbacd58c..1a3eff7fe9 100644 --- a/apps/labrinth/src/database/models/delphi_report_item.rs +++ b/apps/labrinth/src/database/models/delphi_report_item.rs @@ -55,7 +55,16 @@ impl DBDelphiReport { /// A severity level reported by Delphi. #[derive( - Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash, sqlx::Type, + Deserialize, + Serialize, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + sqlx::Type, + utoipa::ToSchema, )] #[serde(rename_all = "UPPERCASE")] #[sqlx(type_name = "delphi_severity", rename_all = "snake_case")] diff --git a/apps/labrinth/src/database/models/ids.rs b/apps/labrinth/src/database/models/ids.rs index 5c2c37cc42..266cafa2e0 100644 --- a/apps/labrinth/src/database/models/ids.rs +++ b/apps/labrinth/src/database/models/ids.rs @@ -152,6 +152,7 @@ macro_rules! id_type { Eq, PartialEq, Hash, + utoipa::ToSchema, )] #[sqlx(transparent)] pub struct $name(pub $type); diff --git a/apps/labrinth/src/models/v3/threads.rs b/apps/labrinth/src/models/v3/threads.rs index a1a32214bd..5b918899d2 100644 --- a/apps/labrinth/src/models/v3/threads.rs +++ b/apps/labrinth/src/models/v3/threads.rs @@ -7,7 +7,7 @@ use ariadne::ids::UserId; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct Thread { pub id: ThreadId, #[serde(rename = "type")] @@ -18,7 +18,7 @@ pub struct Thread { pub members: Vec, } -#[derive(Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct ThreadMessage { pub id: ThreadMessageId, pub author_id: Option, @@ -27,7 +27,7 @@ pub struct ThreadMessage { pub hide_identity: bool, } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[serde(tag = "type", rename_all = "snake_case")] pub enum MessageBody { Text { @@ -50,7 +50,9 @@ pub enum MessageBody { }, } -#[derive(Serialize, Deserialize, Eq, PartialEq, Copy, Clone)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "snake_case")] pub enum ThreadType { Report, diff --git a/apps/labrinth/src/models/v3/users.rs b/apps/labrinth/src/models/v3/users.rs index d8b0a2e822..0f276fc8fa 100644 --- a/apps/labrinth/src/models/v3/users.rs +++ b/apps/labrinth/src/models/v3/users.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; pub const DELETED_USER: UserId = UserId(127155982985829); bitflags::bitflags! { - #[derive(Copy, Clone, Debug)] + #[derive(Debug, Clone, Copy)] pub struct Badges: u64 { const MIDAS = 1 << 0; const EARLY_MODPACK_ADOPTER = 1 << 1; @@ -21,6 +21,23 @@ bitflags::bitflags! { } } +impl utoipa::PartialSchema for Badges { + fn schema() -> utoipa::openapi::RefOr { + u64::schema() + } +} + +impl utoipa::ToSchema for Badges { + fn schemas( + schemas: &mut Vec<( + String, + utoipa::openapi::RefOr, + )>, + ) { + u64::schemas(schemas); + } +} + bitflags_serde_impl!(Badges, u64); impl Default for Badges { @@ -29,7 +46,7 @@ impl Default for Badges { } } -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct User { pub id: UserId, pub username: String, @@ -52,7 +69,7 @@ pub struct User { pub github_id: Option, } -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct UserPayoutData { pub paypal_address: Option, pub paypal_country: Option, @@ -137,7 +154,9 @@ impl User { } } -#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] +#[derive( + Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "lowercase")] pub enum Role { Developer, diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index bd9551ed8a..436595dca5 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -1,28 +1,151 @@ -use actix_web::{HttpRequest, get, web}; +use std::collections::HashMap; + +use actix_web::{HttpRequest, get, post, web}; +use serde::{Deserialize, Serialize}; use sqlx::PgPool; +use tokio_stream::StreamExt; use crate::{ auth::check_is_moderator_from_headers, - database::redis::RedisPool, - models::pats::Scopes, + database::{ + models::{ + DelphiReportId, DelphiReportIssueDetailsId, DelphiReportIssueId, + ProjectTypeId, categories::ProjectType, + delphi_report_item::DelphiSeverity, + }, + redis::RedisPool, + }, + models::{pats::Scopes, projects::Project, threads::Thread}, queue::session::AuthQueue, routes::{ApiError, internal::moderation::ProjectsRequestOptions}, + util::error::Context, }; pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { - cfg.service(get_projects); + cfg.service(search_projects); +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct SearchProjects { + #[serde(default = "default_limit")] + pub limit: u64, + #[serde(default)] + pub page: u64, + #[serde(default)] + pub filter: SearchProjectsFilter, + #[serde(default = "default_sort_by")] + pub sort_by: SearchProjectsSort, +} + +fn default_limit() -> u64 { + 20 +} + +fn default_sort_by() -> SearchProjectsSort { + SearchProjectsSort::Oldest +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)] +pub struct SearchProjectsFilter { + pub project_type: Vec, +} + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + utoipa::ToSchema, +)] +pub enum SearchProjectsSort { + Oldest, + Newest, +} + +#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] +pub struct ProjectReview { + pub project: Project, + pub project_owner: (), + pub thread: Thread, + /// Why this project was flagged. + pub flag_reason: FlagReason, + /// What files were flagged in this review. + pub files: Vec, +} + +/// Why a project was flagged for technical review. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + utoipa::ToSchema, +)] +#[serde(rename_all = "snake_case")] +pub enum FlagReason { + /// Delphi anti-malware scanner flagged a file in the project. + Delphi, } -/// Gets all projects which are awaiting technical review. +/// Details of a JAR file which was flagged for technical review, as part of +/// a [`ProjectReview`]. +#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] +pub struct FileReview { + /// Name of the flagged file. + pub file_name: String, + /// Size of the flagged file, in bytes. + pub file_size: u64, + /// What issues appeared in the file. + pub issues: Vec, +} + +/// Issue raised by Delphi in a flagged file. +/// +/// The issue is scoped to the JAR, not any specific class, but issues can be +/// raised because they appeared in a class - see [`FileIssueDetails`]. +#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] +pub struct FileIssue { + /// ID of the issue. + pub issue_id: DelphiReportIssueId, + /// Delphi-determined kind of issue that this is, e.g. `OBFUSCATED_NAMES`. + /// + /// Labrinth does not know the full set of kinds of issues, so this is kept + /// as a string. + pub kind: String, + /// How important is this issue, as flagged by Delphi? + pub severity: DelphiSeverity, + /// Details of why this issue might have been raised, such as what file it + /// was found in. + pub details: Vec, +} + +/// Occurrence of a [`FileIssue`] in a specific class in a scanned JAR file. +#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] +pub struct FileIssueDetails { + /// Name of the Java class in which this issue was found. + pub class_name: String, + /// Decompiled, pretty-printed source of the Java class. + pub decompiled_source: String, +} + +/// Searches all projects which are awaiting technical review. #[utoipa::path] -#[get("")] -async fn get_projects( +#[post("/search")] +async fn search_projects( req: HttpRequest, pool: web::Data, redis: web::Data, - request_opts: web::Query, session_queue: web::Data, -) -> Result<(), ApiError> { + search_req: web::Json, +) -> Result>, ApiError> { check_is_moderator_from_headers( &req, &**pool, @@ -32,12 +155,84 @@ async fn get_projects( ) .await?; - sqlx::query!( - " - SELECT id FROM delphi_reports - ORDER BY created + let sort_by = match search_req.sort_by { + SearchProjectsSort::Oldest => 0, + SearchProjectsSort::Newest => 1, + }; + let limit = search_req.limit.max(50); + let offset = limit * search_req.page; + + let limit = + i64::try_from(limit).wrap_request_err("limit cannot fit into `i64`")?; + let offset = i64::try_from(offset) + .wrap_request_err("offset cannot fit into `i64`")?; + + let mut reports = Vec::new(); + let mut project_ids = Vec::new(); - " + let mut rows = sqlx::query!( + r#" + SELECT + dr.id AS report_id, + m.id AS project_id, + dr.created AS report_created, + dri.issue_type AS issue_type, + drid.internal_class_name AS issue_detail_class_name, + drid.decompiled_source AS issue_detail_decompiled_source, + drid.severity AS "issue_detail_severity: DelphiSeverity" + FROM delphi_reports dr + + -- fetch the project this report is for, and its type + INNER JOIN files f ON f.id = dr.file_id + INNER JOIN versions v ON v.id = f.version_id + INNER JOIN mods m ON m.id = v.mod_id + LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id + INNER JOIN categories c ON c.id = mc.joining_category_id + + -- fetch report issues and details + LEFT JOIN delphi_report_issues dri ON dri.report_id = dr.id + LEFT JOIN delphi_report_issue_details drid ON drid.issue_id = dri.id + + -- filtering + WHERE + -- project type + (cardinality($1::int[]) = 0 OR c.project_type = ANY($1::int[])) + + -- sorting + ORDER BY + CASE WHEN $2 = ' + -- when sorting on TIMESTAMPTZ columns, we extract the int value of the time + -- so that we can sort by an integer, which we can negate + -- (we can't negate a TIMESTAMPTZ) + + -- oldest + WHEN $2 = 0 THEN EXTRACT(EPOCH FROM created) + -- newest + WHEN $2 = 1 THEN -EXTRACT(EPOCH FROM created) + END + + -- pagination + LIMIT $3 + OFFSET $4 + "#, + &search_req.filter.project_type.iter().map(|ty| ty.0).collect::>(), + sort_by, + limit, + offset, ) - .fetch(&**pool) + .fetch(&**pool); + while let Some(row) = rows + .next() + .await + .transpose() + .wrap_internal_err("failed to fetch reports") + { + project_ids.push(row.project_id); + reports.push(ProjectReview { + project: (), + project_owner: (), + }); + } + + Ok(()) } From 520cc8edebccab83a95e501ade8eeae3d87f00c6 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Mon, 10 Nov 2025 23:54:09 +0000 Subject: [PATCH 18/65] wip: report issues return --- Cargo.lock | 1 + apps/labrinth/Cargo.toml | 1 + .../src/database/models/thread_item.rs | 4 +- .../routes/internal/moderation/tech_review.rs | 169 +++++++++++++----- 4 files changed, 131 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3cea3b20f3..5ed7289af8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4535,6 +4535,7 @@ dependencies = [ "hyper-util", "iana-time-zone", "image", + "indexmap 2.11.4", "itertools 0.14.0", "jemalloc_pprof", "json-patch 4.1.0", diff --git a/apps/labrinth/Cargo.toml b/apps/labrinth/Cargo.toml index 6920d146dd..b69d84cd58 100644 --- a/apps/labrinth/Cargo.toml +++ b/apps/labrinth/Cargo.toml @@ -66,6 +66,7 @@ image = { workspace = true, features = [ "tiff", "webp", ] } +indexmap = { workspace = true } itertools = { workspace = true } json-patch = { workspace = true } lettre = { workspace = true } diff --git a/apps/labrinth/src/database/models/thread_item.rs b/apps/labrinth/src/database/models/thread_item.rs index 04275d62e1..d01a1b3842 100644 --- a/apps/labrinth/src/database/models/thread_item.rs +++ b/apps/labrinth/src/database/models/thread_item.rs @@ -11,7 +11,7 @@ pub struct ThreadBuilder { pub report_id: Option, } -#[derive(Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct DBThread { pub id: DBThreadId, @@ -30,7 +30,7 @@ pub struct ThreadMessageBuilder { pub hide_identity: bool, } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct DBThreadMessage { pub id: DBThreadMessageId, pub thread_id: DBThreadId, diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 436595dca5..73473dd883 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -1,6 +1,9 @@ -use std::collections::HashMap; +use std::{collections::HashMap, fmt}; -use actix_web::{HttpRequest, get, post, web}; +use actix_web::{HttpRequest, post, web}; +use chrono::{DateTime, Utc}; +use eyre::eyre; +use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use tokio_stream::StreamExt; @@ -8,16 +11,19 @@ use tokio_stream::StreamExt; use crate::{ auth::check_is_moderator_from_headers, database::{ + DBProject, models::{ - DelphiReportId, DelphiReportIssueDetailsId, DelphiReportIssueId, - ProjectTypeId, categories::ProjectType, - delphi_report_item::DelphiSeverity, + DBProjectId, DBThread, DBThreadId, DelphiReportId, + DelphiReportIssueId, ProjectTypeId, + delphi_report_item::{ + DBDelphiReportIssue, DBDelphiReportIssueDetails, DelphiSeverity, + }, }, redis::RedisPool, }, models::{pats::Scopes, projects::Project, threads::Thread}, queue::session::AuthQueue, - routes::{ApiError, internal::moderation::ProjectsRequestOptions}, + routes::ApiError, util::error::Context, }; @@ -42,7 +48,7 @@ fn default_limit() -> u64 { } fn default_sort_by() -> SearchProjectsSort { - SearchProjectsSort::Oldest + SearchProjectsSort::CreatedAsc } #[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)] @@ -62,15 +68,30 @@ pub struct SearchProjectsFilter { utoipa::ToSchema, )] pub enum SearchProjectsSort { - Oldest, - Newest, + CreatedAsc, + CreatedDesc, +} + +impl fmt::Display for SearchProjectsSort { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = serde_json::to_value(*self).unwrap(); + let s = s.as_str().unwrap(); + write!(f, "{s}") + } } #[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] pub struct ProjectReview { pub project: Project, pub project_owner: (), - pub thread: Thread, + pub thread: DBThread, + pub reports: Vec, +} + +#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] +pub struct ProjectReport { + /// When this report was created. + pub created_at: DateTime, /// Why this project was flagged. pub flag_reason: FlagReason, /// What files were flagged in this review. @@ -146,6 +167,20 @@ async fn search_projects( session_queue: web::Data, search_req: web::Json, ) -> Result>, ApiError> { + #[derive(Debug)] + struct ProjectRecord { + reports: IndexMap, + } + + #[derive(Debug)] + struct ReportRecord { + created: DateTime, + issues: IndexMap, + } + + #[derive(Debug)] + struct IssueRecord {} + check_is_moderator_from_headers( &req, &**pool, @@ -155,10 +190,7 @@ async fn search_projects( ) .await?; - let sort_by = match search_req.sort_by { - SearchProjectsSort::Oldest => 0, - SearchProjectsSort::Newest => 1, - }; + let sort_by = search_req.sort_by.to_string(); let limit = search_req.limit.max(50); let offset = limit * search_req.page; @@ -167,27 +199,31 @@ async fn search_projects( let offset = i64::try_from(offset) .wrap_request_err("offset cannot fit into `i64`")?; - let mut reports = Vec::new(); - let mut project_ids = Vec::new(); + let mut project_records = IndexMap::::new(); + let mut project_ids = Vec::::new(); + let mut thread_ids = Vec::::new(); let mut rows = sqlx::query!( r#" SELECT - dr.id AS report_id, - m.id AS project_id, - dr.created AS report_created, - dri.issue_type AS issue_type, - drid.internal_class_name AS issue_detail_class_name, - drid.decompiled_source AS issue_detail_decompiled_source, - drid.severity AS "issue_detail_severity: DelphiSeverity" + dr.id AS "report_id!: DelphiReportId", + m.id AS "project_id!: DBProjectId", + t.id AS "project_thread_id!: DBThreadId", + dr.created AS "report_created!", + dri.id AS "issue_id", + dri.issue_type AS "issue_type?", + drid.internal_class_name AS "issue_detail_class_name?", + drid.decompiled_source AS "issue_detail_decompiled_source?", + drid.severity AS "issue_detail_severity?: DelphiSeverity" FROM delphi_reports dr - -- fetch the project this report is for, and its type + -- fetch the project this report is for, its type, and thread INNER JOIN files f ON f.id = dr.file_id INNER JOIN versions v ON v.id = f.version_id INNER JOIN mods m ON m.id = v.mod_id LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id INNER JOIN categories c ON c.id = mc.joining_category_id + INNER JOIN threads t ON t.mod_id = m.id -- fetch report issues and details LEFT JOIN delphi_report_issues dri ON dri.report_id = dr.id @@ -200,39 +236,88 @@ async fn search_projects( -- sorting ORDER BY - CASE WHEN $2 = ' - -- when sorting on TIMESTAMPTZ columns, we extract the int value of the time - -- so that we can sort by an integer, which we can negate - -- (we can't negate a TIMESTAMPTZ) - - -- oldest - WHEN $2 = 0 THEN EXTRACT(EPOCH FROM created) - -- newest - WHEN $2 = 1 THEN -EXTRACT(EPOCH FROM created) - END + CASE WHEN $2 = 'created_asc' THEN created ELSE TO_TIMESTAMP(0) END ASC, + CASE WHEN $2 = 'created_desc' THEN created ELSE TO_TIMESTAMP(0) END DESC -- pagination LIMIT $3 OFFSET $4 "#, - &search_req.filter.project_type.iter().map(|ty| ty.0).collect::>(), - sort_by, + &search_req + .filter + .project_type + .iter() + .map(|ty| ty.0) + .collect::>(), + &sort_by, limit, offset, ) .fetch(&**pool); + while let Some(row) = rows .next() .await .transpose() - .wrap_internal_err("failed to fetch reports") + .wrap_internal_err("failed to fetch reports")? { project_ids.push(row.project_id); - reports.push(ProjectReview { - project: (), - project_owner: (), - }); + thread_ids.push(row.project_thread_id); + + let project = + project_records.entry(row.project_id).or_insert_with(|| { + ProjectRecord { + reports: IndexMap::new(), + } + }); + let report = + project + .reports + .entry(row.report_id) + .or_insert(|| ReportRecord { + created: row.report_created, + issues: IndexMap::new(), + }); + report.issues.entry(row.issue) + // .push(ReportRecord { + // created: row.report_created, + // issues: + // flag_reason: FlagReason::Delphi, + // files: + // created: row.report_created, + // }); } - Ok(()) + let projects = DBProject::get_many_ids(&project_ids, &**pool, &redis) + .await + .wrap_internal_err("failed to fetch projects")? + .into_iter() + .map(|project| (project.inner.id, Project::from(project))) + .collect::>(); + let threads = DBThread::get_many(&thread_ids, &**pool) + .await + .wrap_internal_err("failed to fetch threads")? + .into_iter() + .map(|thread| (thread.id, thread)) + .collect::>(); + + let projects = project_records.into_iter().map(|(project_id, reports)| { + let project = + projects.get(&project_id).wrap_internal_err_with(|| { + eyre!("no fetched project with ID {project_id:?}") + })?; + let thread = threads + .get(&DBThreadId::from(project.thread_id)) + .wrap_internal_err_with(|| { + eyre!("no fetched thread with ID {:?}", project.thread_id) + })?; + Ok::<_, ApiError>(ProjectReview { + project: project.clone(), + project_owner: (), + thread: thread.clone(), + reports, + }) + }); + + Ok(web::Json(projects)) } From ae53eab14eb6bf3d8b1fe16050ae5e462542b4ce Mon Sep 17 00:00:00 2001 From: aecsocket Date: Wed, 12 Nov 2025 19:17:47 +0000 Subject: [PATCH 19/65] wip --- .../routes/internal/moderation/tech_review.rs | 62 +++++++++++++------ 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 73473dd883..3356ad00c3 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -13,7 +13,7 @@ use crate::{ database::{ DBProject, models::{ - DBProjectId, DBThread, DBThreadId, DelphiReportId, + DBFileId, DBProjectId, DBThread, DBThreadId, DelphiReportId, DelphiReportIssueId, ProjectTypeId, delphi_report_item::{ DBDelphiReportIssue, DBDelphiReportIssueDetails, DelphiSeverity, @@ -179,7 +179,10 @@ async fn search_projects( } #[derive(Debug)] - struct IssueRecord {} + struct IssueRecord { + file_name: String, + file_size: u64, + } check_is_moderator_from_headers( &req, @@ -202,6 +205,7 @@ async fn search_projects( let mut project_records = IndexMap::::new(); let mut project_ids = Vec::::new(); let mut thread_ids = Vec::::new(); + let mut file_ids = Vec::::new(); let mut rows = sqlx::query!( r#" @@ -212,6 +216,8 @@ async fn search_projects( dr.created AS "report_created!", dri.id AS "issue_id", dri.issue_type AS "issue_type?", + f.filename AS "file_name?", + f.size AS "file_size?", drid.internal_class_name AS "issue_detail_class_name?", drid.decompiled_source AS "issue_detail_decompiled_source?", drid.severity AS "issue_detail_severity?: DelphiSeverity" @@ -278,7 +284,7 @@ async fn search_projects( created: row.report_created, issues: IndexMap::new(), }); - report.issues.entry(row.issue) + // report.issues.entry(row.issue).or_inser // .push(ReportRecord { // created: row.report_created, // issues: @@ -301,23 +307,41 @@ async fn search_projects( .map(|thread| (thread.id, thread)) .collect::>(); - let projects = project_records.into_iter().map(|(project_id, reports)| { - let project = - projects.get(&project_id).wrap_internal_err_with(|| { - eyre!("no fetched project with ID {project_id:?}") - })?; - let thread = threads - .get(&DBThreadId::from(project.thread_id)) - .wrap_internal_err_with(|| { - eyre!("no fetched thread with ID {:?}", project.thread_id) - })?; - Ok::<_, ApiError>(ProjectReview { - project: project.clone(), - project_owner: (), - thread: thread.clone(), - reports, + let projects = project_records + .into_iter() + .map(|(project_id, project_record)| { + let project = + projects.get(&project_id).wrap_internal_err_with(|| { + eyre!("no fetched project with ID {project_id:?}") + })?; + let thread = threads + .get(&DBThreadId::from(project.thread_id)) + .wrap_internal_err_with(|| { + eyre!("no fetched thread with ID {:?}", project.thread_id) + })?; + Ok::<_, ApiError>(ProjectReview { + project: project.clone(), + project_owner: (), + thread: thread.clone(), + reports: project_record + .reports + .into_iter() + .map(|(_, report_record)| ProjectReport { + created_at: report_record.created, + flag_reason: FlagReason::Delphi, + files: report_record + .issues + .into_iter() + .map(|(_, issue_record)| FileReview { + file_name: issue_record.file_name, + file_size: issue_record.file_size, + }) + .collect(), + }) + .collect(), + }) }) - }); + .collect::, _>>()?; Ok(web::Json(projects)) } From 32d1ae29d10828e7c941ade88d053797eef4c178 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Thu, 13 Nov 2025 14:52:56 +0000 Subject: [PATCH 20/65] wip: returning more data --- .../20250810155316_delphi-reports.sql | 2 +- .../src/database/models/delphi_report_item.rs | 19 ++- .../routes/internal/moderation/tech_review.rs | 123 +++++++++++++++--- 3 files changed, 118 insertions(+), 26 deletions(-) diff --git a/apps/labrinth/migrations/20250810155316_delphi-reports.sql b/apps/labrinth/migrations/20250810155316_delphi-reports.sql index e2851c18d0..7cbd19afa7 100644 --- a/apps/labrinth/migrations/20250810155316_delphi-reports.sql +++ b/apps/labrinth/migrations/20250810155316_delphi-reports.sql @@ -1,6 +1,6 @@ CREATE TYPE delphi_severity AS ENUM ('low', 'medium', 'high', 'severe'); -CREATE TYPE delphi_report_issue_status AS ENUM ('pending', 'approved', 'rejected'); +CREATE TYPE delphi_report_issue_status AS ENUM ('pending', 'safe', 'unsafe'); -- A Delphi analysis report for a project version CREATE TABLE delphi_reports ( diff --git a/apps/labrinth/src/database/models/delphi_report_item.rs b/apps/labrinth/src/database/models/delphi_report_item.rs index 1a3eff7fe9..a2b4cfcb33 100644 --- a/apps/labrinth/src/database/models/delphi_report_item.rs +++ b/apps/labrinth/src/database/models/delphi_report_item.rs @@ -87,20 +87,29 @@ pub struct DBDelphiReportIssue { /// An status a Delphi report issue can have. #[derive( - Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash, sqlx::Type, + Deserialize, + Serialize, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + sqlx::Type, + utoipa::ToSchema, )] #[serde(rename_all = "snake_case")] #[sqlx(type_name = "delphi_report_issue_status", rename_all = "snake_case")] pub enum DelphiReportIssueStatus { /// The issue is pending review by the moderation team. Pending, - /// The issue has been approved (i.e., reviewed as a valid, true positive). - /// The affected artifact has thus been verified to be potentially malicious. - Approved, /// The issue has been rejected (i.e., reviewed as a false positive). /// The affected artifact has thus been verified to be clean, other issues /// with it notwithstanding. - Rejected, + Safe, + /// The issue has been approved (i.e., reviewed as a valid, true positive). + /// The affected artifact has thus been verified to be potentially malicious. + Unsafe, } impl Display for DelphiReportIssueStatus { diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 3356ad00c3..9a626c02fb 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -14,21 +14,22 @@ use crate::{ DBProject, models::{ DBFileId, DBProjectId, DBThread, DBThreadId, DelphiReportId, - DelphiReportIssueId, ProjectTypeId, + DelphiReportIssueDetailsId, DelphiReportIssueId, ProjectTypeId, delphi_report_item::{ - DBDelphiReportIssue, DBDelphiReportIssueDetails, DelphiSeverity, + DBDelphiReportIssue, DBDelphiReportIssueDetails, + DelphiReportIssueStatus, DelphiSeverity, }, }, redis::RedisPool, }, models::{pats::Scopes, projects::Project, threads::Thread}, queue::session::AuthQueue, - routes::ApiError, + routes::{ApiError, internal::moderation::Ownership}, util::error::Context, }; pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { - cfg.service(search_projects); + cfg.service(search_projects).service(update_issue); } #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] @@ -83,7 +84,7 @@ impl fmt::Display for SearchProjectsSort { #[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] pub struct ProjectReview { pub project: Project, - pub project_owner: (), + pub project_owner: Ownership, pub thread: DBThread, pub reports: Vec, } @@ -123,7 +124,7 @@ pub struct FileReview { /// Name of the flagged file. pub file_name: String, /// Size of the flagged file, in bytes. - pub file_size: u64, + pub file_size: i32, /// What issues appeared in the file. pub issues: Vec, } @@ -143,6 +144,8 @@ pub struct FileIssue { pub kind: String, /// How important is this issue, as flagged by Delphi? pub severity: DelphiSeverity, + /// Is this issue valid (malicious) or a false positive (safe)? + pub status: DelphiReportIssueStatus, /// Details of why this issue might have been raised, such as what file it /// was found in. pub details: Vec, @@ -181,7 +184,9 @@ async fn search_projects( #[derive(Debug)] struct IssueRecord { file_name: String, - file_size: u64, + file_size: i32, + issue_type: String, + details: IndexMap, } check_is_moderator_from_headers( @@ -214,10 +219,11 @@ async fn search_projects( m.id AS "project_id!: DBProjectId", t.id AS "project_thread_id!: DBThreadId", dr.created AS "report_created!", - dri.id AS "issue_id", + dri.id AS "issue_id?: DelphiReportIssueId", dri.issue_type AS "issue_type?", f.filename AS "file_name?", f.size AS "file_size?", + drid.id AS "issue_detail_id?: DelphiReportIssueDetailsId", drid.internal_class_name AS "issue_detail_class_name?", drid.decompiled_source AS "issue_detail_decompiled_source?", drid.severity AS "issue_detail_severity?: DelphiSeverity" @@ -277,21 +283,46 @@ async fn search_projects( } }); let report = - project - .reports - .entry(row.report_id) - .or_insert(|| ReportRecord { + project.reports.entry(row.report_id).or_insert_with(|| { + ReportRecord { created: row.report_created, issues: IndexMap::new(), + } + }); + + let ( + Some(issue_id), + Some(file_name), + Some(file_size), + Some(issue_type), + ) = (row.issue_id, row.file_name, row.file_size, row.issue_type) + else { + continue; + }; + let issue = + report + .issues + .entry(issue_id) + .or_insert_with(|| IssueRecord { + file_name, + file_size, + issue_type, + details: IndexMap::new(), }); - // report.issues.entry(row.issue).or_inser - // .push(ReportRecord { - // created: row.report_created, - // issues: - // flag_reason: FlagReason::Delphi, - // files: - // created: row.report_created, - // }); + + let (Some(issue_detail_id), Some(class_name), Some(decompiled_source)) = ( + row.issue_detail_id, + row.issue_detail_class_name, + row.issue_detail_decompiled_source, + ) else { + continue; + }; + issue.details.entry(issue_detail_id).or_insert_with(|| { + FileIssueDetails { + class_name, + decompiled_source, + } + }); } let projects = DBProject::get_many_ids(&project_ids, &**pool, &redis) @@ -335,6 +366,15 @@ async fn search_projects( .map(|(_, issue_record)| FileReview { file_name: issue_record.file_name, file_size: issue_record.file_size, + issues: issue_record + .details + .into_iter() + .map(|(issue_id, detail)| FileIssue { + issue_id, + kind: issue_record, + kind: details, + }) + .collect(), }) .collect(), }) @@ -345,3 +385,46 @@ async fn search_projects( Ok(web::Json(projects)) } + +/// Updates the state of a technical review issue. +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct UpdateIssue { + /// Status to set the issue to. + pub status: DelphiReportIssueStatus, +} + +#[utoipa::path] +#[post("/issue/{id}")] +async fn update_issue( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, + update_req: web::Json, + path: web::Path<(DelphiReportIssueId,)>, +) -> Result<(), ApiError> { + check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_WRITE, + ) + .await?; + let (issue_id,) = path.into_inner(); + + sqlx::query!( + " + UPDATE delphi_report_issues + SET status = $1 + WHERE id = $2 + ", + update_req.status as DelphiReportIssueStatus, + issue_id as DelphiReportIssueId, + ) + .execute(&**pool) + .await + .wrap_internal_err("failed to update issue")?; + + Ok(()) +} From 7d347b06a1da4b801b6353f9ef0840a4dc82968c Mon Sep 17 00:00:00 2001 From: aecsocket Date: Thu, 13 Nov 2025 18:20:09 +0000 Subject: [PATCH 21/65] wip --- ...724e9a4d5b9765d52305f99f859f939c2e854.json | 4 +- ...c9e2bd27ac9a87987eafd79b06f1c4ecdb659.json | 26 ++++ ...fc053676964380451b3f461e3276f3a26bbff.json | 8 +- ...956b0bf816248c131c16954793f0eda09c1ec.json | 103 +++++++++++++ apps/labrinth/src/database/models/ids.rs | 2 +- .../src/routes/internal/moderation/mod.rs | 77 ++-------- .../routes/internal/moderation/ownership.rs | 143 ++++++++++++++++++ .../routes/internal/moderation/tech_review.rs | 122 +++++++++------ 8 files changed, 369 insertions(+), 116 deletions(-) create mode 100644 apps/labrinth/.sqlx/query-b1df83f4592701f8aa03f6d16bac9e2bd27ac9a87987eafd79b06f1c4ecdb659.json create mode 100644 apps/labrinth/.sqlx/query-e48e36c7535052093fa4f0d1759956b0bf816248c131c16954793f0eda09c1ec.json create mode 100644 apps/labrinth/src/routes/internal/moderation/ownership.rs diff --git a/apps/labrinth/.sqlx/query-10a332091be118f580d50ceb7a8724e9a4d5b9765d52305f99f859f939c2e854.json b/apps/labrinth/.sqlx/query-10a332091be118f580d50ceb7a8724e9a4d5b9765d52305f99f859f939c2e854.json index 31ff65e350..7e30ece2ec 100644 --- a/apps/labrinth/.sqlx/query-10a332091be118f580d50ceb7a8724e9a4d5b9765d52305f99f859f939c2e854.json +++ b/apps/labrinth/.sqlx/query-10a332091be118f580d50ceb7a8724e9a4d5b9765d52305f99f859f939c2e854.json @@ -19,8 +19,8 @@ "kind": { "Enum": [ "pending", - "approved", - "rejected" + "safe", + "unsafe" ] } } diff --git a/apps/labrinth/.sqlx/query-b1df83f4592701f8aa03f6d16bac9e2bd27ac9a87987eafd79b06f1c4ecdb659.json b/apps/labrinth/.sqlx/query-b1df83f4592701f8aa03f6d16bac9e2bd27ac9a87987eafd79b06f1c4ecdb659.json new file mode 100644 index 0000000000..216435cf2b --- /dev/null +++ b/apps/labrinth/.sqlx/query-b1df83f4592701f8aa03f6d16bac9e2bd27ac9a87987eafd79b06f1c4ecdb659.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE delphi_report_issues\n SET status = $1\n WHERE id = $2\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + { + "Custom": { + "name": "delphi_report_issue_status", + "kind": { + "Enum": [ + "pending", + "safe", + "unsafe" + ] + } + } + }, + "Int8" + ] + }, + "nullable": [] + }, + "hash": "b1df83f4592701f8aa03f6d16bac9e2bd27ac9a87987eafd79b06f1c4ecdb659" +} diff --git a/apps/labrinth/.sqlx/query-c0ef7e1f2ddc02604c14a94235afc053676964380451b3f461e3276f3a26bbff.json b/apps/labrinth/.sqlx/query-c0ef7e1f2ddc02604c14a94235afc053676964380451b3f461e3276f3a26bbff.json index f31751fe42..7e536646ef 100644 --- a/apps/labrinth/.sqlx/query-c0ef7e1f2ddc02604c14a94235afc053676964380451b3f461e3276f3a26bbff.json +++ b/apps/labrinth/.sqlx/query-c0ef7e1f2ddc02604c14a94235afc053676964380451b3f461e3276f3a26bbff.json @@ -27,8 +27,8 @@ "kind": { "Enum": [ "pending", - "approved", - "rejected" + "safe", + "unsafe" ] } } @@ -96,8 +96,8 @@ "kind": { "Enum": [ "pending", - "approved", - "rejected" + "safe", + "unsafe" ] } } diff --git a/apps/labrinth/.sqlx/query-e48e36c7535052093fa4f0d1759956b0bf816248c131c16954793f0eda09c1ec.json b/apps/labrinth/.sqlx/query-e48e36c7535052093fa4f0d1759956b0bf816248c131c16954793f0eda09c1ec.json new file mode 100644 index 0000000000..c39018099c --- /dev/null +++ b/apps/labrinth/.sqlx/query-e48e36c7535052093fa4f0d1759956b0bf816248c131c16954793f0eda09c1ec.json @@ -0,0 +1,103 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n dr.id AS \"report_id!: DelphiReportId\",\n m.id AS \"project_id!: DBProjectId\",\n t.id AS \"project_thread_id!: DBThreadId\",\n dr.created AS \"report_created!\",\n dri.id AS \"issue_id?: DelphiReportIssueId\",\n dri.issue_type AS \"issue_type?\",\n f.filename AS \"file_name?\",\n f.size AS \"file_size?\",\n drid.id AS \"issue_detail_id?: DelphiReportIssueDetailsId\",\n drid.internal_class_name AS \"issue_detail_class_name?\",\n drid.decompiled_source AS \"issue_detail_decompiled_source?\",\n drid.severity AS \"issue_detail_severity?: DelphiSeverity\"\n FROM delphi_reports dr\n\n -- fetch the project this report is for, its type, and thread\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n INNER JOIN categories c ON c.id = mc.joining_category_id\n INNER JOIN threads t ON t.mod_id = m.id\n\n -- fetch report issues and details\n LEFT JOIN delphi_report_issues dri ON dri.report_id = dr.id\n LEFT JOIN delphi_report_issue_details drid ON drid.issue_id = dri.id\n\n -- filtering\n WHERE\n -- project type\n (cardinality($1::int[]) = 0 OR c.project_type = ANY($1::int[]))\n\n -- sorting\n ORDER BY\n CASE WHEN $2 = 'created_asc' THEN created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $2 = 'created_desc' THEN created ELSE TO_TIMESTAMP(0) END DESC\n\n -- pagination\n LIMIT $3\n OFFSET $4\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "report_id!: DelphiReportId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "project_id!: DBProjectId", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "project_thread_id!: DBThreadId", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "report_created!", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "issue_id?: DelphiReportIssueId", + "type_info": "Int8" + }, + { + "ordinal": 5, + "name": "issue_type?", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "file_name?", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "file_size?", + "type_info": "Int4" + }, + { + "ordinal": 8, + "name": "issue_detail_id?: DelphiReportIssueDetailsId", + "type_info": "Int8" + }, + { + "ordinal": 9, + "name": "issue_detail_class_name?", + "type_info": "Text" + }, + { + "ordinal": 10, + "name": "issue_detail_decompiled_source?", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "issue_detail_severity?: DelphiSeverity", + "type_info": { + "Custom": { + "name": "delphi_severity", + "kind": { + "Enum": [ + "low", + "medium", + "high", + "severe" + ] + } + } + } + } + ], + "parameters": { + "Left": [ + "Int4Array", + "Text", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + true + ] + }, + "hash": "e48e36c7535052093fa4f0d1759956b0bf816248c131c16954793f0eda09c1ec" +} diff --git a/apps/labrinth/src/database/models/ids.rs b/apps/labrinth/src/database/models/ids.rs index 266cafa2e0..10f8cc8ae9 100644 --- a/apps/labrinth/src/database/models/ids.rs +++ b/apps/labrinth/src/database/models/ids.rs @@ -94,7 +94,7 @@ macro_rules! generate_bulk_ids { macro_rules! impl_db_id_interface { ($id_struct:ident, $db_id_struct:ident, $(, generator: $generator_function:ident @ $db_table:expr, $(bulk_generator: $bulk_generator_function:ident,)?)?) => { - #[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, PartialEq, Eq, Hash)] + #[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, PartialEq, Eq, Hash, utoipa::ToSchema)] #[sqlx(transparent)] pub struct $db_id_struct(pub i64); diff --git a/apps/labrinth/src/routes/internal/moderation/mod.rs b/apps/labrinth/src/routes/internal/moderation/mod.rs index e5968d67e6..29b7331253 100644 --- a/apps/labrinth/src/routes/internal/moderation/mod.rs +++ b/apps/labrinth/src/routes/internal/moderation/mod.rs @@ -1,8 +1,7 @@ use super::ApiError; use crate::database; -use crate::database::models::{DBOrganization, DBTeamId, DBTeamMember, DBUser}; use crate::database::redis::RedisPool; -use crate::models::ids::{OrganizationId, TeamId}; +use crate::models::ids::OrganizationId; use crate::models::projects::{Project, ProjectStatus}; use crate::queue::moderation::{ApprovalType, IdentifiedFile, MissingMetadata}; use crate::queue::session::AuthQueue; @@ -10,11 +9,12 @@ use crate::util::error::Context; use crate::{auth::check_is_moderator_from_headers, models::pats::Scopes}; use actix_web::{HttpRequest, get, post, web}; use ariadne::ids::{UserId, random_base62}; -use eyre::eyre; +use ownership::get_projects_ownership; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use std::collections::HashMap; +mod ownership; mod tech_review; pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { @@ -53,7 +53,7 @@ pub struct FetchedProject { } /// Fetched information on who owns a project. -#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] +#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema, Clone)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum Ownership { /// Project is owned by a team, and this is the team owner. @@ -135,73 +135,20 @@ pub async fn get_projects_internal( .map(crate::models::projects::Project::from) .collect::>(); - let team_ids = projects - .iter() - .map(|project| project.team_id) - .map(DBTeamId::from) - .collect::>(); - let org_ids = projects - .iter() - .filter_map(|project| project.organization) - .collect::>(); - - let team_members = - DBTeamMember::get_from_team_full_many(&team_ids, &**pool, &redis) - .await - .wrap_internal_err("failed to fetch team members")?; - let users = DBUser::get_many_ids( - &team_members - .iter() - .map(|member| member.user_id) - .collect::>(), - &**pool, - &redis, - ) - .await - .wrap_internal_err("failed to fetch user data of team members")?; - let orgs = DBOrganization::get_many(&org_ids, &**pool, &redis) + let ownerships = get_projects_ownership(&projects, &pool, &redis) .await - .wrap_internal_err("failed to fetch organizations")?; - - let map_project = |project: Project| -> Result { - let project_id = project.id; - let ownership = if let Some(org_id) = project.organization { - let org = orgs - .iter() - .find(|org| OrganizationId::from(org.id) == org_id) - .wrap_internal_err_with(|| { - eyre!( - "project {project_id} is owned by an invalid organization {org_id}" - ) - })?; - - Ownership::Organization { - id: OrganizationId::from(org.id), - name: org.name.clone(), - icon_url: org.icon_url.clone(), - } - } else { - let team_id = project.team_id; - let team_owner = team_members.iter().find(|member| TeamId::from(member.team_id) == team_id && member.is_owner) - .wrap_internal_err_with(|| eyre!("project {project_id} is owned by a team {team_id} which has no valid owner"))?; - let team_owner_id = team_owner.user_id; - let user = users.iter().find(|user| user.id == team_owner_id) - .wrap_internal_err_with(|| eyre!("project {project_id} is owned by a team {team_id} which has owner {} which does not exist", UserId::from(team_owner_id)))?; - - Ownership::User { - id: UserId::from(user.id), - name: user.username.clone(), - icon_url: user.avatar_url.clone(), - } - }; + .wrap_internal_err("failed to fetch project ownerships")?; - Ok(FetchedProject { ownership, project }) - }; + let map_project = + |(project, ownership): (Project, Ownership)| -> FetchedProject { + FetchedProject { ownership, project } + }; let projects = projects .into_iter() + .zip(ownerships) .map(map_project) - .collect::, _>>()?; + .collect::>(); Ok(web::Json(projects)) } diff --git a/apps/labrinth/src/routes/internal/moderation/ownership.rs b/apps/labrinth/src/routes/internal/moderation/ownership.rs new file mode 100644 index 0000000000..46fba8af1a --- /dev/null +++ b/apps/labrinth/src/routes/internal/moderation/ownership.rs @@ -0,0 +1,143 @@ +use crate::database::models::{DBOrganization, DBTeamId, DBTeamMember, DBUser}; +use crate::database::redis::RedisPool; +use crate::models::ids::OrganizationId; +use crate::routes::internal::moderation::Ownership; +use crate::util::error::Context; +use ariadne::ids::UserId; +use eyre::eyre; +use sqlx::PgPool; + +/// Fetches ownership information for a single project. +pub async fn get_project_ownership( + project_id: crate::models::ids::ProjectId, + team_id: crate::models::ids::TeamId, + organization: Option, + pool: &PgPool, + redis: &RedisPool, +) -> Result { + if let Some(org_id) = organization { + let org = DBOrganization::get(&org_id.to_string(), pool, redis) + .await + .wrap_internal_err("failed to fetch organization")? + .ok_or_else(|| { + crate::routes::ApiError::Internal( + eyre!("project {project_id} is owned by an invalid organization {org_id}") + ) + })?; + + Ok(Ownership::Organization { + id: OrganizationId::from(org.id), + name: org.name.clone(), + icon_url: org.icon_url, + }) + } else { + let team_members = DBTeamMember::get_from_team_full( + DBTeamId::from(team_id), + pool, + redis, + ) + .await + .wrap_internal_err("failed to fetch team members")?; + + let team_owner = team_members + .iter() + .find(|member| member.is_owner) + .ok_or_else(|| { + crate::routes::ApiError::Internal(eyre!( + "project {project_id} is owned by a team {team_id} which has no valid owner" + )) + })?; + + let user = DBUser::get(&team_owner.user_id.0.to_string(), pool, redis) + .await + .wrap_internal_err("failed to fetch user data")? + .ok_or_else(|| { + crate::routes::ApiError::Internal(eyre!( + "project {project_id} is owned by a team {team_id} which has owner {} which does not exist", + ariadne::ids::UserId::from(team_owner.user_id) + )) + })?; + + Ok(Ownership::User { + id: ariadne::ids::UserId::from(user.id), + name: user.username.clone(), + icon_url: user.avatar_url, + }) + } +} + +/// Fetches ownership information for multiple projects efficiently +pub async fn get_projects_ownership( + projects: &[crate::models::projects::Project], + pool: &PgPool, + redis: &RedisPool, +) -> Result, crate::routes::ApiError> { + let team_ids = projects + .iter() + .map(|project| project.team_id) + .map(DBTeamId::from) + .collect::>(); + let org_ids = projects + .iter() + .filter_map(|project| project.organization) + .collect::>(); + + let team_members = + DBTeamMember::get_from_team_full_many(&team_ids, pool, redis) + .await + .wrap_internal_err("failed to fetch team members")?; + let users = DBUser::get_many_ids( + &team_members + .iter() + .map(|member| member.user_id) + .collect::>(), + pool, + redis, + ) + .await + .wrap_internal_err("failed to fetch user data of team members")?; + let orgs = DBOrganization::get_many(&org_ids, pool, redis) + .await + .wrap_internal_err("failed to fetch organizations")?; + + let mut ownerships = Vec::with_capacity(projects.len()); + + for project in projects { + let project_id = project.id; + let ownership = if let Some(org_id) = project.organization { + let org = orgs + .iter() + .find(|org| OrganizationId::from(org.id) == org_id) + .wrap_internal_err_with(|| { + eyre!( + "project {project_id} is owned by an invalid organization {org_id}" + ) + })?; + + Ownership::Organization { + id: OrganizationId::from(org.id), + name: org.name.clone(), + icon_url: org.icon_url.clone(), + } + } else { + let team_id = project.team_id; + let team_owner = team_members.iter().find(|member| { + crate::models::ids::TeamId::from(member.team_id) == team_id && member.is_owner + }) + .wrap_internal_err_with(|| eyre!("project {project_id} is owned by a team {team_id} which has no valid owner"))?; + let team_owner_id = team_owner.user_id; + let user = users.iter().find(|user| user.id == team_owner_id) + .wrap_internal_err_with(|| eyre!("project {project_id} is owned by a team {team_id} which has owner {} which does not exist", UserId::from(team_owner_id)))?; + + Ownership::User { + id: ariadne::ids::UserId::from(user.id), + name: user.username.clone(), + icon_url: user.avatar_url.clone(), + } + }; + + ownerships.push(ownership); + } + + Ok(ownerships) +} diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 9a626c02fb..691aa33b35 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; use sqlx::PgPool; use tokio_stream::StreamExt; +use super::ownership::get_projects_ownership; use crate::{ auth::check_is_moderator_from_headers, database::{ @@ -15,14 +16,11 @@ use crate::{ models::{ DBFileId, DBProjectId, DBThread, DBThreadId, DelphiReportId, DelphiReportIssueDetailsId, DelphiReportIssueId, ProjectTypeId, - delphi_report_item::{ - DBDelphiReportIssue, DBDelphiReportIssueDetails, - DelphiReportIssueStatus, DelphiSeverity, - }, + delphi_report_item::{DelphiReportIssueStatus, DelphiSeverity}, }, redis::RedisPool, }, - models::{pats::Scopes, projects::Project, threads::Thread}, + models::{pats::Scopes, projects::Project}, queue::session::AuthQueue, routes::{ApiError, internal::moderation::Ownership}, util::error::Context, @@ -32,6 +30,7 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { cfg.service(search_projects).service(update_issue); } +/// Arguments for searching project technical reviews. #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct SearchProjects { #[serde(default = "default_limit")] @@ -148,12 +147,12 @@ pub struct FileIssue { pub status: DelphiReportIssueStatus, /// Details of why this issue might have been raised, such as what file it /// was found in. - pub details: Vec, + pub details: Vec, } /// Occurrence of a [`FileIssue`] in a specific class in a scanned JAR file. #[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] -pub struct FileIssueDetails { +pub struct FileIssueDetail { /// Name of the Java class in which this issue was found. pub class_name: String, /// Decompiled, pretty-printed source of the Java class. @@ -178,15 +177,21 @@ async fn search_projects( #[derive(Debug)] struct ReportRecord { created: DateTime, - issues: IndexMap, + files: IndexMap, } #[derive(Debug)] - struct IssueRecord { + struct FileRecord { file_name: String, file_size: i32, + issues: IndexMap, + } + + #[derive(Debug)] + struct IssueRecord { issue_type: String, - details: IndexMap, + status: DelphiReportIssueStatus, + details: IndexMap, } check_is_moderator_from_headers( @@ -210,19 +215,22 @@ async fn search_projects( let mut project_records = IndexMap::::new(); let mut project_ids = Vec::::new(); let mut thread_ids = Vec::::new(); - let mut file_ids = Vec::::new(); + let _file_ids = Vec::::new(); let mut rows = sqlx::query!( r#" SELECT dr.id AS "report_id!: DelphiReportId", + f.id AS "file_id!: DBFileId", + f.filename AS "file_name!", + f.size AS "file_size!", m.id AS "project_id!: DBProjectId", t.id AS "project_thread_id!: DBThreadId", dr.created AS "report_created!", - dri.id AS "issue_id?: DelphiReportIssueId", - dri.issue_type AS "issue_type?", - f.filename AS "file_name?", - f.size AS "file_size?", + dri.id AS "issue_id!: DelphiReportIssueId", + dri.issue_type AS "issue_type!", + dri.status AS "issue_status!: DelphiReportIssueStatus", + -- maybe null drid.id AS "issue_detail_id?: DelphiReportIssueDetailsId", drid.internal_class_name AS "issue_detail_class_name?", drid.decompiled_source AS "issue_detail_decompiled_source?", @@ -238,7 +246,7 @@ async fn search_projects( INNER JOIN threads t ON t.mod_id = m.id -- fetch report issues and details - LEFT JOIN delphi_report_issues dri ON dri.report_id = dr.id + INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id LEFT JOIN delphi_report_issue_details drid ON drid.issue_id = dri.id -- filtering @@ -286,27 +294,24 @@ async fn search_projects( project.reports.entry(row.report_id).or_insert_with(|| { ReportRecord { created: row.report_created, - issues: IndexMap::new(), + files: IndexMap::new(), } }); - - let ( - Some(issue_id), - Some(file_name), - Some(file_size), - Some(issue_type), - ) = (row.issue_id, row.file_name, row.file_size, row.issue_type) - else { - continue; - }; - let issue = + let file = report - .issues - .entry(issue_id) + .files + .entry(row.file_id) + .or_insert_with(|| FileRecord { + file_name: row.file_name, + file_size: row.file_size, + issues: IndexMap::new(), + }); + let issue = + file.issues + .entry(row.issue_id) .or_insert_with(|| IssueRecord { - file_name, - file_size, - issue_type, + issue_type: row.issue_type, + status: row.issue_status, details: IndexMap::new(), }); @@ -318,7 +323,7 @@ async fn search_projects( continue; }; issue.details.entry(issue_detail_id).or_insert_with(|| { - FileIssueDetails { + FileIssueDetail { class_name, decompiled_source, } @@ -338,6 +343,18 @@ async fn search_projects( .map(|thread| (thread.id, thread)) .collect::>(); + let project_list: Vec = projects.values().cloned().collect(); + + let ownerships = get_projects_ownership(&project_list, &pool, &redis) + .await + .wrap_internal_err("failed to fetch project ownerships")?; + + let ownership_map = projects + .keys() + .copied() + .zip(ownerships) + .collect::>(); + let projects = project_records .into_iter() .map(|(project_id, project_record)| { @@ -352,7 +369,12 @@ async fn search_projects( })?; Ok::<_, ApiError>(ProjectReview { project: project.clone(), - project_owner: (), + project_owner: ownership_map + .get(&project_id) + .cloned() + .wrap_internal_err_with(|| { + eyre!("no owner for {project_id:?}") + })?, thread: thread.clone(), reports: project_record .reports @@ -361,18 +383,30 @@ async fn search_projects( created_at: report_record.created, flag_reason: FlagReason::Delphi, files: report_record - .issues + .files .into_iter() - .map(|(_, issue_record)| FileReview { - file_name: issue_record.file_name, - file_size: issue_record.file_size, - issues: issue_record - .details + .map(|(_, file)| FileReview { + file_name: file.file_name, + file_size: file.file_size, + issues: file + .issues .into_iter() - .map(|(issue_id, detail)| FileIssue { + .map(|(issue_id, issue)| FileIssue { issue_id, - kind: issue_record, - kind: details, + kind: issue.issue_type.clone(), + status: issue.status, + details: issue + .details + .into_iter() + .map(|(_, detail)| { + FileIssueDetail { + class_name: detail + .class_name, + decompiled_source: detail + .decompiled_source, + } + }) + .collect(), }) .collect(), }) From 9f4699be485dabaa45c2327cd3865298a9d78037 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Thu, 13 Nov 2025 18:39:19 +0000 Subject: [PATCH 22/65] Fix up db query --- ...6bcab9fcba55df818cab6e98721a324659526.json | 144 ++++++++++++++++++ ...956b0bf816248c131c16954793f0eda09c1ec.json | 103 ------------- .../routes/internal/moderation/ownership.rs | 59 ------- .../routes/internal/moderation/tech_review.rs | 23 ++- 4 files changed, 163 insertions(+), 166 deletions(-) create mode 100644 apps/labrinth/.sqlx/query-a06d0f682b1e576e634b02102ab6bcab9fcba55df818cab6e98721a324659526.json delete mode 100644 apps/labrinth/.sqlx/query-e48e36c7535052093fa4f0d1759956b0bf816248c131c16954793f0eda09c1ec.json diff --git a/apps/labrinth/.sqlx/query-a06d0f682b1e576e634b02102ab6bcab9fcba55df818cab6e98721a324659526.json b/apps/labrinth/.sqlx/query-a06d0f682b1e576e634b02102ab6bcab9fcba55df818cab6e98721a324659526.json new file mode 100644 index 0000000000..de08e37ea9 --- /dev/null +++ b/apps/labrinth/.sqlx/query-a06d0f682b1e576e634b02102ab6bcab9fcba55df818cab6e98721a324659526.json @@ -0,0 +1,144 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n dr.id AS \"report_id!: DelphiReportId\",\n f.id AS \"file_id!: DBFileId\",\n f.filename AS \"file_name!\",\n f.size AS \"file_size!\",\n m.id AS \"project_id!: DBProjectId\",\n t.id AS \"project_thread_id!: DBThreadId\",\n dr.created AS \"report_created!\",\n dr.severity AS \"report_severity!: DelphiSeverity\",\n dri.id AS \"issue_id!: DelphiReportIssueId\",\n dri.issue_type AS \"issue_type!\",\n dri.status AS \"issue_status!: DelphiReportIssueStatus\",\n -- maybe null\n drid.id AS \"issue_detail_id?: DelphiReportIssueDetailsId\",\n drid.internal_class_name AS \"issue_detail_class_name?\",\n drid.decompiled_source AS \"issue_detail_decompiled_source?\",\n drid.severity AS \"issue_detail_severity?: DelphiSeverity\"\n FROM delphi_reports dr\n\n -- fetch the project this report is for, its type, and thread\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n INNER JOIN categories c ON c.id = mc.joining_category_id\n INNER JOIN threads t ON t.mod_id = m.id\n\n -- fetch report issues and details\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n LEFT JOIN delphi_report_issue_details drid ON drid.issue_id = dri.id\n\n -- filtering\n WHERE\n -- project type\n (cardinality($1::int[]) = 0 OR c.project_type = ANY($1::int[]))\n\n -- sorting\n ORDER BY\n CASE WHEN $2 = 'created_asc' THEN created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $2 = 'created_desc' THEN created ELSE TO_TIMESTAMP(0) END DESC\n\n -- pagination\n LIMIT $3\n OFFSET $4\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "report_id!: DelphiReportId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "file_id!: DBFileId", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "file_name!", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "file_size!", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "project_id!: DBProjectId", + "type_info": "Int8" + }, + { + "ordinal": 5, + "name": "project_thread_id!: DBThreadId", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "report_created!", + "type_info": "Timestamptz" + }, + { + "ordinal": 7, + "name": "report_severity!: DelphiSeverity", + "type_info": { + "Custom": { + "name": "delphi_severity", + "kind": { + "Enum": [ + "low", + "medium", + "high", + "severe" + ] + } + } + } + }, + { + "ordinal": 8, + "name": "issue_id!: DelphiReportIssueId", + "type_info": "Int8" + }, + { + "ordinal": 9, + "name": "issue_type!", + "type_info": "Text" + }, + { + "ordinal": 10, + "name": "issue_status!: DelphiReportIssueStatus", + "type_info": { + "Custom": { + "name": "delphi_report_issue_status", + "kind": { + "Enum": [ + "pending", + "safe", + "unsafe" + ] + } + } + } + }, + { + "ordinal": 11, + "name": "issue_detail_id?: DelphiReportIssueDetailsId", + "type_info": "Int8" + }, + { + "ordinal": 12, + "name": "issue_detail_class_name?", + "type_info": "Text" + }, + { + "ordinal": 13, + "name": "issue_detail_decompiled_source?", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "issue_detail_severity?: DelphiSeverity", + "type_info": { + "Custom": { + "name": "delphi_severity", + "kind": { + "Enum": [ + "low", + "medium", + "high", + "severe" + ] + } + } + } + } + ], + "parameters": { + "Left": [ + "Int4Array", + "Text", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + true + ] + }, + "hash": "a06d0f682b1e576e634b02102ab6bcab9fcba55df818cab6e98721a324659526" +} diff --git a/apps/labrinth/.sqlx/query-e48e36c7535052093fa4f0d1759956b0bf816248c131c16954793f0eda09c1ec.json b/apps/labrinth/.sqlx/query-e48e36c7535052093fa4f0d1759956b0bf816248c131c16954793f0eda09c1ec.json deleted file mode 100644 index c39018099c..0000000000 --- a/apps/labrinth/.sqlx/query-e48e36c7535052093fa4f0d1759956b0bf816248c131c16954793f0eda09c1ec.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n dr.id AS \"report_id!: DelphiReportId\",\n m.id AS \"project_id!: DBProjectId\",\n t.id AS \"project_thread_id!: DBThreadId\",\n dr.created AS \"report_created!\",\n dri.id AS \"issue_id?: DelphiReportIssueId\",\n dri.issue_type AS \"issue_type?\",\n f.filename AS \"file_name?\",\n f.size AS \"file_size?\",\n drid.id AS \"issue_detail_id?: DelphiReportIssueDetailsId\",\n drid.internal_class_name AS \"issue_detail_class_name?\",\n drid.decompiled_source AS \"issue_detail_decompiled_source?\",\n drid.severity AS \"issue_detail_severity?: DelphiSeverity\"\n FROM delphi_reports dr\n\n -- fetch the project this report is for, its type, and thread\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n INNER JOIN categories c ON c.id = mc.joining_category_id\n INNER JOIN threads t ON t.mod_id = m.id\n\n -- fetch report issues and details\n LEFT JOIN delphi_report_issues dri ON dri.report_id = dr.id\n LEFT JOIN delphi_report_issue_details drid ON drid.issue_id = dri.id\n\n -- filtering\n WHERE\n -- project type\n (cardinality($1::int[]) = 0 OR c.project_type = ANY($1::int[]))\n\n -- sorting\n ORDER BY\n CASE WHEN $2 = 'created_asc' THEN created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $2 = 'created_desc' THEN created ELSE TO_TIMESTAMP(0) END DESC\n\n -- pagination\n LIMIT $3\n OFFSET $4\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "report_id!: DelphiReportId", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "project_id!: DBProjectId", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "project_thread_id!: DBThreadId", - "type_info": "Int8" - }, - { - "ordinal": 3, - "name": "report_created!", - "type_info": "Timestamptz" - }, - { - "ordinal": 4, - "name": "issue_id?: DelphiReportIssueId", - "type_info": "Int8" - }, - { - "ordinal": 5, - "name": "issue_type?", - "type_info": "Text" - }, - { - "ordinal": 6, - "name": "file_name?", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "file_size?", - "type_info": "Int4" - }, - { - "ordinal": 8, - "name": "issue_detail_id?: DelphiReportIssueDetailsId", - "type_info": "Int8" - }, - { - "ordinal": 9, - "name": "issue_detail_class_name?", - "type_info": "Text" - }, - { - "ordinal": 10, - "name": "issue_detail_decompiled_source?", - "type_info": "Text" - }, - { - "ordinal": 11, - "name": "issue_detail_severity?: DelphiSeverity", - "type_info": { - "Custom": { - "name": "delphi_severity", - "kind": { - "Enum": [ - "low", - "medium", - "high", - "severe" - ] - } - } - } - } - ], - "parameters": { - "Left": [ - "Int4Array", - "Text", - "Int8", - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - true, - true, - true, - true - ] - }, - "hash": "e48e36c7535052093fa4f0d1759956b0bf816248c131c16954793f0eda09c1ec" -} diff --git a/apps/labrinth/src/routes/internal/moderation/ownership.rs b/apps/labrinth/src/routes/internal/moderation/ownership.rs index 46fba8af1a..5979811cee 100644 --- a/apps/labrinth/src/routes/internal/moderation/ownership.rs +++ b/apps/labrinth/src/routes/internal/moderation/ownership.rs @@ -7,65 +7,6 @@ use ariadne::ids::UserId; use eyre::eyre; use sqlx::PgPool; -/// Fetches ownership information for a single project. -pub async fn get_project_ownership( - project_id: crate::models::ids::ProjectId, - team_id: crate::models::ids::TeamId, - organization: Option, - pool: &PgPool, - redis: &RedisPool, -) -> Result { - if let Some(org_id) = organization { - let org = DBOrganization::get(&org_id.to_string(), pool, redis) - .await - .wrap_internal_err("failed to fetch organization")? - .ok_or_else(|| { - crate::routes::ApiError::Internal( - eyre!("project {project_id} is owned by an invalid organization {org_id}") - ) - })?; - - Ok(Ownership::Organization { - id: OrganizationId::from(org.id), - name: org.name.clone(), - icon_url: org.icon_url, - }) - } else { - let team_members = DBTeamMember::get_from_team_full( - DBTeamId::from(team_id), - pool, - redis, - ) - .await - .wrap_internal_err("failed to fetch team members")?; - - let team_owner = team_members - .iter() - .find(|member| member.is_owner) - .ok_or_else(|| { - crate::routes::ApiError::Internal(eyre!( - "project {project_id} is owned by a team {team_id} which has no valid owner" - )) - })?; - - let user = DBUser::get(&team_owner.user_id.0.to_string(), pool, redis) - .await - .wrap_internal_err("failed to fetch user data")? - .ok_or_else(|| { - crate::routes::ApiError::Internal(eyre!( - "project {project_id} is owned by a team {team_id} which has owner {} which does not exist", - ariadne::ids::UserId::from(team_owner.user_id) - )) - })?; - - Ok(Ownership::User { - id: ariadne::ids::UserId::from(user.id), - name: user.username.clone(), - icon_url: user.avatar_url, - }) - } -} - /// Fetches ownership information for multiple projects efficiently pub async fn get_projects_ownership( projects: &[crate::models::projects::Project], diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 691aa33b35..286379d3c3 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -94,6 +94,8 @@ pub struct ProjectReport { pub created_at: DateTime, /// Why this project was flagged. pub flag_reason: FlagReason, + /// According to this report, how likely is the project malicious? + pub severity: DelphiSeverity, /// What files were flagged in this review. pub files: Vec, } @@ -141,8 +143,6 @@ pub struct FileIssue { /// Labrinth does not know the full set of kinds of issues, so this is kept /// as a string. pub kind: String, - /// How important is this issue, as flagged by Delphi? - pub severity: DelphiSeverity, /// Is this issue valid (malicious) or a false positive (safe)? pub status: DelphiReportIssueStatus, /// Details of why this issue might have been raised, such as what file it @@ -157,6 +157,8 @@ pub struct FileIssueDetail { pub class_name: String, /// Decompiled, pretty-printed source of the Java class. pub decompiled_source: String, + /// How important is this issue, as flagged by Delphi? + pub severity: DelphiSeverity, } /// Searches all projects which are awaiting technical review. @@ -177,6 +179,7 @@ async fn search_projects( #[derive(Debug)] struct ReportRecord { created: DateTime, + severity: DelphiSeverity, files: IndexMap, } @@ -227,6 +230,7 @@ async fn search_projects( m.id AS "project_id!: DBProjectId", t.id AS "project_thread_id!: DBThreadId", dr.created AS "report_created!", + dr.severity AS "report_severity!: DelphiSeverity", dri.id AS "issue_id!: DelphiReportIssueId", dri.issue_type AS "issue_type!", dri.status AS "issue_status!: DelphiReportIssueStatus", @@ -294,6 +298,7 @@ async fn search_projects( project.reports.entry(row.report_id).or_insert_with(|| { ReportRecord { created: row.report_created, + severity: row.report_severity, files: IndexMap::new(), } }); @@ -315,17 +320,25 @@ async fn search_projects( details: IndexMap::new(), }); - let (Some(issue_detail_id), Some(class_name), Some(decompiled_source)) = ( + let ( + Some(issue_detail_id), + Some(class_name), + Some(decompiled_source), + Some(severity), + ) = ( row.issue_detail_id, row.issue_detail_class_name, row.issue_detail_decompiled_source, - ) else { + row.issue_detail_severity, + ) + else { continue; }; issue.details.entry(issue_detail_id).or_insert_with(|| { FileIssueDetail { class_name, decompiled_source, + severity, } }); } @@ -382,6 +395,7 @@ async fn search_projects( .map(|(_, report_record)| ProjectReport { created_at: report_record.created, flag_reason: FlagReason::Delphi, + severity: report_record.severity, files: report_record .files .into_iter() @@ -404,6 +418,7 @@ async fn search_projects( .class_name, decompiled_source: detail .decompiled_source, + severity: detail.severity, } }) .collect(), From 06f34d9db06a938aa496b926ec5c208150aa8df7 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Thu, 13 Nov 2025 21:12:48 +0000 Subject: [PATCH 23/65] Delphi configuration to talk to Labrinth --- apps/labrinth/src/routes/internal/delphi.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/labrinth/src/routes/internal/delphi.rs b/apps/labrinth/src/routes/internal/delphi.rs index 6960dfbcd3..db9e8a034e 100644 --- a/apps/labrinth/src/routes/internal/delphi.rs +++ b/apps/labrinth/src/routes/internal/delphi.rs @@ -133,6 +133,8 @@ async fn ingest_report( redis: web::Data, web::Json(report): web::Json, ) -> Result { + tracing::error!("!! INGEST !!"); + if report.issues.is_empty() { info!("No issues found for file {}", report.url); return Ok(HttpResponse::NoContent().finish()); From e5d8d9afb6676c2beb3e3ea7e6e0abbcce450d6e Mon Sep 17 00:00:00 2001 From: aecsocket Date: Sat, 15 Nov 2025 23:09:25 +0000 Subject: [PATCH 24/65] Get Delphi working with Labrinth --- .../20250810155316_delphi-reports.sql | 3 +- .../src/database/models/delphi_report_item.rs | 10 ++-- apps/labrinth/src/routes/internal/delphi.rs | 47 ++++++++++++++----- .../routes/internal/moderation/tech_review.rs | 16 +++---- 4 files changed, 51 insertions(+), 25 deletions(-) diff --git a/apps/labrinth/migrations/20250810155316_delphi-reports.sql b/apps/labrinth/migrations/20250810155316_delphi-reports.sql index 7cbd19afa7..2aa7a75913 100644 --- a/apps/labrinth/migrations/20250810155316_delphi-reports.sql +++ b/apps/labrinth/migrations/20250810155316_delphi-reports.sql @@ -39,7 +39,8 @@ CREATE TABLE delphi_report_issue_details ( issue_id BIGINT NOT NULL REFERENCES delphi_report_issues (id) ON DELETE CASCADE ON UPDATE CASCADE, - internal_class_name TEXT NOT NULL, + key TEXT NOT NULL, + file_path TEXT NOT NULL, decompiled_source TEXT, data JSONB NOT NULL, severity DELPHI_SEVERITY NOT NULL diff --git a/apps/labrinth/src/database/models/delphi_report_item.rs b/apps/labrinth/src/database/models/delphi_report_item.rs index a2b4cfcb33..be8b698ad9 100644 --- a/apps/labrinth/src/database/models/delphi_report_item.rs +++ b/apps/labrinth/src/database/models/delphi_report_item.rs @@ -249,8 +249,9 @@ impl DBDelphiReportIssue { #[derive(Debug, Deserialize, Serialize)] pub struct DBDelphiReportIssueDetails { pub id: DelphiReportIssueDetailsId, + pub key: String, pub issue_id: DelphiReportIssueId, - pub internal_class_name: InternalJavaClassName, + pub file_path: String, pub decompiled_source: Option, pub data: Json>, pub severity: DelphiSeverity, @@ -263,12 +264,13 @@ impl DBDelphiReportIssueDetails { ) -> Result { Ok(DelphiReportIssueDetailsId(sqlx::query_scalar!( " - INSERT INTO delphi_report_issue_details (issue_id, internal_class_name, decompiled_source, data, severity) - VALUES ($1, $2, $3, $4, $5) + INSERT INTO delphi_report_issue_details (issue_id, key, file_path, decompiled_source, data, severity) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING id ", self.issue_id as DelphiReportIssueId, - self.internal_class_name.0, + self.key, + self.file_path, self.decompiled_source.as_ref().map(|decompiled_source| &decompiled_source.0), &self.data as &Json>, self.severity as DelphiSeverity, diff --git a/apps/labrinth/src/routes/internal/delphi.rs b/apps/labrinth/src/routes/internal/delphi.rs index db9e8a034e..44268c4486 100644 --- a/apps/labrinth/src/routes/internal/delphi.rs +++ b/apps/labrinth/src/routes/internal/delphi.rs @@ -6,7 +6,7 @@ use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT}; use serde::Deserialize; use sqlx::PgPool; use tokio::sync::Mutex; -use tracing::info; +use tracing::{info, warn}; use crate::{ auth::check_is_moderator_from_headers, @@ -18,7 +18,6 @@ use crate::{ DBDelphiReport, DBDelphiReportIssue, DBDelphiReportIssueDetails, DecompiledJavaClassSource, DelphiReportIssueStatus, DelphiReportListOrder, DelphiSeverity, - InternalJavaClassName, }, }, redis::RedisPool, @@ -61,7 +60,8 @@ static DELPHI_CLIENT: LazyLock = LazyLock::new(|| { #[derive(Deserialize)] struct DelphiReportIssueDetails { - pub internal_class_name: InternalJavaClassName, + pub file: String, + pub key: String, pub decompiled_source: Option, pub data: HashMap, pub severity: DelphiSeverity, @@ -94,14 +94,14 @@ impl DelphiReport { for (issue, trace) in &self.issues { for DelphiReportIssueDetails { - internal_class_name, + file, decompiled_source, .. } in trace { write!( &mut message_header, - "\n issue {issue} found at class `{internal_class_name}`:\n```\n{}\n```", + "\n issue {issue} found at class `{file}`:\n```\n{}\n```", decompiled_source.as_ref().map_or( "No decompiled source available", |decompiled_source| &**decompiled_source @@ -131,13 +131,21 @@ pub struct DelphiRunParameters { async fn ingest_report( pool: web::Data, redis: web::Data, - web::Json(report): web::Json, -) -> Result { - tracing::error!("!! INGEST !!"); + report: web::Bytes, + // web::Json(report): web::Json, +) -> Result<(), ApiError> { + info!( + "Json: {}", + serde_json::to_string_pretty( + &serde_json::from_slice::(&report).unwrap() + ) + .unwrap() + ); + let report = serde_json::from_slice::(&report).unwrap(); if report.issues.is_empty() { info!("No issues found for file {}", report.url); - return Ok(HttpResponse::NoContent().finish()); + return Ok(()); } report.send_to_slack(&pool, &redis).await.ok(); @@ -155,6 +163,12 @@ async fn ingest_report( .upsert(&mut transaction) .await?; + warn!( + "Delphi found {} issues in file {}", + report.issues.len(), + report.url + ); + for (issue_type, issue_details) in report.issues { let issue_id = DBDelphiReportIssue { id: DelphiReportIssueId(0), // This will be set by the database @@ -176,7 +190,8 @@ async fn ingest_report( DBDelphiReportIssueDetails { id: DelphiReportIssueDetailsId(0), // This will be set by the database issue_id, - internal_class_name: issue_detail.internal_class_name, + key: issue_detail.key, + file_path: issue_detail.file, decompiled_source: issue_detail.decompiled_source, data: issue_detail.data.into(), severity: issue_detail.severity, @@ -188,7 +203,7 @@ async fn ingest_report( transaction.commit().await?; - Ok(HttpResponse::NoContent().finish()) + Ok(()) } pub async fn run( @@ -216,10 +231,18 @@ pub async fn run( run_parameters.file_id.0 ); + // fix for local file paths + // TODO: should we fix this upstream in whatever inserts the files row? + let url = if file_data.url.starts_with("/") { + format!("file://{}", file_data.url) + } else { + file_data.url + }; + DELPHI_CLIENT .post(dotenvy::var("DELPHI_URL")?) .json(&serde_json::json!({ - "url": file_data.url, + "url": url, "project_id": ProjectId(file_data.project_id.0 as u64), "version_id": VersionId(file_data.version_id.0 as u64), "file_id": run_parameters.file_id, diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 286379d3c3..b87e925cb2 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -67,6 +67,7 @@ pub struct SearchProjectsFilter { Deserialize, utoipa::ToSchema, )] +#[serde(rename_all = "snake_case")] pub enum SearchProjectsSort { CreatedAsc, CreatedDesc, @@ -153,8 +154,8 @@ pub struct FileIssue { /// Occurrence of a [`FileIssue`] in a specific class in a scanned JAR file. #[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] pub struct FileIssueDetail { - /// Name of the Java class in which this issue was found. - pub class_name: String, + /// Name of the Java class path in which this issue was found. + pub file_path: String, /// Decompiled, pretty-printed source of the Java class. pub decompiled_source: String, /// How important is this issue, as flagged by Delphi? @@ -236,7 +237,7 @@ async fn search_projects( dri.status AS "issue_status!: DelphiReportIssueStatus", -- maybe null drid.id AS "issue_detail_id?: DelphiReportIssueDetailsId", - drid.internal_class_name AS "issue_detail_class_name?", + drid.file_path AS "issue_detail_file_path?", drid.decompiled_source AS "issue_detail_decompiled_source?", drid.severity AS "issue_detail_severity?: DelphiSeverity" FROM delphi_reports dr @@ -322,12 +323,12 @@ async fn search_projects( let ( Some(issue_detail_id), - Some(class_name), + Some(file_path), Some(decompiled_source), Some(severity), ) = ( row.issue_detail_id, - row.issue_detail_class_name, + row.issue_detail_file_path, row.issue_detail_decompiled_source, row.issue_detail_severity, ) @@ -336,7 +337,7 @@ async fn search_projects( }; issue.details.entry(issue_detail_id).or_insert_with(|| { FileIssueDetail { - class_name, + file_path, decompiled_source, severity, } @@ -414,8 +415,7 @@ async fn search_projects( .into_iter() .map(|(_, detail)| { FileIssueDetail { - class_name: detail - .class_name, + file_path: detail.file_path, decompiled_source: detail .decompiled_source, severity: detail.severity, From f1741ed0428920de09027ab4ddbd7a7855e955d6 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Sat, 15 Nov 2025 23:18:48 +0000 Subject: [PATCH 25/65] Add Delphi dummy fixture --- .../fixtures/delphi-report-2025-11-15.sql | 90 +++++++++++++++++++ .../src/database/models/delphi_report_item.rs | 9 +- 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 apps/labrinth/fixtures/delphi-report-2025-11-15.sql diff --git a/apps/labrinth/fixtures/delphi-report-2025-11-15.sql b/apps/labrinth/fixtures/delphi-report-2025-11-15.sql new file mode 100644 index 0000000000..389159672a --- /dev/null +++ b/apps/labrinth/fixtures/delphi-report-2025-11-15.sql @@ -0,0 +1,90 @@ +-- +-- PostgreSQL database dump +-- + +\restrict RGysBmMc8KFBQ9AssusGyNPozUiB43hdmIPxlv5KSWbX7tdW7XVMPpMginvod9K + +-- Dumped from database version 17.6 +-- Dumped by pg_dump version 17.6 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET transaction_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Data for Name: delphi_reports; Type: TABLE DATA; Schema: public; Owner: labrinth +-- + +COPY public.delphi_reports (id, file_id, delphi_version, artifact_url, created, severity) FROM stdin; +1 157529403422109 3 file:///tmp/modrinth/data/CaG4Mr66/versions/fMzLDsVA/sodium.jar 2025-11-15 23:01:30.012526+00 high +\. + + +-- +-- Data for Name: delphi_report_issues; Type: TABLE DATA; Schema: public; Owner: labrinth +-- + +COPY public.delphi_report_issues (id, report_id, issue_type, status) FROM stdin; +1 1 runtime_exec_usage pending +2 1 hardcoded_url pending +3 1 classloader_usage pending +4 1 obfuscated_names pending +5 1 main_method pending +\. + + +-- +-- Data for Name: delphi_report_issue_details; Type: TABLE DATA; Schema: public; Owner: labrinth +-- + +COPY public.delphi_report_issue_details (id, issue_id, key, file_path, decompiled_source, data, severity) FROM stdin; +1 1 d670186a0e5210fc2b9332a2163849740f19bec59a99d890bef0ae9e6608f83d net/caffeinemc/mods/sodium/desktop/utils/browse/XDGImpl package net.caffeinemc.mods.sodium.desktop.utils.browse;\n\nimport java.io.IOException;\nimport java.util.Locale;\n\nclass XDGImpl implements BrowseUrlHandler {\n public static boolean isSupported() {\n String os = System.getProperty("os.name").toLowerCase(Locale.ROOT);\n return os.equals("linux");\n }\n\n @Override\n public void browseTo(String url) throws IOException {\n Process process = Runtime.getRuntime().exec(new String[]{"xdg-open", url});\n\n try {\n int result = process.waitFor();\n if (result != 0) {\n throw new IOException("xdg-open exited with code: %d".formatted(result));\n }\n } catch (InterruptedException var4) {\n throw new RuntimeException(var4);\n }\n }\n}\n {} medium +2 1 317dd815f60f04f1cef5d855e30f6a2719570c583ef49ae94ca2b563179fc1fa net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterProbe package net.caffeinemc.mods.sodium.client.compatibility.environment.probe;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.stream.Stream;\nimport net.caffeinemc.mods.sodium.client.compatibility.environment.OsUtils;\nimport net.caffeinemc.mods.sodium.client.platform.windows.api.d3dkmt.D3DKMT;\nimport org.jetbrains.annotations.Nullable;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class GraphicsAdapterProbe {\n private static final Logger LOGGER = LoggerFactory.getLogger("Sodium-GraphicsAdapterProbe");\n private static final Set LINUX_PCI_CLASSES = Set.of("0x030000", "0x030001", "0x030200", "0x038000");\n private static List ADAPTERS = List.of();\n\n public static void findAdapters() {\n LOGGER.info("Searching for graphics cards...");\n\n List adapters;\n try {\n adapters = switch (OsUtils.getOs()) {\n case WIN -> findAdapters$Windows();\n case LINUX -> findAdapters$Linux();\n default -> null;\n };\n } catch (Exception var3) {\n LOGGER.error("Failed to find graphics adapters!", var3);\n return;\n }\n\n if (adapters != null) {\n if (adapters.isEmpty()) {\n LOGGER.warn(\n "Could not find any graphics adapters! Probably the device is not on a bus we can probe, or there are no devices supporting 3D acceleration."\n );\n } else {\n for (GraphicsAdapterInfo adapter : adapters) {\n LOGGER.info("Found graphics adapter: {}", adapter);\n }\n }\n\n ADAPTERS = adapters;\n }\n }\n\n private static List findAdapters$Windows() {\n return D3DKMT.findGraphicsAdapters();\n }\n\n private static List findAdapters$Linux() {\n ArrayList results = new ArrayList();\n\n try {\n Stream devices = Files.list(Path.of("/sys/bus/pci/devices/"));\n\n try {\n for (Path devicePath : devices::iterator) {\n String deviceClass = Files.readString(devicePath.resolve("class")).trim();\n if (LINUX_PCI_CLASSES.contains(deviceClass)) {\n String pciVendorId = Files.readString(devicePath.resolve("vendor")).trim();\n String pciDeviceId = Files.readString(devicePath.resolve("device")).trim();\n GraphicsAdapterVendor adapterVendor = GraphicsAdapterVendor.fromPciVendorId(pciVendorId);\n String adapterName = getPciDeviceName$Linux(pciVendorId, pciDeviceId);\n if (adapterName == null) {\n adapterName = "";\n }\n\n GraphicsAdapterInfo.LinuxPciAdapterInfo info = new GraphicsAdapterInfo.LinuxPciAdapterInfo(\n adapterVendor, adapterName, pciVendorId, pciDeviceId\n );\n results.add(info);\n }\n }\n } catch (Throwable var12) {\n if (devices != null) {\n try {\n devices.close();\n } catch (Throwable var11) {\n var12.addSuppressed(var11);\n }\n }\n\n throw var12;\n }\n\n if (devices != null) {\n devices.close();\n }\n } catch (IOException var13) {\n }\n\n return results;\n }\n\n @Nullable\n private static String getPciDeviceName$Linux(String vendorId, String deviceId) {\n String deviceFilter = vendorId.substring(2) + ":" + deviceId.substring(2);\n\n try {\n Process process = Runtime.getRuntime().exec(new String[]{"lspci", "-vmm", "-d", deviceFilter});\n int result = process.waitFor();\n if (result != 0) {\n throw new IOException("lspci exited with error code: %s".formatted(result));\n } else {\n BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));\n\n String var7;\n label40: {\n String line;\n try {\n while ((line = reader.readLine()) != null) {\n if (line.startsWith("Device:")) {\n var7 = line.substring("Device:".length()).trim();\n break label40;\n }\n }\n } catch (Throwable var9) {\n try {\n reader.close();\n } catch (Throwable var8) {\n var9.addSuppressed(var8);\n }\n\n throw var9;\n }\n\n reader.close();\n throw new IOException("lspci did not return a device name");\n }\n\n reader.close();\n return var7;\n }\n } catch (Throwable var10) {\n LOGGER.warn("Failed to query PCI device name for %s:%s".formatted(vendorId, deviceId), var10);\n return null;\n }\n }\n\n public static Collection getAdapters() {\n if (ADAPTERS == null) {\n LOGGER.error("Graphics adapters not probed yet; returning an empty list.");\n return Collections.emptyList();\n } else {\n return ADAPTERS;\n }\n }\n}\n {} medium +3 2 5ba58b7f9dcc59f14c8a0fd9b78c23a19723791bd9006ed408d43557ea24abb4 net/caffeinemc/mods/sodium/desktop/LaunchWarn package net.caffeinemc.mods.sodium.desktop;\n\nimport java.awt.GraphicsEnvironment;\nimport java.io.IOException;\nimport javax.swing.JDialog;\nimport javax.swing.JOptionPane;\nimport javax.swing.UIManager;\nimport javax.swing.UnsupportedLookAndFeelException;\nimport net.caffeinemc.mods.sodium.desktop.utils.browse.BrowseUrlHandler;\n\npublic class LaunchWarn {\n private static final String HELP_URL = "https://link.caffeinemc.net/guides/sodium/installation";\n private static final String RICH_MESSAGE = "

You have tried to launch Sodium (a Minecraft mod) directly, but it is not an executable program or mod installer. Instead, you must install Fabric Loader for Minecraft, and then place this file in your mods directory.

If this is your first time installing mods with Fabric Loader, then click the \\"Help\\" button for an installation guide.

";\n private static final String FALLBACK_MESSAGE = "

You have tried to launch Sodium (a Minecraft mod) directly, but it is not an executable program or mod installer. Instead, you must install Fabric Loader for Minecraft, and then place this file in your mods directory.

If this is your first time installing mods with Fabric Loader, then visit https://link.caffeinemc.net/guides/sodium/installation for an installation guide.

";\n private static final String FAILED_TO_BROWSE_MESSAGE = "

Failed to open the default browser! Your system may be misconfigured. Please open the URL https://link.caffeinemc.net/guides/sodium/installation manually.

";\n public static final String WINDOW_TITLE = "Sodium";\n\n public static void main(String[] args) {\n if (GraphicsEnvironment.isHeadless()) {\n showHeadlessError();\n } else {\n showGraphicalError();\n }\n }\n\n private static void showHeadlessError() {\n System.err\n .println(\n "

You have tried to launch Sodium (a Minecraft mod) directly, but it is not an executable program or mod installer. Instead, you must install Fabric Loader for Minecraft, and then place this file in your mods directory.

If this is your first time installing mods with Fabric Loader, then visit https://link.caffeinemc.net/guides/sodium/installation for an installation guide.

"\n );\n }\n\n private static void showGraphicalError() {\n trySetSystemLookAndFeel();\n trySetSystemFontPreferences();\n BrowseUrlHandler browseUrlHandler = BrowseUrlHandler.createImplementation();\n if (browseUrlHandler != null) {\n showRichGraphicalDialog(browseUrlHandler);\n } else {\n showFallbackGraphicalDialog();\n }\n\n System.exit(0);\n }\n\n private static void showRichGraphicalDialog(BrowseUrlHandler browseUrlHandler) {\n int selectedOption = showDialogBox(\n "

You have tried to launch Sodium (a Minecraft mod) directly, but it is not an executable program or mod installer. Instead, you must install Fabric Loader for Minecraft, and then place this file in your mods directory.

If this is your first time installing mods with Fabric Loader, then click the \\"Help\\" button for an installation guide.

",\n "Sodium",\n 0,\n 1,\n new String[]{"Help", "Close"},\n 0\n );\n if (selectedOption == 0) {\n log("Opening URL: https://link.caffeinemc.net/guides/sodium/installation");\n\n try {\n browseUrlHandler.browseTo("https://link.caffeinemc.net/guides/sodium/installation");\n } catch (IOException var3) {\n log("Failed to open default web browser!", var3);\n showDialogBox(\n "

Failed to open the default browser! Your system may be misconfigured. Please open the URL https://link.caffeinemc.net/guides/sodium/installation manually.

",\n "Sodium",\n -1,\n 2,\n null,\n -1\n );\n }\n }\n }\n\n private static void showFallbackGraphicalDialog() {\n showDialogBox(\n "

You have tried to launch Sodium (a Minecraft mod) directly, but it is not an executable program or mod installer. Instead, you must install Fabric Loader for Minecraft, and then place this file in your mods directory.

If this is your first time installing mods with Fabric Loader, then visit https://link.caffeinemc.net/guides/sodium/installation for an installation guide.

",\n "Sodium",\n -1,\n 1,\n null,\n null\n );\n }\n\n private static int showDialogBox(String message, String title, int optionType, int messageType, String[] options, Object initialValue) {\n JOptionPane pane = new JOptionPane(message, messageType, optionType, null, options, initialValue);\n JDialog dialog = pane.createDialog(title);\n dialog.setVisible(true);\n Object selectedValue = pane.getValue();\n if (selectedValue == null) {\n return -1;\n } else if (options == null) {\n return selectedValue instanceof Integer ? (Integer)selectedValue : -1;\n } else {\n for (int counter = 0; counter < options.length; counter++) {\n String option = options[counter];\n if (option.equals(selectedValue)) {\n return counter;\n }\n }\n\n return -1;\n }\n }\n\n private static void trySetSystemLookAndFeel() {\n try {\n UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());\n } catch (UnsupportedLookAndFeelException | ReflectiveOperationException var1) {\n }\n }\n\n private static void trySetSystemFontPreferences() {\n System.setProperty("awt.useSystemAAFontSettings", "on");\n }\n\n private static void log(String message) {\n System.err.println(message);\n }\n\n private static void log(String message, Throwable exception) {\n System.err.println(message);\n exception.printStackTrace(System.err);\n }\n}\n {"url": "https://link.caffeinemc.net/guides/sodium/installation"} low +4 2 34a4ceb119311f669d4b3b036dfef9f93c1e86f765582ebf556b92486766f861 net/caffeinemc/mods/sodium/client/gui/SodiumOptionsGUI package net.caffeinemc.mods.sodium.client.gui;\n\nimport com.google.common.collect.UnmodifiableIterator;\nimport java.io.IOException;\nimport java.time.Instant;\nimport java.time.temporal.ChronoUnit;\nimport java.util.ArrayList;\nimport java.util.EnumSet;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.stream.Stream;\nimport net.caffeinemc.mods.sodium.client.SodiumClientMod;\nimport net.caffeinemc.mods.sodium.client.console.Console;\nimport net.caffeinemc.mods.sodium.client.console.message.MessageLevel;\nimport net.caffeinemc.mods.sodium.client.data.fingerprint.HashedFingerprint;\nimport net.caffeinemc.mods.sodium.client.gui.options.Option;\nimport net.caffeinemc.mods.sodium.client.gui.options.OptionFlag;\nimport net.caffeinemc.mods.sodium.client.gui.options.OptionGroup;\nimport net.caffeinemc.mods.sodium.client.gui.options.OptionImpact;\nimport net.caffeinemc.mods.sodium.client.gui.options.OptionPage;\nimport net.caffeinemc.mods.sodium.client.gui.options.control.Control;\nimport net.caffeinemc.mods.sodium.client.gui.options.control.ControlElement;\nimport net.caffeinemc.mods.sodium.client.gui.options.storage.OptionStorage;\nimport net.caffeinemc.mods.sodium.client.gui.prompt.ScreenPrompt;\nimport net.caffeinemc.mods.sodium.client.gui.prompt.ScreenPromptable;\nimport net.caffeinemc.mods.sodium.client.gui.screen.ConfigCorruptedScreen;\nimport net.caffeinemc.mods.sodium.client.gui.widgets.AbstractWidget;\nimport net.caffeinemc.mods.sodium.client.gui.widgets.FlatButtonWidget;\nimport net.caffeinemc.mods.sodium.client.services.PlatformRuntimeInformation;\nimport net.caffeinemc.mods.sodium.client.util.Dim2i;\nimport net.minecraft.class_124;\nimport net.minecraft.class_156;\nimport net.minecraft.class_2561;\nimport net.minecraft.class_310;\nimport net.minecraft.class_332;\nimport net.minecraft.class_364;\nimport net.minecraft.class_437;\nimport net.minecraft.class_446;\nimport net.minecraft.class_5250;\nimport net.minecraft.class_5348;\nimport net.minecraft.class_5481;\nimport org.jetbrains.annotations.Nullable;\n\npublic class SodiumOptionsGUI extends class_437 implements ScreenPromptable {\n private final List pages = new ArrayList();\n private final List> controls = new ArrayList();\n private final class_437 prevScreen;\n private OptionPage currentPage;\n private FlatButtonWidget applyButton;\n private FlatButtonWidget closeButton;\n private FlatButtonWidget undoButton;\n private FlatButtonWidget donateButton;\n private FlatButtonWidget hideDonateButton;\n private boolean hasPendingChanges;\n private ControlElement hoveredElement;\n @Nullable\n private ScreenPrompt prompt;\n private static final List DONATION_PROMPT_MESSAGE = List.of(\n class_5348.method_29433(new class_5348[]{class_2561.method_43470("Hello!")}),\n class_5348.method_29433(\n new class_5348[]{\n class_2561.method_43470("It seems that you've been enjoying "),\n class_2561.method_43470("Sodium").method_54663(2616210),\n class_2561.method_43470(", the powerful and open rendering optimization mod for Minecraft.")\n }\n ),\n class_5348.method_29433(\n new class_5348[]{\n class_2561.method_43470("Mods like these are complex. They require "),\n class_2561.method_43470("thousands of hours").method_54663(16739840),\n class_2561.method_43470(" of development, debugging, and tuning to create the experience that players have come to expect.")\n }\n ),\n class_5348.method_29433(\n new class_5348[]{\n class_2561.method_43470("If you'd like to show your token of appreciation, and support the development of our mod in the process, then consider "),\n class_2561.method_43470("buying us a coffee").method_54663(15550926),\n class_2561.method_43470(".")\n }\n ),\n class_5348.method_29433(new class_5348[]{class_2561.method_43470("And thanks again for using our mod! We hope it helps you (and your computer.)")})\n );\n\n private SodiumOptionsGUI(class_437 prevScreen) {\n super(class_2561.method_43470("Sodium Renderer Settings"));\n this.prevScreen = prevScreen;\n this.pages.add(SodiumGameOptionPages.general());\n this.pages.add(SodiumGameOptionPages.quality());\n this.pages.add(SodiumGameOptionPages.performance());\n this.pages.add(SodiumGameOptionPages.advanced());\n this.checkPromptTimers();\n }\n\n private void checkPromptTimers() {\n if (!PlatformRuntimeInformation.getInstance().isDevelopmentEnvironment()) {\n SodiumGameOptions options = SodiumClientMod.options();\n if (!options.notifications.hasSeenDonationPrompt) {\n HashedFingerprint fingerprint = null;\n\n try {\n fingerprint = HashedFingerprint.loadFromDisk();\n } catch (Throwable var5) {\n SodiumClientMod.logger().error("Failed to read the fingerprint from disk", var5);\n }\n\n if (fingerprint != null) {\n Instant now = Instant.now();\n Instant threshold = Instant.ofEpochSecond(fingerprint.timestamp()).plus(3L, ChronoUnit.DAYS);\n if (now.isAfter(threshold)) {\n this.openDonationPrompt(options);\n }\n }\n }\n }\n }\n\n private void openDonationPrompt(SodiumGameOptions options) {\n ScreenPrompt prompt = new ScreenPrompt(\n this, DONATION_PROMPT_MESSAGE, 320, 190, new ScreenPrompt.Action(class_2561.method_43470("Buy us a coffee"), this::openDonationPage)\n );\n prompt.method_25365(true);\n options.notifications.hasSeenDonationPrompt = true;\n\n try {\n SodiumGameOptions.writeToDisk(options);\n } catch (IOException var4) {\n SodiumClientMod.logger().error("Failed to update config file", var4);\n }\n }\n\n public static class_437 createScreen(class_437 currentScreen) {\n return (class_437)(SodiumClientMod.options().isReadOnly()\n ? new ConfigCorruptedScreen(currentScreen, SodiumOptionsGUI::new)\n : new SodiumOptionsGUI(currentScreen));\n }\n\n public void setPage(OptionPage page) {\n this.currentPage = page;\n this.rebuildGUI();\n }\n\n protected void method_25426() {\n super.method_25426();\n this.rebuildGUI();\n if (this.prompt != null) {\n this.prompt.init();\n }\n }\n\n private void rebuildGUI() {\n this.controls.clear();\n this.method_37067();\n if (this.currentPage == null) {\n if (this.pages.isEmpty()) {\n throw new IllegalStateException("No pages are available?!");\n }\n\n this.currentPage = (OptionPage)this.pages.get(0);\n }\n\n this.rebuildGUIPages();\n this.rebuildGUIOptions();\n this.undoButton = new FlatButtonWidget(\n new Dim2i(this.field_22789 - 211, this.field_22790 - 30, 65, 20), class_2561.method_43471("sodium.options.buttons.undo"), this::undoChanges\n );\n this.applyButton = new FlatButtonWidget(\n new Dim2i(this.field_22789 - 142, this.field_22790 - 30, 65, 20), class_2561.method_43471("sodium.options.buttons.apply"), this::applyChanges\n );\n this.closeButton = new FlatButtonWidget(\n new Dim2i(this.field_22789 - 73, this.field_22790 - 30, 65, 20), class_2561.method_43471("gui.done"), this::method_25419\n );\n this.donateButton = new FlatButtonWidget(\n new Dim2i(this.field_22789 - 128, 6, 100, 20), class_2561.method_43471("sodium.options.buttons.donate"), this::openDonationPage\n );\n this.hideDonateButton = new FlatButtonWidget(new Dim2i(this.field_22789 - 26, 6, 20, 20), class_2561.method_43470("x"), this::hideDonationButton);\n if (SodiumClientMod.options().notifications.hasClearedDonationButton) {\n this.setDonationButtonVisibility(false);\n }\n\n this.method_37063(this.undoButton);\n this.method_37063(this.applyButton);\n this.method_37063(this.closeButton);\n this.method_37063(this.donateButton);\n this.method_37063(this.hideDonateButton);\n }\n\n private void setDonationButtonVisibility(boolean value) {\n this.donateButton.setVisible(value);\n this.hideDonateButton.setVisible(value);\n }\n\n private void hideDonationButton() {\n SodiumGameOptions options = SodiumClientMod.options();\n options.notifications.hasClearedDonationButton = true;\n\n try {\n SodiumGameOptions.writeToDisk(options);\n } catch (IOException var3) {\n throw new RuntimeException("Failed to save configuration", var3);\n }\n\n this.setDonationButtonVisibility(false);\n }\n\n private void rebuildGUIPages() {\n int x = 6;\n int y = 6;\n\n for (OptionPage page : this.pages) {\n int width = 12 + this.field_22793.method_27525(page.getName());\n FlatButtonWidget button = new FlatButtonWidget(new Dim2i(x, y, width, 18), page.getName(), () -> this.setPage(page));\n button.setSelected(this.currentPage == page);\n x += width + 6;\n this.method_37063(button);\n }\n }\n\n private void rebuildGUIOptions() {\n int x = 6;\n int y = 28;\n\n for (UnmodifiableIterator var3 = this.currentPage.getGroups().iterator(); var3.hasNext(); y += 4) {\n OptionGroup group = (OptionGroup)var3.next();\n\n for (UnmodifiableIterator var5 = group.getOptions().iterator(); var5.hasNext(); y += 18) {\n Option option = (Option)var5.next();\n Control control = option.getControl();\n ControlElement element = control.createElement(new Dim2i(x, y, 240, 18));\n this.method_37063(element);\n this.controls.add(element);\n }\n }\n }\n\n public void method_25394(class_332 graphics, int mouseX, int mouseY, float delta) {\n this.updateControls();\n super.method_25394(graphics, this.prompt != null ? -1 : mouseX, this.prompt != null ? -1 : mouseY, delta);\n if (this.hoveredElement != null) {\n this.renderOptionTooltip(graphics, this.hoveredElement);\n }\n\n if (this.prompt != null) {\n this.prompt.method_25394(graphics, mouseX, mouseY, delta);\n }\n }\n\n private void updateControls() {\n ControlElement hovered = (ControlElement)this.getActiveControls()\n .filter(AbstractWidget::isHovered)\n .findFirst()\n .orElse((ControlElement)this.getActiveControls().filter(AbstractWidget::method_25370).findFirst().orElse(null));\n boolean hasChanges = this.getAllOptions().anyMatch(Option::hasChanged);\n\n for (OptionPage page : this.pages) {\n UnmodifiableIterator var5 = page.getOptions().iterator();\n\n while (var5.hasNext()) {\n Option option = (Option)var5.next();\n if (option.hasChanged()) {\n hasChanges = true;\n }\n }\n }\n\n this.applyButton.setEnabled(hasChanges);\n this.undoButton.setVisible(hasChanges);\n this.closeButton.setEnabled(!hasChanges);\n this.hasPendingChanges = hasChanges;\n this.hoveredElement = hovered;\n }\n\n private Stream> getAllOptions() {\n return this.pages.stream().flatMap(s -> s.getOptions().stream());\n }\n\n private Stream> getActiveControls() {\n return this.controls.stream();\n }\n\n private void renderOptionTooltip(class_332 graphics, ControlElement element) {\n Dim2i dim = element.getDimensions();\n int textPadding = 3;\n int boxPadding = 3;\n int boxY = dim.y();\n int boxX = dim.getLimitX() + boxPadding;\n int boxWidth = Math.min(200, this.field_22789 - boxX - boxPadding);\n Option option = element.getOption();\n int splitWidth = boxWidth - textPadding * 2;\n List tooltip = new ArrayList(this.field_22793.method_1728(option.getTooltip(), splitWidth));\n OptionImpact impact = option.getImpact();\n if (impact != null) {\n class_5250 impactText = class_2561.method_43469("sodium.options.performance_impact_string", new Object[]{impact.getLocalizedName()});\n tooltip.addAll(this.field_22793.method_1728(impactText.method_27692(class_124.field_1080), splitWidth));\n }\n\n int boxHeight = tooltip.size() * 12 + boxPadding;\n int boxYLimit = boxY + boxHeight;\n int boxYCutoff = this.field_22790 - 40;\n if (boxYLimit > boxYCutoff) {\n boxY -= boxYLimit - boxYCutoff;\n }\n\n graphics.method_25296(boxX, boxY, boxX + boxWidth, boxY + boxHeight, -536870912, -536870912);\n\n for (int i = 0; i < tooltip.size(); i++) {\n graphics.method_35720(this.field_22793, (class_5481)tooltip.get(i), boxX + textPadding, boxY + textPadding + i * 12, -1);\n }\n }\n\n private void applyChanges() {\n HashSet> dirtyStorages = new HashSet();\n EnumSet flags = EnumSet.noneOf(OptionFlag.class);\n this.getAllOptions().forEach(option -> {\n if (option.hasChanged()) {\n option.applyChanges();\n flags.addAll(option.getFlags());\n dirtyStorages.add(option.getStorage());\n }\n });\n class_310 client = class_310.method_1551();\n if (client.field_1687 != null) {\n if (flags.contains(OptionFlag.REQUIRES_RENDERER_RELOAD)) {\n client.field_1769.method_3279();\n } else if (flags.contains(OptionFlag.REQUIRES_RENDERER_UPDATE)) {\n client.field_1769.method_3292();\n }\n }\n\n if (flags.contains(OptionFlag.REQUIRES_ASSET_RELOAD)) {\n client.method_24041((Integer)client.field_1690.method_42563().method_41753());\n client.method_1513();\n }\n\n if (flags.contains(OptionFlag.REQUIRES_VIDEOMODE_RELOAD)) {\n client.method_22683().method_4475();\n }\n\n if (flags.contains(OptionFlag.REQUIRES_GAME_RESTART)) {\n Console.instance().logMessage(MessageLevel.WARN, "sodium.console.game_restart", true, 10.0);\n }\n\n for (OptionStorage storage : dirtyStorages) {\n storage.save();\n }\n }\n\n private void undoChanges() {\n this.getAllOptions().forEach(Option::reset);\n }\n\n private void openDonationPage() {\n class_156.method_668().method_670("https://caffeinemc.net/donate");\n }\n\n public boolean method_25404(int keyCode, int scanCode, int modifiers) {\n if (this.prompt != null && this.prompt.method_25404(keyCode, scanCode, modifiers)) {\n return true;\n } else if (this.prompt == null && keyCode == 80 && (modifiers & 1) != 0) {\n class_310.method_1551().method_1507(new class_446(this.prevScreen, class_310.method_1551(), class_310.method_1551().field_1690));\n return true;\n } else {\n return super.method_25404(keyCode, scanCode, modifiers);\n }\n }\n\n public boolean method_25402(double mouseX, double mouseY, int button) {\n if (this.prompt != null) {\n return this.prompt.method_25402(mouseX, mouseY, button);\n } else {\n boolean clicked = super.method_25402(mouseX, mouseY, button);\n if (!clicked) {\n this.method_25395(null);\n return true;\n } else {\n return clicked;\n }\n }\n }\n\n public boolean method_25422() {\n return !this.hasPendingChanges;\n }\n\n public void method_25419() {\n this.field_22787.method_1507(this.prevScreen);\n }\n\n public List method_25396() {\n return this.prompt == null ? super.method_25396() : this.prompt.getWidgets();\n }\n\n @Override\n public void setPrompt(@Nullable ScreenPrompt prompt) {\n this.prompt = prompt;\n }\n\n @Nullable\n @Override\n public ScreenPrompt getPrompt() {\n return this.prompt;\n }\n\n @Override\n public Dim2i getDimensions() {\n return new Dim2i(0, 0, this.field_22789, this.field_22790);\n }\n}\n {"url": "https://caffeinemc.net/donate"} low +5 2 2048cee1aed753e10480183673ffb5e685de2ce414e99e93f7d1dd11a87a19af net/caffeinemc/mods/sodium/client/compatibility/checks/PreLaunchChecks package net.caffeinemc.mods.sodium.client.compatibility.checks;\n\nimport net.caffeinemc.mods.sodium.client.platform.PlatformHelper;\nimport org.lwjgl.Version;\n\npublic class PreLaunchChecks {\n private static final String REQUIRED_LWJGL_VERSION = "3.3.3";\n\n public static void checkEnvironment() {\n if (BugChecks.ISSUE_2561) {\n checkLwjglRuntimeVersion();\n }\n }\n\n private static void checkLwjglRuntimeVersion() {\n if (!isUsingKnownCompatibleLwjglVersion()) {\n String advice;\n if (isUsingPrismLauncher()) {\n advice = "It appears you are using Prism Launcher to start the game. You can likely fix this problem by opening your instance settings and navigating to the Versionsection in the sidebar.";\n } else {\n advice = "You must change the LWJGL version in your launcher to continue. This is usually controlled by the settings for a profile or instance in your launcher.";\n }\n\n String message = "The game failed to start because the currently active LWJGL version is not compatible.\\n\\nInstalled version: ###CURRENT_VERSION###\\nRequired version: ###REQUIRED_VERSION###\\n\\n###ADVICE_STRING###"\n .replace("###CURRENT_VERSION###", Version.getVersion())\n .replace("###REQUIRED_VERSION###", "3.3.3")\n .replace("###ADVICE_STRING###", advice);\n PlatformHelper.showCriticalErrorAndClose(\n null, "Sodium Renderer - Unsupported LWJGL", message, "https://link.caffeinemc.net/help/sodium/runtime-issue/lwjgl3/gh-2561"\n );\n }\n }\n\n private static boolean isUsingKnownCompatibleLwjglVersion() {\n return Version.getVersion().startsWith("3.3.3");\n }\n\n private static boolean isUsingPrismLauncher() {\n return getLauncherBrand().equalsIgnoreCase("PrismLauncher");\n }\n\n private static String getLauncherBrand() {\n return System.getProperty("minecraft.launcher.brand", "unknown");\n }\n}\n {"url": "https://link.caffeinemc.net/help/sodium/runtime-issue/lwjgl3/gh-2561"} low +6 2 0b9d53bc482f11c0d8c71a9689645132f0a50249838091b3e6f95fefbc279075 net/caffeinemc/mods/sodium/client/compatibility/checks/ModuleScanner package net.caffeinemc.mods.sodium.client.compatibility.checks;\n\nimport com.sun.jna.Platform;\nimport com.sun.jna.platform.win32.Kernel32;\nimport com.sun.jna.platform.win32.Kernel32Util;\nimport com.sun.jna.platform.win32.Tlhelp32.MODULEENTRY32W;\nimport java.nio.file.Files;\nimport java.nio.file.LinkOption;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport net.caffeinemc.mods.sodium.client.platform.MessageBox;\nimport net.caffeinemc.mods.sodium.client.platform.NativeWindowHandle;\nimport net.caffeinemc.mods.sodium.client.platform.windows.WindowsFileVersion;\nimport net.caffeinemc.mods.sodium.client.platform.windows.api.version.Version;\nimport net.caffeinemc.mods.sodium.client.platform.windows.api.version.VersionFixedFileInfoStruct;\nimport net.caffeinemc.mods.sodium.client.platform.windows.api.version.VersionInfo;\nimport org.jetbrains.annotations.Nullable;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class ModuleScanner {\n private static final Logger LOGGER = LoggerFactory.getLogger("Sodium-Win32ModuleChecks");\n private static final String[] RTSS_HOOKS_MODULE_NAMES = new String[]{"RTSSHooks64.dll", "RTSSHooks.dll"};\n private static final String[] ASUS_GPU_TWEAK_MODULE_NAMES = new String[]{\n "GTIII-OSD64-GL.dll", "GTIII-OSD-GL.dll", "GTIII-OSD64-VK.dll", "GTIII-OSD-VK.dll", "GTIII-OSD64.dll", "GTIII-OSD.dll"\n };\n\n public static void checkModules(NativeWindowHandle window) {\n List modules;\n try {\n modules = listModules();\n } catch (Throwable var3) {\n LOGGER.warn("Failed to scan the currently loaded modules", var3);\n return;\n }\n\n if (!modules.isEmpty()) {\n if (BugChecks.ISSUE_2048 && isModuleLoaded(modules, RTSS_HOOKS_MODULE_NAMES)) {\n checkRTSSModules(window);\n }\n\n if (BugChecks.ISSUE_2637 && isModuleLoaded(modules, ASUS_GPU_TWEAK_MODULE_NAMES)) {\n checkASUSGpuTweakIII(window);\n }\n }\n }\n\n private static List listModules() {\n if (!Platform.isWindows()) {\n return List.of();\n } else {\n int pid = Kernel32.INSTANCE.GetCurrentProcessId();\n ArrayList modules = new ArrayList();\n\n for (MODULEENTRY32W module : Kernel32Util.getModules(pid)) {\n modules.add(module.szModule());\n }\n\n return Collections.unmodifiableList(modules);\n }\n }\n\n private static void checkRTSSModules(NativeWindowHandle window) {\n LOGGER.warn("RivaTuner Statistics Server (RTSS) has injected into the process! Attempting to apply workarounds for compatibility...");\n WindowsFileVersion version = null;\n\n try {\n version = findRTSSModuleVersion();\n } catch (Throwable var3) {\n LOGGER.warn("Exception thrown while reading file version", var3);\n }\n\n if (version == null) {\n LOGGER.warn("Could not determine version of RivaTuner Statistics Server");\n } else {\n LOGGER.info("Detected RivaTuner Statistics Server version: {}", version);\n }\n\n if (version == null || !isRTSSCompatible(version)) {\n MessageBox.showMessageBox(\n window,\n MessageBox.IconType.ERROR,\n "Sodium Renderer",\n "You appear to be using an older version of RivaTuner Statistics Server (RTSS) which is not compatible with Sodium.\\n\\nYou must either update to a newer version (7.3.4 and later) or close the RivaTuner Statistics Server application.\\n\\nFor more information on how to solve this problem, click the 'Help' button.",\n "https://link.caffeinemc.net/help/sodium/incompatible-software/rivatuner-statistics-server/gh-2048"\n );\n throw new RuntimeException(\n "The installed version of RivaTuner Statistics Server (RTSS) is not compatible with Sodium, see here for more details: https://link.caffeinemc.net/help/sodium/incompatible-software/rivatuner-statistics-server/gh-2048"\n );\n }\n }\n\n private static boolean isRTSSCompatible(WindowsFileVersion version) {\n int x = version.x();\n int y = version.y();\n int z = version.z();\n return x > 7 || x == 7 && y > 3 || x == 7 && y == 3 && z >= 4;\n }\n\n private static void checkASUSGpuTweakIII(NativeWindowHandle window) {\n MessageBox.showMessageBox(\n window,\n MessageBox.IconType.ERROR,\n "Sodium Renderer",\n "ASUS GPU Tweak III is not compatible with Minecraft, and causes extreme performance issues and severe graphical corruption when used with Minecraft.\\n\\nYou *must* do one of the following things to continue:\\n\\na) Open the settings of ASUS GPU Tweak III, enable the Blacklist option, click \\"Browse from file...\\", and select the Java runtime (javaw.exe) which is used by Minecraft.\\n\\nb) Completely uninstall the ASUS GPU Tweak III application.\\n\\nFor more information on how to solve this problem, click the 'Help' button.",\n "https://link.caffeinemc.net/help/sodium/incompatible-software/asus-gtiii/gh-2637"\n );\n throw new RuntimeException(\n "ASUS GPU Tweak III is not compatible with Minecraft, see here for more details: https://link.caffeinemc.net/help/sodium/incompatible-software/asus-gtiii/gh-2637"\n );\n }\n\n @Nullable\n private static WindowsFileVersion findRTSSModuleVersion() {\n long module;\n try {\n module = net.caffeinemc.mods.sodium.client.platform.windows.api.Kernel32.getModuleHandleByNames(RTSS_HOOKS_MODULE_NAMES);\n } catch (Throwable var9) {\n LOGGER.warn("Failed to locate module", var9);\n return null;\n }\n\n String moduleFileName;\n try {\n moduleFileName = net.caffeinemc.mods.sodium.client.platform.windows.api.Kernel32.getModuleFileName(module);\n } catch (Throwable var8) {\n LOGGER.warn("Failed to get path of module", var8);\n return null;\n }\n\n Path modulePath = Path.of(moduleFileName);\n Path moduleDirectory = modulePath.getParent();\n LOGGER.info("Searching directory: {}", moduleDirectory);\n Path executablePath = moduleDirectory.resolve("RTSS.exe");\n if (!Files.exists(executablePath, new LinkOption[0])) {\n LOGGER.warn("Could not find executable: {}", executablePath);\n return null;\n } else {\n LOGGER.info("Parsing file: {}", executablePath);\n VersionInfo version = Version.getModuleFileVersion(executablePath.toAbsolutePath().toString());\n if (version == null) {\n LOGGER.warn("Couldn't find version structure");\n return null;\n } else {\n VersionFixedFileInfoStruct fileVersion = version.queryFixedFileInfo();\n if (fileVersion == null) {\n LOGGER.warn("Couldn't query file version");\n return null;\n } else {\n return WindowsFileVersion.fromFileVersion(fileVersion);\n }\n }\n }\n }\n\n private static boolean isModuleLoaded(List modules, String[] names) {\n for (String name : names) {\n for (String module : modules) {\n if (module.equalsIgnoreCase(name)) {\n return true;\n }\n }\n }\n\n return false;\n }\n}\n {"url": "https://link.caffeinemc.net/help/sodium/incompatible-software/asus-gtiii/gh-2637"} low +7 2 c7ef03b142e9371c5b10fd54ee7a22060872bd3addb96c6fa21ab10ccdc4b481 net/caffeinemc/mods/sodium/client/compatibility/checks/ModuleScanner package net.caffeinemc.mods.sodium.client.compatibility.checks;\n\nimport com.sun.jna.Platform;\nimport com.sun.jna.platform.win32.Kernel32;\nimport com.sun.jna.platform.win32.Kernel32Util;\nimport com.sun.jna.platform.win32.Tlhelp32.MODULEENTRY32W;\nimport java.nio.file.Files;\nimport java.nio.file.LinkOption;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport net.caffeinemc.mods.sodium.client.platform.MessageBox;\nimport net.caffeinemc.mods.sodium.client.platform.NativeWindowHandle;\nimport net.caffeinemc.mods.sodium.client.platform.windows.WindowsFileVersion;\nimport net.caffeinemc.mods.sodium.client.platform.windows.api.version.Version;\nimport net.caffeinemc.mods.sodium.client.platform.windows.api.version.VersionFixedFileInfoStruct;\nimport net.caffeinemc.mods.sodium.client.platform.windows.api.version.VersionInfo;\nimport org.jetbrains.annotations.Nullable;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class ModuleScanner {\n private static final Logger LOGGER = LoggerFactory.getLogger("Sodium-Win32ModuleChecks");\n private static final String[] RTSS_HOOKS_MODULE_NAMES = new String[]{"RTSSHooks64.dll", "RTSSHooks.dll"};\n private static final String[] ASUS_GPU_TWEAK_MODULE_NAMES = new String[]{\n "GTIII-OSD64-GL.dll", "GTIII-OSD-GL.dll", "GTIII-OSD64-VK.dll", "GTIII-OSD-VK.dll", "GTIII-OSD64.dll", "GTIII-OSD.dll"\n };\n\n public static void checkModules(NativeWindowHandle window) {\n List modules;\n try {\n modules = listModules();\n } catch (Throwable var3) {\n LOGGER.warn("Failed to scan the currently loaded modules", var3);\n return;\n }\n\n if (!modules.isEmpty()) {\n if (BugChecks.ISSUE_2048 && isModuleLoaded(modules, RTSS_HOOKS_MODULE_NAMES)) {\n checkRTSSModules(window);\n }\n\n if (BugChecks.ISSUE_2637 && isModuleLoaded(modules, ASUS_GPU_TWEAK_MODULE_NAMES)) {\n checkASUSGpuTweakIII(window);\n }\n }\n }\n\n private static List listModules() {\n if (!Platform.isWindows()) {\n return List.of();\n } else {\n int pid = Kernel32.INSTANCE.GetCurrentProcessId();\n ArrayList modules = new ArrayList();\n\n for (MODULEENTRY32W module : Kernel32Util.getModules(pid)) {\n modules.add(module.szModule());\n }\n\n return Collections.unmodifiableList(modules);\n }\n }\n\n private static void checkRTSSModules(NativeWindowHandle window) {\n LOGGER.warn("RivaTuner Statistics Server (RTSS) has injected into the process! Attempting to apply workarounds for compatibility...");\n WindowsFileVersion version = null;\n\n try {\n version = findRTSSModuleVersion();\n } catch (Throwable var3) {\n LOGGER.warn("Exception thrown while reading file version", var3);\n }\n\n if (version == null) {\n LOGGER.warn("Could not determine version of RivaTuner Statistics Server");\n } else {\n LOGGER.info("Detected RivaTuner Statistics Server version: {}", version);\n }\n\n if (version == null || !isRTSSCompatible(version)) {\n MessageBox.showMessageBox(\n window,\n MessageBox.IconType.ERROR,\n "Sodium Renderer",\n "You appear to be using an older version of RivaTuner Statistics Server (RTSS) which is not compatible with Sodium.\\n\\nYou must either update to a newer version (7.3.4 and later) or close the RivaTuner Statistics Server application.\\n\\nFor more information on how to solve this problem, click the 'Help' button.",\n "https://link.caffeinemc.net/help/sodium/incompatible-software/rivatuner-statistics-server/gh-2048"\n );\n throw new RuntimeException(\n "The installed version of RivaTuner Statistics Server (RTSS) is not compatible with Sodium, see here for more details: https://link.caffeinemc.net/help/sodium/incompatible-software/rivatuner-statistics-server/gh-2048"\n );\n }\n }\n\n private static boolean isRTSSCompatible(WindowsFileVersion version) {\n int x = version.x();\n int y = version.y();\n int z = version.z();\n return x > 7 || x == 7 && y > 3 || x == 7 && y == 3 && z >= 4;\n }\n\n private static void checkASUSGpuTweakIII(NativeWindowHandle window) {\n MessageBox.showMessageBox(\n window,\n MessageBox.IconType.ERROR,\n "Sodium Renderer",\n "ASUS GPU Tweak III is not compatible with Minecraft, and causes extreme performance issues and severe graphical corruption when used with Minecraft.\\n\\nYou *must* do one of the following things to continue:\\n\\na) Open the settings of ASUS GPU Tweak III, enable the Blacklist option, click \\"Browse from file...\\", and select the Java runtime (javaw.exe) which is used by Minecraft.\\n\\nb) Completely uninstall the ASUS GPU Tweak III application.\\n\\nFor more information on how to solve this problem, click the 'Help' button.",\n "https://link.caffeinemc.net/help/sodium/incompatible-software/asus-gtiii/gh-2637"\n );\n throw new RuntimeException(\n "ASUS GPU Tweak III is not compatible with Minecraft, see here for more details: https://link.caffeinemc.net/help/sodium/incompatible-software/asus-gtiii/gh-2637"\n );\n }\n\n @Nullable\n private static WindowsFileVersion findRTSSModuleVersion() {\n long module;\n try {\n module = net.caffeinemc.mods.sodium.client.platform.windows.api.Kernel32.getModuleHandleByNames(RTSS_HOOKS_MODULE_NAMES);\n } catch (Throwable var9) {\n LOGGER.warn("Failed to locate module", var9);\n return null;\n }\n\n String moduleFileName;\n try {\n moduleFileName = net.caffeinemc.mods.sodium.client.platform.windows.api.Kernel32.getModuleFileName(module);\n } catch (Throwable var8) {\n LOGGER.warn("Failed to get path of module", var8);\n return null;\n }\n\n Path modulePath = Path.of(moduleFileName);\n Path moduleDirectory = modulePath.getParent();\n LOGGER.info("Searching directory: {}", moduleDirectory);\n Path executablePath = moduleDirectory.resolve("RTSS.exe");\n if (!Files.exists(executablePath, new LinkOption[0])) {\n LOGGER.warn("Could not find executable: {}", executablePath);\n return null;\n } else {\n LOGGER.info("Parsing file: {}", executablePath);\n VersionInfo version = Version.getModuleFileVersion(executablePath.toAbsolutePath().toString());\n if (version == null) {\n LOGGER.warn("Couldn't find version structure");\n return null;\n } else {\n VersionFixedFileInfoStruct fileVersion = version.queryFixedFileInfo();\n if (fileVersion == null) {\n LOGGER.warn("Couldn't query file version");\n return null;\n } else {\n return WindowsFileVersion.fromFileVersion(fileVersion);\n }\n }\n }\n }\n\n private static boolean isModuleLoaded(List modules, String[] names) {\n for (String name : names) {\n for (String module : modules) {\n if (module.equalsIgnoreCase(name)) {\n return true;\n }\n }\n }\n\n return false;\n }\n}\n {"url": "https://link.caffeinemc.net/help/sodium/incompatible-software/rivatuner-statistics-server/gh-2048"} low +8 2 54aa6d079497c9fc459f84c660a303496d18fa17c35c0e22cbe2160924de212e net/caffeinemc/mods/sodium/client/compatibility/checks/GraphicsDriverChecks package net.caffeinemc.mods.sodium.client.compatibility.checks;\n\nimport net.caffeinemc.mods.sodium.client.compatibility.environment.GlContextInfo;\nimport net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterVendor;\nimport net.caffeinemc.mods.sodium.client.compatibility.workarounds.intel.IntelWorkarounds;\nimport net.caffeinemc.mods.sodium.client.compatibility.workarounds.nvidia.NvidiaDriverVersion;\nimport net.caffeinemc.mods.sodium.client.compatibility.workarounds.nvidia.NvidiaWorkarounds;\nimport net.caffeinemc.mods.sodium.client.platform.NativeWindowHandle;\nimport net.caffeinemc.mods.sodium.client.platform.PlatformHelper;\nimport net.caffeinemc.mods.sodium.client.platform.windows.WindowsFileVersion;\n\nclass GraphicsDriverChecks {\n static void postContextInit(NativeWindowHandle window, GlContextInfo context) {\n GraphicsAdapterVendor vendor = GraphicsAdapterVendor.fromContext(context);\n if (vendor != GraphicsAdapterVendor.UNKNOWN) {\n if (vendor == GraphicsAdapterVendor.INTEL && BugChecks.ISSUE_899) {\n WindowsFileVersion installedVersion = IntelWorkarounds.findIntelDriverMatchingBug899();\n if (installedVersion != null) {\n String installedVersionString = installedVersion.toString();\n PlatformHelper.showCriticalErrorAndClose(\n window,\n "Sodium Renderer - Unsupported Driver",\n "The game failed to start because the currently installed Intel Graphics Driver is not compatible.\\n\\nInstalled version: ###CURRENT_DRIVER###\\nRequired version: 10.18.10.5161 (or newer)\\n\\nPlease click the 'Help' button to read more about how to fix this problem."\n .replace("###CURRENT_DRIVER###", installedVersionString),\n "https://link.caffeinemc.net/help/sodium/graphics-driver/windows/intel/gh-899"\n );\n }\n }\n\n if (vendor == GraphicsAdapterVendor.NVIDIA && BugChecks.ISSUE_1486) {\n WindowsFileVersion installedVersion = NvidiaWorkarounds.findNvidiaDriverMatchingBug1486();\n if (installedVersion != null) {\n String installedVersionString = NvidiaDriverVersion.parse(installedVersion).toString();\n PlatformHelper.showCriticalErrorAndClose(\n window,\n "Sodium Renderer - Unsupported Driver",\n "The game failed to start because the currently installed NVIDIA Graphics Driver is not compatible.\\n\\nInstalled version: ###CURRENT_DRIVER###\\nRequired version: 536.23 (or newer)\\n\\nPlease click the 'Help' button to read more about how to fix this problem."\n .replace("###CURRENT_DRIVER###", installedVersionString),\n "https://link.caffeinemc.net/help/sodium/graphics-driver/windows/nvidia/gh-1486"\n );\n }\n }\n }\n }\n}\n {"url": "https://link.caffeinemc.net/help/sodium/graphics-driver/windows/nvidia/gh-1486"} low +9 2 5ff865ff6c2e250096fb15b9a943c645deb8558eddc72fa1b492748eb2c78b32 net/caffeinemc/mods/sodium/client/compatibility/checks/GraphicsDriverChecks package net.caffeinemc.mods.sodium.client.compatibility.checks;\n\nimport net.caffeinemc.mods.sodium.client.compatibility.environment.GlContextInfo;\nimport net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterVendor;\nimport net.caffeinemc.mods.sodium.client.compatibility.workarounds.intel.IntelWorkarounds;\nimport net.caffeinemc.mods.sodium.client.compatibility.workarounds.nvidia.NvidiaDriverVersion;\nimport net.caffeinemc.mods.sodium.client.compatibility.workarounds.nvidia.NvidiaWorkarounds;\nimport net.caffeinemc.mods.sodium.client.platform.NativeWindowHandle;\nimport net.caffeinemc.mods.sodium.client.platform.PlatformHelper;\nimport net.caffeinemc.mods.sodium.client.platform.windows.WindowsFileVersion;\n\nclass GraphicsDriverChecks {\n static void postContextInit(NativeWindowHandle window, GlContextInfo context) {\n GraphicsAdapterVendor vendor = GraphicsAdapterVendor.fromContext(context);\n if (vendor != GraphicsAdapterVendor.UNKNOWN) {\n if (vendor == GraphicsAdapterVendor.INTEL && BugChecks.ISSUE_899) {\n WindowsFileVersion installedVersion = IntelWorkarounds.findIntelDriverMatchingBug899();\n if (installedVersion != null) {\n String installedVersionString = installedVersion.toString();\n PlatformHelper.showCriticalErrorAndClose(\n window,\n "Sodium Renderer - Unsupported Driver",\n "The game failed to start because the currently installed Intel Graphics Driver is not compatible.\\n\\nInstalled version: ###CURRENT_DRIVER###\\nRequired version: 10.18.10.5161 (or newer)\\n\\nPlease click the 'Help' button to read more about how to fix this problem."\n .replace("###CURRENT_DRIVER###", installedVersionString),\n "https://link.caffeinemc.net/help/sodium/graphics-driver/windows/intel/gh-899"\n );\n }\n }\n\n if (vendor == GraphicsAdapterVendor.NVIDIA && BugChecks.ISSUE_1486) {\n WindowsFileVersion installedVersion = NvidiaWorkarounds.findNvidiaDriverMatchingBug1486();\n if (installedVersion != null) {\n String installedVersionString = NvidiaDriverVersion.parse(installedVersion).toString();\n PlatformHelper.showCriticalErrorAndClose(\n window,\n "Sodium Renderer - Unsupported Driver",\n "The game failed to start because the currently installed NVIDIA Graphics Driver is not compatible.\\n\\nInstalled version: ###CURRENT_DRIVER###\\nRequired version: 536.23 (or newer)\\n\\nPlease click the 'Help' button to read more about how to fix this problem."\n .replace("###CURRENT_DRIVER###", installedVersionString),\n "https://link.caffeinemc.net/help/sodium/graphics-driver/windows/nvidia/gh-1486"\n );\n }\n }\n }\n }\n}\n {"url": "https://link.caffeinemc.net/help/sodium/graphics-driver/windows/intel/gh-899"} low +10 2 dcf24bb91e7861b7a382958053c3efc201684e93df7ce058e849effa3f947fb0 net/caffeinemc/mods/sodium/client/checks/ResourcePackScanner package net.caffeinemc.mods.sodium.client.checks;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Set;\nimport net.caffeinemc.mods.sodium.client.console.Console;\nimport net.caffeinemc.mods.sodium.client.console.message.MessageLevel;\nimport net.minecraft.class_3258;\nimport net.minecraft.class_3259;\nimport net.minecraft.class_3262;\nimport net.minecraft.class_3264;\nimport net.minecraft.class_3300;\nimport org.jetbrains.annotations.NotNull;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class ResourcePackScanner {\n private static final Logger LOGGER = LoggerFactory.getLogger("Sodium-ResourcePackScanner");\n private static final Set SHADER_PROGRAM_BLACKLIST = Set.of(\n "rendertype_solid.vsh",\n "rendertype_solid.fsh",\n "rendertype_solid.json",\n "rendertype_cutout_mipped.vsh",\n "rendertype_cutout_mipped.fsh",\n "rendertype_cutout_mipped.json",\n "rendertype_cutout.vsh",\n "rendertype_cutout.fsh",\n "rendertype_cutout.json",\n "rendertype_translucent.vsh",\n "rendertype_translucent.fsh",\n "rendertype_translucent.json",\n "rendertype_tripwire.vsh",\n "rendertype_tripwire.fsh",\n "rendertype_tripwire.json",\n "rendertype_clouds.vsh",\n "rendertype_clouds.fsh",\n "rendertype_clouds.json"\n );\n private static final Set SHADER_INCLUDE_BLACKLIST = Set.of("light.glsl", "fog.glsl");\n\n public static void checkIfCoreShaderLoaded(class_3300 manager) {\n List outputs = manager.method_29213()\n .filter(ResourcePackScanner::isExternalResourcePack)\n .map(ResourcePackScanner::scanResources)\n .toList();\n printToasts(outputs);\n printCompatibilityReport(outputs);\n }\n\n private static void printToasts(Collection resourcePacks) {\n List incompatibleResourcePacks = resourcePacks.stream().filter(pack -> !pack.shaderPrograms.isEmpty()).toList();\n List likelyIncompatibleResourcePacks = resourcePacks.stream()\n .filter(pack -> !pack.shaderIncludes.isEmpty())\n .filter(pack -> !incompatibleResourcePacks.contains(pack))\n .toList();\n boolean shown = false;\n if (!incompatibleResourcePacks.isEmpty()) {\n showConsoleMessage("sodium.console.core_shaders_error", true, MessageLevel.SEVERE);\n\n for (ResourcePackScanner.ScannedResourcePack entry : incompatibleResourcePacks) {\n showConsoleMessage(getResourcePackName(entry.resourcePack), false, MessageLevel.SEVERE);\n }\n\n shown = true;\n }\n\n if (!likelyIncompatibleResourcePacks.isEmpty()) {\n showConsoleMessage("sodium.console.core_shaders_warn", true, MessageLevel.WARN);\n\n for (ResourcePackScanner.ScannedResourcePack entry : likelyIncompatibleResourcePacks) {\n showConsoleMessage(getResourcePackName(entry.resourcePack), false, MessageLevel.WARN);\n }\n\n shown = true;\n }\n\n if (shown) {\n showConsoleMessage("sodium.console.core_shaders_info", true, MessageLevel.INFO);\n }\n }\n\n private static void printCompatibilityReport(Collection scanResults) {\n StringBuilder builder = new StringBuilder();\n\n for (ResourcePackScanner.ScannedResourcePack entry : scanResults) {\n if (!entry.shaderPrograms.isEmpty() || !entry.shaderIncludes.isEmpty()) {\n builder.append("- Resource pack: ").append(getResourcePackName(entry.resourcePack)).append("\\n");\n if (!entry.shaderPrograms.isEmpty()) {\n emitProblem(\n builder,\n "The resource pack replaces terrain shaders, which are not supported",\n "https://github.com/CaffeineMC/sodium/wiki/Resource-Packs",\n entry.shaderPrograms\n );\n }\n\n if (!entry.shaderIncludes.isEmpty()) {\n emitProblem(\n builder,\n "The resource pack modifies shader include files, which are not fully supported",\n "https://github.com/CaffeineMC/sodium/wiki/Resource-Packs",\n entry.shaderIncludes\n );\n }\n }\n }\n\n if (!builder.isEmpty()) {\n LOGGER.error("The following compatibility issues were found with installed resource packs:\\n{}", builder);\n }\n }\n\n private static void emitProblem(StringBuilder builder, String description, String url, List resources) {\n builder.append("\\t- Problem found: ").append("\\n");\n builder.append("\\t\\t- Description:\\n\\t\\t\\t").append(description).append("\\n");\n builder.append("\\t\\t- More information: ").append(url).append("\\n");\n builder.append("\\t\\t- Files: ").append("\\n");\n\n for (String resource : resources) {\n builder.append("\\t\\t\\t- ").append(resource).append("\\n");\n }\n }\n\n @NotNull\n private static ResourcePackScanner.ScannedResourcePack scanResources(class_3262 resourcePack) {\n List ignoredShaders = determineIgnoredShaders(resourcePack);\n if (!ignoredShaders.isEmpty()) {\n LOGGER.warn(\n "Resource pack '{}' indicates the following shaders should be ignored: {}", getResourcePackName(resourcePack), String.join(", ", ignoredShaders)\n );\n }\n\n ArrayList unsupportedShaderPrograms = new ArrayList();\n ArrayList unsupportedShaderIncludes = new ArrayList();\n resourcePack.method_14408(class_3264.field_14188, "minecraft", "shaders", (identifier, supplier) -> {\n String path = identifier.method_12832();\n String name = path.substring(path.lastIndexOf(47) + 1);\n if (!ignoredShaders.contains(name)) {\n if (SHADER_PROGRAM_BLACKLIST.contains(name)) {\n unsupportedShaderPrograms.add(path);\n } else if (SHADER_INCLUDE_BLACKLIST.contains(name)) {\n unsupportedShaderIncludes.add(path);\n }\n }\n });\n return new ResourcePackScanner.ScannedResourcePack(resourcePack, unsupportedShaderPrograms, unsupportedShaderIncludes);\n }\n\n private static boolean isExternalResourcePack(class_3262 pack) {\n return pack instanceof class_3259 || pack instanceof class_3258;\n }\n\n private static String getResourcePackName(class_3262 pack) {\n String path = pack.method_14409();\n return path.startsWith("file/") ? path.substring(5) : path;\n }\n\n private static List determineIgnoredShaders(class_3262 resourcePack) {\n ArrayList ignoredShaders = new ArrayList();\n\n try {\n SodiumResourcePackMetadata meta = (SodiumResourcePackMetadata)resourcePack.method_14407(SodiumResourcePackMetadata.SERIALIZER);\n if (meta != null) {\n ignoredShaders.addAll(meta.ignoredShaders());\n }\n } catch (IOException var3) {\n LOGGER.error("Failed to load pack.mcmeta file for resource pack '{}'", resourcePack.method_14409());\n }\n\n return ignoredShaders;\n }\n\n private static void showConsoleMessage(String message, boolean translatable, MessageLevel messageLevel) {\n Console.instance().logMessage(messageLevel, message, translatable, 12.5);\n }\n\n private record ScannedResourcePack(class_3262 resourcePack, ArrayList shaderPrograms, ArrayList shaderIncludes) {\n }\n}\n {"url": "https://github.com/CaffeineMC/sodium/wiki/Resource-Packs"} low +11 3 fc7e089f517eab447befde28ce1b5b2438bc5a08131eb338adeaaacbdef7d6cf net/fabricmc/fabric/impl/base/event/EventFactoryImpl \N {} low +12 4 967302d02a45f4cfa29af6604a50d12097295caa1aabff33b1a3d8e7638f9962 net/caffeinemc/mods/sodium/client/platform/windows/WindowsFileVersion package net.caffeinemc.mods.sodium.client.platform.windows;\n\nimport net.caffeinemc.mods.sodium.client.platform.windows.api.version.VersionFixedFileInfoStruct;\nimport org.jetbrains.annotations.NotNull;\n\npublic record WindowsFileVersion(int x, int y, int z, int w) {\n @NotNull\n public static WindowsFileVersion fromFileVersion(VersionFixedFileInfoStruct fileVersion) {\n int x = fileVersion.getFileVersionMostSignificantBits() >>> 16 & 65535;\n int y = fileVersion.getFileVersionMostSignificantBits() >>> 0 & 65535;\n int z = fileVersion.getFileVersionLeastSignificantBits() >>> 16 & 65535;\n int w = fileVersion.getFileVersionLeastSignificantBits() >>> 0 & 65535;\n return new WindowsFileVersion(x, y, z, w);\n }\n\n public String toString() {\n return "%s.%s.%s.%s".formatted(this.x, this.y, this.z, this.w);\n }\n}\n {} high +13 5 7005a5b5d443c84d758eceb963351a357d93b5c15eeb19caf20aae99d65a623b net/caffeinemc/mods/sodium/desktop/LaunchWarn package net.caffeinemc.mods.sodium.desktop;\n\nimport java.awt.GraphicsEnvironment;\nimport java.io.IOException;\nimport javax.swing.JDialog;\nimport javax.swing.JOptionPane;\nimport javax.swing.UIManager;\nimport javax.swing.UnsupportedLookAndFeelException;\nimport net.caffeinemc.mods.sodium.desktop.utils.browse.BrowseUrlHandler;\n\npublic class LaunchWarn {\n private static final String HELP_URL = "https://link.caffeinemc.net/guides/sodium/installation";\n private static final String RICH_MESSAGE = "

You have tried to launch Sodium (a Minecraft mod) directly, but it is not an executable program or mod installer. Instead, you must install Fabric Loader for Minecraft, and then place this file in your mods directory.

If this is your first time installing mods with Fabric Loader, then click the \\"Help\\" button for an installation guide.

";\n private static final String FALLBACK_MESSAGE = "

You have tried to launch Sodium (a Minecraft mod) directly, but it is not an executable program or mod installer. Instead, you must install Fabric Loader for Minecraft, and then place this file in your mods directory.

If this is your first time installing mods with Fabric Loader, then visit https://link.caffeinemc.net/guides/sodium/installation for an installation guide.

";\n private static final String FAILED_TO_BROWSE_MESSAGE = "

Failed to open the default browser! Your system may be misconfigured. Please open the URL https://link.caffeinemc.net/guides/sodium/installation manually.

";\n public static final String WINDOW_TITLE = "Sodium";\n\n public static void main(String[] args) {\n if (GraphicsEnvironment.isHeadless()) {\n showHeadlessError();\n } else {\n showGraphicalError();\n }\n }\n\n private static void showHeadlessError() {\n System.err\n .println(\n "

You have tried to launch Sodium (a Minecraft mod) directly, but it is not an executable program or mod installer. Instead, you must install Fabric Loader for Minecraft, and then place this file in your mods directory.

If this is your first time installing mods with Fabric Loader, then visit https://link.caffeinemc.net/guides/sodium/installation for an installation guide.

"\n );\n }\n\n private static void showGraphicalError() {\n trySetSystemLookAndFeel();\n trySetSystemFontPreferences();\n BrowseUrlHandler browseUrlHandler = BrowseUrlHandler.createImplementation();\n if (browseUrlHandler != null) {\n showRichGraphicalDialog(browseUrlHandler);\n } else {\n showFallbackGraphicalDialog();\n }\n\n System.exit(0);\n }\n\n private static void showRichGraphicalDialog(BrowseUrlHandler browseUrlHandler) {\n int selectedOption = showDialogBox(\n "

You have tried to launch Sodium (a Minecraft mod) directly, but it is not an executable program or mod installer. Instead, you must install Fabric Loader for Minecraft, and then place this file in your mods directory.

If this is your first time installing mods with Fabric Loader, then click the \\"Help\\" button for an installation guide.

",\n "Sodium",\n 0,\n 1,\n new String[]{"Help", "Close"},\n 0\n );\n if (selectedOption == 0) {\n log("Opening URL: https://link.caffeinemc.net/guides/sodium/installation");\n\n try {\n browseUrlHandler.browseTo("https://link.caffeinemc.net/guides/sodium/installation");\n } catch (IOException var3) {\n log("Failed to open default web browser!", var3);\n showDialogBox(\n "

Failed to open the default browser! Your system may be misconfigured. Please open the URL https://link.caffeinemc.net/guides/sodium/installation manually.

",\n "Sodium",\n -1,\n 2,\n null,\n -1\n );\n }\n }\n }\n\n private static void showFallbackGraphicalDialog() {\n showDialogBox(\n "

You have tried to launch Sodium (a Minecraft mod) directly, but it is not an executable program or mod installer. Instead, you must install Fabric Loader for Minecraft, and then place this file in your mods directory.

If this is your first time installing mods with Fabric Loader, then visit https://link.caffeinemc.net/guides/sodium/installation for an installation guide.

",\n "Sodium",\n -1,\n 1,\n null,\n null\n );\n }\n\n private static int showDialogBox(String message, String title, int optionType, int messageType, String[] options, Object initialValue) {\n JOptionPane pane = new JOptionPane(message, messageType, optionType, null, options, initialValue);\n JDialog dialog = pane.createDialog(title);\n dialog.setVisible(true);\n Object selectedValue = pane.getValue();\n if (selectedValue == null) {\n return -1;\n } else if (options == null) {\n return selectedValue instanceof Integer ? (Integer)selectedValue : -1;\n } else {\n for (int counter = 0; counter < options.length; counter++) {\n String option = options[counter];\n if (option.equals(selectedValue)) {\n return counter;\n }\n }\n\n return -1;\n }\n }\n\n private static void trySetSystemLookAndFeel() {\n try {\n UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());\n } catch (UnsupportedLookAndFeelException | ReflectiveOperationException var1) {\n }\n }\n\n private static void trySetSystemFontPreferences() {\n System.setProperty("awt.useSystemAAFontSettings", "on");\n }\n\n private static void log(String message) {\n System.err.println(message);\n }\n\n private static void log(String message, Throwable exception) {\n System.err.println(message);\n exception.printStackTrace(System.err);\n }\n}\n {} low +\. + + +-- +-- Name: delphi_report_issue_details_id_seq; Type: SEQUENCE SET; Schema: public; Owner: labrinth +-- + +SELECT pg_catalog.setval('public.delphi_report_issue_details_id_seq', 13, true); + + +-- +-- Name: delphi_report_issues_id_seq; Type: SEQUENCE SET; Schema: public; Owner: labrinth +-- + +SELECT pg_catalog.setval('public.delphi_report_issues_id_seq', 5, true); + + +-- +-- Name: delphi_reports_id_seq; Type: SEQUENCE SET; Schema: public; Owner: labrinth +-- + +SELECT pg_catalog.setval('public.delphi_reports_id_seq', 1, true); + + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict RGysBmMc8KFBQ9AssusGyNPozUiB43hdmIPxlv5KSWbX7tdW7XVMPpMginvod9K diff --git a/apps/labrinth/src/database/models/delphi_report_item.rs b/apps/labrinth/src/database/models/delphi_report_item.rs index be8b698ad9..0f402cfb8f 100644 --- a/apps/labrinth/src/database/models/delphi_report_item.rs +++ b/apps/labrinth/src/database/models/delphi_report_item.rs @@ -66,12 +66,19 @@ impl DBDelphiReport { sqlx::Type, utoipa::ToSchema, )] -#[serde(rename_all = "UPPERCASE")] +// The canonical serialized form of this enum is the snake_case representation. +// We add `alias`es so we can deserialize it from how Delphi sends it, +// which follows the Java conventions of `SCREAMING_SNAKE_CASE`. +#[serde(rename_all = "snake_case")] #[sqlx(type_name = "delphi_severity", rename_all = "snake_case")] pub enum DelphiSeverity { + #[serde(alias = "LOW")] Low, + #[serde(alias = "MEDIUM")] Medium, + #[serde(alias = "HIGH")] High, + #[serde(alias = "SEVERE")] Severe, } From a722a6ab382ced5c4a062942b3f9a8646bcfb00e Mon Sep 17 00:00:00 2001 From: aecsocket Date: Sat, 15 Nov 2025 23:42:09 +0000 Subject: [PATCH 26/65] Better Delphi logging --- ...b2249314019825c43f2b391847c782ce97fa.json} | 6 ++-- ...efd9e422151b683d9897a071ee0c4bac1cd4.json} | 5 +-- apps/labrinth/src/routes/internal/delphi.rs | 35 +++++++++---------- 3 files changed, 23 insertions(+), 23 deletions(-) rename apps/labrinth/.sqlx/{query-a06d0f682b1e576e634b02102ab6bcab9fcba55df818cab6e98721a324659526.json => query-900f4266e8926fc0458709081eb2b2249314019825c43f2b391847c782ce97fa.json} (71%) rename apps/labrinth/.sqlx/{query-26a43c77d3c1e875889141d209f01763de4c51bd16c100549261b11c5a4142b8.json => query-b65094517546487e43b65a76aa38efd9e422151b683d9897a071ee0c4bac1cd4.json} (72%) diff --git a/apps/labrinth/.sqlx/query-a06d0f682b1e576e634b02102ab6bcab9fcba55df818cab6e98721a324659526.json b/apps/labrinth/.sqlx/query-900f4266e8926fc0458709081eb2b2249314019825c43f2b391847c782ce97fa.json similarity index 71% rename from apps/labrinth/.sqlx/query-a06d0f682b1e576e634b02102ab6bcab9fcba55df818cab6e98721a324659526.json rename to apps/labrinth/.sqlx/query-900f4266e8926fc0458709081eb2b2249314019825c43f2b391847c782ce97fa.json index de08e37ea9..176c40ec3c 100644 --- a/apps/labrinth/.sqlx/query-a06d0f682b1e576e634b02102ab6bcab9fcba55df818cab6e98721a324659526.json +++ b/apps/labrinth/.sqlx/query-900f4266e8926fc0458709081eb2b2249314019825c43f2b391847c782ce97fa.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n dr.id AS \"report_id!: DelphiReportId\",\n f.id AS \"file_id!: DBFileId\",\n f.filename AS \"file_name!\",\n f.size AS \"file_size!\",\n m.id AS \"project_id!: DBProjectId\",\n t.id AS \"project_thread_id!: DBThreadId\",\n dr.created AS \"report_created!\",\n dr.severity AS \"report_severity!: DelphiSeverity\",\n dri.id AS \"issue_id!: DelphiReportIssueId\",\n dri.issue_type AS \"issue_type!\",\n dri.status AS \"issue_status!: DelphiReportIssueStatus\",\n -- maybe null\n drid.id AS \"issue_detail_id?: DelphiReportIssueDetailsId\",\n drid.internal_class_name AS \"issue_detail_class_name?\",\n drid.decompiled_source AS \"issue_detail_decompiled_source?\",\n drid.severity AS \"issue_detail_severity?: DelphiSeverity\"\n FROM delphi_reports dr\n\n -- fetch the project this report is for, its type, and thread\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n INNER JOIN categories c ON c.id = mc.joining_category_id\n INNER JOIN threads t ON t.mod_id = m.id\n\n -- fetch report issues and details\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n LEFT JOIN delphi_report_issue_details drid ON drid.issue_id = dri.id\n\n -- filtering\n WHERE\n -- project type\n (cardinality($1::int[]) = 0 OR c.project_type = ANY($1::int[]))\n\n -- sorting\n ORDER BY\n CASE WHEN $2 = 'created_asc' THEN created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $2 = 'created_desc' THEN created ELSE TO_TIMESTAMP(0) END DESC\n\n -- pagination\n LIMIT $3\n OFFSET $4\n ", + "query": "\n SELECT\n dr.id AS \"report_id!: DelphiReportId\",\n f.id AS \"file_id!: DBFileId\",\n f.filename AS \"file_name!\",\n f.size AS \"file_size!\",\n m.id AS \"project_id!: DBProjectId\",\n t.id AS \"project_thread_id!: DBThreadId\",\n dr.created AS \"report_created!\",\n dr.severity AS \"report_severity!: DelphiSeverity\",\n dri.id AS \"issue_id!: DelphiReportIssueId\",\n dri.issue_type AS \"issue_type!\",\n dri.status AS \"issue_status!: DelphiReportIssueStatus\",\n -- maybe null\n drid.id AS \"issue_detail_id?: DelphiReportIssueDetailsId\",\n drid.file_path AS \"issue_detail_file_path?\",\n drid.decompiled_source AS \"issue_detail_decompiled_source?\",\n drid.severity AS \"issue_detail_severity?: DelphiSeverity\"\n FROM delphi_reports dr\n\n -- fetch the project this report is for, its type, and thread\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n INNER JOIN categories c ON c.id = mc.joining_category_id\n INNER JOIN threads t ON t.mod_id = m.id\n\n -- fetch report issues and details\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n LEFT JOIN delphi_report_issue_details drid ON drid.issue_id = dri.id\n\n -- filtering\n WHERE\n -- project type\n (cardinality($1::int[]) = 0 OR c.project_type = ANY($1::int[]))\n\n -- sorting\n ORDER BY\n CASE WHEN $2 = 'created_asc' THEN created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $2 = 'created_desc' THEN created ELSE TO_TIMESTAMP(0) END DESC\n\n -- pagination\n LIMIT $3\n OFFSET $4\n ", "describe": { "columns": [ { @@ -88,7 +88,7 @@ }, { "ordinal": 12, - "name": "issue_detail_class_name?", + "name": "issue_detail_file_path?", "type_info": "Text" }, { @@ -140,5 +140,5 @@ true ] }, - "hash": "a06d0f682b1e576e634b02102ab6bcab9fcba55df818cab6e98721a324659526" + "hash": "900f4266e8926fc0458709081eb2b2249314019825c43f2b391847c782ce97fa" } diff --git a/apps/labrinth/.sqlx/query-26a43c77d3c1e875889141d209f01763de4c51bd16c100549261b11c5a4142b8.json b/apps/labrinth/.sqlx/query-b65094517546487e43b65a76aa38efd9e422151b683d9897a071ee0c4bac1cd4.json similarity index 72% rename from apps/labrinth/.sqlx/query-26a43c77d3c1e875889141d209f01763de4c51bd16c100549261b11c5a4142b8.json rename to apps/labrinth/.sqlx/query-b65094517546487e43b65a76aa38efd9e422151b683d9897a071ee0c4bac1cd4.json index 8806320d11..a0ea4442ee 100644 --- a/apps/labrinth/.sqlx/query-26a43c77d3c1e875889141d209f01763de4c51bd16c100549261b11c5a4142b8.json +++ b/apps/labrinth/.sqlx/query-b65094517546487e43b65a76aa38efd9e422151b683d9897a071ee0c4bac1cd4.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO delphi_report_issue_details (issue_id, internal_class_name, decompiled_source, data, severity)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id\n ", + "query": "\n INSERT INTO delphi_report_issue_details (issue_id, key, file_path, decompiled_source, data, severity)\n VALUES ($1, $2, $3, $4, $5, $6)\n RETURNING id\n ", "describe": { "columns": [ { @@ -14,6 +14,7 @@ "Int8", "Text", "Text", + "Text", "Jsonb", { "Custom": { @@ -34,5 +35,5 @@ false ] }, - "hash": "26a43c77d3c1e875889141d209f01763de4c51bd16c100549261b11c5a4142b8" + "hash": "b65094517546487e43b65a76aa38efd9e422151b683d9897a071ee0c4bac1cd4" } diff --git a/apps/labrinth/src/routes/internal/delphi.rs b/apps/labrinth/src/routes/internal/delphi.rs index 44268c4486..062fa3ecbe 100644 --- a/apps/labrinth/src/routes/internal/delphi.rs +++ b/apps/labrinth/src/routes/internal/delphi.rs @@ -6,7 +6,7 @@ use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT}; use serde::Deserialize; use sqlx::PgPool; use tokio::sync::Mutex; -use tracing::{info, warn}; +use tracing::info; use crate::{ auth::check_is_moderator_from_headers, @@ -72,7 +72,7 @@ struct DelphiReport { pub url: String, pub project_id: crate::models::ids::ProjectId, #[serde(rename = "version_id")] - pub _version_id: crate::models::ids::VersionId, + pub version_id: crate::models::ids::VersionId, pub file_id: crate::models::ids::FileId, /// A sequential, monotonically increasing version number for the /// Delphi version that generated this report. @@ -128,23 +128,23 @@ pub struct DelphiRunParameters { } #[post("ingest", guard = "admin_key_guard")] +#[tracing::instrument( + level = "info", + skip_all, + fields( + %report.url, + %report.file_id, + %report.project_id, + %report.version_id, + ) +)] async fn ingest_report( pool: web::Data, redis: web::Data, - report: web::Bytes, - // web::Json(report): web::Json, + web::Json(report): web::Json, ) -> Result<(), ApiError> { - info!( - "Json: {}", - serde_json::to_string_pretty( - &serde_json::from_slice::(&report).unwrap() - ) - .unwrap() - ); - let report = serde_json::from_slice::(&report).unwrap(); - if report.issues.is_empty() { - info!("No issues found for file {}", report.url); + info!("No issues found for file"); return Ok(()); } @@ -163,10 +163,9 @@ async fn ingest_report( .upsert(&mut transaction) .await?; - warn!( - "Delphi found {} issues in file {}", - report.issues.len(), - report.url + info!( + num_issues = %report.issues.len(), + "Delphi found issues in file", ); for (issue_type, issue_details) in report.issues { From 2c284d030435edc3c09b191c1c61b7e503d7f9b8 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Sun, 16 Nov 2025 00:11:34 +0000 Subject: [PATCH 27/65] Improve utoipa for tech review routes --- apps/labrinth/src/main.rs | 17 ++++++++++++++++- .../routes/internal/moderation/tech_review.rs | 17 +++++++++++++---- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/apps/labrinth/src/main.rs b/apps/labrinth/src/main.rs index f3cab342f0..9484d62224 100644 --- a/apps/labrinth/src/main.rs +++ b/apps/labrinth/src/main.rs @@ -21,6 +21,7 @@ use std::sync::Arc; use tracing::{Instrument, error, info, info_span}; use tracing_actix_web::TracingLogger; use utoipa::OpenApi; +use utoipa::openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}; use utoipa_actix_web::AppExt; use utoipa_swagger_ui::SwaggerUi; @@ -262,9 +263,23 @@ async fn main() -> std::io::Result<()> { } #[derive(utoipa::OpenApi)] -#[openapi(info(title = "Labrinth"))] +#[openapi(info(title = "Labrinth"), modifiers(&SecurityAddon))] struct ApiDoc; +struct SecurityAddon; + +impl utoipa::Modify for SecurityAddon { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + let components = openapi.components.as_mut().unwrap(); + components.add_security_scheme( + "bearer_auth", + SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new( + "authorization", + ))), + ); + } +} + fn log_error(err: &actix_web::Error) { if err.as_response_error().status_code().is_client_error() { tracing::debug!( diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index b87e925cb2..4f5cd71021 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -34,8 +34,10 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct SearchProjects { #[serde(default = "default_limit")] + #[schema(default = 20)] pub limit: u64, #[serde(default)] + #[schema(default = 0)] pub page: u64, #[serde(default)] pub filter: SearchProjectsFilter, @@ -163,7 +165,10 @@ pub struct FileIssueDetail { } /// Searches all projects which are awaiting technical review. -#[utoipa::path] +#[utoipa::path( + security(("bearer_auth" = [])), + responses((status = OK, body = inline(Vec))) +)] #[post("/search")] async fn search_projects( req: HttpRequest, @@ -209,7 +214,7 @@ async fn search_projects( let sort_by = search_req.sort_by.to_string(); let limit = search_req.limit.max(50); - let offset = limit * search_req.page; + let offset = limit.saturating_mul(search_req.page); let limit = i64::try_from(limit).wrap_request_err("limit cannot fit into `i64`")?; @@ -435,14 +440,18 @@ async fn search_projects( Ok(web::Json(projects)) } -/// Updates the state of a technical review issue. +/// See [`update_issue`]. #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct UpdateIssue { /// Status to set the issue to. pub status: DelphiReportIssueStatus, } -#[utoipa::path] +/// Updates the state of a technical review issue. +#[utoipa::path( + security(("bearer_auth" = [])), + responses((status = NO_CONTENT)) +)] #[post("/issue/{id}")] async fn update_issue( req: HttpRequest, From a2ae5cefd06c4737eeb964f846e8fd0b45baf2e3 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Sun, 16 Nov 2025 11:27:57 +0000 Subject: [PATCH 28/65] Add more sorting options for tech review queue --- .../labrinth/src/routes/internal/moderation/tech_review.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 4f5cd71021..3683fc109d 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -73,6 +73,8 @@ pub struct SearchProjectsFilter { pub enum SearchProjectsSort { CreatedAsc, CreatedDesc, + SeverityAsc, + SeverityDesc, } impl fmt::Display for SearchProjectsSort { @@ -267,7 +269,10 @@ async fn search_projects( -- sorting ORDER BY CASE WHEN $2 = 'created_asc' THEN created ELSE TO_TIMESTAMP(0) END ASC, - CASE WHEN $2 = 'created_desc' THEN created ELSE TO_TIMESTAMP(0) END DESC + CASE WHEN $2 = 'created_desc' THEN created ELSE TO_TIMESTAMP(0) END DESC, + CASE WHEN $2 = 'pending_status_first' THEN dri.status ELSE 'pending'::delphi_report_issue_status END ASC, + CASE WHEN $2 = 'severity_asc' THEN dr.severity ELSE 'low'::delphi_severity END ASC, + CASE WHEN $2 = 'severity_desc' THEN dr.severity ELSE 'low'::delphi_severity END DESC -- pagination LIMIT $3 From 419c24fdbc459bf0864f7bf4ac8186e26e398093 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Sun, 16 Nov 2025 11:39:39 +0000 Subject: [PATCH 29/65] Oops join --- apps/labrinth/src/routes/internal/moderation/tech_review.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 3683fc109d..e6f5873296 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -254,9 +254,8 @@ async fn search_projects( INNER JOIN versions v ON v.id = f.version_id INNER JOIN mods m ON m.id = v.mod_id LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id - INNER JOIN categories c ON c.id = mc.joining_category_id + LEFT JOIN categories c ON c.id = mc.joining_category_id INNER JOIN threads t ON t.mod_id = m.id - -- fetch report issues and details INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id LEFT JOIN delphi_report_issue_details drid ON drid.issue_id = dri.id @@ -270,7 +269,6 @@ async fn search_projects( ORDER BY CASE WHEN $2 = 'created_asc' THEN created ELSE TO_TIMESTAMP(0) END ASC, CASE WHEN $2 = 'created_desc' THEN created ELSE TO_TIMESTAMP(0) END DESC, - CASE WHEN $2 = 'pending_status_first' THEN dri.status ELSE 'pending'::delphi_report_issue_status END ASC, CASE WHEN $2 = 'severity_asc' THEN dr.severity ELSE 'low'::delphi_severity END ASC, CASE WHEN $2 = 'severity_desc' THEN dr.severity ELSE 'low'::delphi_severity END DESC From ec9afad5e2f368e59276c5219a739167f1e8f41a Mon Sep 17 00:00:00 2001 From: aecsocket Date: Tue, 18 Nov 2025 18:43:48 +0000 Subject: [PATCH 30/65] New routes for fetching issues and reports --- ...7023c1becd6d30e74a6110c14c0049d156118.json | 22 + ...ad9db6ed1b17ae7002172b4172b959bd7710.json} | 6 +- ...2b2249314019825c43f2b391847c782ce97fa.json | 144 ------ ...8fe0dbf285367b8051070bedbb075c4006c8d.json | 37 ++ ...d00483f055231f07ad5f39ce716a25ec2c6ad.json | 22 + .../src/database/models/delphi_report_item.rs | 87 +--- apps/labrinth/src/routes/internal/delphi.rs | 61 ++- .../routes/internal/moderation/tech_review.rs | 452 +++++++++--------- apps/labrinth/src/util/error.rs | 7 + 9 files changed, 377 insertions(+), 461 deletions(-) create mode 100644 apps/labrinth/.sqlx/query-7099e3a96324aadd3e7e0fbcc5e7023c1becd6d30e74a6110c14c0049d156118.json rename apps/labrinth/.sqlx/{query-c0ef7e1f2ddc02604c14a94235afc053676964380451b3f461e3276f3a26bbff.json => query-749fc694a88b419d30f820b7563aad9db6ed1b17ae7002172b4172b959bd7710.json} (66%) delete mode 100644 apps/labrinth/.sqlx/query-900f4266e8926fc0458709081eb2b2249314019825c43f2b391847c782ce97fa.json create mode 100644 apps/labrinth/.sqlx/query-d8e3e59bce087a32d3475bac8d38fe0dbf285367b8051070bedbb075c4006c8d.json create mode 100644 apps/labrinth/.sqlx/query-e70536fc2d4e45e1075258f618bd00483f055231f07ad5f39ce716a25ec2c6ad.json diff --git a/apps/labrinth/.sqlx/query-7099e3a96324aadd3e7e0fbcc5e7023c1becd6d30e74a6110c14c0049d156118.json b/apps/labrinth/.sqlx/query-7099e3a96324aadd3e7e0fbcc5e7023c1becd6d30e74a6110c14c0049d156118.json new file mode 100644 index 0000000000..0b88c91314 --- /dev/null +++ b/apps/labrinth/.sqlx/query-7099e3a96324aadd3e7e0fbcc5e7023c1becd6d30e74a6110c14c0049d156118.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n to_jsonb(dr)\n || jsonb_build_object(\n 'file_id', f.id,\n 'version_id', v.id,\n 'project_id', v.mod_id,\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'issues', json_array(\n SELECT\n to_jsonb(dri)\n || jsonb_build_object(\n 'details', json_array(\n SELECT to_jsonb(drid)\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ) AS \"data!: sqlx::types::Json\"\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n LEFT JOIN delphi_report_issue_details drid ON drid.issue_id = dri.id\n WHERE dr.id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "data!: sqlx::types::Json", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + null + ] + }, + "hash": "7099e3a96324aadd3e7e0fbcc5e7023c1becd6d30e74a6110c14c0049d156118" +} diff --git a/apps/labrinth/.sqlx/query-c0ef7e1f2ddc02604c14a94235afc053676964380451b3f461e3276f3a26bbff.json b/apps/labrinth/.sqlx/query-749fc694a88b419d30f820b7563aad9db6ed1b17ae7002172b4172b959bd7710.json similarity index 66% rename from apps/labrinth/.sqlx/query-c0ef7e1f2ddc02604c14a94235afc053676964380451b3f461e3276f3a26bbff.json rename to apps/labrinth/.sqlx/query-749fc694a88b419d30f820b7563aad9db6ed1b17ae7002172b4172b959bd7710.json index 7e536646ef..8c1c8e1f3a 100644 --- a/apps/labrinth/.sqlx/query-c0ef7e1f2ddc02604c14a94235afc053676964380451b3f461e3276f3a26bbff.json +++ b/apps/labrinth/.sqlx/query-749fc694a88b419d30f820b7563aad9db6ed1b17ae7002172b4172b959bd7710.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n delphi_report_issues.id AS \"id\", report_id,\n issue_type,\n delphi_report_issues.status AS \"status: DelphiReportIssueStatus\",\n\n file_id, delphi_version, artifact_url, created, severity AS \"severity: DelphiSeverity\",\n json_array(SELECT to_jsonb(delphi_report_issue_details)\n FROM delphi_report_issue_details\n WHERE issue_id = delphi_report_issues.id\n ) AS \"details: sqlx::types::Json>\",\n versions.mod_id AS \"project_id?\", mods.published AS \"project_published?\"\n FROM delphi_report_issues\n INNER JOIN delphi_reports ON delphi_reports.id = report_id\n LEFT OUTER JOIN files ON files.id = file_id\n LEFT OUTER JOIN versions ON versions.id = files.version_id\n LEFT OUTER JOIN mods ON mods.id = versions.mod_id\n WHERE\n (issue_type = $1 OR $1 IS NULL)\n AND (delphi_report_issues.status = $2 OR $2 IS NULL)\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'pending_status_first' THEN delphi_report_issues.status ELSE 'pending'::delphi_report_issue_status END ASC,\n CASE WHEN $3 = 'severity_asc' THEN delphi_reports.severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN delphi_reports.severity ELSE 'low'::delphi_severity END DESC\n OFFSET $5\n LIMIT $4\n ", + "query": "\n SELECT\n delphi_report_issues.id AS \"id\", report_id,\n issue_type,\n delphi_report_issues.status AS \"status: DelphiReportIssueStatus\",\n\n file_id, delphi_version, artifact_url, created, severity AS \"severity: DelphiSeverity\",\n json_array(SELECT to_jsonb(delphi_report_issue_details)\n FROM delphi_report_issue_details\n WHERE issue_id = delphi_report_issues.id\n ) AS \"details: sqlx::types::Json>\",\n versions.mod_id AS \"project_id?\", mods.published AS \"project_published?\"\n FROM delphi_report_issues\n INNER JOIN delphi_reports ON delphi_reports.id = report_id\n LEFT OUTER JOIN files ON files.id = file_id\n LEFT OUTER JOIN versions ON versions.id = files.version_id\n LEFT OUTER JOIN mods ON mods.id = versions.mod_id\n WHERE\n (issue_type = $1 OR $1 IS NULL)\n AND (delphi_report_issues.status = $2 OR $2 IS NULL)\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'pending_status_first' THEN delphi_report_issues.status ELSE 'pending'::delphi_report_issue_status END ASC,\n CASE WHEN $3 = 'severity_asc' THEN delphi_reports.severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN delphi_reports.severity ELSE 'low'::delphi_severity END DESC\n OFFSET $5\n LIMIT $4\n ", "describe": { "columns": [ { @@ -73,7 +73,7 @@ }, { "ordinal": 9, - "name": "details: sqlx::types::Json>", + "name": "details: sqlx::types::Json>", "type_info": "Jsonb" }, { @@ -122,5 +122,5 @@ true ] }, - "hash": "c0ef7e1f2ddc02604c14a94235afc053676964380451b3f461e3276f3a26bbff" + "hash": "749fc694a88b419d30f820b7563aad9db6ed1b17ae7002172b4172b959bd7710" } diff --git a/apps/labrinth/.sqlx/query-900f4266e8926fc0458709081eb2b2249314019825c43f2b391847c782ce97fa.json b/apps/labrinth/.sqlx/query-900f4266e8926fc0458709081eb2b2249314019825c43f2b391847c782ce97fa.json deleted file mode 100644 index 176c40ec3c..0000000000 --- a/apps/labrinth/.sqlx/query-900f4266e8926fc0458709081eb2b2249314019825c43f2b391847c782ce97fa.json +++ /dev/null @@ -1,144 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n dr.id AS \"report_id!: DelphiReportId\",\n f.id AS \"file_id!: DBFileId\",\n f.filename AS \"file_name!\",\n f.size AS \"file_size!\",\n m.id AS \"project_id!: DBProjectId\",\n t.id AS \"project_thread_id!: DBThreadId\",\n dr.created AS \"report_created!\",\n dr.severity AS \"report_severity!: DelphiSeverity\",\n dri.id AS \"issue_id!: DelphiReportIssueId\",\n dri.issue_type AS \"issue_type!\",\n dri.status AS \"issue_status!: DelphiReportIssueStatus\",\n -- maybe null\n drid.id AS \"issue_detail_id?: DelphiReportIssueDetailsId\",\n drid.file_path AS \"issue_detail_file_path?\",\n drid.decompiled_source AS \"issue_detail_decompiled_source?\",\n drid.severity AS \"issue_detail_severity?: DelphiSeverity\"\n FROM delphi_reports dr\n\n -- fetch the project this report is for, its type, and thread\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n INNER JOIN categories c ON c.id = mc.joining_category_id\n INNER JOIN threads t ON t.mod_id = m.id\n\n -- fetch report issues and details\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n LEFT JOIN delphi_report_issue_details drid ON drid.issue_id = dri.id\n\n -- filtering\n WHERE\n -- project type\n (cardinality($1::int[]) = 0 OR c.project_type = ANY($1::int[]))\n\n -- sorting\n ORDER BY\n CASE WHEN $2 = 'created_asc' THEN created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $2 = 'created_desc' THEN created ELSE TO_TIMESTAMP(0) END DESC\n\n -- pagination\n LIMIT $3\n OFFSET $4\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "report_id!: DelphiReportId", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "file_id!: DBFileId", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "file_name!", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "file_size!", - "type_info": "Int4" - }, - { - "ordinal": 4, - "name": "project_id!: DBProjectId", - "type_info": "Int8" - }, - { - "ordinal": 5, - "name": "project_thread_id!: DBThreadId", - "type_info": "Int8" - }, - { - "ordinal": 6, - "name": "report_created!", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "report_severity!: DelphiSeverity", - "type_info": { - "Custom": { - "name": "delphi_severity", - "kind": { - "Enum": [ - "low", - "medium", - "high", - "severe" - ] - } - } - } - }, - { - "ordinal": 8, - "name": "issue_id!: DelphiReportIssueId", - "type_info": "Int8" - }, - { - "ordinal": 9, - "name": "issue_type!", - "type_info": "Text" - }, - { - "ordinal": 10, - "name": "issue_status!: DelphiReportIssueStatus", - "type_info": { - "Custom": { - "name": "delphi_report_issue_status", - "kind": { - "Enum": [ - "pending", - "safe", - "unsafe" - ] - } - } - } - }, - { - "ordinal": 11, - "name": "issue_detail_id?: DelphiReportIssueDetailsId", - "type_info": "Int8" - }, - { - "ordinal": 12, - "name": "issue_detail_file_path?", - "type_info": "Text" - }, - { - "ordinal": 13, - "name": "issue_detail_decompiled_source?", - "type_info": "Text" - }, - { - "ordinal": 14, - "name": "issue_detail_severity?: DelphiSeverity", - "type_info": { - "Custom": { - "name": "delphi_severity", - "kind": { - "Enum": [ - "low", - "medium", - "high", - "severe" - ] - } - } - } - } - ], - "parameters": { - "Left": [ - "Int4Array", - "Text", - "Int8", - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true, - true, - true, - true - ] - }, - "hash": "900f4266e8926fc0458709081eb2b2249314019825c43f2b391847c782ce97fa" -} diff --git a/apps/labrinth/.sqlx/query-d8e3e59bce087a32d3475bac8d38fe0dbf285367b8051070bedbb075c4006c8d.json b/apps/labrinth/.sqlx/query-d8e3e59bce087a32d3475bac8d38fe0dbf285367b8051070bedbb075c4006c8d.json new file mode 100644 index 0000000000..e27ec02523 --- /dev/null +++ b/apps/labrinth/.sqlx/query-d8e3e59bce087a32d3475bac8d38fe0dbf285367b8051070bedbb075c4006c8d.json @@ -0,0 +1,37 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n m.id AS \"project_id: DBProjectId\",\n t.id AS \"project_thread_id: DBThreadId\",\n to_jsonb(dr)\n || jsonb_build_object(\n 'file_id', f.id,\n 'version_id', v.id,\n 'project_id', v.mod_id,\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'issues', json_array(\n SELECT\n to_jsonb(dri)\n || jsonb_build_object(\n 'details', json_array(\n SELECT jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity\n )\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ) AS \"report!: sqlx::types::Json\"\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n INNER JOIN threads t ON t.mod_id = m.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n LEFT JOIN delphi_report_issue_details drid ON drid.issue_id = dri.id\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($1::int[]) = 0 OR c.project_type = ANY($1::int[]))\n\n -- sorting\n ORDER BY\n CASE WHEN $2 = 'created_asc' THEN created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $2 = 'created_desc' THEN created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $2 = 'severity_asc' THEN dr.severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $2 = 'severity_desc' THEN dr.severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $3\n OFFSET $4\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "project_id: DBProjectId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "project_thread_id: DBThreadId", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "report!: sqlx::types::Json", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int4Array", + "Text", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + null + ] + }, + "hash": "d8e3e59bce087a32d3475bac8d38fe0dbf285367b8051070bedbb075c4006c8d" +} diff --git a/apps/labrinth/.sqlx/query-e70536fc2d4e45e1075258f618bd00483f055231f07ad5f39ce716a25ec2c6ad.json b/apps/labrinth/.sqlx/query-e70536fc2d4e45e1075258f618bd00483f055231f07ad5f39ce716a25ec2c6ad.json new file mode 100644 index 0000000000..b36f5b1197 --- /dev/null +++ b/apps/labrinth/.sqlx/query-e70536fc2d4e45e1075258f618bd00483f055231f07ad5f39ce716a25ec2c6ad.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n to_jsonb(dri)\n || jsonb_build_object(\n 'details', json_array(\n SELECT to_jsonb(drid)\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n ) AS \"data!: sqlx::types::Json\"\n FROM delphi_report_issues dri\n LEFT JOIN delphi_report_issue_details drid ON dri.id = drid.issue_id\n WHERE dri.id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "data!: sqlx::types::Json", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + null + ] + }, + "hash": "e70536fc2d4e45e1075258f618bd00483f055231f07ad5f39ce716a25ec2c6ad" +} diff --git a/apps/labrinth/src/database/models/delphi_report_item.rs b/apps/labrinth/src/database/models/delphi_report_item.rs index 0f402cfb8f..8bcaf4d938 100644 --- a/apps/labrinth/src/database/models/delphi_report_item.rs +++ b/apps/labrinth/src/database/models/delphi_report_item.rs @@ -1,7 +1,6 @@ use std::{ collections::HashMap, fmt::{self, Display, Formatter}, - ops::Deref, }; use chrono::{DateTime, Utc}; @@ -149,7 +148,7 @@ impl Display for DelphiReportListOrder { pub struct DelphiReportIssueResult { pub issue: DBDelphiReportIssue, pub report: DBDelphiReport, - pub details: Vec, + pub details: Vec, pub project_id: Option, pub project_published: Option>, } @@ -195,7 +194,7 @@ impl DBDelphiReportIssue { json_array(SELECT to_jsonb(delphi_report_issue_details) FROM delphi_report_issue_details WHERE issue_id = delphi_report_issues.id - ) AS "details: sqlx::types::Json>", + ) AS "details: sqlx::types::Json>", versions.mod_id AS "project_id?", mods.published AS "project_published?" FROM delphi_report_issues INNER JOIN delphi_reports ON delphi_reports.id = report_id @@ -253,18 +252,32 @@ impl DBDelphiReportIssue { /// belongs to a specific issue, and an issue can have zero, one, or /// more details attached to it. (Some issues may be artifact-wide, /// or otherwise not really specific to any particular class.) -#[derive(Debug, Deserialize, Serialize)] -pub struct DBDelphiReportIssueDetails { +#[derive( + Debug, Clone, Deserialize, Serialize, utoipa::ToSchema, sqlx::FromRow, +)] +pub struct ReportIssueDetail { + /// ID of this issue detail. pub id: DelphiReportIssueDetailsId, - pub key: String, + /// ID of the issue this detail belongs to. pub issue_id: DelphiReportIssueId, + /// Opaque identifier for where this issue detail is located, relative to + /// the file scanned. + /// + /// This acts as a stable identifier for an issue detail, even across + /// different versions of the same file. + pub key: String, + /// Name of the Java class path in which this issue was found. pub file_path: String, - pub decompiled_source: Option, - pub data: Json>, + /// Decompiled, pretty-printed source of the Java class. + pub decompiled_source: Option, + /// Extra detail-specific info for this detail. + #[sqlx(json)] + pub data: HashMap, + /// How important is this issue, as flagged by Delphi? pub severity: DelphiSeverity, } -impl DBDelphiReportIssueDetails { +impl ReportIssueDetail { pub async fn insert( &self, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, @@ -278,8 +291,8 @@ impl DBDelphiReportIssueDetails { self.issue_id as DelphiReportIssueId, self.key, self.file_path, - self.decompiled_source.as_ref().map(|decompiled_source| &decompiled_source.0), - &self.data as &Json>, + self.decompiled_source, + sqlx::types::Json(&self.data) as Json<&HashMap>, self.severity as DelphiSeverity, ) .fetch_one(&mut **transaction) @@ -299,55 +312,3 @@ impl DBDelphiReportIssueDetails { .rows_affected()) } } - -/// A [Java class name] with dots replaced by forward slashes (/). -/// -/// Because class names are usually the [binary names] passed to a classloader, top level interfaces and classes -/// have a binary name that matches its canonical, fully qualified name, such canonical names are prefixed by the -/// package path the class is in, and packages usually match the directory structure within a JAR for typical -/// classloaders, this usually (but not necessarily) corresponds to the path to the class file within its JAR. -/// -/// [Java class name]: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Class.html#getName() -/// [binary names]: https://docs.oracle.com/javase/specs/jls/se21/html/jls-13.html#jls-13.1 -#[derive( - Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash, sqlx::Type, -)] -#[serde(transparent)] -#[sqlx(transparent)] -pub struct InternalJavaClassName(String); - -impl Deref for InternalJavaClassName { - type Target = String; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Display for InternalJavaClassName { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -/// The decompiled source code of a Java class. -#[derive( - Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash, sqlx::Type, -)] -#[serde(transparent)] -#[sqlx(transparent)] -pub struct DecompiledJavaClassSource(String); - -impl Deref for DecompiledJavaClassSource { - type Target = String; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Display for DecompiledJavaClassSource { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} diff --git a/apps/labrinth/src/routes/internal/delphi.rs b/apps/labrinth/src/routes/internal/delphi.rs index 062fa3ecbe..35cd1c61ae 100644 --- a/apps/labrinth/src/routes/internal/delphi.rs +++ b/apps/labrinth/src/routes/internal/delphi.rs @@ -2,6 +2,7 @@ use std::{collections::HashMap, fmt::Write, sync::LazyLock, time::Instant}; use actix_web::{HttpRequest, HttpResponse, get, post, put, web}; use chrono::{DateTime, Utc}; +use eyre::eyre; use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT}; use serde::Deserialize; use sqlx::PgPool; @@ -15,9 +16,8 @@ use crate::{ DBFileId, DelphiReportId, DelphiReportIssueDetailsId, DelphiReportIssueId, delphi_report_item::{ - DBDelphiReport, DBDelphiReportIssue, - DBDelphiReportIssueDetails, DecompiledJavaClassSource, - DelphiReportIssueStatus, DelphiReportListOrder, DelphiSeverity, + DBDelphiReport, DBDelphiReportIssue, DelphiReportIssueStatus, + DelphiReportListOrder, DelphiSeverity, ReportIssueDetail, }, }, redis::RedisPool, @@ -28,7 +28,7 @@ use crate::{ }, queue::session::AuthQueue, routes::ApiError, - util::guards::admin_key_guard, + util::{error::Context, guards::admin_key_guard}, }; pub fn config(cfg: &mut web::ServiceConfig) { @@ -62,7 +62,6 @@ static DELPHI_CLIENT: LazyLock = LazyLock::new(|| { struct DelphiReportIssueDetails { pub file: String, pub key: String, - pub decompiled_source: Option, pub data: HashMap, pub severity: DelphiSeverity, } @@ -79,6 +78,9 @@ struct DelphiReport { pub delphi_version: i32, pub issues: HashMap>, pub severity: DelphiSeverity, + /// Map of [`DelphiReportIssueDetails::file`] to the decompiled Java source + /// code. + pub decompiled_sources: HashMap>, } impl DelphiReport { @@ -93,12 +95,10 @@ impl DelphiReport { format!("⚠️ Suspicious traces found at {}", self.url); for (issue, trace) in &self.issues { - for DelphiReportIssueDetails { - file, - decompiled_source, - .. - } in trace - { + for DelphiReportIssueDetails { file, .. } in trace { + let decompiled_source = + self.decompiled_sources.get(file).and_then(|o| o.as_ref()); + write!( &mut message_header, "\n issue {issue} found at class `{file}`:\n```\n{}\n```", @@ -128,6 +128,25 @@ pub struct DelphiRunParameters { } #[post("ingest", guard = "admin_key_guard")] +async fn ingest_report( + pool: web::Data, + redis: web::Data, + web::Json(report): web::Json, +) -> Result<(), ApiError> { + // treat this as an internal error, since it's not a bad request from the + // client's side - it's *our* fault for handling the Delphi schema wrong + // this could happen if Delphi updates and Labrinth doesn't + let report = serde_json::from_value::(report.clone()) + .wrap_internal_err_with(|| { + eyre!( + "Delphi sent a response which does not match our schema\n\n{}", + serde_json::to_string_pretty(&report).unwrap() + ) + })?; + + ingest_report_deserialized(pool, redis, report).await +} + #[tracing::instrument( level = "info", skip_all, @@ -138,10 +157,10 @@ pub struct DelphiRunParameters { %report.version_id, ) )] -async fn ingest_report( +async fn ingest_report_deserialized( pool: web::Data, redis: web::Data, - web::Json(report): web::Json, + report: DelphiReport, ) -> Result<(), ApiError> { if report.issues.is_empty() { info!("No issues found for file"); @@ -179,20 +198,20 @@ async fn ingest_report( .await?; // This is required to handle the case where the same Delphi version is re-run on the same file - DBDelphiReportIssueDetails::remove_all_by_issue_id( - issue_id, - &mut transaction, - ) - .await?; + ReportIssueDetail::remove_all_by_issue_id(issue_id, &mut transaction) + .await?; for issue_detail in issue_details { - DBDelphiReportIssueDetails { + let decompiled_source = + report.decompiled_sources.get(&issue_detail.file); + + ReportIssueDetail { id: DelphiReportIssueDetailsId(0), // This will be set by the database issue_id, key: issue_detail.key, file_path: issue_detail.file, - decompiled_source: issue_detail.decompiled_source, - data: issue_detail.data.into(), + decompiled_source: decompiled_source.cloned().flatten(), + data: issue_detail.data, severity: issue_detail.severity, } .insert(&mut transaction) diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index e6f5873296..fde0920dea 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -1,9 +1,7 @@ use std::{collections::HashMap, fmt}; -use actix_web::{HttpRequest, post, web}; +use actix_web::{HttpRequest, get, post, web}; use chrono::{DateTime, Utc}; -use eyre::eyre; -use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use tokio_stream::StreamExt; @@ -14,20 +12,29 @@ use crate::{ database::{ DBProject, models::{ - DBFileId, DBProjectId, DBThread, DBThreadId, DelphiReportId, - DelphiReportIssueDetailsId, DelphiReportIssueId, ProjectTypeId, - delphi_report_item::{DelphiReportIssueStatus, DelphiSeverity}, + DBFileId, DBProjectId, DBThread, DBThreadId, DBVersionId, + DelphiReportId, DelphiReportIssueId, ProjectTypeId, + delphi_report_item::{ + DelphiReportIssueStatus, DelphiSeverity, ReportIssueDetail, + }, }, redis::RedisPool, }, - models::{pats::Scopes, projects::Project}, + models::{ + ids::{ProjectId, ThreadId}, + pats::Scopes, + projects::Project, + }, queue::session::AuthQueue, routes::{ApiError, internal::moderation::Ownership}, util::error::Context, }; pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { - cfg.service(search_projects).service(update_issue); + cfg.service(search_projects) + .service(get_report) + .service(get_issue) + .service(update_issue); } /// Arguments for searching project technical reviews. @@ -86,47 +93,21 @@ impl fmt::Display for SearchProjectsSort { } #[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] -pub struct ProjectReview { - pub project: Project, - pub project_owner: Ownership, - pub thread: DBThread, - pub reports: Vec, -} - -#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] -pub struct ProjectReport { - /// When this report was created. - pub created_at: DateTime, +pub struct FileReport { + /// ID of this report. + pub id: DelphiReportId, + /// ID of the file that was scanned. + pub file_id: DBFileId, + /// ID of the project version this report is for. + pub version_id: DBVersionId, + /// ID of the project this report is for. + pub project_id: DBProjectId, + /// When the report for this file was created. + pub created: DateTime, /// Why this project was flagged. pub flag_reason: FlagReason, /// According to this report, how likely is the project malicious? pub severity: DelphiSeverity, - /// What files were flagged in this review. - pub files: Vec, -} - -/// Why a project was flagged for technical review. -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - Hash, - Serialize, - Deserialize, - utoipa::ToSchema, -)] -#[serde(rename_all = "snake_case")] -pub enum FlagReason { - /// Delphi anti-malware scanner flagged a file in the project. - Delphi, -} - -/// Details of a JAR file which was flagged for technical review, as part of -/// a [`ProjectReview`]. -#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] -pub struct FileReview { /// Name of the flagged file. pub file_name: String, /// Size of the flagged file, in bytes. @@ -142,34 +123,170 @@ pub struct FileReview { #[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] pub struct FileIssue { /// ID of the issue. - pub issue_id: DelphiReportIssueId, + pub id: DelphiReportIssueId, + /// ID of the report this issue is a part of. + pub report_id: DelphiReportId, /// Delphi-determined kind of issue that this is, e.g. `OBFUSCATED_NAMES`. /// /// Labrinth does not know the full set of kinds of issues, so this is kept /// as a string. - pub kind: String, + pub issue_type: String, /// Is this issue valid (malicious) or a false positive (safe)? pub status: DelphiReportIssueStatus, /// Details of why this issue might have been raised, such as what file it /// was found in. - pub details: Vec, + pub details: Vec, } -/// Occurrence of a [`FileIssue`] in a specific class in a scanned JAR file. +/// Why a project was flagged for technical review. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + utoipa::ToSchema, +)] +#[serde(rename_all = "snake_case")] +pub enum FlagReason { + /// Delphi anti-malware scanner flagged a file in the project. + Delphi, +} + +/// Get info on an issue in a Delphi report. +#[utoipa::path( + security(("bearer_auth" = [])), + responses((status = OK, body = inline(FileIssue))) +)] +#[get("/issue/{issue_id}")] +async fn get_issue( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, + path: web::Path<(DelphiReportIssueId,)>, +) -> Result, ApiError> { + check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_READ, + ) + .await?; + + let (issue_id,) = path.into_inner(); + let row = sqlx::query!( + r#" + SELECT + to_jsonb(dri) + || jsonb_build_object( + 'details', json_array( + SELECT to_jsonb(drid) + FROM delphi_report_issue_details drid + WHERE drid.issue_id = dri.id + ) + ) AS "data!: sqlx::types::Json" + FROM delphi_report_issues dri + LEFT JOIN delphi_report_issue_details drid ON dri.id = drid.issue_id + WHERE dri.id = $1 + "#, + issue_id as DelphiReportIssueId, + ) + .fetch_optional(&**pool) + .await + .wrap_internal_err("failed to fetch issue from database")? + .ok_or(ApiError::NotFound)?; + + Ok(web::Json(row.data.0)) +} + +/// Get info on a specific report for a project. +#[utoipa::path( + security(("bearer_auth" = [])), + responses((status = OK, body = inline(FileReport))) +)] +#[get("/report/{id}")] +async fn get_report( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, + path: web::Path<(DelphiReportId,)>, +) -> Result, ApiError> { + check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_READ, + ) + .await?; + + let (report_id,) = path.into_inner(); + + let row = sqlx::query!( + r#" + SELECT + to_jsonb(dr) + || jsonb_build_object( + 'file_id', f.id, + 'version_id', v.id, + 'project_id', v.mod_id, + 'file_name', f.filename, + 'file_size', f.size, + 'flag_reason', 'delphi', + 'issues', json_array( + SELECT + to_jsonb(dri) + || jsonb_build_object( + 'details', json_array( + SELECT to_jsonb(drid) + FROM delphi_report_issue_details drid + WHERE drid.issue_id = dri.id + ) + ) + FROM delphi_report_issues dri + WHERE dri.report_id = dr.id + ) + ) AS "data!: sqlx::types::Json" + FROM delphi_reports dr + INNER JOIN files f ON f.id = dr.file_id + INNER JOIN versions v ON v.id = f.version_id + INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id + LEFT JOIN delphi_report_issue_details drid ON drid.issue_id = dri.id + WHERE dr.id = $1 + "#, + report_id as DelphiReportId, + ) + .fetch_optional(&**pool) + .await + .wrap_internal_err("failed to fetch report from database")? + .ok_or(ApiError::NotFound)?; + + Ok(web::Json(row.data.0)) +} + +/// See [`search_projects`]. #[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] -pub struct FileIssueDetail { - /// Name of the Java class path in which this issue was found. - pub file_path: String, - /// Decompiled, pretty-printed source of the Java class. - pub decompiled_source: String, - /// How important is this issue, as flagged by Delphi? - pub severity: DelphiSeverity, +pub struct SearchResponse { + /// List of reports returned. + pub reports: Vec, + /// Fetched project information for projects in the returned reports. + pub projects: HashMap, + /// Fetched moderation threads for projects in the returned reports. + pub threads: HashMap, + /// Fetched owner information for projects. + pub ownership: HashMap, } /// Searches all projects which are awaiting technical review. #[utoipa::path( security(("bearer_auth" = [])), - responses((status = OK, body = inline(Vec))) + responses((status = OK, body = inline(Vec))) )] #[post("/search")] async fn search_projects( @@ -178,33 +295,7 @@ async fn search_projects( redis: web::Data, session_queue: web::Data, search_req: web::Json, -) -> Result>, ApiError> { - #[derive(Debug)] - struct ProjectRecord { - reports: IndexMap, - } - - #[derive(Debug)] - struct ReportRecord { - created: DateTime, - severity: DelphiSeverity, - files: IndexMap, - } - - #[derive(Debug)] - struct FileRecord { - file_name: String, - file_size: i32, - issues: IndexMap, - } - - #[derive(Debug)] - struct IssueRecord { - issue_type: String, - status: DelphiReportIssueStatus, - details: IndexMap, - } - +) -> Result, ApiError> { check_is_moderator_from_headers( &req, &**pool, @@ -223,44 +314,55 @@ async fn search_projects( let offset = i64::try_from(offset) .wrap_request_err("offset cannot fit into `i64`")?; - let mut project_records = IndexMap::::new(); + let mut reports = Vec::::new(); let mut project_ids = Vec::::new(); let mut thread_ids = Vec::::new(); - let _file_ids = Vec::::new(); - let mut rows = sqlx::query!( r#" SELECT - dr.id AS "report_id!: DelphiReportId", - f.id AS "file_id!: DBFileId", - f.filename AS "file_name!", - f.size AS "file_size!", - m.id AS "project_id!: DBProjectId", - t.id AS "project_thread_id!: DBThreadId", - dr.created AS "report_created!", - dr.severity AS "report_severity!: DelphiSeverity", - dri.id AS "issue_id!: DelphiReportIssueId", - dri.issue_type AS "issue_type!", - dri.status AS "issue_status!: DelphiReportIssueStatus", - -- maybe null - drid.id AS "issue_detail_id?: DelphiReportIssueDetailsId", - drid.file_path AS "issue_detail_file_path?", - drid.decompiled_source AS "issue_detail_decompiled_source?", - drid.severity AS "issue_detail_severity?: DelphiSeverity" + m.id AS "project_id: DBProjectId", + t.id AS "project_thread_id: DBThreadId", + to_jsonb(dr) + || jsonb_build_object( + 'file_id', f.id, + 'version_id', v.id, + 'project_id', v.mod_id, + 'file_name', f.filename, + 'file_size', f.size, + 'flag_reason', 'delphi', + 'issues', json_array( + SELECT + to_jsonb(dri) + || jsonb_build_object( + 'details', json_array( + SELECT jsonb_build_object( + 'id', drid.id, + 'issue_id', drid.issue_id, + 'key', drid.key, + 'file_path', drid.file_path, + -- ignore `decompiled_source` + 'data', drid.data, + 'severity', drid.severity + ) + FROM delphi_report_issue_details drid + WHERE drid.issue_id = dri.id + ) + ) + FROM delphi_report_issues dri + WHERE dri.report_id = dr.id + ) + ) AS "report!: sqlx::types::Json" FROM delphi_reports dr - - -- fetch the project this report is for, its type, and thread INNER JOIN files f ON f.id = dr.file_id INNER JOIN versions v ON v.id = f.version_id INNER JOIN mods m ON m.id = v.mod_id - LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id - LEFT JOIN categories c ON c.id = mc.joining_category_id INNER JOIN threads t ON t.mod_id = m.id - -- fetch report issues and details INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id LEFT JOIN delphi_report_issue_details drid ON drid.issue_id = dri.id -- filtering + LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id + LEFT JOIN categories c ON c.id = mc.joining_category_id WHERE -- project type (cardinality($1::int[]) = 0 OR c.project_type = ANY($1::int[])) @@ -294,75 +396,24 @@ async fn search_projects( .transpose() .wrap_internal_err("failed to fetch reports")? { + reports.push(row.report.0); project_ids.push(row.project_id); thread_ids.push(row.project_thread_id); - - let project = - project_records.entry(row.project_id).or_insert_with(|| { - ProjectRecord { - reports: IndexMap::new(), - } - }); - let report = - project.reports.entry(row.report_id).or_insert_with(|| { - ReportRecord { - created: row.report_created, - severity: row.report_severity, - files: IndexMap::new(), - } - }); - let file = - report - .files - .entry(row.file_id) - .or_insert_with(|| FileRecord { - file_name: row.file_name, - file_size: row.file_size, - issues: IndexMap::new(), - }); - let issue = - file.issues - .entry(row.issue_id) - .or_insert_with(|| IssueRecord { - issue_type: row.issue_type, - status: row.issue_status, - details: IndexMap::new(), - }); - - let ( - Some(issue_detail_id), - Some(file_path), - Some(decompiled_source), - Some(severity), - ) = ( - row.issue_detail_id, - row.issue_detail_file_path, - row.issue_detail_decompiled_source, - row.issue_detail_severity, - ) - else { - continue; - }; - issue.details.entry(issue_detail_id).or_insert_with(|| { - FileIssueDetail { - file_path, - decompiled_source, - severity, - } - }); } let projects = DBProject::get_many_ids(&project_ids, &**pool, &redis) .await .wrap_internal_err("failed to fetch projects")? .into_iter() - .map(|project| (project.inner.id, Project::from(project))) + .map(|project| { + (ProjectId::from(project.inner.id), Project::from(project)) + }) .collect::>(); let threads = DBThread::get_many(&thread_ids, &**pool) .await .wrap_internal_err("failed to fetch threads")? .into_iter() - .map(|thread| (thread.id, thread)) + .map(|thread| (ThreadId::from(thread.id), thread)) .collect::>(); let project_list: Vec = projects.values().cloned().collect(); @@ -370,77 +421,18 @@ async fn search_projects( let ownerships = get_projects_ownership(&project_list, &pool, &redis) .await .wrap_internal_err("failed to fetch project ownerships")?; - - let ownership_map = projects + let ownership = projects .keys() .copied() .zip(ownerships) .collect::>(); - let projects = project_records - .into_iter() - .map(|(project_id, project_record)| { - let project = - projects.get(&project_id).wrap_internal_err_with(|| { - eyre!("no fetched project with ID {project_id:?}") - })?; - let thread = threads - .get(&DBThreadId::from(project.thread_id)) - .wrap_internal_err_with(|| { - eyre!("no fetched thread with ID {:?}", project.thread_id) - })?; - Ok::<_, ApiError>(ProjectReview { - project: project.clone(), - project_owner: ownership_map - .get(&project_id) - .cloned() - .wrap_internal_err_with(|| { - eyre!("no owner for {project_id:?}") - })?, - thread: thread.clone(), - reports: project_record - .reports - .into_iter() - .map(|(_, report_record)| ProjectReport { - created_at: report_record.created, - flag_reason: FlagReason::Delphi, - severity: report_record.severity, - files: report_record - .files - .into_iter() - .map(|(_, file)| FileReview { - file_name: file.file_name, - file_size: file.file_size, - issues: file - .issues - .into_iter() - .map(|(issue_id, issue)| FileIssue { - issue_id, - kind: issue.issue_type.clone(), - status: issue.status, - details: issue - .details - .into_iter() - .map(|(_, detail)| { - FileIssueDetail { - file_path: detail.file_path, - decompiled_source: detail - .decompiled_source, - severity: detail.severity, - } - }) - .collect(), - }) - .collect(), - }) - .collect(), - }) - .collect(), - }) - }) - .collect::, _>>()?; - - Ok(web::Json(projects)) + Ok(web::Json(SearchResponse { + reports, + projects, + threads, + ownership, + })) } /// See [`update_issue`]. diff --git a/apps/labrinth/src/util/error.rs b/apps/labrinth/src/util/error.rs index 5f9ff343c2..cba9c21971 100644 --- a/apps/labrinth/src/util/error.rs +++ b/apps/labrinth/src/util/error.rs @@ -19,6 +19,7 @@ pub trait Context: Sized { /// Maps the error variant into an [`eyre::Report`] with the given message. #[inline] + #[track_caller] fn wrap_err(self, msg: D) -> Result where D: Send + Sync + Debug + Display + 'static, @@ -28,6 +29,7 @@ pub trait Context: Sized { /// Maps the error variant into an [`ApiError::Internal`] using the closure to create the message. #[inline] + #[track_caller] fn wrap_internal_err_with( self, f: impl FnOnce() -> D, @@ -40,6 +42,7 @@ pub trait Context: Sized { /// Maps the error variant into an [`ApiError::Internal`] with the given message. #[inline] + #[track_caller] fn wrap_internal_err(self, msg: D) -> Result where D: Send + Sync + Debug + Display + 'static, @@ -49,6 +52,7 @@ pub trait Context: Sized { /// Maps the error variant into an [`ApiError::Request`] using the closure to create the message. #[inline] + #[track_caller] fn wrap_request_err_with( self, f: impl FnOnce() -> D, @@ -61,6 +65,7 @@ pub trait Context: Sized { /// Maps the error variant into an [`ApiError::Request`] with the given message. #[inline] + #[track_caller] fn wrap_request_err(self, msg: D) -> Result where D: Send + Sync + Debug + Display + 'static, @@ -70,6 +75,7 @@ pub trait Context: Sized { /// Maps the error variant into an [`ApiError::Auth`] using the closure to create the message. #[inline] + #[track_caller] fn wrap_auth_err_with(self, f: impl FnOnce() -> D) -> Result where D: Send + Sync + Debug + Display + 'static, @@ -79,6 +85,7 @@ pub trait Context: Sized { /// Maps the error variant into an [`ApiError::Auth`] with the given message. #[inline] + #[track_caller] fn wrap_auth_err(self, msg: D) -> Result where D: Send + Sync + Debug + Display + 'static, From fdf2d110c8a55f21623572ac7d9d00879baa2522 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Wed, 19 Nov 2025 12:12:09 +0000 Subject: [PATCH 31/65] Fix which kind of ID is returned in tech review endpoints --- .../routes/internal/moderation/tech_review.rs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index fde0920dea..8b85f4d795 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -12,8 +12,8 @@ use crate::{ database::{ DBProject, models::{ - DBFileId, DBProjectId, DBThread, DBThreadId, DBVersionId, - DelphiReportId, DelphiReportIssueId, ProjectTypeId, + DBProjectId, DBThread, DBThreadId, DelphiReportId, + DelphiReportIssueId, ProjectTypeId, delphi_report_item::{ DelphiReportIssueStatus, DelphiSeverity, ReportIssueDetail, }, @@ -21,7 +21,7 @@ use crate::{ redis::RedisPool, }, models::{ - ids::{ProjectId, ThreadId}, + ids::{FileId, ProjectId, ThreadId, VersionId}, pats::Scopes, projects::Project, }, @@ -97,11 +97,11 @@ pub struct FileReport { /// ID of this report. pub id: DelphiReportId, /// ID of the file that was scanned. - pub file_id: DBFileId, + pub file_id: FileId, /// ID of the project version this report is for. - pub version_id: DBVersionId, + pub version_id: VersionId, /// ID of the project this report is for. - pub project_id: DBProjectId, + pub project_id: ProjectId, /// When the report for this file was created. pub created: DateTime, /// Why this project was flagged. @@ -233,9 +233,9 @@ async fn get_report( SELECT to_jsonb(dr) || jsonb_build_object( - 'file_id', f.id, - 'version_id', v.id, - 'project_id', v.mod_id, + 'file_id', to_base62(f.id), + 'version_id', to_base62(v.id), + 'project_id', to_base62(v.mod_id), 'file_name', f.filename, 'file_size', f.size, 'flag_reason', 'delphi', @@ -324,9 +324,9 @@ async fn search_projects( t.id AS "project_thread_id: DBThreadId", to_jsonb(dr) || jsonb_build_object( - 'file_id', f.id, - 'version_id', v.id, - 'project_id', v.mod_id, + 'file_id', to_base62(f.id), + 'version_id', to_base62(v.id), + 'project_id', to_base62(v.mod_id), 'file_name', f.filename, 'file_size', f.size, 'flag_reason', 'delphi', From 0b1a41d2d9ba66d6b3b83c10c7ae17b9c6f1a41d Mon Sep 17 00:00:00 2001 From: aecsocket Date: Wed, 19 Nov 2025 21:04:13 +0000 Subject: [PATCH 32/65] Deduplicate tech review report rows --- .../routes/internal/moderation/tech_review.rs | 109 +++++++++--------- 1 file changed, 57 insertions(+), 52 deletions(-) diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 8b85f4d795..3b40e5fca6 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -191,7 +191,6 @@ async fn get_issue( ) ) AS "data!: sqlx::types::Json" FROM delphi_report_issues dri - LEFT JOIN delphi_report_issue_details drid ON dri.id = drid.issue_id WHERE dri.id = $1 "#, issue_id as DelphiReportIssueId, @@ -230,7 +229,7 @@ async fn get_report( let row = sqlx::query!( r#" - SELECT + SELECT DISTINCT ON (dr.id) to_jsonb(dr) || jsonb_build_object( 'file_id', to_base62(f.id), @@ -256,8 +255,6 @@ async fn get_report( FROM delphi_reports dr INNER JOIN files f ON f.id = dr.file_id INNER JOIN versions v ON v.id = f.version_id - INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id - LEFT JOIN delphi_report_issue_details drid ON drid.issue_id = dri.id WHERE dr.id = $1 "#, report_id as DelphiReportId, @@ -320,59 +317,67 @@ async fn search_projects( let mut rows = sqlx::query!( r#" SELECT - m.id AS "project_id: DBProjectId", - t.id AS "project_thread_id: DBThreadId", - to_jsonb(dr) - || jsonb_build_object( - 'file_id', to_base62(f.id), - 'version_id', to_base62(v.id), - 'project_id', to_base62(v.mod_id), - 'file_name', f.filename, - 'file_size', f.size, - 'flag_reason', 'delphi', - 'issues', json_array( - SELECT - to_jsonb(dri) - || jsonb_build_object( - 'details', json_array( - SELECT jsonb_build_object( - 'id', drid.id, - 'issue_id', drid.issue_id, - 'key', drid.key, - 'file_path', drid.file_path, - -- ignore `decompiled_source` - 'data', drid.data, - 'severity', drid.severity + project_id AS "project_id: DBProjectId", + project_thread_id AS "project_thread_id: DBThreadId", + report AS "report!: sqlx::types::Json" + FROM ( + SELECT DISTINCT ON (dr.id) + dr.id AS report_id, + dr.created AS report_created, + dr.severity AS report_severity, + m.id AS project_id, + t.id AS project_thread_id, + + to_jsonb(dr) + || jsonb_build_object( + 'file_id', to_base62(f.id), + 'version_id', to_base62(v.id), + 'project_id', to_base62(v.mod_id), + 'file_name', f.filename, + 'file_size', f.size, + 'flag_reason', 'delphi', + 'issues', json_array( + SELECT + to_jsonb(dri) + || jsonb_build_object( + 'details', json_array( + SELECT jsonb_build_object( + 'id', drid.id, + 'issue_id', drid.issue_id, + 'key', drid.key, + 'file_path', drid.file_path, + -- ignore `decompiled_source` + 'data', drid.data, + 'severity', drid.severity + ) + FROM delphi_report_issue_details drid + WHERE drid.issue_id = dri.id ) - FROM delphi_report_issue_details drid - WHERE drid.issue_id = dri.id ) - ) - FROM delphi_report_issues dri - WHERE dri.report_id = dr.id - ) - ) AS "report!: sqlx::types::Json" - FROM delphi_reports dr - INNER JOIN files f ON f.id = dr.file_id - INNER JOIN versions v ON v.id = f.version_id - INNER JOIN mods m ON m.id = v.mod_id - INNER JOIN threads t ON t.mod_id = m.id - INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id - LEFT JOIN delphi_report_issue_details drid ON drid.issue_id = dri.id - - -- filtering - LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id - LEFT JOIN categories c ON c.id = mc.joining_category_id - WHERE - -- project type - (cardinality($1::int[]) = 0 OR c.project_type = ANY($1::int[])) + FROM delphi_report_issues dri + WHERE dri.report_id = dr.id + ) + ) AS report + FROM delphi_reports dr + INNER JOIN files f ON f.id = dr.file_id + INNER JOIN versions v ON v.id = f.version_id + INNER JOIN mods m ON m.id = v.mod_id + INNER JOIN threads t ON t.mod_id = m.id + + -- filtering + LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id + LEFT JOIN categories c ON c.id = mc.joining_category_id + WHERE + -- project type + (cardinality($1::int[]) = 0 OR c.project_type = ANY($1::int[])) + ) t -- sorting ORDER BY - CASE WHEN $2 = 'created_asc' THEN created ELSE TO_TIMESTAMP(0) END ASC, - CASE WHEN $2 = 'created_desc' THEN created ELSE TO_TIMESTAMP(0) END DESC, - CASE WHEN $2 = 'severity_asc' THEN dr.severity ELSE 'low'::delphi_severity END ASC, - CASE WHEN $2 = 'severity_desc' THEN dr.severity ELSE 'low'::delphi_severity END DESC + CASE WHEN $2 = 'created_asc' THEN t.report_created ELSE TO_TIMESTAMP(0) END ASC, + CASE WHEN $2 = 'created_desc' THEN t.report_created ELSE TO_TIMESTAMP(0) END DESC, + CASE WHEN $2 = 'severity_asc' THEN t.report_severity ELSE 'low'::delphi_severity END ASC, + CASE WHEN $2 = 'severity_desc' THEN t.report_severity ELSE 'low'::delphi_severity END DESC -- pagination LIMIT $3 From b19eaa8d9e5319c99788330e9457acd7767a4b98 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Wed, 19 Nov 2025 22:20:56 +0000 Subject: [PATCH 33/65] Reduce info sent for projects --- .../routes/internal/moderation/tech_review.rs | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 3b40e5fca6..c1e171044d 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -273,13 +273,26 @@ pub struct SearchResponse { /// List of reports returned. pub reports: Vec, /// Fetched project information for projects in the returned reports. - pub projects: HashMap, + pub projects: HashMap, /// Fetched moderation threads for projects in the returned reports. pub threads: HashMap, /// Fetched owner information for projects. pub ownership: HashMap, } +/// Limited set of project information returned by [`search_projects`]. +#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] +pub struct ProjectModerationInfo { + /// Projecet ID. + pub id: ProjectId, + /// Project name. + pub name: String, + /// The aggregated project typos of the versions of this project + pub project_types: Vec, + /// The URL of the icon of the project + pub icon_url: Option, +} + /// Searches all projects which are awaiting technical review. #[utoipa::path( security(("bearer_auth" = [])), @@ -434,7 +447,20 @@ async fn search_projects( Ok(web::Json(SearchResponse { reports, - projects, + projects: projects + .into_iter() + .map(|(id, project)| { + ( + id, + ProjectModerationInfo { + id, + name: project.name, + project_types: project.project_types, + icon_url: project.icon_url, + }, + ) + }) + .collect(), threads, ownership, })) From 387c55f5b970c3648d27f6ba0d2fc601101e3e37 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Fri, 21 Nov 2025 16:28:50 +0000 Subject: [PATCH 34/65] Fetch more thread info --- .../routes/internal/moderation/tech_review.rs | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index c1e171044d..270cceee0d 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -12,7 +12,7 @@ use crate::{ database::{ DBProject, models::{ - DBProjectId, DBThread, DBThreadId, DelphiReportId, + DBProjectId, DBThread, DBThreadId, DBUser, DelphiReportId, DelphiReportIssueId, ProjectTypeId, delphi_report_item::{ DelphiReportIssueStatus, DelphiSeverity, ReportIssueDetail, @@ -24,6 +24,7 @@ use crate::{ ids::{FileId, ProjectId, ThreadId, VersionId}, pats::Scopes, projects::Project, + threads::Thread, }, queue::session::AuthQueue, routes::{ApiError, internal::moderation::Ownership}, @@ -275,7 +276,7 @@ pub struct SearchResponse { /// Fetched project information for projects in the returned reports. pub projects: HashMap, /// Fetched moderation threads for projects in the returned reports. - pub threads: HashMap, + pub threads: HashMap, /// Fetched owner information for projects. pub ownership: HashMap, } @@ -285,6 +286,8 @@ pub struct SearchResponse { pub struct ProjectModerationInfo { /// Projecet ID. pub id: ProjectId, + /// Project moderation thread ID. + pub thread_id: ThreadId, /// Project name. pub name: String, /// The aggregated project typos of the versions of this project @@ -306,7 +309,7 @@ async fn search_projects( session_queue: web::Data, search_req: web::Json, ) -> Result, ApiError> { - check_is_moderator_from_headers( + let user = check_is_moderator_from_headers( &req, &**pool, &redis, @@ -427,11 +430,26 @@ async fn search_projects( (ProjectId::from(project.inner.id), Project::from(project)) }) .collect::>(); - let threads = DBThread::get_many(&thread_ids, &**pool) + let db_threads = DBThread::get_many(&thread_ids, &**pool) .await - .wrap_internal_err("failed to fetch threads")? + .wrap_internal_err("failed to fetch threads")?; + let thread_author_ids = db_threads + .iter() + .flat_map(|thread| thread.members.clone()) + .collect::>(); + let thread_authors = + DBUser::get_many_ids(&thread_author_ids, &**pool, &redis) + .await + .wrap_internal_err("failed to fetch thread authors")? + .into_iter() + .map(From::from) + .collect::>(); + let threads = db_threads .into_iter() - .map(|thread| (ThreadId::from(thread.id), thread)) + .map(|thread| { + let thread = Thread::from(thread, thread_authors.clone(), &user); + (thread.id, thread) + }) .collect::>(); let project_list: Vec = projects.values().cloned().collect(); @@ -454,6 +472,7 @@ async fn search_projects( id, ProjectModerationInfo { id, + thread_id: project.thread_id, name: project.name, project_types: project.project_types, icon_url: project.icon_url, From f8f5bfa166719f5295caea5e2e917191853343c5 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Sun, 23 Nov 2025 19:39:44 +0000 Subject: [PATCH 35/65] Address PR comments --- apps/labrinth/src/file_hosting/mock.rs | 4 +++- apps/labrinth/src/routes/internal/delphi.rs | 10 +--------- .../src/routes/internal/moderation/tech_review.rs | 4 ++++ 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/apps/labrinth/src/file_hosting/mock.rs b/apps/labrinth/src/file_hosting/mock.rs index c04f92420f..2565bd287c 100644 --- a/apps/labrinth/src/file_hosting/mock.rs +++ b/apps/labrinth/src/file_hosting/mock.rs @@ -27,7 +27,9 @@ impl FileHost for MockHost { file_publicity: FileHostPublicity, file_bytes: Bytes, ) -> Result { - let path = get_file_path(file_name, file_publicity); + let file_name = urlencoding::decode(file_name) + .map_err(|_| FileHostingError::InvalidFilename)?; + let path = get_file_path(&file_name, file_publicity); std::fs::create_dir_all( path.parent().ok_or(FileHostingError::InvalidFilename)?, )?; diff --git a/apps/labrinth/src/routes/internal/delphi.rs b/apps/labrinth/src/routes/internal/delphi.rs index 35cd1c61ae..0c690634ac 100644 --- a/apps/labrinth/src/routes/internal/delphi.rs +++ b/apps/labrinth/src/routes/internal/delphi.rs @@ -249,18 +249,10 @@ pub async fn run( run_parameters.file_id.0 ); - // fix for local file paths - // TODO: should we fix this upstream in whatever inserts the files row? - let url = if file_data.url.starts_with("/") { - format!("file://{}", file_data.url) - } else { - file_data.url - }; - DELPHI_CLIENT .post(dotenvy::var("DELPHI_URL")?) .json(&serde_json::json!({ - "url": url, + "url": file_data.url, "project_id": ProjectId(file_data.project_id.0 as u64), "version_id": VersionId(file_data.version_id.0 as u64), "file_id": run_parameters.file_id, diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 270cceee0d..eb2d82a854 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -113,6 +113,8 @@ pub struct FileReport { pub file_name: String, /// Size of the flagged file, in bytes. pub file_size: i32, + /// URL to download the flagged file. + pub download_url: String, /// What issues appeared in the file. pub issues: Vec, } @@ -239,6 +241,7 @@ async fn get_report( 'file_name', f.filename, 'file_size', f.size, 'flag_reason', 'delphi', + 'download_url', f.url, 'issues', json_array( SELECT to_jsonb(dri) @@ -352,6 +355,7 @@ async fn search_projects( 'file_name', f.filename, 'file_size', f.size, 'flag_reason', 'delphi', + 'download_url', f.url, 'issues', json_array( SELECT to_jsonb(dri) From 46021609bc017c2324998d362b597636808d57db Mon Sep 17 00:00:00 2001 From: aecsocket Date: Tue, 25 Nov 2025 12:17:43 +0000 Subject: [PATCH 36/65] fix ci --- Cargo.lock | 1 - ...bde760fc9d827acd0a9bc203ce8aa0ad2157c.json | 22 +++++++++++ ...b4561283c2c0a8491fc41ebbd7dd42584d443.json | 37 +++++++++++++++++++ ...7023c1becd6d30e74a6110c14c0049d156118.json | 22 ----------- ...8fe0dbf285367b8051070bedbb075c4006c8d.json | 37 ------------------- ...a07a9bedfa885b171e8141930225c6adaab1.json} | 4 +- apps/labrinth/Cargo.toml | 3 +- 7 files changed, 62 insertions(+), 64 deletions(-) create mode 100644 apps/labrinth/.sqlx/query-2782a1158fe434819eda94e63b3bde760fc9d827acd0a9bc203ce8aa0ad2157c.json create mode 100644 apps/labrinth/.sqlx/query-39d32bb5a6c60600d4c9536bf9db4561283c2c0a8491fc41ebbd7dd42584d443.json delete mode 100644 apps/labrinth/.sqlx/query-7099e3a96324aadd3e7e0fbcc5e7023c1becd6d30e74a6110c14c0049d156118.json delete mode 100644 apps/labrinth/.sqlx/query-d8e3e59bce087a32d3475bac8d38fe0dbf285367b8051070bedbb075c4006c8d.json rename apps/labrinth/.sqlx/{query-e70536fc2d4e45e1075258f618bd00483f055231f07ad5f39ce716a25ec2c6ad.json => query-ff526151c628646e735a2241c20aa07a9bedfa885b171e8141930225c6adaab1.json} (76%) diff --git a/Cargo.lock b/Cargo.lock index 5ed7289af8..3cea3b20f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4535,7 +4535,6 @@ dependencies = [ "hyper-util", "iana-time-zone", "image", - "indexmap 2.11.4", "itertools 0.14.0", "jemalloc_pprof", "json-patch 4.1.0", diff --git a/apps/labrinth/.sqlx/query-2782a1158fe434819eda94e63b3bde760fc9d827acd0a9bc203ce8aa0ad2157c.json b/apps/labrinth/.sqlx/query-2782a1158fe434819eda94e63b3bde760fc9d827acd0a9bc203ce8aa0ad2157c.json new file mode 100644 index 0000000000..0fcaf0889e --- /dev/null +++ b/apps/labrinth/.sqlx/query-2782a1158fe434819eda94e63b3bde760fc9d827acd0a9bc203ce8aa0ad2157c.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT DISTINCT ON (dr.id)\n to_jsonb(dr)\n || jsonb_build_object(\n 'file_id', to_base62(f.id),\n 'version_id', to_base62(v.id),\n 'project_id', to_base62(v.mod_id),\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'download_url', f.url,\n 'issues', json_array(\n SELECT\n to_jsonb(dri)\n || jsonb_build_object(\n 'details', json_array(\n SELECT to_jsonb(drid)\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ) AS \"data!: sqlx::types::Json\"\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n WHERE dr.id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "data!: sqlx::types::Json", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + null + ] + }, + "hash": "2782a1158fe434819eda94e63b3bde760fc9d827acd0a9bc203ce8aa0ad2157c" +} diff --git a/apps/labrinth/.sqlx/query-39d32bb5a6c60600d4c9536bf9db4561283c2c0a8491fc41ebbd7dd42584d443.json b/apps/labrinth/.sqlx/query-39d32bb5a6c60600d4c9536bf9db4561283c2c0a8491fc41ebbd7dd42584d443.json new file mode 100644 index 0000000000..7740063567 --- /dev/null +++ b/apps/labrinth/.sqlx/query-39d32bb5a6c60600d4c9536bf9db4561283c2c0a8491fc41ebbd7dd42584d443.json @@ -0,0 +1,37 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (dr.id)\n dr.id AS report_id,\n dr.created AS report_created,\n dr.severity AS report_severity,\n m.id AS project_id,\n t.id AS project_thread_id,\n\n to_jsonb(dr)\n || jsonb_build_object(\n 'file_id', to_base62(f.id),\n 'version_id', to_base62(v.id),\n 'project_id', to_base62(v.mod_id),\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'download_url', f.url,\n 'issues', json_array(\n SELECT\n to_jsonb(dri)\n || jsonb_build_object(\n 'details', json_array(\n SELECT jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity\n )\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ) AS report\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n INNER JOIN threads t ON t.mod_id = m.id\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($1::int[]) = 0 OR c.project_type = ANY($1::int[]))\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $2 = 'created_asc' THEN t.report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $2 = 'created_desc' THEN t.report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $2 = 'severity_asc' THEN t.report_severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $2 = 'severity_desc' THEN t.report_severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $3\n OFFSET $4\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "project_id: DBProjectId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "project_thread_id: DBThreadId", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "report!: sqlx::types::Json", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int4Array", + "Text", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + null + ] + }, + "hash": "39d32bb5a6c60600d4c9536bf9db4561283c2c0a8491fc41ebbd7dd42584d443" +} diff --git a/apps/labrinth/.sqlx/query-7099e3a96324aadd3e7e0fbcc5e7023c1becd6d30e74a6110c14c0049d156118.json b/apps/labrinth/.sqlx/query-7099e3a96324aadd3e7e0fbcc5e7023c1becd6d30e74a6110c14c0049d156118.json deleted file mode 100644 index 0b88c91314..0000000000 --- a/apps/labrinth/.sqlx/query-7099e3a96324aadd3e7e0fbcc5e7023c1becd6d30e74a6110c14c0049d156118.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n to_jsonb(dr)\n || jsonb_build_object(\n 'file_id', f.id,\n 'version_id', v.id,\n 'project_id', v.mod_id,\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'issues', json_array(\n SELECT\n to_jsonb(dri)\n || jsonb_build_object(\n 'details', json_array(\n SELECT to_jsonb(drid)\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ) AS \"data!: sqlx::types::Json\"\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n LEFT JOIN delphi_report_issue_details drid ON drid.issue_id = dri.id\n WHERE dr.id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "data!: sqlx::types::Json", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - null - ] - }, - "hash": "7099e3a96324aadd3e7e0fbcc5e7023c1becd6d30e74a6110c14c0049d156118" -} diff --git a/apps/labrinth/.sqlx/query-d8e3e59bce087a32d3475bac8d38fe0dbf285367b8051070bedbb075c4006c8d.json b/apps/labrinth/.sqlx/query-d8e3e59bce087a32d3475bac8d38fe0dbf285367b8051070bedbb075c4006c8d.json deleted file mode 100644 index e27ec02523..0000000000 --- a/apps/labrinth/.sqlx/query-d8e3e59bce087a32d3475bac8d38fe0dbf285367b8051070bedbb075c4006c8d.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n m.id AS \"project_id: DBProjectId\",\n t.id AS \"project_thread_id: DBThreadId\",\n to_jsonb(dr)\n || jsonb_build_object(\n 'file_id', f.id,\n 'version_id', v.id,\n 'project_id', v.mod_id,\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'issues', json_array(\n SELECT\n to_jsonb(dri)\n || jsonb_build_object(\n 'details', json_array(\n SELECT jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity\n )\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ) AS \"report!: sqlx::types::Json\"\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n INNER JOIN threads t ON t.mod_id = m.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n LEFT JOIN delphi_report_issue_details drid ON drid.issue_id = dri.id\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($1::int[]) = 0 OR c.project_type = ANY($1::int[]))\n\n -- sorting\n ORDER BY\n CASE WHEN $2 = 'created_asc' THEN created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $2 = 'created_desc' THEN created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $2 = 'severity_asc' THEN dr.severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $2 = 'severity_desc' THEN dr.severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $3\n OFFSET $4\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "project_id: DBProjectId", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "project_thread_id: DBThreadId", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "report!: sqlx::types::Json", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Int4Array", - "Text", - "Int8", - "Int8" - ] - }, - "nullable": [ - false, - false, - null - ] - }, - "hash": "d8e3e59bce087a32d3475bac8d38fe0dbf285367b8051070bedbb075c4006c8d" -} diff --git a/apps/labrinth/.sqlx/query-e70536fc2d4e45e1075258f618bd00483f055231f07ad5f39ce716a25ec2c6ad.json b/apps/labrinth/.sqlx/query-ff526151c628646e735a2241c20aa07a9bedfa885b171e8141930225c6adaab1.json similarity index 76% rename from apps/labrinth/.sqlx/query-e70536fc2d4e45e1075258f618bd00483f055231f07ad5f39ce716a25ec2c6ad.json rename to apps/labrinth/.sqlx/query-ff526151c628646e735a2241c20aa07a9bedfa885b171e8141930225c6adaab1.json index b36f5b1197..46f684c829 100644 --- a/apps/labrinth/.sqlx/query-e70536fc2d4e45e1075258f618bd00483f055231f07ad5f39ce716a25ec2c6ad.json +++ b/apps/labrinth/.sqlx/query-ff526151c628646e735a2241c20aa07a9bedfa885b171e8141930225c6adaab1.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n to_jsonb(dri)\n || jsonb_build_object(\n 'details', json_array(\n SELECT to_jsonb(drid)\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n ) AS \"data!: sqlx::types::Json\"\n FROM delphi_report_issues dri\n LEFT JOIN delphi_report_issue_details drid ON dri.id = drid.issue_id\n WHERE dri.id = $1\n ", + "query": "\n SELECT\n to_jsonb(dri)\n || jsonb_build_object(\n 'details', json_array(\n SELECT to_jsonb(drid)\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n ) AS \"data!: sqlx::types::Json\"\n FROM delphi_report_issues dri\n WHERE dri.id = $1\n ", "describe": { "columns": [ { @@ -18,5 +18,5 @@ null ] }, - "hash": "e70536fc2d4e45e1075258f618bd00483f055231f07ad5f39ce716a25ec2c6ad" + "hash": "ff526151c628646e735a2241c20aa07a9bedfa885b171e8141930225c6adaab1" } diff --git a/apps/labrinth/Cargo.toml b/apps/labrinth/Cargo.toml index b69d84cd58..6f5f88352d 100644 --- a/apps/labrinth/Cargo.toml +++ b/apps/labrinth/Cargo.toml @@ -66,13 +66,12 @@ image = { workspace = true, features = [ "tiff", "webp", ] } -indexmap = { workspace = true } itertools = { workspace = true } json-patch = { workspace = true } lettre = { workspace = true } meilisearch-sdk = { workspace = true, features = ["reqwest"] } modrinth-maxmind = { workspace = true } -modrinth-util = { workspace = true, features = ["decimal", "utoipa"] } +modrinth-util = { workspace = true } muralpay = { workspace = true, features = ["mock", "utoipa"] } murmur2 = { workspace = true } paste = { workspace = true } From ef9ade5fba00adc38d1b2124cdd877d671c78e67 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Wed, 26 Nov 2025 14:10:58 +0000 Subject: [PATCH 37/65] fix postgres version mismatch --- ...bde760fc9d827acd0a9bc203ce8aa0ad2157c.json | 22 ---------- ...79de6c2e84f442b481f42c2f9ab40b547ac7f.json | 37 ++++++++++++++++ ...b4561283c2c0a8491fc41ebbd7dd42584d443.json | 37 ---------------- ...52530ffe2a76b7bfbbd2053e87455be8772a1.json | 22 ++++++++++ ...66b6696052ac6d5ebe131b9e7242104f700af.json | 22 ++++++++++ ...ac8460088b70cd115c3d5ebf9474aa4d54fa.json} | 6 +-- ...aa07a9bedfa885b171e8141930225c6adaab1.json | 22 ---------- .../src/database/models/delphi_report_item.rs | 5 ++- .../routes/internal/moderation/tech_review.rs | 43 +++++++++++-------- 9 files changed, 114 insertions(+), 102 deletions(-) delete mode 100644 apps/labrinth/.sqlx/query-2782a1158fe434819eda94e63b3bde760fc9d827acd0a9bc203ce8aa0ad2157c.json create mode 100644 apps/labrinth/.sqlx/query-365407c545cc4a55236f56e65a579de6c2e84f442b481f42c2f9ab40b547ac7f.json delete mode 100644 apps/labrinth/.sqlx/query-39d32bb5a6c60600d4c9536bf9db4561283c2c0a8491fc41ebbd7dd42584d443.json create mode 100644 apps/labrinth/.sqlx/query-47b4d61218e7d38d63a7f96efe152530ffe2a76b7bfbbd2053e87455be8772a1.json create mode 100644 apps/labrinth/.sqlx/query-7d1f49699e242f3e002afee9bf466b6696052ac6d5ebe131b9e7242104f700af.json rename apps/labrinth/.sqlx/{query-749fc694a88b419d30f820b7563aad9db6ed1b17ae7002172b4172b959bd7710.json => query-d9b6a1b7fb133f7aec7599e06e21ac8460088b70cd115c3d5ebf9474aa4d54fa.json} (61%) delete mode 100644 apps/labrinth/.sqlx/query-ff526151c628646e735a2241c20aa07a9bedfa885b171e8141930225c6adaab1.json diff --git a/apps/labrinth/.sqlx/query-2782a1158fe434819eda94e63b3bde760fc9d827acd0a9bc203ce8aa0ad2157c.json b/apps/labrinth/.sqlx/query-2782a1158fe434819eda94e63b3bde760fc9d827acd0a9bc203ce8aa0ad2157c.json deleted file mode 100644 index 0fcaf0889e..0000000000 --- a/apps/labrinth/.sqlx/query-2782a1158fe434819eda94e63b3bde760fc9d827acd0a9bc203ce8aa0ad2157c.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT DISTINCT ON (dr.id)\n to_jsonb(dr)\n || jsonb_build_object(\n 'file_id', to_base62(f.id),\n 'version_id', to_base62(v.id),\n 'project_id', to_base62(v.mod_id),\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'download_url', f.url,\n 'issues', json_array(\n SELECT\n to_jsonb(dri)\n || jsonb_build_object(\n 'details', json_array(\n SELECT to_jsonb(drid)\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ) AS \"data!: sqlx::types::Json\"\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n WHERE dr.id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "data!: sqlx::types::Json", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - null - ] - }, - "hash": "2782a1158fe434819eda94e63b3bde760fc9d827acd0a9bc203ce8aa0ad2157c" -} diff --git a/apps/labrinth/.sqlx/query-365407c545cc4a55236f56e65a579de6c2e84f442b481f42c2f9ab40b547ac7f.json b/apps/labrinth/.sqlx/query-365407c545cc4a55236f56e65a579de6c2e84f442b481f42c2f9ab40b547ac7f.json new file mode 100644 index 0000000000..52796b1e9a --- /dev/null +++ b/apps/labrinth/.sqlx/query-365407c545cc4a55236f56e65a579de6c2e84f442b481f42c2f9ab40b547ac7f.json @@ -0,0 +1,37 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (dr.id)\n dr.id AS report_id,\n dr.created AS report_created,\n dr.severity AS report_severity,\n m.id AS project_id,\n t.id AS project_thread_id,\n\n to_jsonb(dr)\n || jsonb_build_object(\n 'file_id', to_base62(f.id),\n 'version_id', to_base62(v.id),\n 'project_id', to_base62(v.mod_id),\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT json_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT json_agg(\n jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity\n )\n )\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ) AS report\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n INNER JOIN threads t ON t.mod_id = m.id\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($1::int[]) = 0 OR c.project_type = ANY($1::int[]))\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $2 = 'created_asc' THEN t.report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $2 = 'created_desc' THEN t.report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $2 = 'severity_asc' THEN t.report_severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $2 = 'severity_desc' THEN t.report_severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $3\n OFFSET $4\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "project_id: DBProjectId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "project_thread_id: DBThreadId", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "report!: sqlx::types::Json", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int4Array", + "Text", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + null + ] + }, + "hash": "365407c545cc4a55236f56e65a579de6c2e84f442b481f42c2f9ab40b547ac7f" +} diff --git a/apps/labrinth/.sqlx/query-39d32bb5a6c60600d4c9536bf9db4561283c2c0a8491fc41ebbd7dd42584d443.json b/apps/labrinth/.sqlx/query-39d32bb5a6c60600d4c9536bf9db4561283c2c0a8491fc41ebbd7dd42584d443.json deleted file mode 100644 index 7740063567..0000000000 --- a/apps/labrinth/.sqlx/query-39d32bb5a6c60600d4c9536bf9db4561283c2c0a8491fc41ebbd7dd42584d443.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (dr.id)\n dr.id AS report_id,\n dr.created AS report_created,\n dr.severity AS report_severity,\n m.id AS project_id,\n t.id AS project_thread_id,\n\n to_jsonb(dr)\n || jsonb_build_object(\n 'file_id', to_base62(f.id),\n 'version_id', to_base62(v.id),\n 'project_id', to_base62(v.mod_id),\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'download_url', f.url,\n 'issues', json_array(\n SELECT\n to_jsonb(dri)\n || jsonb_build_object(\n 'details', json_array(\n SELECT jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity\n )\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ) AS report\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n INNER JOIN threads t ON t.mod_id = m.id\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($1::int[]) = 0 OR c.project_type = ANY($1::int[]))\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $2 = 'created_asc' THEN t.report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $2 = 'created_desc' THEN t.report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $2 = 'severity_asc' THEN t.report_severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $2 = 'severity_desc' THEN t.report_severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $3\n OFFSET $4\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "project_id: DBProjectId", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "project_thread_id: DBThreadId", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "report!: sqlx::types::Json", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Int4Array", - "Text", - "Int8", - "Int8" - ] - }, - "nullable": [ - false, - false, - null - ] - }, - "hash": "39d32bb5a6c60600d4c9536bf9db4561283c2c0a8491fc41ebbd7dd42584d443" -} diff --git a/apps/labrinth/.sqlx/query-47b4d61218e7d38d63a7f96efe152530ffe2a76b7bfbbd2053e87455be8772a1.json b/apps/labrinth/.sqlx/query-47b4d61218e7d38d63a7f96efe152530ffe2a76b7bfbbd2053e87455be8772a1.json new file mode 100644 index 0000000000..4bbdebb887 --- /dev/null +++ b/apps/labrinth/.sqlx/query-47b4d61218e7d38d63a7f96efe152530ffe2a76b7bfbbd2053e87455be8772a1.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT DISTINCT ON (dr.id)\n to_jsonb(dr)\n || jsonb_build_object(\n 'file_id', to_base62(f.id),\n 'version_id', to_base62(v.id),\n 'project_id', to_base62(v.mod_id),\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT json_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT json_agg(to_jsonb(drid))\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ) AS \"data!: sqlx::types::Json\"\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n WHERE dr.id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "data!: sqlx::types::Json", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + null + ] + }, + "hash": "47b4d61218e7d38d63a7f96efe152530ffe2a76b7bfbbd2053e87455be8772a1" +} diff --git a/apps/labrinth/.sqlx/query-7d1f49699e242f3e002afee9bf466b6696052ac6d5ebe131b9e7242104f700af.json b/apps/labrinth/.sqlx/query-7d1f49699e242f3e002afee9bf466b6696052ac6d5ebe131b9e7242104f700af.json new file mode 100644 index 0000000000..bfbd66d754 --- /dev/null +++ b/apps/labrinth/.sqlx/query-7d1f49699e242f3e002afee9bf466b6696052ac6d5ebe131b9e7242104f700af.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT json_agg(to_jsonb(drid))\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n ) AS \"data!: sqlx::types::Json\"\n FROM delphi_report_issues dri\n WHERE dri.id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "data!: sqlx::types::Json", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + null + ] + }, + "hash": "7d1f49699e242f3e002afee9bf466b6696052ac6d5ebe131b9e7242104f700af" +} diff --git a/apps/labrinth/.sqlx/query-749fc694a88b419d30f820b7563aad9db6ed1b17ae7002172b4172b959bd7710.json b/apps/labrinth/.sqlx/query-d9b6a1b7fb133f7aec7599e06e21ac8460088b70cd115c3d5ebf9474aa4d54fa.json similarity index 61% rename from apps/labrinth/.sqlx/query-749fc694a88b419d30f820b7563aad9db6ed1b17ae7002172b4172b959bd7710.json rename to apps/labrinth/.sqlx/query-d9b6a1b7fb133f7aec7599e06e21ac8460088b70cd115c3d5ebf9474aa4d54fa.json index 8c1c8e1f3a..c7719601d7 100644 --- a/apps/labrinth/.sqlx/query-749fc694a88b419d30f820b7563aad9db6ed1b17ae7002172b4172b959bd7710.json +++ b/apps/labrinth/.sqlx/query-d9b6a1b7fb133f7aec7599e06e21ac8460088b70cd115c3d5ebf9474aa4d54fa.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n delphi_report_issues.id AS \"id\", report_id,\n issue_type,\n delphi_report_issues.status AS \"status: DelphiReportIssueStatus\",\n\n file_id, delphi_version, artifact_url, created, severity AS \"severity: DelphiSeverity\",\n json_array(SELECT to_jsonb(delphi_report_issue_details)\n FROM delphi_report_issue_details\n WHERE issue_id = delphi_report_issues.id\n ) AS \"details: sqlx::types::Json>\",\n versions.mod_id AS \"project_id?\", mods.published AS \"project_published?\"\n FROM delphi_report_issues\n INNER JOIN delphi_reports ON delphi_reports.id = report_id\n LEFT OUTER JOIN files ON files.id = file_id\n LEFT OUTER JOIN versions ON versions.id = files.version_id\n LEFT OUTER JOIN mods ON mods.id = versions.mod_id\n WHERE\n (issue_type = $1 OR $1 IS NULL)\n AND (delphi_report_issues.status = $2 OR $2 IS NULL)\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'pending_status_first' THEN delphi_report_issues.status ELSE 'pending'::delphi_report_issue_status END ASC,\n CASE WHEN $3 = 'severity_asc' THEN delphi_reports.severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN delphi_reports.severity ELSE 'low'::delphi_severity END DESC\n OFFSET $5\n LIMIT $4\n ", + "query": "\n SELECT\n delphi_report_issues.id AS \"id\", report_id,\n issue_type,\n delphi_report_issues.status AS \"status: DelphiReportIssueStatus\",\n\n file_id, delphi_version, artifact_url, created, severity AS \"severity: DelphiSeverity\",\n\n -- TODO: replace with `json_array` in Postgres 16\n (\n SELECT json_agg(to_jsonb(delphi_report_issue_details))\n FROM delphi_report_issue_details\n WHERE issue_id = delphi_report_issues.id\n ) AS \"details: sqlx::types::Json>\",\n versions.mod_id AS \"project_id?\", mods.published AS \"project_published?\"\n FROM delphi_report_issues\n INNER JOIN delphi_reports ON delphi_reports.id = report_id\n LEFT OUTER JOIN files ON files.id = file_id\n LEFT OUTER JOIN versions ON versions.id = files.version_id\n LEFT OUTER JOIN mods ON mods.id = versions.mod_id\n WHERE\n (issue_type = $1 OR $1 IS NULL)\n AND (delphi_report_issues.status = $2 OR $2 IS NULL)\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'pending_status_first' THEN delphi_report_issues.status ELSE 'pending'::delphi_report_issue_status END ASC,\n CASE WHEN $3 = 'severity_asc' THEN delphi_reports.severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN delphi_reports.severity ELSE 'low'::delphi_severity END DESC\n OFFSET $5\n LIMIT $4\n ", "describe": { "columns": [ { @@ -74,7 +74,7 @@ { "ordinal": 9, "name": "details: sqlx::types::Json>", - "type_info": "Jsonb" + "type_info": "Json" }, { "ordinal": 10, @@ -122,5 +122,5 @@ true ] }, - "hash": "749fc694a88b419d30f820b7563aad9db6ed1b17ae7002172b4172b959bd7710" + "hash": "d9b6a1b7fb133f7aec7599e06e21ac8460088b70cd115c3d5ebf9474aa4d54fa" } diff --git a/apps/labrinth/.sqlx/query-ff526151c628646e735a2241c20aa07a9bedfa885b171e8141930225c6adaab1.json b/apps/labrinth/.sqlx/query-ff526151c628646e735a2241c20aa07a9bedfa885b171e8141930225c6adaab1.json deleted file mode 100644 index 46f684c829..0000000000 --- a/apps/labrinth/.sqlx/query-ff526151c628646e735a2241c20aa07a9bedfa885b171e8141930225c6adaab1.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n to_jsonb(dri)\n || jsonb_build_object(\n 'details', json_array(\n SELECT to_jsonb(drid)\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n ) AS \"data!: sqlx::types::Json\"\n FROM delphi_report_issues dri\n WHERE dri.id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "data!: sqlx::types::Json", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - null - ] - }, - "hash": "ff526151c628646e735a2241c20aa07a9bedfa885b171e8141930225c6adaab1" -} diff --git a/apps/labrinth/src/database/models/delphi_report_item.rs b/apps/labrinth/src/database/models/delphi_report_item.rs index 8bcaf4d938..0234dff190 100644 --- a/apps/labrinth/src/database/models/delphi_report_item.rs +++ b/apps/labrinth/src/database/models/delphi_report_item.rs @@ -191,7 +191,10 @@ impl DBDelphiReportIssue { delphi_report_issues.status AS "status: DelphiReportIssueStatus", file_id, delphi_version, artifact_url, created, severity AS "severity: DelphiSeverity", - json_array(SELECT to_jsonb(delphi_report_issue_details) + + -- TODO: replace with `json_array` in Postgres 16 + ( + SELECT json_agg(to_jsonb(delphi_report_issue_details)) FROM delphi_report_issue_details WHERE issue_id = delphi_report_issues.id ) AS "details: sqlx::types::Json>", diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index eb2d82a854..1615222c8c 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -187,8 +187,9 @@ async fn get_issue( SELECT to_jsonb(dri) || jsonb_build_object( - 'details', json_array( - SELECT to_jsonb(drid) + -- TODO: replace with `json_array` in Postgres 16 + 'details', ( + SELECT json_agg(to_jsonb(drid)) FROM delphi_report_issue_details drid WHERE drid.issue_id = dri.id ) @@ -242,16 +243,19 @@ async fn get_report( 'file_size', f.size, 'flag_reason', 'delphi', 'download_url', f.url, - 'issues', json_array( - SELECT + -- TODO: replace with `json_array` in Postgres 16 + 'issues', ( + SELECT json_agg( to_jsonb(dri) || jsonb_build_object( - 'details', json_array( - SELECT to_jsonb(drid) + -- TODO: replace with `json_array` in Postgres 16 + 'details', ( + SELECT json_agg(to_jsonb(drid)) FROM delphi_report_issue_details drid WHERE drid.issue_id = dri.id ) ) + ) FROM delphi_report_issues dri WHERE dri.report_id = dr.id ) @@ -356,24 +360,29 @@ async fn search_projects( 'file_size', f.size, 'flag_reason', 'delphi', 'download_url', f.url, - 'issues', json_array( - SELECT + -- TODO: replace with `json_array` in Postgres 16 + 'issues', ( + SELECT json_agg( to_jsonb(dri) || jsonb_build_object( - 'details', json_array( - SELECT jsonb_build_object( - 'id', drid.id, - 'issue_id', drid.issue_id, - 'key', drid.key, - 'file_path', drid.file_path, - -- ignore `decompiled_source` - 'data', drid.data, - 'severity', drid.severity + -- TODO: replace with `json_array` in Postgres 16 + 'details', ( + SELECT json_agg( + jsonb_build_object( + 'id', drid.id, + 'issue_id', drid.issue_id, + 'key', drid.key, + 'file_path', drid.file_path, + -- ignore `decompiled_source` + 'data', drid.data, + 'severity', drid.severity + ) ) FROM delphi_report_issue_details drid WHERE drid.issue_id = dri.id ) ) + ) FROM delphi_report_issues dri WHERE dri.report_id = dr.id ) From 8fe7f20c60a23f5e0b551d6a0a5ac0c0217f0199 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Wed, 26 Nov 2025 21:26:41 +0000 Subject: [PATCH 38/65] fix version creation --- apps/labrinth/src/routes/v3/version_creation.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/labrinth/src/routes/v3/version_creation.rs b/apps/labrinth/src/routes/v3/version_creation.rs index 03ebb81283..d3726df36c 100644 --- a/apps/labrinth/src/routes/v3/version_creation.rs +++ b/apps/labrinth/src/routes/v3/version_creation.rs @@ -935,17 +935,19 @@ pub async fn upload_file( || force_primary || total_files_len == 1; - let file_path = format!( + let file_path_encode = format!( "data/{project_id}/versions/{version_id}/{}", urlencoding::encode(file_name) ); + let file_path = + format!("data/{project_id}/versions/{version_id}/{file_name}"); let upload_data = file_host .upload_file(content_type, &file_path, FileHostPublicity::Public, data) .await?; uploaded_files.push(UploadedFile { - name: file_path.clone(), + name: file_path, publicity: FileHostPublicity::Public, }); @@ -971,7 +973,7 @@ pub async fn upload_file( version_files.push(VersionFileBuilder { filename: file_name.to_string(), - url: format!("{}/{file_path}", dotenvy::var("CDN_URL")?), + url: format!("{}/{file_path_encode}", dotenvy::var("CDN_URL")?), hashes: vec![ models::version_item::HashBuilder { algorithm: "sha1".to_string(), From 1a58c13f5892d9fefc5286c4fd345d714cf2d119 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Sun, 30 Nov 2025 17:56:34 +0000 Subject: [PATCH 39/65] Implement routes --- ...c71674941487c15be1e8ce0ebc78e7c26b34d.json | 15 ++ ...3aaeb30a8da4721959fdee99cf649a8b29e3.json} | 22 ++- ...9698a350098ea5396ce3c327064bb9d7eeb01.json | 26 ++++ ...d07b7d42c37e089403961ee16be0f99958ea0.json | 15 ++ .../20251130173416_delphi_report_verdicts.sql | 2 + .../routes/internal/moderation/tech_review.rs | 141 ++++++++++++++++-- 6 files changed, 201 insertions(+), 20 deletions(-) create mode 100644 apps/labrinth/.sqlx/query-3240e4b5abc9850b5d3c09fafcac71674941487c15be1e8ce0ebc78e7c26b34d.json rename apps/labrinth/.sqlx/{query-365407c545cc4a55236f56e65a579de6c2e84f442b481f42c2f9ab40b547ac7f.json => query-3961aa17ce3219c057c398dca0ed3aaeb30a8da4721959fdee99cf649a8b29e3.json} (77%) create mode 100644 apps/labrinth/.sqlx/query-67f52d745c4b53a9ca70d37aa8c9698a350098ea5396ce3c327064bb9d7eeb01.json create mode 100644 apps/labrinth/.sqlx/query-6cf1862b3c197d42f9183dcbbd3d07b7d42c37e089403961ee16be0f99958ea0.json create mode 100644 apps/labrinth/migrations/20251130173416_delphi_report_verdicts.sql diff --git a/apps/labrinth/.sqlx/query-3240e4b5abc9850b5d3c09fafcac71674941487c15be1e8ce0ebc78e7c26b34d.json b/apps/labrinth/.sqlx/query-3240e4b5abc9850b5d3c09fafcac71674941487c15be1e8ce0ebc78e7c26b34d.json new file mode 100644 index 0000000000..736375ee54 --- /dev/null +++ b/apps/labrinth/.sqlx/query-3240e4b5abc9850b5d3c09fafcac71674941487c15be1e8ce0ebc78e7c26b34d.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE mods\n SET status = $1\n FROM delphi_report_issues dri\n INNER JOIN delphi_reports dr ON dr.id = dri.report_id\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON v.mod_id = m.id\n WHERE dri.id = $2\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + }, + "hash": "3240e4b5abc9850b5d3c09fafcac71674941487c15be1e8ce0ebc78e7c26b34d" +} diff --git a/apps/labrinth/.sqlx/query-365407c545cc4a55236f56e65a579de6c2e84f442b481f42c2f9ab40b547ac7f.json b/apps/labrinth/.sqlx/query-3961aa17ce3219c057c398dca0ed3aaeb30a8da4721959fdee99cf649a8b29e3.json similarity index 77% rename from apps/labrinth/.sqlx/query-365407c545cc4a55236f56e65a579de6c2e84f442b481f42c2f9ab40b547ac7f.json rename to apps/labrinth/.sqlx/query-3961aa17ce3219c057c398dca0ed3aaeb30a8da4721959fdee99cf649a8b29e3.json index 52796b1e9a..134a39e34d 100644 --- a/apps/labrinth/.sqlx/query-365407c545cc4a55236f56e65a579de6c2e84f442b481f42c2f9ab40b547ac7f.json +++ b/apps/labrinth/.sqlx/query-3961aa17ce3219c057c398dca0ed3aaeb30a8da4721959fdee99cf649a8b29e3.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (dr.id)\n dr.id AS report_id,\n dr.created AS report_created,\n dr.severity AS report_severity,\n m.id AS project_id,\n t.id AS project_thread_id,\n\n to_jsonb(dr)\n || jsonb_build_object(\n 'file_id', to_base62(f.id),\n 'version_id', to_base62(v.id),\n 'project_id', to_base62(v.mod_id),\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT json_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT json_agg(\n jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity\n )\n )\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ) AS report\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n INNER JOIN threads t ON t.mod_id = m.id\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($1::int[]) = 0 OR c.project_type = ANY($1::int[]))\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $2 = 'created_asc' THEN t.report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $2 = 'created_desc' THEN t.report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $2 = 'severity_asc' THEN t.report_severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $2 = 'severity_desc' THEN t.report_severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $3\n OFFSET $4\n ", + "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (dr.id)\n dr.id AS report_id,\n dr.created AS report_created,\n dr.severity AS report_severity,\n m.id AS project_id,\n t.id AS project_thread_id,\n\n to_jsonb(dr)\n || jsonb_build_object(\n 'file_id', to_base62(f.id),\n 'version_id', to_base62(v.id),\n 'project_id', to_base62(v.mod_id),\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT json_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT json_agg(\n jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity\n )\n )\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ) AS report\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n INNER JOIN threads t ON t.mod_id = m.id\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[]))\n AND dr.status = $5\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN t.report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN t.report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN t.report_severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN t.report_severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $1\n OFFSET $2\n ", "describe": { "columns": [ { @@ -21,10 +21,22 @@ ], "parameters": { "Left": [ - "Int4Array", - "Text", "Int8", - "Int8" + "Int8", + "Text", + "Int4Array", + { + "Custom": { + "name": "delphi_report_issue_status", + "kind": { + "Enum": [ + "pending", + "safe", + "unsafe" + ] + } + } + } ] }, "nullable": [ @@ -33,5 +45,5 @@ null ] }, - "hash": "365407c545cc4a55236f56e65a579de6c2e84f442b481f42c2f9ab40b547ac7f" + "hash": "3961aa17ce3219c057c398dca0ed3aaeb30a8da4721959fdee99cf649a8b29e3" } diff --git a/apps/labrinth/.sqlx/query-67f52d745c4b53a9ca70d37aa8c9698a350098ea5396ce3c327064bb9d7eeb01.json b/apps/labrinth/.sqlx/query-67f52d745c4b53a9ca70d37aa8c9698a350098ea5396ce3c327064bb9d7eeb01.json new file mode 100644 index 0000000000..419d82de6c --- /dev/null +++ b/apps/labrinth/.sqlx/query-67f52d745c4b53a9ca70d37aa8c9698a350098ea5396ce3c327064bb9d7eeb01.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE delphi_reports dr\n SET status = $1\n WHERE dr.id = $2\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + { + "Custom": { + "name": "delphi_report_issue_status", + "kind": { + "Enum": [ + "pending", + "safe", + "unsafe" + ] + } + } + }, + "Int8" + ] + }, + "nullable": [] + }, + "hash": "67f52d745c4b53a9ca70d37aa8c9698a350098ea5396ce3c327064bb9d7eeb01" +} diff --git a/apps/labrinth/.sqlx/query-6cf1862b3c197d42f9183dcbbd3d07b7d42c37e089403961ee16be0f99958ea0.json b/apps/labrinth/.sqlx/query-6cf1862b3c197d42f9183dcbbd3d07b7d42c37e089403961ee16be0f99958ea0.json new file mode 100644 index 0000000000..f2f509efc7 --- /dev/null +++ b/apps/labrinth/.sqlx/query-6cf1862b3c197d42f9183dcbbd3d07b7d42c37e089403961ee16be0f99958ea0.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE mods\n SET status = $1\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON v.mod_id = m.id\n WHERE dr.id = $2\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + }, + "hash": "6cf1862b3c197d42f9183dcbbd3d07b7d42c37e089403961ee16be0f99958ea0" +} diff --git a/apps/labrinth/migrations/20251130173416_delphi_report_verdicts.sql b/apps/labrinth/migrations/20251130173416_delphi_report_verdicts.sql new file mode 100644 index 0000000000..daa66ce975 --- /dev/null +++ b/apps/labrinth/migrations/20251130173416_delphi_report_verdicts.sql @@ -0,0 +1,2 @@ +ALTER TABLE delphi_reports +ADD COLUMN status delphi_report_issue_status NOT NULL DEFAULT 'pending'; diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 1615222c8c..0ca374c567 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -2,6 +2,7 @@ use std::{collections::HashMap, fmt}; use actix_web::{HttpRequest, get, post, web}; use chrono::{DateTime, Utc}; +use eyre::eyre; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use tokio_stream::StreamExt; @@ -23,7 +24,7 @@ use crate::{ models::{ ids::{FileId, ProjectId, ThreadId, VersionId}, pats::Scopes, - projects::Project, + projects::{Project, ProjectStatus}, threads::Thread, }, queue::session::AuthQueue, @@ -35,6 +36,7 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { cfg.service(search_projects) .service(get_report) .service(get_issue) + .service(update_report) .service(update_issue); } @@ -398,29 +400,31 @@ async fn search_projects( LEFT JOIN categories c ON c.id = mc.joining_category_id WHERE -- project type - (cardinality($1::int[]) = 0 OR c.project_type = ANY($1::int[])) + (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[])) + AND dr.status = $5 ) t -- sorting ORDER BY - CASE WHEN $2 = 'created_asc' THEN t.report_created ELSE TO_TIMESTAMP(0) END ASC, - CASE WHEN $2 = 'created_desc' THEN t.report_created ELSE TO_TIMESTAMP(0) END DESC, - CASE WHEN $2 = 'severity_asc' THEN t.report_severity ELSE 'low'::delphi_severity END ASC, - CASE WHEN $2 = 'severity_desc' THEN t.report_severity ELSE 'low'::delphi_severity END DESC + CASE WHEN $3 = 'created_asc' THEN t.report_created ELSE TO_TIMESTAMP(0) END ASC, + CASE WHEN $3 = 'created_desc' THEN t.report_created ELSE TO_TIMESTAMP(0) END DESC, + CASE WHEN $3 = 'severity_asc' THEN t.report_severity ELSE 'low'::delphi_severity END ASC, + CASE WHEN $3 = 'severity_desc' THEN t.report_severity ELSE 'low'::delphi_severity END DESC -- pagination - LIMIT $3 - OFFSET $4 + LIMIT $1 + OFFSET $2 "#, + limit, + offset, + &sort_by, &search_req .filter .project_type .iter() .map(|ty| ty.0) .collect::>(), - &sort_by, - limit, - offset, + DelphiReportIssueStatus::Pending as _, ) .fetch(&**pool); @@ -498,13 +502,86 @@ async fn search_projects( })) } -/// See [`update_issue`]. +/// See [`update_report`] and [`update_issue`]. #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct UpdateIssue { +pub struct UpdateStatus { /// Status to set the issue to. pub status: DelphiReportIssueStatus, } +/// Updates the state of a project based on a technical review report. +#[utoipa::path( + security(("bearer_auth" = [])), + responses((status = NO_CONTENT)) +)] +#[post("/report/{id}")] +async fn update_report( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, + update_req: web::Json, + path: web::Path<(DelphiReportId,)>, +) -> Result<(), ApiError> { + check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_WRITE, + ) + .await?; + let (report_id,) = path.into_inner(); + + let mut txn = pool + .begin() + .await + .wrap_internal_err("failed to begin transaction")?; + + sqlx::query!( + " + UPDATE delphi_reports dr + SET status = $1 + WHERE dr.id = $2 + ", + update_req.status as _, + report_id as DelphiReportId, + ) + .execute(&mut *txn) + .await + .wrap_internal_err("failed to update report")?; + + if update_req.status == DelphiReportIssueStatus::Unsafe { + let result = sqlx::query!( + " + UPDATE mods + SET status = $1 + FROM delphi_reports dr + INNER JOIN files f ON f.id = dr.file_id + INNER JOIN versions v ON v.id = f.version_id + INNER JOIN mods m ON v.mod_id = m.id + WHERE dr.id = $2 + ", + ProjectStatus::Rejected.as_str(), + report_id as DelphiReportId, + ) + .execute(&mut *txn) + .await + .wrap_internal_err("failed to mark project as rejected")?; + if result.rows_affected() == 0 { + return Err(ApiError::Internal(eyre!( + "no project was marked as rejected" + ))); + } + } + + txn.commit() + .await + .wrap_internal_err("failed to commit transaction")?; + + Ok(()) +} + /// Updates the state of a technical review issue. #[utoipa::path( security(("bearer_auth" = [])), @@ -516,7 +593,7 @@ async fn update_issue( pool: web::Data, redis: web::Data, session_queue: web::Data, - update_req: web::Json, + update_req: web::Json, path: web::Path<(DelphiReportIssueId,)>, ) -> Result<(), ApiError> { check_is_moderator_from_headers( @@ -529,6 +606,11 @@ async fn update_issue( .await?; let (issue_id,) = path.into_inner(); + let mut txn = pool + .begin() + .await + .wrap_internal_err("failed to start transaction")?; + sqlx::query!( " UPDATE delphi_report_issues @@ -538,9 +620,38 @@ async fn update_issue( update_req.status as DelphiReportIssueStatus, issue_id as DelphiReportIssueId, ) - .execute(&**pool) + .execute(&mut *txn) .await .wrap_internal_err("failed to update issue")?; + if update_req.status == DelphiReportIssueStatus::Unsafe { + let result = sqlx::query!( + " + UPDATE mods + SET status = $1 + FROM delphi_report_issues dri + INNER JOIN delphi_reports dr ON dr.id = dri.report_id + INNER JOIN files f ON f.id = dr.file_id + INNER JOIN versions v ON v.id = f.version_id + INNER JOIN mods m ON v.mod_id = m.id + WHERE dri.id = $2 + ", + ProjectStatus::Rejected.as_str(), + issue_id as DelphiReportIssueId, + ) + .execute(&mut *txn) + .await + .wrap_internal_err("failed to mark project as rejected")?; + if result.rows_affected() == 0 { + return Err(ApiError::Internal(eyre!( + "no project was marked as rejected" + ))); + } + } + + txn.commit() + .await + .wrap_internal_err("failed to commit transaction")?; + Ok(()) } From c588c84833c94c454cd12b743b431a9830befc7c Mon Sep 17 00:00:00 2001 From: aecsocket Date: Mon, 1 Dec 2025 00:00:43 +0000 Subject: [PATCH 40/65] fix up tech review --- ...e8ab11a783cdaafa7767a5614227a0098f64e.json} | 18 +++--------------- .../routes/internal/moderation/tech_review.rs | 4 ++-- 2 files changed, 5 insertions(+), 17 deletions(-) rename apps/labrinth/.sqlx/{query-3961aa17ce3219c057c398dca0ed3aaeb30a8da4721959fdee99cf649a8b29e3.json => query-6773964c5557d962a6e488a7856e8ab11a783cdaafa7767a5614227a0098f64e.json} (78%) diff --git a/apps/labrinth/.sqlx/query-3961aa17ce3219c057c398dca0ed3aaeb30a8da4721959fdee99cf649a8b29e3.json b/apps/labrinth/.sqlx/query-6773964c5557d962a6e488a7856e8ab11a783cdaafa7767a5614227a0098f64e.json similarity index 78% rename from apps/labrinth/.sqlx/query-3961aa17ce3219c057c398dca0ed3aaeb30a8da4721959fdee99cf649a8b29e3.json rename to apps/labrinth/.sqlx/query-6773964c5557d962a6e488a7856e8ab11a783cdaafa7767a5614227a0098f64e.json index 134a39e34d..20a74d5803 100644 --- a/apps/labrinth/.sqlx/query-3961aa17ce3219c057c398dca0ed3aaeb30a8da4721959fdee99cf649a8b29e3.json +++ b/apps/labrinth/.sqlx/query-6773964c5557d962a6e488a7856e8ab11a783cdaafa7767a5614227a0098f64e.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (dr.id)\n dr.id AS report_id,\n dr.created AS report_created,\n dr.severity AS report_severity,\n m.id AS project_id,\n t.id AS project_thread_id,\n\n to_jsonb(dr)\n || jsonb_build_object(\n 'file_id', to_base62(f.id),\n 'version_id', to_base62(v.id),\n 'project_id', to_base62(v.mod_id),\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT json_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT json_agg(\n jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity\n )\n )\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ) AS report\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n INNER JOIN threads t ON t.mod_id = m.id\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[]))\n AND dr.status = $5\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN t.report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN t.report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN t.report_severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN t.report_severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $1\n OFFSET $2\n ", + "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (dr.id)\n dr.id AS report_id,\n dr.created AS report_created,\n dr.severity AS report_severity,\n m.id AS project_id,\n t.id AS project_thread_id,\n\n to_jsonb(dr)\n || jsonb_build_object(\n 'file_id', to_base62(f.id),\n 'version_id', to_base62(v.id),\n 'project_id', to_base62(v.mod_id),\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT json_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT json_agg(\n jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity\n )\n )\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ) AS report\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n INNER JOIN threads t ON t.mod_id = m.id\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[]))\n AND NOT m.status = 'draft'\n AND dr.status = 'pending'\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN t.report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN t.report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN t.report_severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN t.report_severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $1\n OFFSET $2\n ", "describe": { "columns": [ { @@ -24,19 +24,7 @@ "Int8", "Int8", "Text", - "Int4Array", - { - "Custom": { - "name": "delphi_report_issue_status", - "kind": { - "Enum": [ - "pending", - "safe", - "unsafe" - ] - } - } - } + "Int4Array" ] }, "nullable": [ @@ -45,5 +33,5 @@ null ] }, - "hash": "3961aa17ce3219c057c398dca0ed3aaeb30a8da4721959fdee99cf649a8b29e3" + "hash": "6773964c5557d962a6e488a7856e8ab11a783cdaafa7767a5614227a0098f64e" } diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 0ca374c567..73012f85f3 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -401,7 +401,8 @@ async fn search_projects( WHERE -- project type (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[])) - AND dr.status = $5 + AND NOT m.status = 'draft' + AND dr.status = 'pending' ) t -- sorting @@ -424,7 +425,6 @@ async fn search_projects( .iter() .map(|ty| ty.0) .collect::>(), - DelphiReportIssueStatus::Pending as _, ) .fetch(&**pool); From c0ce9db529374b83ce5360c65d7ad8fecf4ebdbf Mon Sep 17 00:00:00 2001 From: aecsocket Date: Thu, 4 Dec 2025 11:10:46 +0000 Subject: [PATCH 41/65] Allow adding a moderation comment to Delphi rejections --- ...c71674941487c15be1e8ce0ebc78e7c26b34d.json | 15 -- ...af1ce18c75873335cc01462f1be8b5f3e404b.json | 29 ++++ ...d07b7d42c37e089403961ee16be0f99958ea0.json | 15 -- ...75b109f8124cdd0097e3aeca92c5f3ad0e082.json | 34 ++++ ...c9e2bd27ac9a87987eafd79b06f1c4ecdb659.json | 26 --- .../routes/internal/moderation/tech_review.rs | 157 +++++++++++------- apps/labrinth/src/routes/v3/threads.rs | 42 +++-- 7 files changed, 190 insertions(+), 128 deletions(-) delete mode 100644 apps/labrinth/.sqlx/query-3240e4b5abc9850b5d3c09fafcac71674941487c15be1e8ce0ebc78e7c26b34d.json create mode 100644 apps/labrinth/.sqlx/query-6c0b05d02cb9526280661d7aed5af1ce18c75873335cc01462f1be8b5f3e404b.json delete mode 100644 apps/labrinth/.sqlx/query-6cf1862b3c197d42f9183dcbbd3d07b7d42c37e089403961ee16be0f99958ea0.json create mode 100644 apps/labrinth/.sqlx/query-734a12969dbe3175bb765d356c175b109f8124cdd0097e3aeca92c5f3ad0e082.json delete mode 100644 apps/labrinth/.sqlx/query-b1df83f4592701f8aa03f6d16bac9e2bd27ac9a87987eafd79b06f1c4ecdb659.json diff --git a/apps/labrinth/.sqlx/query-3240e4b5abc9850b5d3c09fafcac71674941487c15be1e8ce0ebc78e7c26b34d.json b/apps/labrinth/.sqlx/query-3240e4b5abc9850b5d3c09fafcac71674941487c15be1e8ce0ebc78e7c26b34d.json deleted file mode 100644 index 736375ee54..0000000000 --- a/apps/labrinth/.sqlx/query-3240e4b5abc9850b5d3c09fafcac71674941487c15be1e8ce0ebc78e7c26b34d.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE mods\n SET status = $1\n FROM delphi_report_issues dri\n INNER JOIN delphi_reports dr ON dr.id = dri.report_id\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON v.mod_id = m.id\n WHERE dri.id = $2\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Int8" - ] - }, - "nullable": [] - }, - "hash": "3240e4b5abc9850b5d3c09fafcac71674941487c15be1e8ce0ebc78e7c26b34d" -} diff --git a/apps/labrinth/.sqlx/query-6c0b05d02cb9526280661d7aed5af1ce18c75873335cc01462f1be8b5f3e404b.json b/apps/labrinth/.sqlx/query-6c0b05d02cb9526280661d7aed5af1ce18c75873335cc01462f1be8b5f3e404b.json new file mode 100644 index 0000000000..7680b1cce2 --- /dev/null +++ b/apps/labrinth/.sqlx/query-6c0b05d02cb9526280661d7aed5af1ce18c75873335cc01462f1be8b5f3e404b.json @@ -0,0 +1,29 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE mods\n SET status = $1\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON v.mod_id = m.id\n INNER JOIN threads t ON t.mod_id = m.id\n WHERE dr.id = $2\n RETURNING\n m.id AS \"project_id: DBProjectId\",\n t.id AS \"thread_id: DBThreadId\"\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "project_id: DBProjectId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "thread_id: DBThreadId", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "6c0b05d02cb9526280661d7aed5af1ce18c75873335cc01462f1be8b5f3e404b" +} diff --git a/apps/labrinth/.sqlx/query-6cf1862b3c197d42f9183dcbbd3d07b7d42c37e089403961ee16be0f99958ea0.json b/apps/labrinth/.sqlx/query-6cf1862b3c197d42f9183dcbbd3d07b7d42c37e089403961ee16be0f99958ea0.json deleted file mode 100644 index f2f509efc7..0000000000 --- a/apps/labrinth/.sqlx/query-6cf1862b3c197d42f9183dcbbd3d07b7d42c37e089403961ee16be0f99958ea0.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE mods\n SET status = $1\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON v.mod_id = m.id\n WHERE dr.id = $2\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Int8" - ] - }, - "nullable": [] - }, - "hash": "6cf1862b3c197d42f9183dcbbd3d07b7d42c37e089403961ee16be0f99958ea0" -} diff --git a/apps/labrinth/.sqlx/query-734a12969dbe3175bb765d356c175b109f8124cdd0097e3aeca92c5f3ad0e082.json b/apps/labrinth/.sqlx/query-734a12969dbe3175bb765d356c175b109f8124cdd0097e3aeca92c5f3ad0e082.json new file mode 100644 index 0000000000..cc0d1ef5cd --- /dev/null +++ b/apps/labrinth/.sqlx/query-734a12969dbe3175bb765d356c175b109f8124cdd0097e3aeca92c5f3ad0e082.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE delphi_report_issues dri\n SET status = $1\n FROM delphi_reports dr\n WHERE dri.id = $2 AND dr.id = dri.report_id\n RETURNING dr.id AS \"report_id: DelphiReportId\"\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "report_id: DelphiReportId", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + { + "Custom": { + "name": "delphi_report_issue_status", + "kind": { + "Enum": [ + "pending", + "safe", + "unsafe" + ] + } + } + }, + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "734a12969dbe3175bb765d356c175b109f8124cdd0097e3aeca92c5f3ad0e082" +} diff --git a/apps/labrinth/.sqlx/query-b1df83f4592701f8aa03f6d16bac9e2bd27ac9a87987eafd79b06f1c4ecdb659.json b/apps/labrinth/.sqlx/query-b1df83f4592701f8aa03f6d16bac9e2bd27ac9a87987eafd79b06f1c4ecdb659.json deleted file mode 100644 index 216435cf2b..0000000000 --- a/apps/labrinth/.sqlx/query-b1df83f4592701f8aa03f6d16bac9e2bd27ac9a87987eafd79b06f1c4ecdb659.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE delphi_report_issues\n SET status = $1\n WHERE id = $2\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - { - "Custom": { - "name": "delphi_report_issue_status", - "kind": { - "Enum": [ - "pending", - "safe", - "unsafe" - ] - } - } - }, - "Int8" - ] - }, - "nullable": [] - }, - "hash": "b1df83f4592701f8aa03f6d16bac9e2bd27ac9a87987eafd79b06f1c4ecdb659" -} diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 73012f85f3..381c812360 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -2,9 +2,8 @@ use std::{collections::HashMap, fmt}; use actix_web::{HttpRequest, get, post, web}; use chrono::{DateTime, Utc}; -use eyre::eyre; use serde::{Deserialize, Serialize}; -use sqlx::PgPool; +use sqlx::{PgPool, PgTransaction}; use tokio_stream::StreamExt; use super::ownership::get_projects_ownership; @@ -25,10 +24,15 @@ use crate::{ ids::{FileId, ProjectId, ThreadId, VersionId}, pats::Scopes, projects::{Project, ProjectStatus}, - threads::Thread, + threads::{MessageBody, Thread}, + users::User, }, queue::session::AuthQueue, - routes::{ApiError, internal::moderation::Ownership}, + routes::{ + ApiError, + internal::moderation::Ownership, + v3::threads::{NewThreadMessage, thread_send_message_internal}, + }, util::error::Context, }; @@ -507,6 +511,68 @@ async fn search_projects( pub struct UpdateStatus { /// Status to set the issue to. pub status: DelphiReportIssueStatus, + /// If `status` is [`DelphiReportIssueStatus::Unsafe`], provides a message + /// to reject the project. + pub message: Option, +} + +async fn maybe_reject_project( + report_id: DelphiReportId, + txn: &mut PgTransaction<'_>, + update_req: &UpdateStatus, + user: &User, + pool: &PgPool, + redis: &RedisPool, +) -> Result<(), ApiError> { + if update_req.status != DelphiReportIssueStatus::Unsafe { + return Ok(()); + }; + + let record = sqlx::query!( + r#" + UPDATE mods + SET status = $1 + FROM delphi_reports dr + INNER JOIN files f ON f.id = dr.file_id + INNER JOIN versions v ON v.id = f.version_id + INNER JOIN mods m ON v.mod_id = m.id + INNER JOIN threads t ON t.mod_id = m.id + WHERE dr.id = $2 + RETURNING + m.id AS "project_id: DBProjectId", + t.id AS "thread_id: DBThreadId" + "#, + ProjectStatus::Rejected.as_str(), + report_id as DelphiReportId, + ) + .fetch_one(&mut **txn) + .await + .wrap_internal_err("failed to mark project as rejected")?; + + if let Some(body) = &update_req.message { + thread_send_message_internal( + user, + record.thread_id.into(), + pool, + NewThreadMessage { + body: MessageBody::Text { + body: body.clone(), + private: true, + replying_to: None, + associated_images: Vec::new(), + }, + }, + redis, + ) + .await + .wrap_internal_err("failed to add moderation thread message")?; + } + + DBProject::clear_cache(record.project_id, None, None, redis) + .await + .wrap_internal_err("failed to clear project cache")?; + + Ok(()) } /// Updates the state of a project based on a technical review report. @@ -523,7 +589,7 @@ async fn update_report( update_req: web::Json, path: web::Path<(DelphiReportId,)>, ) -> Result<(), ApiError> { - check_is_moderator_from_headers( + let user = check_is_moderator_from_headers( &req, &**pool, &redis, @@ -551,29 +617,15 @@ async fn update_report( .await .wrap_internal_err("failed to update report")?; - if update_req.status == DelphiReportIssueStatus::Unsafe { - let result = sqlx::query!( - " - UPDATE mods - SET status = $1 - FROM delphi_reports dr - INNER JOIN files f ON f.id = dr.file_id - INNER JOIN versions v ON v.id = f.version_id - INNER JOIN mods m ON v.mod_id = m.id - WHERE dr.id = $2 - ", - ProjectStatus::Rejected.as_str(), - report_id as DelphiReportId, - ) - .execute(&mut *txn) - .await - .wrap_internal_err("failed to mark project as rejected")?; - if result.rows_affected() == 0 { - return Err(ApiError::Internal(eyre!( - "no project was marked as rejected" - ))); - } - } + maybe_reject_project( + report_id, + &mut txn, + &update_req, + &user, + &pool, + &redis, + ) + .await?; txn.commit() .await @@ -596,7 +648,7 @@ async fn update_issue( update_req: web::Json, path: web::Path<(DelphiReportIssueId,)>, ) -> Result<(), ApiError> { - check_is_moderator_from_headers( + let user = check_is_moderator_from_headers( &req, &**pool, &redis, @@ -611,43 +663,30 @@ async fn update_issue( .await .wrap_internal_err("failed to start transaction")?; - sqlx::query!( - " - UPDATE delphi_report_issues + let record = sqlx::query!( + r#" + UPDATE delphi_report_issues dri SET status = $1 - WHERE id = $2 - ", + FROM delphi_reports dr + WHERE dri.id = $2 AND dr.id = dri.report_id + RETURNING dr.id AS "report_id: DelphiReportId" + "#, update_req.status as DelphiReportIssueStatus, issue_id as DelphiReportIssueId, ) - .execute(&mut *txn) + .fetch_one(&mut *txn) .await .wrap_internal_err("failed to update issue")?; - if update_req.status == DelphiReportIssueStatus::Unsafe { - let result = sqlx::query!( - " - UPDATE mods - SET status = $1 - FROM delphi_report_issues dri - INNER JOIN delphi_reports dr ON dr.id = dri.report_id - INNER JOIN files f ON f.id = dr.file_id - INNER JOIN versions v ON v.id = f.version_id - INNER JOIN mods m ON v.mod_id = m.id - WHERE dri.id = $2 - ", - ProjectStatus::Rejected.as_str(), - issue_id as DelphiReportIssueId, - ) - .execute(&mut *txn) - .await - .wrap_internal_err("failed to mark project as rejected")?; - if result.rows_affected() == 0 { - return Err(ApiError::Internal(eyre!( - "no project was marked as rejected" - ))); - } - } + maybe_reject_project( + record.report_id, + &mut txn, + &update_req, + &user, + &pool, + &redis, + ) + .await?; txn.commit() .await diff --git a/apps/labrinth/src/routes/v3/threads.rs b/apps/labrinth/src/routes/v3/threads.rs index 1a55f7b797..4d4d24735b 100644 --- a/apps/labrinth/src/routes/v3/threads.rs +++ b/apps/labrinth/src/routes/v3/threads.rs @@ -380,7 +380,25 @@ pub async fn thread_send_message( .await? .1; - let string: database::models::DBThreadId = info.into_inner().0.into(); + thread_send_message_internal( + &user, + info.into_inner().0, + &pool, + new_message.into_inner(), + &redis, + ) + .await?; + Ok(HttpResponse::NoContent().finish()) +} + +pub async fn thread_send_message_internal( + user: &User, + thread_id: ThreadId, + pool: &PgPool, + new_message: NewThreadMessage, + redis: &RedisPool, +) -> Result<(), ApiError> { + let string: database::models::DBThreadId = thread_id.into(); let is_private: bool; @@ -406,7 +424,7 @@ pub async fn thread_send_message( if let Some(replying_to) = replying_to { let thread_message = database::models::DBThreadMessage::get( (*replying_to).into(), - &**pool, + pool, ) .await?; @@ -431,7 +449,7 @@ pub async fn thread_send_message( )); } - let result = database::models::DBThread::get(string, &**pool).await?; + let result = database::models::DBThread::get(string, pool).await?; if let Some(thread) = result { if !is_authorized_thread(&thread, &user, &pool).await? { @@ -450,10 +468,9 @@ pub async fn thread_send_message( .await?; if let Some(project_id) = thread.project_id { - let project = database::models::DBProject::get_id( - project_id, &**pool, &redis, - ) - .await?; + let project = + database::models::DBProject::get_id(project_id, pool, &redis) + .await?; if let Some(project) = project && project.inner.status != ProjectStatus::Processing @@ -463,7 +480,7 @@ pub async fn thread_send_message( let members = database::models::DBTeamMember::get_from_team_full( project.inner.team_id, - &**pool, + pool, &redis, ) .await?; @@ -496,10 +513,9 @@ pub async fn thread_send_message( .await?; } } else if let Some(report_id) = thread.report_id { - let report = database::models::report_item::DBReport::get( - report_id, &**pool, - ) - .await?; + let report = + database::models::report_item::DBReport::get(report_id, pool) + .await?; if let Some(report) = report { if report.closed && !user.role.is_mod() { @@ -570,7 +586,7 @@ pub async fn thread_send_message( transaction.commit().await?; - Ok(HttpResponse::NoContent().body("")) + Ok(()) } else { Err(ApiError::NotFound) } From 799a2a92c62f5fc29d5c036d552405f9ae7d49b8 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Thu, 4 Dec 2025 11:22:27 +0000 Subject: [PATCH 42/65] fix up rebase --- apps/labrinth/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/labrinth/Cargo.toml b/apps/labrinth/Cargo.toml index 6f5f88352d..6920d146dd 100644 --- a/apps/labrinth/Cargo.toml +++ b/apps/labrinth/Cargo.toml @@ -71,7 +71,7 @@ json-patch = { workspace = true } lettre = { workspace = true } meilisearch-sdk = { workspace = true, features = ["reqwest"] } modrinth-maxmind = { workspace = true } -modrinth-util = { workspace = true } +modrinth-util = { workspace = true, features = ["decimal", "utoipa"] } muralpay = { workspace = true, features = ["mock", "utoipa"] } murmur2 = { workspace = true } paste = { workspace = true } From 2e672c720dfafe2bed6ea73644e5bf969a01f3bd Mon Sep 17 00:00:00 2001 From: aecsocket Date: Fri, 5 Dec 2025 20:22:02 +0000 Subject: [PATCH 43/65] exclude rejected projects from tech review --- ...a97b23c5d96b97b1005709635a2028c26589ed50e781392f15fd.json} | 4 ++-- apps/labrinth/src/routes/internal/moderation/tech_review.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename apps/labrinth/.sqlx/{query-6773964c5557d962a6e488a7856e8ab11a783cdaafa7767a5614227a0098f64e.json => query-d0d29a17cfc6a97b23c5d96b97b1005709635a2028c26589ed50e781392f15fd.json} (82%) diff --git a/apps/labrinth/.sqlx/query-6773964c5557d962a6e488a7856e8ab11a783cdaafa7767a5614227a0098f64e.json b/apps/labrinth/.sqlx/query-d0d29a17cfc6a97b23c5d96b97b1005709635a2028c26589ed50e781392f15fd.json similarity index 82% rename from apps/labrinth/.sqlx/query-6773964c5557d962a6e488a7856e8ab11a783cdaafa7767a5614227a0098f64e.json rename to apps/labrinth/.sqlx/query-d0d29a17cfc6a97b23c5d96b97b1005709635a2028c26589ed50e781392f15fd.json index 20a74d5803..cbcb9d744c 100644 --- a/apps/labrinth/.sqlx/query-6773964c5557d962a6e488a7856e8ab11a783cdaafa7767a5614227a0098f64e.json +++ b/apps/labrinth/.sqlx/query-d0d29a17cfc6a97b23c5d96b97b1005709635a2028c26589ed50e781392f15fd.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (dr.id)\n dr.id AS report_id,\n dr.created AS report_created,\n dr.severity AS report_severity,\n m.id AS project_id,\n t.id AS project_thread_id,\n\n to_jsonb(dr)\n || jsonb_build_object(\n 'file_id', to_base62(f.id),\n 'version_id', to_base62(v.id),\n 'project_id', to_base62(v.mod_id),\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT json_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT json_agg(\n jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity\n )\n )\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ) AS report\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n INNER JOIN threads t ON t.mod_id = m.id\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[]))\n AND NOT m.status = 'draft'\n AND dr.status = 'pending'\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN t.report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN t.report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN t.report_severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN t.report_severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $1\n OFFSET $2\n ", + "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (dr.id)\n dr.id AS report_id,\n dr.created AS report_created,\n dr.severity AS report_severity,\n m.id AS project_id,\n t.id AS project_thread_id,\n\n to_jsonb(dr)\n || jsonb_build_object(\n 'file_id', to_base62(f.id),\n 'version_id', to_base62(v.id),\n 'project_id', to_base62(v.mod_id),\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT json_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT json_agg(\n jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity\n )\n )\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ) AS report\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n INNER JOIN threads t ON t.mod_id = m.id\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[]))\n AND m.status NOT IN ('draft', 'rejected')\n AND dr.status = 'pending'\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN t.report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN t.report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN t.report_severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN t.report_severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $1\n OFFSET $2\n ", "describe": { "columns": [ { @@ -33,5 +33,5 @@ null ] }, - "hash": "6773964c5557d962a6e488a7856e8ab11a783cdaafa7767a5614227a0098f64e" + "hash": "d0d29a17cfc6a97b23c5d96b97b1005709635a2028c26589ed50e781392f15fd" } diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 381c812360..dc483c5a1a 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -405,7 +405,7 @@ async fn search_projects( WHERE -- project type (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[])) - AND NOT m.status = 'draft' + AND m.status NOT IN ('draft', 'rejected') AND dr.status = 'pending' ) t From 68ac53f509bc61027902f48bf62c99b510e7ac2a Mon Sep 17 00:00:00 2001 From: aecsocket Date: Sun, 7 Dec 2025 17:09:02 +0000 Subject: [PATCH 44/65] add status change msg to tech review thread --- .../routes/internal/moderation/tech_review.rs | 17 ++++++++++++++++- apps/labrinth/src/routes/v3/threads.rs | 16 ++++++++-------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index dc483c5a1a..c8cb78c32c 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -17,6 +17,7 @@ use crate::{ delphi_report_item::{ DelphiReportIssueStatus, DelphiSeverity, ReportIssueDetail, }, + thread_item::ThreadMessageBuilder, }, redis::RedisPool, }, @@ -540,7 +541,8 @@ async fn maybe_reject_project( WHERE dr.id = $2 RETURNING m.id AS "project_id: DBProjectId", - t.id AS "thread_id: DBThreadId" + t.id AS "thread_id: DBThreadId", + (SELECT status FROM mods WHERE id = m.id) AS "old_status!" "#, ProjectStatus::Rejected.as_str(), report_id as DelphiReportId, @@ -568,6 +570,19 @@ async fn maybe_reject_project( .wrap_internal_err("failed to add moderation thread message")?; } + ThreadMessageBuilder { + author_id: Some(user.id.into()), + body: MessageBody::StatusChange { + new_status: ProjectStatus::Rejected, + old_status: ProjectStatus::from_string(&record.old_status), + }, + thread_id: record.thread_id, + hide_identity: user.role.is_mod(), + } + .insert(txn) + .await + .wrap_internal_err("failed to add status change message")?; + DBProject::clear_cache(record.project_id, None, None, redis) .await .wrap_internal_err("failed to clear project cache")?; diff --git a/apps/labrinth/src/routes/v3/threads.rs b/apps/labrinth/src/routes/v3/threads.rs index 4d4d24735b..90233000ea 100644 --- a/apps/labrinth/src/routes/v3/threads.rs +++ b/apps/labrinth/src/routes/v3/threads.rs @@ -452,7 +452,7 @@ pub async fn thread_send_message_internal( let result = database::models::DBThread::get(string, pool).await?; if let Some(thread) = result { - if !is_authorized_thread(&thread, &user, &pool).await? { + if !is_authorized_thread(&thread, user, pool).await? { return Err(ApiError::NotFound); } @@ -469,7 +469,7 @@ pub async fn thread_send_message_internal( if let Some(project_id) = thread.project_id { let project = - database::models::DBProject::get_id(project_id, pool, &redis) + database::models::DBProject::get_id(project_id, pool, redis) .await?; if let Some(project) = project @@ -481,7 +481,7 @@ pub async fn thread_send_message_internal( database::models::DBTeamMember::get_from_team_full( project.inner.team_id, pool, - &redis, + redis, ) .await?; @@ -496,7 +496,7 @@ pub async fn thread_send_message_internal( .insert_many( members.iter().map(|x| x.user_id).collect(), &mut transaction, - &redis, + redis, ) .await?; @@ -508,7 +508,7 @@ pub async fn thread_send_message_internal( .insert_many( members.iter().map(|x| x.user_id).collect(), &mut transaction, - &redis, + redis, ) .await?; } @@ -533,7 +533,7 @@ pub async fn thread_send_message_internal( report_id: Some(report.id.into()), }, } - .insert(report.reporter, &mut transaction, &redis) + .insert(report.reporter, &mut transaction, redis) .await?; } } @@ -547,7 +547,7 @@ pub async fn thread_send_message_internal( if let Some(db_image) = image_item::DBImage::get( (*image_id).into(), &mut *transaction, - &redis, + redis, ) .await? { @@ -574,7 +574,7 @@ pub async fn thread_send_message_internal( .execute(&mut *transaction) .await?; - image_item::DBImage::clear_cache(image.id.into(), &redis) + image_item::DBImage::clear_cache(image.id.into(), redis) .await?; } else { return Err(ApiError::InvalidInput(format!( From 8b80e8df0ce4e6519dbbc3e1c1020de4e5207537 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Sun, 7 Dec 2025 23:10:29 +0000 Subject: [PATCH 45/65] cargo sqlx prepare --- ...0aee10c39237993878d96750a24acce425ae80f6d74.json} | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) rename apps/labrinth/.sqlx/{query-6c0b05d02cb9526280661d7aed5af1ce18c75873335cc01462f1be8b5f3e404b.json => query-24a4b1fd6f7aad9c8fe700aee10c39237993878d96750a24acce425ae80f6d74.json} (70%) diff --git a/apps/labrinth/.sqlx/query-6c0b05d02cb9526280661d7aed5af1ce18c75873335cc01462f1be8b5f3e404b.json b/apps/labrinth/.sqlx/query-24a4b1fd6f7aad9c8fe700aee10c39237993878d96750a24acce425ae80f6d74.json similarity index 70% rename from apps/labrinth/.sqlx/query-6c0b05d02cb9526280661d7aed5af1ce18c75873335cc01462f1be8b5f3e404b.json rename to apps/labrinth/.sqlx/query-24a4b1fd6f7aad9c8fe700aee10c39237993878d96750a24acce425ae80f6d74.json index 7680b1cce2..091b9b808d 100644 --- a/apps/labrinth/.sqlx/query-6c0b05d02cb9526280661d7aed5af1ce18c75873335cc01462f1be8b5f3e404b.json +++ b/apps/labrinth/.sqlx/query-24a4b1fd6f7aad9c8fe700aee10c39237993878d96750a24acce425ae80f6d74.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n UPDATE mods\n SET status = $1\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON v.mod_id = m.id\n INNER JOIN threads t ON t.mod_id = m.id\n WHERE dr.id = $2\n RETURNING\n m.id AS \"project_id: DBProjectId\",\n t.id AS \"thread_id: DBThreadId\"\n ", + "query": "\n UPDATE mods\n SET status = $1\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON v.mod_id = m.id\n INNER JOIN threads t ON t.mod_id = m.id\n WHERE dr.id = $2\n RETURNING\n m.id AS \"project_id: DBProjectId\",\n t.id AS \"thread_id: DBThreadId\",\n (SELECT status FROM mods WHERE id = m.id) AS \"old_status!\"\n ", "describe": { "columns": [ { @@ -12,6 +12,11 @@ "ordinal": 1, "name": "thread_id: DBThreadId", "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "old_status!", + "type_info": "Varchar" } ], "parameters": { @@ -22,8 +27,9 @@ }, "nullable": [ false, - false + false, + null ] }, - "hash": "6c0b05d02cb9526280661d7aed5af1ce18c75873335cc01462f1be8b5f3e404b" + "hash": "24a4b1fd6f7aad9c8fe700aee10c39237993878d96750a24acce425ae80f6d74" } From dc918f8c69f677e9de335e9a574a6176fd970fec Mon Sep 17 00:00:00 2001 From: aecsocket Date: Tue, 9 Dec 2025 13:38:17 +0000 Subject: [PATCH 46/65] also ignore withheld projects --- apps/labrinth/src/routes/internal/moderation/tech_review.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index c8cb78c32c..b4d2ec4358 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -406,7 +406,7 @@ async fn search_projects( WHERE -- project type (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[])) - AND m.status NOT IN ('draft', 'rejected') + AND m.status NOT IN ('draft', 'rejected', 'withheld') AND dr.status = 'pending' ) t From 8b4e1d736895c4259e8c3adc9464fda6693ce4bf Mon Sep 17 00:00:00 2001 From: aecsocket Date: Tue, 9 Dec 2025 13:57:51 +0000 Subject: [PATCH 47/65] More filtering on issue search --- .../src/routes/internal/moderation/tech_review.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index b4d2ec4358..d13a016aed 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -392,6 +392,17 @@ async fn search_projects( ) FROM delphi_report_issues dri WHERE dri.report_id = dr.id + AND NOT EXISTS ( + -- exclude issues with types that have been marked as safe for this project + SELECT 1 + FROM delphi_report_issues dri_safe + INNER JOIN delphi_reports dr_safe ON dr_safe.id = dri_safe.report_id + INNER JOIN files f_safe ON f_safe.id = dr_safe.file_id + INNER JOIN versions v_safe ON v_safe.id = f_safe.version_id + WHERE dri_safe.issue_type = dri.issue_type + AND dri_safe.status = 'safe' + AND v_safe.mod_id = m.id + ) ) ) AS report FROM delphi_reports dr From f0a388c1612cb4c7930ea90c9cff44c1988f4271 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Thu, 11 Dec 2025 13:22:55 +0000 Subject: [PATCH 48/65] wip: report routes --- .../src/database/models/delphi_report_item.rs | 26 +- apps/labrinth/src/models/v3/threads.rs | 4 + .../routes/internal/moderation/tech_review.rs | 241 +++++++++--------- 3 files changed, 147 insertions(+), 124 deletions(-) diff --git a/apps/labrinth/src/database/models/delphi_report_item.rs b/apps/labrinth/src/database/models/delphi_report_item.rs index 0234dff190..ebe6ed437b 100644 --- a/apps/labrinth/src/database/models/delphi_report_item.rs +++ b/apps/labrinth/src/database/models/delphi_report_item.rs @@ -91,7 +91,7 @@ pub struct DBDelphiReportIssue { pub status: DelphiReportIssueStatus, } -/// An status a Delphi report issue can have. +/// A status a Delphi report issue can have. #[derive( Deserialize, Serialize, @@ -124,6 +124,30 @@ impl Display for DelphiReportIssueStatus { } } +/// What verdict a moderator can give to a project flagged for technical review. +#[derive( + Deserialize, + Serialize, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + sqlx::Type, + utoipa::ToSchema, +)] +#[serde(rename_all = "snake_case")] +pub enum DelphiVerdict { + /// The issue has been rejected (i.e., reviewed as a false positive). + /// The affected artifact has thus been verified to be clean, other issues + /// with it notwithstanding. + Safe, + /// The issue has been approved (i.e., reviewed as a valid, true positive). + /// The affected artifact has thus been verified to be potentially malicious. + Unsafe, +} + /// An order in which Delphi report issues can be sorted during queries. #[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash)] #[serde(rename_all = "snake_case")] diff --git a/apps/labrinth/src/models/v3/threads.rs b/apps/labrinth/src/models/v3/threads.rs index 5b918899d2..095b5b4b55 100644 --- a/apps/labrinth/src/models/v3/threads.rs +++ b/apps/labrinth/src/models/v3/threads.rs @@ -1,3 +1,4 @@ +use crate::database::models::delphi_report_item::DelphiVerdict; use crate::models::ids::{ ImageId, ProjectId, ReportId, ThreadId, ThreadMessageId, }; @@ -42,6 +43,9 @@ pub enum MessageBody { new_status: ProjectStatus, old_status: ProjectStatus, }, + TechReview { + verdict: DelphiVerdict, + }, ThreadClosure, ThreadReopen, Deleted { diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index d13a016aed..6cdfc19c3b 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, fmt}; use actix_web::{HttpRequest, get, post, web}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use sqlx::{PgPool, PgTransaction}; +use sqlx::PgPool; use tokio_stream::StreamExt; use super::ownership::get_projects_ownership; @@ -15,7 +15,8 @@ use crate::{ DBProjectId, DBThread, DBThreadId, DBUser, DelphiReportId, DelphiReportIssueId, ProjectTypeId, delphi_report_item::{ - DelphiReportIssueStatus, DelphiSeverity, ReportIssueDetail, + DelphiReportIssueStatus, DelphiSeverity, DelphiVerdict, + ReportIssueDetail, }, thread_item::ThreadMessageBuilder, }, @@ -24,16 +25,11 @@ use crate::{ models::{ ids::{FileId, ProjectId, ThreadId, VersionId}, pats::Scopes, - projects::{Project, ProjectStatus}, + projects::Project, threads::{MessageBody, Thread}, - users::User, }, queue::session::AuthQueue, - routes::{ - ApiError, - internal::moderation::Ownership, - v3::threads::{NewThreadMessage, thread_send_message_internal}, - }, + routes::{ApiError, internal::moderation::Ownership}, util::error::Context, }; @@ -392,17 +388,17 @@ async fn search_projects( ) FROM delphi_report_issues dri WHERE dri.report_id = dr.id - AND NOT EXISTS ( - -- exclude issues with types that have been marked as safe for this project - SELECT 1 - FROM delphi_report_issues dri_safe - INNER JOIN delphi_reports dr_safe ON dr_safe.id = dri_safe.report_id - INNER JOIN files f_safe ON f_safe.id = dr_safe.file_id - INNER JOIN versions v_safe ON v_safe.id = f_safe.version_id - WHERE dri_safe.issue_type = dri.issue_type - AND dri_safe.status = 'safe' - AND v_safe.mod_id = m.id - ) + -- AND NOT EXISTS ( + -- -- exclude issues with types that have been marked as safe for this project + -- SELECT 1 + -- FROM delphi_report_issues dri_safe + -- INNER JOIN delphi_reports dr_safe ON dr_safe.id = dri_safe.report_id + -- INNER JOIN files f_safe ON f_safe.id = dr_safe.file_id + -- INNER JOIN versions v_safe ON v_safe.id = f_safe.version_id + -- WHERE dri_safe.issue_type = dri.issue_type + -- AND dri_safe.status = 'safe' + -- AND v_safe.mod_id = m.id + -- ) ) ) AS report FROM delphi_reports dr @@ -522,84 +518,71 @@ async fn search_projects( #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct UpdateStatus { /// Status to set the issue to. - pub status: DelphiReportIssueStatus, + pub status: DelphiVerdict, /// If `status` is [`DelphiReportIssueStatus::Unsafe`], provides a message /// to reject the project. pub message: Option, } -async fn maybe_reject_project( - report_id: DelphiReportId, - txn: &mut PgTransaction<'_>, - update_req: &UpdateStatus, - user: &User, - pool: &PgPool, - redis: &RedisPool, -) -> Result<(), ApiError> { - if update_req.status != DelphiReportIssueStatus::Unsafe { - return Ok(()); - }; - - let record = sqlx::query!( - r#" - UPDATE mods - SET status = $1 - FROM delphi_reports dr - INNER JOIN files f ON f.id = dr.file_id - INNER JOIN versions v ON v.id = f.version_id - INNER JOIN mods m ON v.mod_id = m.id - INNER JOIN threads t ON t.mod_id = m.id - WHERE dr.id = $2 - RETURNING - m.id AS "project_id: DBProjectId", - t.id AS "thread_id: DBThreadId", - (SELECT status FROM mods WHERE id = m.id) AS "old_status!" - "#, - ProjectStatus::Rejected.as_str(), - report_id as DelphiReportId, - ) - .fetch_one(&mut **txn) - .await - .wrap_internal_err("failed to mark project as rejected")?; - - if let Some(body) = &update_req.message { - thread_send_message_internal( - user, - record.thread_id.into(), - pool, - NewThreadMessage { - body: MessageBody::Text { - body: body.clone(), - private: true, - replying_to: None, - associated_images: Vec::new(), - }, - }, - redis, - ) - .await - .wrap_internal_err("failed to add moderation thread message")?; - } - - ThreadMessageBuilder { - author_id: Some(user.id.into()), - body: MessageBody::StatusChange { - new_status: ProjectStatus::Rejected, - old_status: ProjectStatus::from_string(&record.old_status), - }, - thread_id: record.thread_id, - hide_identity: user.role.is_mod(), - } - .insert(txn) - .await - .wrap_internal_err("failed to add status change message")?; - - DBProject::clear_cache(record.project_id, None, None, redis) - .await - .wrap_internal_err("failed to clear project cache")?; - - Ok(()) -} +// async fn maybe_reject_project( +// report_id: DelphiReportId, +// txn: &mut PgTransaction<'_>, +// update_req: &UpdateStatus, +// user: &User, +// pool: &PgPool, +// redis: &RedisPool, +// ) -> Result<(), ApiError> { +// if update_req.status != DelphiVerdict::Unsafe { +// return Ok(()); +// }; + +// let record = sqlx::query!( +// r#" +// UPDATE mods +// SET status = $1 +// FROM delphi_reports dr +// INNER JOIN files f ON f.id = dr.file_id +// INNER JOIN versions v ON v.id = f.version_id +// INNER JOIN mods m ON v.mod_id = m.id +// INNER JOIN threads t ON t.mod_id = m.id +// WHERE dr.id = $2 +// RETURNING +// m.id AS "project_id: DBProjectId", +// t.id AS "thread_id: DBThreadId", +// (SELECT status FROM mods WHERE id = m.id) AS "old_status!" +// "#, +// ProjectStatus::Rejected.as_str(), +// report_id as DelphiReportId, +// ) +// .fetch_one(&mut **txn) +// .await +// .wrap_internal_err("failed to mark project as rejected")?; + +// if let Some(body) = &update_req.message { +// thread_send_message_internal( +// user, +// record.thread_id.into(), +// pool, +// NewThreadMessage { +// body: MessageBody::Text { +// body: body.clone(), +// private: true, +// replying_to: None, +// associated_images: Vec::new(), +// }, +// }, +// redis, +// ) +// .await +// .wrap_internal_err("failed to add moderation thread message")?; +// } + +// // DBProject::clear_cache(record.project_id, None, None, redis) +// // .await +// // .wrap_internal_err("failed to clear project cache")?; + +// Ok(()) +// } /// Updates the state of a project based on a technical review report. #[utoipa::path( @@ -630,28 +613,43 @@ async fn update_report( .await .wrap_internal_err("failed to begin transaction")?; - sqlx::query!( - " - UPDATE delphi_reports dr + let status = match update_req.status { + DelphiVerdict::Safe => DelphiReportIssueStatus::Safe, + DelphiVerdict::Unsafe => DelphiReportIssueStatus::Unsafe, + }; + let record = sqlx::query!( + r#" + UPDATE delphi_reports SET status = $1 + FROM delphi_reports dr + INNER JOIN files f ON f.id = dr.file_id + INNER JOIN versions v ON v.id = f.version_id + INNER JOIN mods m ON v.mod_id = m.id + INNER JOIN threads t ON t.mod_id = m.id WHERE dr.id = $2 - ", - update_req.status as _, - report_id as DelphiReportId, + RETURNING + m.id AS "project_id: DBProjectId", + t.id AS "thread_id: DBThreadId", + (SELECT status FROM mods WHERE id = m.id) AS "old_status!" + "#, + status as _, + report_id as _, ) - .execute(&mut *txn) + .fetch_one(&mut *txn) .await .wrap_internal_err("failed to update report")?; - maybe_reject_project( - report_id, - &mut txn, - &update_req, - &user, - &pool, - &redis, - ) - .await?; + ThreadMessageBuilder { + author_id: Some(user.id.into()), + body: MessageBody::TechReview { + verdict: update_req.status, + }, + thread_id: record.thread_id, + hide_identity: user.role.is_mod(), + } + .insert(&mut txn) + .await + .wrap_internal_err("failed to add tech review message")?; txn.commit() .await @@ -661,6 +659,9 @@ async fn update_report( } /// Updates the state of a technical review issue. +/// +/// This will not automatically reject the project for malware, but just flag +/// this issue with a verdict. #[utoipa::path( security(("bearer_auth" = [])), responses((status = NO_CONTENT)) @@ -674,7 +675,7 @@ async fn update_issue( update_req: web::Json, path: web::Path<(DelphiReportIssueId,)>, ) -> Result<(), ApiError> { - let user = check_is_moderator_from_headers( + let _user = check_is_moderator_from_headers( &req, &**pool, &redis, @@ -689,7 +690,11 @@ async fn update_issue( .await .wrap_internal_err("failed to start transaction")?; - let record = sqlx::query!( + let status = match update_req.status { + DelphiVerdict::Safe => DelphiReportIssueStatus::Safe, + DelphiVerdict::Unsafe => DelphiReportIssueStatus::Unsafe, + }; + let _record = sqlx::query!( r#" UPDATE delphi_report_issues dri SET status = $1 @@ -697,23 +702,13 @@ async fn update_issue( WHERE dri.id = $2 AND dr.id = dri.report_id RETURNING dr.id AS "report_id: DelphiReportId" "#, - update_req.status as DelphiReportIssueStatus, - issue_id as DelphiReportIssueId, + status as _, + issue_id as _, ) .fetch_one(&mut *txn) .await .wrap_internal_err("failed to update issue")?; - maybe_reject_project( - record.report_id, - &mut txn, - &update_req, - &user, - &pool, - &redis, - ) - .await?; - txn.commit() .await .wrap_internal_err("failed to commit transaction")?; From c962bf4037ade767aa0e726e06181ca65822911a Mon Sep 17 00:00:00 2001 From: aecsocket Date: Thu, 11 Dec 2025 14:05:39 +0000 Subject: [PATCH 49/65] Fix up for build --- apps/labrinth/src/models/v2/threads.rs | 10 ++ .../routes/internal/moderation/tech_review.rs | 160 +++++++++++++++--- apps/labrinth/src/routes/mod.rs | 17 ++ 3 files changed, 163 insertions(+), 24 deletions(-) diff --git a/apps/labrinth/src/models/v2/threads.rs b/apps/labrinth/src/models/v2/threads.rs index d5aa45bfd8..969d35622d 100644 --- a/apps/labrinth/src/models/v2/threads.rs +++ b/apps/labrinth/src/models/v2/threads.rs @@ -94,6 +94,16 @@ impl From for LegacyMessageBody { new_status, old_status, }, + crate::models::v3::threads::MessageBody::TechReview { verdict } => { + LegacyMessageBody::Text { + body: format!( + "(legacy) Reviewed technical report and gave verdict {verdict:?}" + ), + private: true, + replying_to: None, + associated_images: Vec::new(), + } + } crate::models::v3::threads::MessageBody::ThreadClosure => { LegacyMessageBody::ThreadClosure } diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 6cdfc19c3b..1987d8eb57 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -25,7 +25,7 @@ use crate::{ models::{ ids::{FileId, ProjectId, ThreadId, VersionId}, pats::Scopes, - projects::Project, + projects::{Project, ProjectStatus}, threads::{MessageBody, Thread}, }, queue::session::AuthQueue, @@ -37,7 +37,7 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { cfg.service(search_projects) .service(get_report) .service(get_issue) - .service(update_report) + .service(submit_report) .service(update_issue); } @@ -294,7 +294,7 @@ pub struct SearchResponse { /// Limited set of project information returned by [`search_projects`]. #[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] pub struct ProjectModerationInfo { - /// Projecet ID. + /// Project ID. pub id: ProjectId, /// Project moderation thread ID. pub thread_id: ThreadId, @@ -514,16 +514,6 @@ async fn search_projects( })) } -/// See [`update_report`] and [`update_issue`]. -#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct UpdateStatus { - /// Status to set the issue to. - pub status: DelphiVerdict, - /// If `status` is [`DelphiReportIssueStatus::Unsafe`], provides a message - /// to reject the project. - pub message: Option, -} - // async fn maybe_reject_project( // report_id: DelphiReportId, // txn: &mut PgTransaction<'_>, @@ -584,18 +574,34 @@ pub struct UpdateStatus { // Ok(()) // } -/// Updates the state of a project based on a technical review report. +/// See [`submit_report`]. +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct SubmitReport { + /// Moderator message to send to the thread when rejecting the project. + pub message: Option, +} + +/// Submits a verdict for a technical report of a project. +/// +/// Before this is called, all issues for this project must have been marked as +/// either safe or unsafe. Otherwise, this will error with +/// [`ApiError::TechReviewIssuesWithNoVerdict`], providing the issue IDs which +/// are still unmarked. +/// +/// If at least one issue is [`DelphiReportIssueStatus::Unsafe`], the report +/// will be marked as unsafe and the project will be rejected. Otherwise, the +/// report will be marked as safe. #[utoipa::path( security(("bearer_auth" = [])), responses((status = NO_CONTENT)) )] #[post("/report/{id}")] -async fn update_report( +async fn submit_report( req: HttpRequest, pool: web::Data, redis: web::Data, session_queue: web::Data, - update_req: web::Json, + submit_report: web::Json, path: web::Path<(DelphiReportId,)>, ) -> Result<(), ApiError> { let user = check_is_moderator_from_headers( @@ -613,9 +619,52 @@ async fn update_report( .await .wrap_internal_err("failed to begin transaction")?; - let status = match update_req.status { - DelphiVerdict::Safe => DelphiReportIssueStatus::Safe, - DelphiVerdict::Unsafe => DelphiReportIssueStatus::Unsafe, + let pending_issues = sqlx::query!( + " + SELECT + dri.id AS issue_id + FROM delphi_report_issues dri + INNER JOIN delphi_reports dr ON dr.id = dri.report_id + WHERE + dr.id = $1 + AND dri.status = 'pending' + ", + report_id as _, + ) + .fetch_all(&mut *txn) + .await + .wrap_internal_err("failed to fetch pending issues")?; + + if !pending_issues.is_empty() { + return Err(ApiError::TechReviewIssuesWithNoVerdict { + issues: pending_issues + .into_iter() + .map(|record| DelphiReportIssueId(record.issue_id)) + .collect(), + }); + } + + let has_unsafe_issues = sqlx::query!( + r#" + SELECT EXISTS( + SELECT 1 FROM delphi_report_issues dri + WHERE + dri.report_id = $1 + AND dri.status = 'unsafe' + LIMIT 1 + ) AS "has_unsafe_issues!" + "#, + report_id as _, + ) + .fetch_one(&mut *txn) + .await + .wrap_internal_err("failed to fetch unsafe issues")? + .has_unsafe_issues; + + let (verdict, status) = if has_unsafe_issues { + (DelphiVerdict::Unsafe, DelphiReportIssueStatus::Unsafe) + } else { + (DelphiVerdict::Safe, DelphiReportIssueStatus::Safe) }; let record = sqlx::query!( r#" @@ -639,11 +688,26 @@ async fn update_report( .await .wrap_internal_err("failed to update report")?; + if let Some(body) = submit_report.0.message { + ThreadMessageBuilder { + author_id: Some(user.id.into()), + body: MessageBody::Text { + body, + private: true, + replying_to: None, + associated_images: Vec::new(), + }, + thread_id: record.thread_id, + hide_identity: user.role.is_mod(), + } + .insert(&mut txn) + .await + .wrap_internal_err("failed to add moderator message")?; + } + ThreadMessageBuilder { author_id: Some(user.id.into()), - body: MessageBody::TechReview { - verdict: update_req.status, - }, + body: MessageBody::TechReview { verdict }, thread_id: record.thread_id, hide_identity: user.role.is_mod(), } @@ -651,6 +715,47 @@ async fn update_report( .await .wrap_internal_err("failed to add tech review message")?; + if verdict == DelphiVerdict::Unsafe { + let record = sqlx::query!( + r#" + UPDATE mods + SET status = $1 + FROM delphi_reports dr + INNER JOIN files f ON f.id = dr.file_id + INNER JOIN versions v ON v.id = f.version_id + INNER JOIN mods m ON v.mod_id = m.id + INNER JOIN threads t ON t.mod_id = m.id + WHERE dr.id = $2 + RETURNING + m.id AS "project_id: DBProjectId", + t.id AS "thread_id: DBThreadId", + (SELECT status FROM mods WHERE id = m.id) AS "old_status!" + "#, + ProjectStatus::Rejected.as_str(), + report_id as DelphiReportId, + ) + .fetch_one(&mut *txn) + .await + .wrap_internal_err("failed to mark project as rejected")?; + + ThreadMessageBuilder { + author_id: Some(user.id.into()), + body: MessageBody::StatusChange { + new_status: ProjectStatus::Rejected, + old_status: ProjectStatus::from_string(&record.old_status), + }, + thread_id: record.thread_id, + hide_identity: user.role.is_mod(), + } + .insert(&mut txn) + .await + .wrap_internal_err("failed to add tech review message")?; + + DBProject::clear_cache(record.project_id, None, None, &redis) + .await + .wrap_internal_err("failed to clear project cache")?; + } + txn.commit() .await .wrap_internal_err("failed to commit transaction")?; @@ -658,6 +763,13 @@ async fn update_report( Ok(()) } +/// See [`update_issue`]. +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct UpdateIssue { + /// What the moderator has decided the outcome of this issue is. + pub verdict: DelphiVerdict, +} + /// Updates the state of a technical review issue. /// /// This will not automatically reject the project for malware, but just flag @@ -672,7 +784,7 @@ async fn update_issue( pool: web::Data, redis: web::Data, session_queue: web::Data, - update_req: web::Json, + update_req: web::Json, path: web::Path<(DelphiReportIssueId,)>, ) -> Result<(), ApiError> { let _user = check_is_moderator_from_headers( @@ -690,7 +802,7 @@ async fn update_issue( .await .wrap_internal_err("failed to start transaction")?; - let status = match update_req.status { + let status = match update_req.verdict { DelphiVerdict::Safe => DelphiReportIssueStatus::Safe, DelphiVerdict::Unsafe => DelphiReportIssueStatus::Unsafe, }; diff --git a/apps/labrinth/src/routes/mod.rs b/apps/labrinth/src/routes/mod.rs index f5b445a3ad..f79a391a80 100644 --- a/apps/labrinth/src/routes/mod.rs +++ b/apps/labrinth/src/routes/mod.rs @@ -1,3 +1,4 @@ +use crate::database::models::DelphiReportIssueId; use crate::file_hosting::FileHostingError; use crate::routes::analytics::{page_view_ingest, playtime_ingest}; use crate::util::cors::default_cors; @@ -7,6 +8,7 @@ use actix_files::Files; use actix_web::http::StatusCode; use actix_web::{HttpResponse, web}; use futures::FutureExt; +use serde_json::json; pub mod internal; pub mod v2; @@ -165,6 +167,8 @@ pub enum ApiError { Delphi(reqwest::Error), #[error(transparent)] Mural(#[from] Box), + #[error("report still has {} issues with no verdict", issues.len())] + TechReviewIssuesWithNoVerdict { issues: Vec }, } impl ApiError { @@ -207,6 +211,9 @@ impl ApiError { Self::Slack(..) => "slack_error", Self::Delphi(..) => "delphi_error", Self::Mural(..) => "mural_error", + Self::TechReviewIssuesWithNoVerdict { .. } => { + "tech_review_issues_with_no_verdict" + } }, description: match self { Self::Internal(e) => format!("{e:#?}"), @@ -216,6 +223,13 @@ impl ApiError { }, details: match self { Self::Mural(err) => serde_json::to_value(err.clone()).ok(), + Self::TechReviewIssuesWithNoVerdict { issues } => { + let issues = serde_json::to_value(issues) + .expect("issues should never fail to serialize"); + Some(json!({ + "issues": issues + })) + } _ => None, }, } @@ -261,6 +275,9 @@ impl actix_web::ResponseError for ApiError { Self::Slack(..) => StatusCode::INTERNAL_SERVER_ERROR, Self::Delphi(..) => StatusCode::INTERNAL_SERVER_ERROR, Self::Mural(..) => StatusCode::BAD_REQUEST, + Self::TechReviewIssuesWithNoVerdict { .. } => { + StatusCode::BAD_REQUEST + } } } From f00c2489de9955a83b18ea8793211522469945ee Mon Sep 17 00:00:00 2001 From: aecsocket Date: Thu, 11 Dec 2025 14:42:24 +0000 Subject: [PATCH 50/65] cargo sqlx prepare --- ...f4f61b5e4fab7674563355646d35d0bc4e349.json | 22 +++++++++ ...c39237993878d96750a24acce425ae80f6d74.json | 35 -------------- ...35aa22f43f2431cee23d2994b1d10b94f093.json} | 4 +- ...9698a350098ea5396ce3c327064bb9d7eeb01.json | 26 ----------- ...add34bf62552a5bfd5d0a9637e88641997d6e.json | 35 ++++++++++++++ ...b721371ab3f09782f69ec9cf4c3b4255444f7.json | 22 +++++++++ ...7e5abe8dd39f0e42a8c9478d02e4c919bafca.json | 46 +++++++++++++++++++ 7 files changed, 127 insertions(+), 63 deletions(-) create mode 100644 apps/labrinth/.sqlx/query-190db3c55d352a23d823fa4f98df4f61b5e4fab7674563355646d35d0bc4e349.json delete mode 100644 apps/labrinth/.sqlx/query-24a4b1fd6f7aad9c8fe700aee10c39237993878d96750a24acce425ae80f6d74.json rename apps/labrinth/.sqlx/{query-d0d29a17cfc6a97b23c5d96b97b1005709635a2028c26589ed50e781392f15fd.json => query-488fd3f6728813a1e83f45d74fff35aa22f43f2431cee23d2994b1d10b94f093.json} (58%) delete mode 100644 apps/labrinth/.sqlx/query-67f52d745c4b53a9ca70d37aa8c9698a350098ea5396ce3c327064bb9d7eeb01.json create mode 100644 apps/labrinth/.sqlx/query-a0e44cc4133730034bf8ead188eadd34bf62552a5bfd5d0a9637e88641997d6e.json create mode 100644 apps/labrinth/.sqlx/query-a8e7a2496ab037761cb5c80704fb721371ab3f09782f69ec9cf4c3b4255444f7.json create mode 100644 apps/labrinth/.sqlx/query-f8cad5334a48dbe91cb6d4a0ac57e5abe8dd39f0e42a8c9478d02e4c919bafca.json diff --git a/apps/labrinth/.sqlx/query-190db3c55d352a23d823fa4f98df4f61b5e4fab7674563355646d35d0bc4e349.json b/apps/labrinth/.sqlx/query-190db3c55d352a23d823fa4f98df4f61b5e4fab7674563355646d35d0bc4e349.json new file mode 100644 index 0000000000..5a281f3c6f --- /dev/null +++ b/apps/labrinth/.sqlx/query-190db3c55d352a23d823fa4f98df4f61b5e4fab7674563355646d35d0bc4e349.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT EXISTS(\n SELECT 1 FROM delphi_report_issues dri\n WHERE\n dri.report_id = $1\n AND dri.status = 'unsafe'\n LIMIT 1\n ) AS \"has_unsafe_issues!\"\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "has_unsafe_issues!", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + null + ] + }, + "hash": "190db3c55d352a23d823fa4f98df4f61b5e4fab7674563355646d35d0bc4e349" +} diff --git a/apps/labrinth/.sqlx/query-24a4b1fd6f7aad9c8fe700aee10c39237993878d96750a24acce425ae80f6d74.json b/apps/labrinth/.sqlx/query-24a4b1fd6f7aad9c8fe700aee10c39237993878d96750a24acce425ae80f6d74.json deleted file mode 100644 index 091b9b808d..0000000000 --- a/apps/labrinth/.sqlx/query-24a4b1fd6f7aad9c8fe700aee10c39237993878d96750a24acce425ae80f6d74.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE mods\n SET status = $1\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON v.mod_id = m.id\n INNER JOIN threads t ON t.mod_id = m.id\n WHERE dr.id = $2\n RETURNING\n m.id AS \"project_id: DBProjectId\",\n t.id AS \"thread_id: DBThreadId\",\n (SELECT status FROM mods WHERE id = m.id) AS \"old_status!\"\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "project_id: DBProjectId", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "thread_id: DBThreadId", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "old_status!", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Varchar", - "Int8" - ] - }, - "nullable": [ - false, - false, - null - ] - }, - "hash": "24a4b1fd6f7aad9c8fe700aee10c39237993878d96750a24acce425ae80f6d74" -} diff --git a/apps/labrinth/.sqlx/query-d0d29a17cfc6a97b23c5d96b97b1005709635a2028c26589ed50e781392f15fd.json b/apps/labrinth/.sqlx/query-488fd3f6728813a1e83f45d74fff35aa22f43f2431cee23d2994b1d10b94f093.json similarity index 58% rename from apps/labrinth/.sqlx/query-d0d29a17cfc6a97b23c5d96b97b1005709635a2028c26589ed50e781392f15fd.json rename to apps/labrinth/.sqlx/query-488fd3f6728813a1e83f45d74fff35aa22f43f2431cee23d2994b1d10b94f093.json index cbcb9d744c..07ce3c49fb 100644 --- a/apps/labrinth/.sqlx/query-d0d29a17cfc6a97b23c5d96b97b1005709635a2028c26589ed50e781392f15fd.json +++ b/apps/labrinth/.sqlx/query-488fd3f6728813a1e83f45d74fff35aa22f43f2431cee23d2994b1d10b94f093.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (dr.id)\n dr.id AS report_id,\n dr.created AS report_created,\n dr.severity AS report_severity,\n m.id AS project_id,\n t.id AS project_thread_id,\n\n to_jsonb(dr)\n || jsonb_build_object(\n 'file_id', to_base62(f.id),\n 'version_id', to_base62(v.id),\n 'project_id', to_base62(v.mod_id),\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT json_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT json_agg(\n jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity\n )\n )\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ) AS report\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n INNER JOIN threads t ON t.mod_id = m.id\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[]))\n AND m.status NOT IN ('draft', 'rejected')\n AND dr.status = 'pending'\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN t.report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN t.report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN t.report_severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN t.report_severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $1\n OFFSET $2\n ", + "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (dr.id)\n dr.id AS report_id,\n dr.created AS report_created,\n dr.severity AS report_severity,\n m.id AS project_id,\n t.id AS project_thread_id,\n\n to_jsonb(dr)\n || jsonb_build_object(\n 'file_id', to_base62(f.id),\n 'version_id', to_base62(v.id),\n 'project_id', to_base62(v.mod_id),\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT json_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT json_agg(\n jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity\n )\n )\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n -- AND NOT EXISTS (\n -- -- exclude issues with types that have been marked as safe for this project\n -- SELECT 1\n -- FROM delphi_report_issues dri_safe\n -- INNER JOIN delphi_reports dr_safe ON dr_safe.id = dri_safe.report_id\n -- INNER JOIN files f_safe ON f_safe.id = dr_safe.file_id\n -- INNER JOIN versions v_safe ON v_safe.id = f_safe.version_id\n -- WHERE dri_safe.issue_type = dri.issue_type\n -- AND dri_safe.status = 'safe'\n -- AND v_safe.mod_id = m.id\n -- )\n )\n ) AS report\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n INNER JOIN threads t ON t.mod_id = m.id\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[]))\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n AND dr.status = 'pending'\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN t.report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN t.report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN t.report_severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN t.report_severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $1\n OFFSET $2\n ", "describe": { "columns": [ { @@ -33,5 +33,5 @@ null ] }, - "hash": "d0d29a17cfc6a97b23c5d96b97b1005709635a2028c26589ed50e781392f15fd" + "hash": "488fd3f6728813a1e83f45d74fff35aa22f43f2431cee23d2994b1d10b94f093" } diff --git a/apps/labrinth/.sqlx/query-67f52d745c4b53a9ca70d37aa8c9698a350098ea5396ce3c327064bb9d7eeb01.json b/apps/labrinth/.sqlx/query-67f52d745c4b53a9ca70d37aa8c9698a350098ea5396ce3c327064bb9d7eeb01.json deleted file mode 100644 index 419d82de6c..0000000000 --- a/apps/labrinth/.sqlx/query-67f52d745c4b53a9ca70d37aa8c9698a350098ea5396ce3c327064bb9d7eeb01.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE delphi_reports dr\n SET status = $1\n WHERE dr.id = $2\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - { - "Custom": { - "name": "delphi_report_issue_status", - "kind": { - "Enum": [ - "pending", - "safe", - "unsafe" - ] - } - } - }, - "Int8" - ] - }, - "nullable": [] - }, - "hash": "67f52d745c4b53a9ca70d37aa8c9698a350098ea5396ce3c327064bb9d7eeb01" -} diff --git a/apps/labrinth/.sqlx/query-a0e44cc4133730034bf8ead188eadd34bf62552a5bfd5d0a9637e88641997d6e.json b/apps/labrinth/.sqlx/query-a0e44cc4133730034bf8ead188eadd34bf62552a5bfd5d0a9637e88641997d6e.json new file mode 100644 index 0000000000..1298855cf5 --- /dev/null +++ b/apps/labrinth/.sqlx/query-a0e44cc4133730034bf8ead188eadd34bf62552a5bfd5d0a9637e88641997d6e.json @@ -0,0 +1,35 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE mods\n SET status = $1\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON v.mod_id = m.id\n INNER JOIN threads t ON t.mod_id = m.id\n WHERE dr.id = $2\n RETURNING\n m.id AS \"project_id: DBProjectId\",\n t.id AS \"thread_id: DBThreadId\",\n (SELECT status FROM mods WHERE id = m.id) AS \"old_status!\"\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "project_id: DBProjectId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "thread_id: DBThreadId", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "old_status!", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [ + false, + false, + null + ] + }, + "hash": "a0e44cc4133730034bf8ead188eadd34bf62552a5bfd5d0a9637e88641997d6e" +} diff --git a/apps/labrinth/.sqlx/query-a8e7a2496ab037761cb5c80704fb721371ab3f09782f69ec9cf4c3b4255444f7.json b/apps/labrinth/.sqlx/query-a8e7a2496ab037761cb5c80704fb721371ab3f09782f69ec9cf4c3b4255444f7.json new file mode 100644 index 0000000000..9b315f3c1a --- /dev/null +++ b/apps/labrinth/.sqlx/query-a8e7a2496ab037761cb5c80704fb721371ab3f09782f69ec9cf4c3b4255444f7.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n dri.id AS issue_id\n FROM delphi_report_issues dri\n INNER JOIN delphi_reports dr ON dr.id = dri.report_id\n WHERE\n dr.id = $1\n AND dri.status = 'pending'\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "issue_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "a8e7a2496ab037761cb5c80704fb721371ab3f09782f69ec9cf4c3b4255444f7" +} diff --git a/apps/labrinth/.sqlx/query-f8cad5334a48dbe91cb6d4a0ac57e5abe8dd39f0e42a8c9478d02e4c919bafca.json b/apps/labrinth/.sqlx/query-f8cad5334a48dbe91cb6d4a0ac57e5abe8dd39f0e42a8c9478d02e4c919bafca.json new file mode 100644 index 0000000000..5fc956b4cf --- /dev/null +++ b/apps/labrinth/.sqlx/query-f8cad5334a48dbe91cb6d4a0ac57e5abe8dd39f0e42a8c9478d02e4c919bafca.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE delphi_reports\n SET status = $1\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON v.mod_id = m.id\n INNER JOIN threads t ON t.mod_id = m.id\n WHERE dr.id = $2\n RETURNING\n m.id AS \"project_id: DBProjectId\",\n t.id AS \"thread_id: DBThreadId\",\n (SELECT status FROM mods WHERE id = m.id) AS \"old_status!\"\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "project_id: DBProjectId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "thread_id: DBThreadId", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "old_status!", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + { + "Custom": { + "name": "delphi_report_issue_status", + "kind": { + "Enum": [ + "pending", + "safe", + "unsafe" + ] + } + } + }, + "Int8" + ] + }, + "nullable": [ + false, + false, + null + ] + }, + "hash": "f8cad5334a48dbe91cb6d4a0ac57e5abe8dd39f0e42a8c9478d02e4c919bafca" +} From 4a9cd5ba983a1935b923204e2ee8f3f51137f385 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Thu, 11 Dec 2025 16:05:18 +0000 Subject: [PATCH 51/65] fix thread message privacy --- apps/labrinth/src/models/v3/threads.rs | 23 +++++++++++++---------- apps/labrinth/src/routes/v3/threads.rs | 9 +-------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/apps/labrinth/src/models/v3/threads.rs b/apps/labrinth/src/models/v3/threads.rs index 095b5b4b55..4f371caab0 100644 --- a/apps/labrinth/src/models/v3/threads.rs +++ b/apps/labrinth/src/models/v3/threads.rs @@ -54,6 +54,18 @@ pub enum MessageBody { }, } +impl MessageBody { + pub fn is_private(&self) -> bool { + match self { + Self::Text { private, .. } | Self::Deleted { private } => *private, + Self::TechReview { .. } => true, + Self::StatusChange { .. } + | Self::ThreadClosure + | Self::ThreadReopen => false, + } + } +} + #[derive( Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema, )] @@ -106,16 +118,7 @@ impl Thread { messages: data .messages .into_iter() - .filter(|x| { - if let MessageBody::Text { private, .. } = x.body { - !private || user.role.is_mod() - } else if let MessageBody::Deleted { private, .. } = x.body - { - !private || user.role.is_mod() - } else { - true - } - }) + .filter(|x| user.role.is_mod() || x.body.is_private()) .map(|x| ThreadMessage::from(x, user)) .collect(), members: users, diff --git a/apps/labrinth/src/routes/v3/threads.rs b/apps/labrinth/src/routes/v3/threads.rs index 90233000ea..852ecab217 100644 --- a/apps/labrinth/src/routes/v3/threads.rs +++ b/apps/labrinth/src/routes/v3/threads.rs @@ -646,14 +646,7 @@ pub async fn message_delete( .await?; } - let private = if let MessageBody::Text { private, .. } = thread.body { - private - } else if let MessageBody::Deleted { private, .. } = thread.body { - private - } else { - false - }; - + let private = thread.body.is_private(); database::models::DBThreadMessage::remove_full( thread.id, private, From fc31d61ebb86d8c065ea0e045b4f5b27ac182d2e Mon Sep 17 00:00:00 2001 From: aecsocket Date: Fri, 12 Dec 2025 15:42:16 +0000 Subject: [PATCH 52/65] New tech review search route --- .../routes/internal/moderation/tech_review.rs | 190 +++++++++--------- 1 file changed, 100 insertions(+), 90 deletions(-) diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 1987d8eb57..6bb2a66dd6 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -99,13 +99,9 @@ impl fmt::Display for SearchProjectsSort { #[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] pub struct FileReport { /// ID of this report. - pub id: DelphiReportId, + pub report_id: DelphiReportId, /// ID of the file that was scanned. pub file_id: FileId, - /// ID of the project version this report is for. - pub version_id: VersionId, - /// ID of the project this report is for. - pub project_id: ProjectId, /// When the report for this file was created. pub created: DateTime, /// Why this project was flagged. @@ -281,8 +277,8 @@ async fn get_report( /// See [`search_projects`]. #[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] pub struct SearchResponse { - /// List of reports returned. - pub reports: Vec, + /// List of reported projects returned, and their report data. + pub project_reports: Vec, /// Fetched project information for projects in the returned reports. pub projects: HashMap, /// Fetched moderation threads for projects in the returned reports. @@ -291,6 +287,27 @@ pub struct SearchResponse { pub ownership: HashMap, } +/// Single project's reports from a search response. +#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] +pub struct ProjectReport { + /// ID of the project this report is for. + pub project_id: ProjectId, + /// Highest severity of any report of any file of any version under this + /// project. + pub max_severity: DelphiSeverity, + /// Reports for this project's versions. + pub versions: Vec, +} + +/// Single project version's reports from a search response. +#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] +pub struct VersionReport { + /// ID of the project version this report is for. + pub version_id: VersionId, + /// Reports for this version's files. + pub files: Vec, +} + /// Limited set of project information returned by [`search_projects`]. #[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] pub struct ProjectModerationInfo { @@ -337,75 +354,80 @@ async fn search_projects( let offset = i64::try_from(offset) .wrap_request_err("offset cannot fit into `i64`")?; - let mut reports = Vec::::new(); + let mut project_reports = Vec::::new(); let mut project_ids = Vec::::new(); let mut thread_ids = Vec::::new(); + let mut rows = sqlx::query!( r#" SELECT project_id AS "project_id: DBProjectId", project_thread_id AS "project_thread_id: DBThreadId", - report AS "report!: sqlx::types::Json" + report AS "report!: sqlx::types::Json" FROM ( - SELECT DISTINCT ON (dr.id) - dr.id AS report_id, - dr.created AS report_created, - dr.severity AS report_severity, - m.id AS project_id, - t.id AS project_thread_id, - - to_jsonb(dr) - || jsonb_build_object( - 'file_id', to_base62(f.id), - 'version_id', to_base62(v.id), - 'project_id', to_base62(v.mod_id), - 'file_name', f.filename, - 'file_size', f.size, - 'flag_reason', 'delphi', - 'download_url', f.url, + SELECT DISTINCT ON (m.id) + m.id AS project_id, + t.id AS project_thread_id, + MAX(dr.severity) AS severity, + MIN(dr.created) AS earliest_report_created, + MAX(dr.created) AS latest_report_created, + + jsonb_build_object( + 'project_id', to_base62(m.id), + 'max_severity', MAX(dr.severity), -- TODO: replace with `json_array` in Postgres 16 - 'issues', ( - SELECT json_agg( - to_jsonb(dri) - || jsonb_build_object( - -- TODO: replace with `json_array` in Postgres 16 - 'details', ( - SELECT json_agg( - jsonb_build_object( - 'id', drid.id, - 'issue_id', drid.issue_id, - 'key', drid.key, - 'file_path', drid.file_path, - -- ignore `decompiled_source` - 'data', drid.data, - 'severity', drid.severity + 'versions', ( + SELECT json_agg(jsonb_build_object( + 'version_id', to_base62(v.id), + -- TODO: replace with `json_array` in Postgres 16 + 'files', ( + SELECT json_agg(jsonb_build_object( + 'report_id', dr.id, + 'file_id', to_base62(f.id), + 'created', dr.created, + 'flag_reason', 'delphi', + 'severity', dr.severity, + 'file_name', f.filename, + 'file_size', f.size, + 'download_url', f.url, + -- TODO: replace with `json_array` in Postgres 16 + 'issues', ( + SELECT json_agg( + to_jsonb(dri) + || jsonb_build_object( + -- TODO: replace with `json_array` in Postgres 16 + 'details', ( + SELECT json_agg( + jsonb_build_object( + 'id', drid.id, + 'issue_id', drid.issue_id, + 'key', drid.key, + 'file_path', drid.file_path, + -- ignore `decompiled_source` + 'data', drid.data, + 'severity', drid.severity + ) + ) + FROM delphi_report_issue_details drid + WHERE drid.issue_id = dri.id + ) + ) ) + FROM delphi_report_issues dri + WHERE dri.report_id = dr.id ) - FROM delphi_report_issue_details drid - WHERE drid.issue_id = dri.id - ) + )) + FROM delphi_reports dr + WHERE dr.file_id = f.id ) - ) - FROM delphi_report_issues dri - WHERE dri.report_id = dr.id - -- AND NOT EXISTS ( - -- -- exclude issues with types that have been marked as safe for this project - -- SELECT 1 - -- FROM delphi_report_issues dri_safe - -- INNER JOIN delphi_reports dr_safe ON dr_safe.id = dri_safe.report_id - -- INNER JOIN files f_safe ON f_safe.id = dr_safe.file_id - -- INNER JOIN versions v_safe ON v_safe.id = f_safe.version_id - -- WHERE dri_safe.issue_type = dri.issue_type - -- AND dri_safe.status = 'safe' - -- AND v_safe.mod_id = m.id - -- ) + )) ) ) AS report - FROM delphi_reports dr - INNER JOIN files f ON f.id = dr.file_id - INNER JOIN versions v ON v.id = f.version_id - INNER JOIN mods m ON m.id = v.mod_id + FROM mods m INNER JOIN threads t ON t.mod_id = m.id + INNER JOIN versions v ON v.mod_id = m.id + INNER JOIN files f ON f.version_id = v.id + INNER JOIN delphi_reports dr ON dr.file_id = f.id -- filtering LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id @@ -415,14 +437,16 @@ async fn search_projects( (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[])) AND m.status NOT IN ('draft', 'rejected', 'withheld') AND dr.status = 'pending' + + GROUP BY m.id, t.id ) t -- sorting ORDER BY - CASE WHEN $3 = 'created_asc' THEN t.report_created ELSE TO_TIMESTAMP(0) END ASC, - CASE WHEN $3 = 'created_desc' THEN t.report_created ELSE TO_TIMESTAMP(0) END DESC, - CASE WHEN $3 = 'severity_asc' THEN t.report_severity ELSE 'low'::delphi_severity END ASC, - CASE WHEN $3 = 'severity_desc' THEN t.report_severity ELSE 'low'::delphi_severity END DESC + CASE WHEN $3 = 'created_asc' THEN t.earliest_report_created ELSE TO_TIMESTAMP(0) END ASC, + CASE WHEN $3 = 'created_desc' THEN t.latest_report_created ELSE TO_TIMESTAMP(0) END DESC, + CASE WHEN $3 = 'severity_asc' THEN t.severity ELSE 'low'::delphi_severity END ASC, + CASE WHEN $3 = 'severity_desc' THEN t.severity ELSE 'low'::delphi_severity END DESC -- pagination LIMIT $1 @@ -446,7 +470,7 @@ async fn search_projects( .transpose() .wrap_internal_err("failed to fetch reports")? { - reports.push(row.report.0); + project_reports.push(row.report.0); project_ids.push(row.project_id); thread_ids.push(row.project_thread_id); } @@ -493,7 +517,7 @@ async fn search_projects( .collect::>(); Ok(web::Json(SearchResponse { - reports, + project_reports, projects: projects .into_iter() .map(|(id, project)| { @@ -577,6 +601,9 @@ async fn search_projects( /// See [`submit_report`]. #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct SubmitReport { + /// Does the moderator think this report shows that the project is safe or + /// unsafe? + pub verdict: DelphiVerdict, /// Moderator message to send to the thread when rejecting the project. pub message: Option, } @@ -602,7 +629,7 @@ async fn submit_report( redis: web::Data, session_queue: web::Data, submit_report: web::Json, - path: web::Path<(DelphiReportId,)>, + path: web::Path<(ProjectId,)>, ) -> Result<(), ApiError> { let user = check_is_moderator_from_headers( &req, @@ -612,7 +639,7 @@ async fn submit_report( Scopes::PROJECT_WRITE, ) .await?; - let (report_id,) = path.into_inner(); + let (project_id,) = path.into_inner(); let mut txn = pool .begin() @@ -644,27 +671,10 @@ async fn submit_report( }); } - let has_unsafe_issues = sqlx::query!( - r#" - SELECT EXISTS( - SELECT 1 FROM delphi_report_issues dri - WHERE - dri.report_id = $1 - AND dri.status = 'unsafe' - LIMIT 1 - ) AS "has_unsafe_issues!" - "#, - report_id as _, - ) - .fetch_one(&mut *txn) - .await - .wrap_internal_err("failed to fetch unsafe issues")? - .has_unsafe_issues; - - let (verdict, status) = if has_unsafe_issues { - (DelphiVerdict::Unsafe, DelphiReportIssueStatus::Unsafe) - } else { - (DelphiVerdict::Safe, DelphiReportIssueStatus::Safe) + let verdict = submit_report.verdict; + let status = match verdict { + DelphiVerdict::Unsafe => DelphiReportIssueStatus::Unsafe, + DelphiVerdict::Safe => DelphiReportIssueStatus::Safe, }; let record = sqlx::query!( r#" From 550889cccbe5c41123a02413293754bc095b4ec2 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Fri, 12 Dec 2025 16:19:52 +0000 Subject: [PATCH 53/65] submit route --- ...22dbdc650cf9ae1e83fedbed6316e4a5bb59.json} | 18 ++------ ...f4f61b5e4fab7674563355646d35d0bc4e349.json | 22 ---------- ...19ef7bcbb341c7ffea3d13acd250bb20e6d07.json | 29 +++++++++++++ ...062c222066d1e1c0dd984d3737769224ff14.json} | 4 +- ...f35aa22f43f2431cee23d2994b1d10b94f093.json | 37 ---------------- ...2bb237eda055ee03f919756aa83a19a549059.json | 37 ++++++++++++++++ ...add34bf62552a5bfd5d0a9637e88641997d6e.json | 35 --------------- .../routes/internal/moderation/tech_review.rs | 43 ++++++++----------- 8 files changed, 90 insertions(+), 135 deletions(-) rename apps/labrinth/.sqlx/{query-f8cad5334a48dbe91cb6d4a0ac57e5abe8dd39f0e42a8c9478d02e4c919bafca.json => query-0f8b6eb867828bdd2a802806b63f22dbdc650cf9ae1e83fedbed6316e4a5bb59.json} (56%) delete mode 100644 apps/labrinth/.sqlx/query-190db3c55d352a23d823fa4f98df4f61b5e4fab7674563355646d35d0bc4e349.json create mode 100644 apps/labrinth/.sqlx/query-3473715e4ff6efb6707f73e8ddf19ef7bcbb341c7ffea3d13acd250bb20e6d07.json rename apps/labrinth/.sqlx/{query-a8e7a2496ab037761cb5c80704fb721371ab3f09782f69ec9cf4c3b4255444f7.json => query-3aa8ce79914c3043a2e4ffb8f246062c222066d1e1c0dd984d3737769224ff14.json} (50%) delete mode 100644 apps/labrinth/.sqlx/query-488fd3f6728813a1e83f45d74fff35aa22f43f2431cee23d2994b1d10b94f093.json create mode 100644 apps/labrinth/.sqlx/query-86d6883faca78874fd684f04ba52bb237eda055ee03f919756aa83a19a549059.json delete mode 100644 apps/labrinth/.sqlx/query-a0e44cc4133730034bf8ead188eadd34bf62552a5bfd5d0a9637e88641997d6e.json diff --git a/apps/labrinth/.sqlx/query-f8cad5334a48dbe91cb6d4a0ac57e5abe8dd39f0e42a8c9478d02e4c919bafca.json b/apps/labrinth/.sqlx/query-0f8b6eb867828bdd2a802806b63f22dbdc650cf9ae1e83fedbed6316e4a5bb59.json similarity index 56% rename from apps/labrinth/.sqlx/query-f8cad5334a48dbe91cb6d4a0ac57e5abe8dd39f0e42a8c9478d02e4c919bafca.json rename to apps/labrinth/.sqlx/query-0f8b6eb867828bdd2a802806b63f22dbdc650cf9ae1e83fedbed6316e4a5bb59.json index 5fc956b4cf..80d759bf92 100644 --- a/apps/labrinth/.sqlx/query-f8cad5334a48dbe91cb6d4a0ac57e5abe8dd39f0e42a8c9478d02e4c919bafca.json +++ b/apps/labrinth/.sqlx/query-0f8b6eb867828bdd2a802806b63f22dbdc650cf9ae1e83fedbed6316e4a5bb59.json @@ -1,22 +1,12 @@ { "db_name": "PostgreSQL", - "query": "\n UPDATE delphi_reports\n SET status = $1\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON v.mod_id = m.id\n INNER JOIN threads t ON t.mod_id = m.id\n WHERE dr.id = $2\n RETURNING\n m.id AS \"project_id: DBProjectId\",\n t.id AS \"thread_id: DBThreadId\",\n (SELECT status FROM mods WHERE id = m.id) AS \"old_status!\"\n ", + "query": "\n UPDATE delphi_reports\n SET status = $1\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON v.mod_id = m.id\n INNER JOIN threads t ON t.mod_id = m.id\n WHERE m.id = $2\n RETURNING\n t.id AS \"thread_id: DBThreadId\"\n ", "describe": { "columns": [ { "ordinal": 0, - "name": "project_id: DBProjectId", - "type_info": "Int8" - }, - { - "ordinal": 1, "name": "thread_id: DBThreadId", "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "old_status!", - "type_info": "Varchar" } ], "parameters": { @@ -37,10 +27,8 @@ ] }, "nullable": [ - false, - false, - null + false ] }, - "hash": "f8cad5334a48dbe91cb6d4a0ac57e5abe8dd39f0e42a8c9478d02e4c919bafca" + "hash": "0f8b6eb867828bdd2a802806b63f22dbdc650cf9ae1e83fedbed6316e4a5bb59" } diff --git a/apps/labrinth/.sqlx/query-190db3c55d352a23d823fa4f98df4f61b5e4fab7674563355646d35d0bc4e349.json b/apps/labrinth/.sqlx/query-190db3c55d352a23d823fa4f98df4f61b5e4fab7674563355646d35d0bc4e349.json deleted file mode 100644 index 5a281f3c6f..0000000000 --- a/apps/labrinth/.sqlx/query-190db3c55d352a23d823fa4f98df4f61b5e4fab7674563355646d35d0bc4e349.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT EXISTS(\n SELECT 1 FROM delphi_report_issues dri\n WHERE\n dri.report_id = $1\n AND dri.status = 'unsafe'\n LIMIT 1\n ) AS \"has_unsafe_issues!\"\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "has_unsafe_issues!", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - null - ] - }, - "hash": "190db3c55d352a23d823fa4f98df4f61b5e4fab7674563355646d35d0bc4e349" -} diff --git a/apps/labrinth/.sqlx/query-3473715e4ff6efb6707f73e8ddf19ef7bcbb341c7ffea3d13acd250bb20e6d07.json b/apps/labrinth/.sqlx/query-3473715e4ff6efb6707f73e8ddf19ef7bcbb341c7ffea3d13acd250bb20e6d07.json new file mode 100644 index 0000000000..f4a8644122 --- /dev/null +++ b/apps/labrinth/.sqlx/query-3473715e4ff6efb6707f73e8ddf19ef7bcbb341c7ffea3d13acd250bb20e6d07.json @@ -0,0 +1,29 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE mods\n SET status = $1\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n WHERE m.id = $2\n RETURNING\n t.id AS \"thread_id: DBThreadId\",\n (SELECT status FROM mods WHERE id = m.id) AS \"old_status!\"\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "thread_id: DBThreadId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "old_status!", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [ + false, + null + ] + }, + "hash": "3473715e4ff6efb6707f73e8ddf19ef7bcbb341c7ffea3d13acd250bb20e6d07" +} diff --git a/apps/labrinth/.sqlx/query-a8e7a2496ab037761cb5c80704fb721371ab3f09782f69ec9cf4c3b4255444f7.json b/apps/labrinth/.sqlx/query-3aa8ce79914c3043a2e4ffb8f246062c222066d1e1c0dd984d3737769224ff14.json similarity index 50% rename from apps/labrinth/.sqlx/query-a8e7a2496ab037761cb5c80704fb721371ab3f09782f69ec9cf4c3b4255444f7.json rename to apps/labrinth/.sqlx/query-3aa8ce79914c3043a2e4ffb8f246062c222066d1e1c0dd984d3737769224ff14.json index 9b315f3c1a..bd0908f30e 100644 --- a/apps/labrinth/.sqlx/query-a8e7a2496ab037761cb5c80704fb721371ab3f09782f69ec9cf4c3b4255444f7.json +++ b/apps/labrinth/.sqlx/query-3aa8ce79914c3043a2e4ffb8f246062c222066d1e1c0dd984d3737769224ff14.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n dri.id AS issue_id\n FROM delphi_report_issues dri\n INNER JOIN delphi_reports dr ON dr.id = dri.report_id\n WHERE\n dr.id = $1\n AND dri.status = 'pending'\n ", + "query": "\n SELECT\n dri.id AS issue_id\n FROM delphi_report_issues dri\n INNER JOIN delphi_reports dr ON dr.id = dri.report_id\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n WHERE\n m.id = $1\n AND dr.status = 'pending'\n AND dri.status = 'pending'\n ", "describe": { "columns": [ { @@ -18,5 +18,5 @@ false ] }, - "hash": "a8e7a2496ab037761cb5c80704fb721371ab3f09782f69ec9cf4c3b4255444f7" + "hash": "3aa8ce79914c3043a2e4ffb8f246062c222066d1e1c0dd984d3737769224ff14" } diff --git a/apps/labrinth/.sqlx/query-488fd3f6728813a1e83f45d74fff35aa22f43f2431cee23d2994b1d10b94f093.json b/apps/labrinth/.sqlx/query-488fd3f6728813a1e83f45d74fff35aa22f43f2431cee23d2994b1d10b94f093.json deleted file mode 100644 index 07ce3c49fb..0000000000 --- a/apps/labrinth/.sqlx/query-488fd3f6728813a1e83f45d74fff35aa22f43f2431cee23d2994b1d10b94f093.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (dr.id)\n dr.id AS report_id,\n dr.created AS report_created,\n dr.severity AS report_severity,\n m.id AS project_id,\n t.id AS project_thread_id,\n\n to_jsonb(dr)\n || jsonb_build_object(\n 'file_id', to_base62(f.id),\n 'version_id', to_base62(v.id),\n 'project_id', to_base62(v.mod_id),\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT json_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT json_agg(\n jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity\n )\n )\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n -- AND NOT EXISTS (\n -- -- exclude issues with types that have been marked as safe for this project\n -- SELECT 1\n -- FROM delphi_report_issues dri_safe\n -- INNER JOIN delphi_reports dr_safe ON dr_safe.id = dri_safe.report_id\n -- INNER JOIN files f_safe ON f_safe.id = dr_safe.file_id\n -- INNER JOIN versions v_safe ON v_safe.id = f_safe.version_id\n -- WHERE dri_safe.issue_type = dri.issue_type\n -- AND dri_safe.status = 'safe'\n -- AND v_safe.mod_id = m.id\n -- )\n )\n ) AS report\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n INNER JOIN threads t ON t.mod_id = m.id\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[]))\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n AND dr.status = 'pending'\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN t.report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN t.report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN t.report_severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN t.report_severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $1\n OFFSET $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "project_id: DBProjectId", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "project_thread_id: DBThreadId", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "report!: sqlx::types::Json", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Text", - "Int4Array" - ] - }, - "nullable": [ - false, - false, - null - ] - }, - "hash": "488fd3f6728813a1e83f45d74fff35aa22f43f2431cee23d2994b1d10b94f093" -} diff --git a/apps/labrinth/.sqlx/query-86d6883faca78874fd684f04ba52bb237eda055ee03f919756aa83a19a549059.json b/apps/labrinth/.sqlx/query-86d6883faca78874fd684f04ba52bb237eda055ee03f919756aa83a19a549059.json new file mode 100644 index 0000000000..f8b037b52e --- /dev/null +++ b/apps/labrinth/.sqlx/query-86d6883faca78874fd684f04ba52bb237eda055ee03f919756aa83a19a549059.json @@ -0,0 +1,37 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (m.id)\n m.id AS project_id,\n t.id AS project_thread_id,\n MAX(dr.severity) AS severity,\n MIN(dr.created) AS earliest_report_created,\n MAX(dr.created) AS latest_report_created,\n\n jsonb_build_object(\n 'project_id', to_base62(m.id),\n 'max_severity', MAX(dr.severity),\n -- TODO: replace with `json_array` in Postgres 16\n 'versions', (\n SELECT json_agg(jsonb_build_object(\n 'version_id', to_base62(v.id),\n -- TODO: replace with `json_array` in Postgres 16\n 'files', (\n SELECT json_agg(jsonb_build_object(\n 'report_id', dr.id,\n 'file_id', to_base62(f.id),\n 'created', dr.created,\n 'flag_reason', 'delphi',\n 'severity', dr.severity,\n 'file_name', f.filename,\n 'file_size', f.size,\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT json_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT json_agg(\n jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity\n )\n )\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ))\n FROM delphi_reports dr\n WHERE dr.file_id = f.id\n )\n ))\n )\n ) AS report\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[]))\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n AND dr.status = 'pending'\n\n GROUP BY m.id, t.id\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN t.earliest_report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN t.latest_report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN t.severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN t.severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $1\n OFFSET $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "project_id: DBProjectId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "project_thread_id: DBThreadId", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "report!: sqlx::types::Json", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Text", + "Int4Array" + ] + }, + "nullable": [ + false, + false, + null + ] + }, + "hash": "86d6883faca78874fd684f04ba52bb237eda055ee03f919756aa83a19a549059" +} diff --git a/apps/labrinth/.sqlx/query-a0e44cc4133730034bf8ead188eadd34bf62552a5bfd5d0a9637e88641997d6e.json b/apps/labrinth/.sqlx/query-a0e44cc4133730034bf8ead188eadd34bf62552a5bfd5d0a9637e88641997d6e.json deleted file mode 100644 index 1298855cf5..0000000000 --- a/apps/labrinth/.sqlx/query-a0e44cc4133730034bf8ead188eadd34bf62552a5bfd5d0a9637e88641997d6e.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE mods\n SET status = $1\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON v.mod_id = m.id\n INNER JOIN threads t ON t.mod_id = m.id\n WHERE dr.id = $2\n RETURNING\n m.id AS \"project_id: DBProjectId\",\n t.id AS \"thread_id: DBThreadId\",\n (SELECT status FROM mods WHERE id = m.id) AS \"old_status!\"\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "project_id: DBProjectId", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "thread_id: DBThreadId", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "old_status!", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Varchar", - "Int8" - ] - }, - "nullable": [ - false, - false, - null - ] - }, - "hash": "a0e44cc4133730034bf8ead188eadd34bf62552a5bfd5d0a9637e88641997d6e" -} diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 6bb2a66dd6..7c7fa04af5 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -608,21 +608,17 @@ pub struct SubmitReport { pub message: Option, } -/// Submits a verdict for a technical report of a project. +/// Submits a verdict for a project based on its technical reports. /// -/// Before this is called, all issues for this project must have been marked as -/// either safe or unsafe. Otherwise, this will error with +/// Before this is called, all issues for this project's reports must have been +/// marked as either safe or unsafe. Otherwise, this will error with /// [`ApiError::TechReviewIssuesWithNoVerdict`], providing the issue IDs which /// are still unmarked. -/// -/// If at least one issue is [`DelphiReportIssueStatus::Unsafe`], the report -/// will be marked as unsafe and the project will be rejected. Otherwise, the -/// report will be marked as safe. #[utoipa::path( security(("bearer_auth" = [])), responses((status = NO_CONTENT)) )] -#[post("/report/{id}")] +#[post("/submit/{project_id}")] async fn submit_report( req: HttpRequest, pool: web::Data, @@ -640,6 +636,7 @@ async fn submit_report( ) .await?; let (project_id,) = path.into_inner(); + let project_id = DBProjectId::from(project_id); let mut txn = pool .begin() @@ -652,11 +649,15 @@ async fn submit_report( dri.id AS issue_id FROM delphi_report_issues dri INNER JOIN delphi_reports dr ON dr.id = dri.report_id + INNER JOIN files f ON f.id = dr.file_id + INNER JOIN versions v ON v.id = f.version_id + INNER JOIN mods m ON m.id = v.mod_id WHERE - dr.id = $1 + m.id = $1 + AND dr.status = 'pending' AND dri.status = 'pending' ", - report_id as _, + project_id as _, ) .fetch_all(&mut *txn) .await @@ -685,18 +686,16 @@ async fn submit_report( INNER JOIN versions v ON v.id = f.version_id INNER JOIN mods m ON v.mod_id = m.id INNER JOIN threads t ON t.mod_id = m.id - WHERE dr.id = $2 + WHERE m.id = $2 RETURNING - m.id AS "project_id: DBProjectId", - t.id AS "thread_id: DBThreadId", - (SELECT status FROM mods WHERE id = m.id) AS "old_status!" + t.id AS "thread_id: DBThreadId" "#, status as _, - report_id as _, + project_id as _, ) .fetch_one(&mut *txn) .await - .wrap_internal_err("failed to update report")?; + .wrap_internal_err("failed to update reports")?; if let Some(body) = submit_report.0.message { ThreadMessageBuilder { @@ -730,19 +729,15 @@ async fn submit_report( r#" UPDATE mods SET status = $1 - FROM delphi_reports dr - INNER JOIN files f ON f.id = dr.file_id - INNER JOIN versions v ON v.id = f.version_id - INNER JOIN mods m ON v.mod_id = m.id + FROM mods m INNER JOIN threads t ON t.mod_id = m.id - WHERE dr.id = $2 + WHERE m.id = $2 RETURNING - m.id AS "project_id: DBProjectId", t.id AS "thread_id: DBThreadId", (SELECT status FROM mods WHERE id = m.id) AS "old_status!" "#, ProjectStatus::Rejected.as_str(), - report_id as DelphiReportId, + project_id as _, ) .fetch_one(&mut *txn) .await @@ -761,7 +756,7 @@ async fn submit_report( .await .wrap_internal_err("failed to add tech review message")?; - DBProject::clear_cache(record.project_id, None, None, &redis) + DBProject::clear_cache(project_id, None, None, &redis) .await .wrap_internal_err("failed to clear project cache")?; } From e90101ef724a3d9edb416cfe0f6d07a17faee1fe Mon Sep 17 00:00:00 2001 From: aecsocket Date: Sat, 13 Dec 2025 14:55:20 +0000 Subject: [PATCH 54/65] details have statuses now --- ...f22dbdc650cf9ae1e83fedbed6316e4a5bb59.json | 34 ---- ...724e9a4d5b9765d52305f99f859f939c2e854.json | 35 ---- ...ee7a6510aef3c210ffbc18bb12e556f30b84.json} | 16 +- ...90683636bbde53a51ee61fa18ef49cea8c3a.json} | 7 +- ...6062c222066d1e1c0dd984d3737769224ff14.json | 22 --- ...39198f064dd0bfa53ad27db598928f62598f0.json | 22 +++ ...b02663526b5635cf58f4095efe8755c7d3786.json | 24 +++ ...75b109f8124cdd0097e3aeca92c5f3ad0e082.json | 34 ---- ...d9d06749cea63ae8650d83506eb074688917.json} | 4 +- ...1ac8460088b70cd115c3d5ebf9474aa4d54fa.json | 126 ------------- ...e19dffcfa710e5b7095cbe33ca58b000b310e.json | 26 +++ ...ea29fa063b416c18dc857132127db95ff17f3.json | 22 +++ .../20251213141500_delphi_report_fixes.sql | 8 + .../src/database/models/delphi_report_item.rs | 92 +--------- apps/labrinth/src/routes/internal/delphi.rs | 97 +--------- .../src/routes/internal/moderation/mod.rs | 15 +- .../routes/internal/moderation/tech_review.rs | 170 ++++++------------ apps/labrinth/src/routes/mod.rs | 20 ++- 18 files changed, 211 insertions(+), 563 deletions(-) delete mode 100644 apps/labrinth/.sqlx/query-0f8b6eb867828bdd2a802806b63f22dbdc650cf9ae1e83fedbed6316e4a5bb59.json delete mode 100644 apps/labrinth/.sqlx/query-10a332091be118f580d50ceb7a8724e9a4d5b9765d52305f99f859f939c2e854.json rename apps/labrinth/.sqlx/{query-b65094517546487e43b65a76aa38efd9e422151b683d9897a071ee0c4bac1cd4.json => query-1116dd6bc5024f0908147f820d76ee7a6510aef3c210ffbc18bb12e556f30b84.json} (57%) rename apps/labrinth/.sqlx/{query-ccb0315ff52ea4402f53508334a7288fc9f8e77ffd7bce665441ff682384cbf9.json => query-33f26ce7e262d7c5707d05fe926390683636bbde53a51ee61fa18ef49cea8c3a.json} (50%) delete mode 100644 apps/labrinth/.sqlx/query-3aa8ce79914c3043a2e4ffb8f246062c222066d1e1c0dd984d3737769224ff14.json create mode 100644 apps/labrinth/.sqlx/query-506a7cb24b4ff9d0d3a49ef801039198f064dd0bfa53ad27db598928f62598f0.json create mode 100644 apps/labrinth/.sqlx/query-5411cc0153106bfe95f2341fd85b02663526b5635cf58f4095efe8755c7d3786.json delete mode 100644 apps/labrinth/.sqlx/query-734a12969dbe3175bb765d356c175b109f8124cdd0097e3aeca92c5f3ad0e082.json rename apps/labrinth/.sqlx/{query-86d6883faca78874fd684f04ba52bb237eda055ee03f919756aa83a19a549059.json => query-b96e50f4324fda4adbe5742a356ed9d06749cea63ae8650d83506eb074688917.json} (57%) delete mode 100644 apps/labrinth/.sqlx/query-d9b6a1b7fb133f7aec7599e06e21ac8460088b70cd115c3d5ebf9474aa4d54fa.json create mode 100644 apps/labrinth/.sqlx/query-e6c6348e0bc2bcbac825468a867e19dffcfa710e5b7095cbe33ca58b000b310e.json create mode 100644 apps/labrinth/.sqlx/query-f6432d7a3c67e058c0e9da42f23ea29fa063b416c18dc857132127db95ff17f3.json create mode 100644 apps/labrinth/migrations/20251213141500_delphi_report_fixes.sql diff --git a/apps/labrinth/.sqlx/query-0f8b6eb867828bdd2a802806b63f22dbdc650cf9ae1e83fedbed6316e4a5bb59.json b/apps/labrinth/.sqlx/query-0f8b6eb867828bdd2a802806b63f22dbdc650cf9ae1e83fedbed6316e4a5bb59.json deleted file mode 100644 index 80d759bf92..0000000000 --- a/apps/labrinth/.sqlx/query-0f8b6eb867828bdd2a802806b63f22dbdc650cf9ae1e83fedbed6316e4a5bb59.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE delphi_reports\n SET status = $1\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON v.mod_id = m.id\n INNER JOIN threads t ON t.mod_id = m.id\n WHERE m.id = $2\n RETURNING\n t.id AS \"thread_id: DBThreadId\"\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "thread_id: DBThreadId", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - { - "Custom": { - "name": "delphi_report_issue_status", - "kind": { - "Enum": [ - "pending", - "safe", - "unsafe" - ] - } - } - }, - "Int8" - ] - }, - "nullable": [ - false - ] - }, - "hash": "0f8b6eb867828bdd2a802806b63f22dbdc650cf9ae1e83fedbed6316e4a5bb59" -} diff --git a/apps/labrinth/.sqlx/query-10a332091be118f580d50ceb7a8724e9a4d5b9765d52305f99f859f939c2e854.json b/apps/labrinth/.sqlx/query-10a332091be118f580d50ceb7a8724e9a4d5b9765d52305f99f859f939c2e854.json deleted file mode 100644 index 7e30ece2ec..0000000000 --- a/apps/labrinth/.sqlx/query-10a332091be118f580d50ceb7a8724e9a4d5b9765d52305f99f859f939c2e854.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO delphi_report_issues (report_id, issue_type, status)\n VALUES ($1, $2, $3)\n ON CONFLICT (report_id, issue_type) DO UPDATE SET status = $3\n RETURNING id\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8", - "Text", - { - "Custom": { - "name": "delphi_report_issue_status", - "kind": { - "Enum": [ - "pending", - "safe", - "unsafe" - ] - } - } - } - ] - }, - "nullable": [ - false - ] - }, - "hash": "10a332091be118f580d50ceb7a8724e9a4d5b9765d52305f99f859f939c2e854" -} diff --git a/apps/labrinth/.sqlx/query-b65094517546487e43b65a76aa38efd9e422151b683d9897a071ee0c4bac1cd4.json b/apps/labrinth/.sqlx/query-1116dd6bc5024f0908147f820d76ee7a6510aef3c210ffbc18bb12e556f30b84.json similarity index 57% rename from apps/labrinth/.sqlx/query-b65094517546487e43b65a76aa38efd9e422151b683d9897a071ee0c4bac1cd4.json rename to apps/labrinth/.sqlx/query-1116dd6bc5024f0908147f820d76ee7a6510aef3c210ffbc18bb12e556f30b84.json index a0ea4442ee..d7d5856672 100644 --- a/apps/labrinth/.sqlx/query-b65094517546487e43b65a76aa38efd9e422151b683d9897a071ee0c4bac1cd4.json +++ b/apps/labrinth/.sqlx/query-1116dd6bc5024f0908147f820d76ee7a6510aef3c210ffbc18bb12e556f30b84.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO delphi_report_issue_details (issue_id, key, file_path, decompiled_source, data, severity)\n VALUES ($1, $2, $3, $4, $5, $6)\n RETURNING id\n ", + "query": "\n INSERT INTO delphi_report_issue_details (issue_id, key, file_path, decompiled_source, data, severity, status)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n RETURNING id\n ", "describe": { "columns": [ { @@ -28,6 +28,18 @@ ] } } + }, + { + "Custom": { + "name": "delphi_report_issue_status", + "kind": { + "Enum": [ + "pending", + "safe", + "unsafe" + ] + } + } } ] }, @@ -35,5 +47,5 @@ false ] }, - "hash": "b65094517546487e43b65a76aa38efd9e422151b683d9897a071ee0c4bac1cd4" + "hash": "1116dd6bc5024f0908147f820d76ee7a6510aef3c210ffbc18bb12e556f30b84" } diff --git a/apps/labrinth/.sqlx/query-ccb0315ff52ea4402f53508334a7288fc9f8e77ffd7bce665441ff682384cbf9.json b/apps/labrinth/.sqlx/query-33f26ce7e262d7c5707d05fe926390683636bbde53a51ee61fa18ef49cea8c3a.json similarity index 50% rename from apps/labrinth/.sqlx/query-ccb0315ff52ea4402f53508334a7288fc9f8e77ffd7bce665441ff682384cbf9.json rename to apps/labrinth/.sqlx/query-33f26ce7e262d7c5707d05fe926390683636bbde53a51ee61fa18ef49cea8c3a.json index 81d4a51df8..f050e6daa2 100644 --- a/apps/labrinth/.sqlx/query-ccb0315ff52ea4402f53508334a7288fc9f8e77ffd7bce665441ff682384cbf9.json +++ b/apps/labrinth/.sqlx/query-33f26ce7e262d7c5707d05fe926390683636bbde53a51ee61fa18ef49cea8c3a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT id FROM mods\n WHERE status = $1\n ORDER BY queued ASC\n OFFSET $3\n LIMIT $2\n ", + "query": "\n INSERT INTO delphi_report_issues (report_id, issue_type)\n VALUES ($1, $2)\n RETURNING id\n ", "describe": { "columns": [ { @@ -11,14 +11,13 @@ ], "parameters": { "Left": [ - "Text", "Int8", - "Int8" + "Text" ] }, "nullable": [ false ] }, - "hash": "ccb0315ff52ea4402f53508334a7288fc9f8e77ffd7bce665441ff682384cbf9" + "hash": "33f26ce7e262d7c5707d05fe926390683636bbde53a51ee61fa18ef49cea8c3a" } diff --git a/apps/labrinth/.sqlx/query-3aa8ce79914c3043a2e4ffb8f246062c222066d1e1c0dd984d3737769224ff14.json b/apps/labrinth/.sqlx/query-3aa8ce79914c3043a2e4ffb8f246062c222066d1e1c0dd984d3737769224ff14.json deleted file mode 100644 index bd0908f30e..0000000000 --- a/apps/labrinth/.sqlx/query-3aa8ce79914c3043a2e4ffb8f246062c222066d1e1c0dd984d3737769224ff14.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n dri.id AS issue_id\n FROM delphi_report_issues dri\n INNER JOIN delphi_reports dr ON dr.id = dri.report_id\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n WHERE\n m.id = $1\n AND dr.status = 'pending'\n AND dri.status = 'pending'\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "issue_id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false - ] - }, - "hash": "3aa8ce79914c3043a2e4ffb8f246062c222066d1e1c0dd984d3737769224ff14" -} diff --git a/apps/labrinth/.sqlx/query-506a7cb24b4ff9d0d3a49ef801039198f064dd0bfa53ad27db598928f62598f0.json b/apps/labrinth/.sqlx/query-506a7cb24b4ff9d0d3a49ef801039198f064dd0bfa53ad27db598928f62598f0.json new file mode 100644 index 0000000000..ff1d49e597 --- /dev/null +++ b/apps/labrinth/.sqlx/query-506a7cb24b4ff9d0d3a49ef801039198f064dd0bfa53ad27db598928f62598f0.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n drid.id AS issue_detail_id\n FROM mods m\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_report_issue_details drid ON drid.issue_id = dri.id\n WHERE\n m.id = $1\n AND drid.status = 'pending'\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "issue_detail_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "506a7cb24b4ff9d0d3a49ef801039198f064dd0bfa53ad27db598928f62598f0" +} diff --git a/apps/labrinth/.sqlx/query-5411cc0153106bfe95f2341fd85b02663526b5635cf58f4095efe8755c7d3786.json b/apps/labrinth/.sqlx/query-5411cc0153106bfe95f2341fd85b02663526b5635cf58f4095efe8755c7d3786.json new file mode 100644 index 0000000000..e359d92a4f --- /dev/null +++ b/apps/labrinth/.sqlx/query-5411cc0153106bfe95f2341fd85b02663526b5635cf58f4095efe8755c7d3786.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT m.id FROM mods m\n\n -- exclude projects in tech review queue\n LEFT JOIN versions v ON v.mod_id = m.id\n LEFT JOIN files f ON f.version_id = v.id\n LEFT JOIN delphi_reports dr ON dr.file_id = f.id\n\n WHERE\n m.status = $1\n AND dr.file_id IS NULL\n\n ORDER BY m.queued ASC\n OFFSET $3\n LIMIT $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Int8", + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "5411cc0153106bfe95f2341fd85b02663526b5635cf58f4095efe8755c7d3786" +} diff --git a/apps/labrinth/.sqlx/query-734a12969dbe3175bb765d356c175b109f8124cdd0097e3aeca92c5f3ad0e082.json b/apps/labrinth/.sqlx/query-734a12969dbe3175bb765d356c175b109f8124cdd0097e3aeca92c5f3ad0e082.json deleted file mode 100644 index cc0d1ef5cd..0000000000 --- a/apps/labrinth/.sqlx/query-734a12969dbe3175bb765d356c175b109f8124cdd0097e3aeca92c5f3ad0e082.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE delphi_report_issues dri\n SET status = $1\n FROM delphi_reports dr\n WHERE dri.id = $2 AND dr.id = dri.report_id\n RETURNING dr.id AS \"report_id: DelphiReportId\"\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "report_id: DelphiReportId", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - { - "Custom": { - "name": "delphi_report_issue_status", - "kind": { - "Enum": [ - "pending", - "safe", - "unsafe" - ] - } - } - }, - "Int8" - ] - }, - "nullable": [ - false - ] - }, - "hash": "734a12969dbe3175bb765d356c175b109f8124cdd0097e3aeca92c5f3ad0e082" -} diff --git a/apps/labrinth/.sqlx/query-86d6883faca78874fd684f04ba52bb237eda055ee03f919756aa83a19a549059.json b/apps/labrinth/.sqlx/query-b96e50f4324fda4adbe5742a356ed9d06749cea63ae8650d83506eb074688917.json similarity index 57% rename from apps/labrinth/.sqlx/query-86d6883faca78874fd684f04ba52bb237eda055ee03f919756aa83a19a549059.json rename to apps/labrinth/.sqlx/query-b96e50f4324fda4adbe5742a356ed9d06749cea63ae8650d83506eb074688917.json index f8b037b52e..d10966ef91 100644 --- a/apps/labrinth/.sqlx/query-86d6883faca78874fd684f04ba52bb237eda055ee03f919756aa83a19a549059.json +++ b/apps/labrinth/.sqlx/query-b96e50f4324fda4adbe5742a356ed9d06749cea63ae8650d83506eb074688917.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (m.id)\n m.id AS project_id,\n t.id AS project_thread_id,\n MAX(dr.severity) AS severity,\n MIN(dr.created) AS earliest_report_created,\n MAX(dr.created) AS latest_report_created,\n\n jsonb_build_object(\n 'project_id', to_base62(m.id),\n 'max_severity', MAX(dr.severity),\n -- TODO: replace with `json_array` in Postgres 16\n 'versions', (\n SELECT json_agg(jsonb_build_object(\n 'version_id', to_base62(v.id),\n -- TODO: replace with `json_array` in Postgres 16\n 'files', (\n SELECT json_agg(jsonb_build_object(\n 'report_id', dr.id,\n 'file_id', to_base62(f.id),\n 'created', dr.created,\n 'flag_reason', 'delphi',\n 'severity', dr.severity,\n 'file_name', f.filename,\n 'file_size', f.size,\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT json_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT json_agg(\n jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity\n )\n )\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ))\n FROM delphi_reports dr\n WHERE dr.file_id = f.id\n )\n ))\n )\n ) AS report\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[]))\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n AND dr.status = 'pending'\n\n GROUP BY m.id, t.id\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN t.earliest_report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN t.latest_report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN t.severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN t.severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $1\n OFFSET $2\n ", + "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (m.id)\n m.id AS project_id,\n t.id AS project_thread_id,\n MAX(dr.severity) AS severity,\n MIN(dr.created) AS earliest_report_created,\n MAX(dr.created) AS latest_report_created,\n\n jsonb_build_object(\n 'project_id', to_base62(m.id),\n 'max_severity', MAX(dr.severity),\n -- TODO: replace with `json_array` in Postgres 16\n 'versions', (\n SELECT json_agg(jsonb_build_object(\n 'version_id', to_base62(v.id),\n -- TODO: replace with `json_array` in Postgres 16\n 'files', (\n SELECT json_agg(jsonb_build_object(\n 'report_id', dr.id,\n 'file_id', to_base62(f.id),\n 'created', dr.created,\n 'flag_reason', 'delphi',\n 'severity', dr.severity,\n 'file_name', f.filename,\n 'file_size', f.size,\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT json_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT json_agg(\n jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity,\n 'status', drid.status\n )\n )\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ))\n FROM delphi_reports dr\n WHERE dr.file_id = f.id\n )\n ))\n )\n ) AS report\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n\n -- only return projects with at least 1 pending drid\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_report_issue_details drid\n ON drid.issue_id = dri.id AND drid.status = 'pending'\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[]))\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n\n GROUP BY m.id, t.id\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN t.earliest_report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN t.latest_report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN t.severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN t.severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $1\n OFFSET $2\n ", "describe": { "columns": [ { @@ -33,5 +33,5 @@ null ] }, - "hash": "86d6883faca78874fd684f04ba52bb237eda055ee03f919756aa83a19a549059" + "hash": "b96e50f4324fda4adbe5742a356ed9d06749cea63ae8650d83506eb074688917" } diff --git a/apps/labrinth/.sqlx/query-d9b6a1b7fb133f7aec7599e06e21ac8460088b70cd115c3d5ebf9474aa4d54fa.json b/apps/labrinth/.sqlx/query-d9b6a1b7fb133f7aec7599e06e21ac8460088b70cd115c3d5ebf9474aa4d54fa.json deleted file mode 100644 index c7719601d7..0000000000 --- a/apps/labrinth/.sqlx/query-d9b6a1b7fb133f7aec7599e06e21ac8460088b70cd115c3d5ebf9474aa4d54fa.json +++ /dev/null @@ -1,126 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n delphi_report_issues.id AS \"id\", report_id,\n issue_type,\n delphi_report_issues.status AS \"status: DelphiReportIssueStatus\",\n\n file_id, delphi_version, artifact_url, created, severity AS \"severity: DelphiSeverity\",\n\n -- TODO: replace with `json_array` in Postgres 16\n (\n SELECT json_agg(to_jsonb(delphi_report_issue_details))\n FROM delphi_report_issue_details\n WHERE issue_id = delphi_report_issues.id\n ) AS \"details: sqlx::types::Json>\",\n versions.mod_id AS \"project_id?\", mods.published AS \"project_published?\"\n FROM delphi_report_issues\n INNER JOIN delphi_reports ON delphi_reports.id = report_id\n LEFT OUTER JOIN files ON files.id = file_id\n LEFT OUTER JOIN versions ON versions.id = files.version_id\n LEFT OUTER JOIN mods ON mods.id = versions.mod_id\n WHERE\n (issue_type = $1 OR $1 IS NULL)\n AND (delphi_report_issues.status = $2 OR $2 IS NULL)\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'pending_status_first' THEN delphi_report_issues.status ELSE 'pending'::delphi_report_issue_status END ASC,\n CASE WHEN $3 = 'severity_asc' THEN delphi_reports.severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN delphi_reports.severity ELSE 'low'::delphi_severity END DESC\n OFFSET $5\n LIMIT $4\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "report_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "issue_type", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "status: DelphiReportIssueStatus", - "type_info": { - "Custom": { - "name": "delphi_report_issue_status", - "kind": { - "Enum": [ - "pending", - "safe", - "unsafe" - ] - } - } - } - }, - { - "ordinal": 4, - "name": "file_id", - "type_info": "Int8" - }, - { - "ordinal": 5, - "name": "delphi_version", - "type_info": "Int4" - }, - { - "ordinal": 6, - "name": "artifact_url", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "created", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "severity: DelphiSeverity", - "type_info": { - "Custom": { - "name": "delphi_severity", - "kind": { - "Enum": [ - "low", - "medium", - "high", - "severe" - ] - } - } - } - }, - { - "ordinal": 9, - "name": "details: sqlx::types::Json>", - "type_info": "Json" - }, - { - "ordinal": 10, - "name": "project_id?", - "type_info": "Int8" - }, - { - "ordinal": 11, - "name": "project_published?", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Text", - { - "Custom": { - "name": "delphi_report_issue_status", - "kind": { - "Enum": [ - "pending", - "safe", - "unsafe" - ] - } - } - }, - "Text", - "Int8", - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - true, - false, - false, - false, - false, - null, - true, - true - ] - }, - "hash": "d9b6a1b7fb133f7aec7599e06e21ac8460088b70cd115c3d5ebf9474aa4d54fa" -} diff --git a/apps/labrinth/.sqlx/query-e6c6348e0bc2bcbac825468a867e19dffcfa710e5b7095cbe33ca58b000b310e.json b/apps/labrinth/.sqlx/query-e6c6348e0bc2bcbac825468a867e19dffcfa710e5b7095cbe33ca58b000b310e.json new file mode 100644 index 0000000000..1cd41afdb1 --- /dev/null +++ b/apps/labrinth/.sqlx/query-e6c6348e0bc2bcbac825468a867e19dffcfa710e5b7095cbe33ca58b000b310e.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE delphi_report_issue_details drid\n SET status = $1\n WHERE drid.id = $2\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + { + "Custom": { + "name": "delphi_report_issue_status", + "kind": { + "Enum": [ + "pending", + "safe", + "unsafe" + ] + } + } + }, + "Int8" + ] + }, + "nullable": [] + }, + "hash": "e6c6348e0bc2bcbac825468a867e19dffcfa710e5b7095cbe33ca58b000b310e" +} diff --git a/apps/labrinth/.sqlx/query-f6432d7a3c67e058c0e9da42f23ea29fa063b416c18dc857132127db95ff17f3.json b/apps/labrinth/.sqlx/query-f6432d7a3c67e058c0e9da42f23ea29fa063b416c18dc857132127db95ff17f3.json new file mode 100644 index 0000000000..aa78274bb1 --- /dev/null +++ b/apps/labrinth/.sqlx/query-f6432d7a3c67e058c0e9da42f23ea29fa063b416c18dc857132127db95ff17f3.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT t.id AS \"thread_id: DBThreadId\"\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n WHERE m.id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "thread_id: DBThreadId", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "f6432d7a3c67e058c0e9da42f23ea29fa063b416c18dc857132127db95ff17f3" +} diff --git a/apps/labrinth/migrations/20251213141500_delphi_report_fixes.sql b/apps/labrinth/migrations/20251213141500_delphi_report_fixes.sql new file mode 100644 index 0000000000..e5fa7189cd --- /dev/null +++ b/apps/labrinth/migrations/20251213141500_delphi_report_fixes.sql @@ -0,0 +1,8 @@ +ALTER TABLE delphi_reports +DROP COLUMN status; + +ALTER TABLE delphi_report_issues +DROP COLUMN status; + +ALTER TABLE delphi_report_issue_details +ADD COLUMN status DELPHI_REPORT_ISSUE_STATUS NOT NULL; diff --git a/apps/labrinth/src/database/models/delphi_report_item.rs b/apps/labrinth/src/database/models/delphi_report_item.rs index ebe6ed437b..bf4fe53fc6 100644 --- a/apps/labrinth/src/database/models/delphi_report_item.rs +++ b/apps/labrinth/src/database/models/delphi_report_item.rs @@ -88,7 +88,6 @@ pub struct DBDelphiReportIssue { pub id: DelphiReportIssueId, pub report_id: DelphiReportId, pub issue_type: String, - pub status: DelphiReportIssueStatus, } /// A status a Delphi report issue can have. @@ -106,7 +105,7 @@ pub struct DBDelphiReportIssue { )] #[serde(rename_all = "snake_case")] #[sqlx(type_name = "delphi_report_issue_status", rename_all = "snake_case")] -pub enum DelphiReportIssueStatus { +pub enum DelphiStatus { /// The issue is pending review by the moderation team. Pending, /// The issue has been rejected (i.e., reviewed as a false positive). @@ -118,7 +117,7 @@ pub enum DelphiReportIssueStatus { Unsafe, } -impl Display for DelphiReportIssueStatus { +impl Display for DelphiStatus { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.serialize(f) } @@ -185,93 +184,17 @@ impl DBDelphiReportIssue { Ok(DelphiReportIssueId( sqlx::query_scalar!( " - INSERT INTO delphi_report_issues (report_id, issue_type, status) - VALUES ($1, $2, $3) - ON CONFLICT (report_id, issue_type) DO UPDATE SET status = $3 + INSERT INTO delphi_report_issues (report_id, issue_type) + VALUES ($1, $2) RETURNING id ", self.report_id as DelphiReportId, self.issue_type, - self.status as DelphiReportIssueStatus, ) .fetch_one(&mut **transaction) .await?, )) } - - pub async fn find_all_by( - ty: Option, - status: Option, - order_by: Option, - count: Option, - offset: Option, - exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>, - ) -> Result, DatabaseError> { - Ok(sqlx::query!( - r#" - SELECT - delphi_report_issues.id AS "id", report_id, - issue_type, - delphi_report_issues.status AS "status: DelphiReportIssueStatus", - - file_id, delphi_version, artifact_url, created, severity AS "severity: DelphiSeverity", - - -- TODO: replace with `json_array` in Postgres 16 - ( - SELECT json_agg(to_jsonb(delphi_report_issue_details)) - FROM delphi_report_issue_details - WHERE issue_id = delphi_report_issues.id - ) AS "details: sqlx::types::Json>", - versions.mod_id AS "project_id?", mods.published AS "project_published?" - FROM delphi_report_issues - INNER JOIN delphi_reports ON delphi_reports.id = report_id - LEFT OUTER JOIN files ON files.id = file_id - LEFT OUTER JOIN versions ON versions.id = files.version_id - LEFT OUTER JOIN mods ON mods.id = versions.mod_id - WHERE - (issue_type = $1 OR $1 IS NULL) - AND (delphi_report_issues.status = $2 OR $2 IS NULL) - ORDER BY - CASE WHEN $3 = 'created_asc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END ASC, - CASE WHEN $3 = 'created_desc' THEN delphi_reports.created ELSE TO_TIMESTAMP(0) END DESC, - CASE WHEN $3 = 'pending_status_first' THEN delphi_report_issues.status ELSE 'pending'::delphi_report_issue_status END ASC, - CASE WHEN $3 = 'severity_asc' THEN delphi_reports.severity ELSE 'low'::delphi_severity END ASC, - CASE WHEN $3 = 'severity_desc' THEN delphi_reports.severity ELSE 'low'::delphi_severity END DESC - OFFSET $5 - LIMIT $4 - "#, - ty, - status as Option, - order_by.map(|order_by| order_by.to_string()), - count.map(|count| count as i64), - offset, - ) - .map(|row| DelphiReportIssueResult { - issue: DBDelphiReportIssue { - id: DelphiReportIssueId(row.id), - report_id: DelphiReportId(row.report_id), - issue_type: row.issue_type, - status: row.status, - }, - report: DBDelphiReport { - id: DelphiReportId(row.report_id), - file_id: row.file_id.map(DBFileId), - delphi_version: row.delphi_version, - artifact_url: row.artifact_url, - created: row.created, - severity: row.severity, - }, - details: row - .details - .into_iter() - .flat_map(|details_list| details_list.0) - .collect(), - project_id: row.project_id.map(DBProjectId), - project_published: row.project_published, - }) - .fetch_all(exec) - .await?) - } } /// The details of a Delphi report issue, which contain data about a @@ -302,6 +225,8 @@ pub struct ReportIssueDetail { pub data: HashMap, /// How important is this issue, as flagged by Delphi? pub severity: DelphiSeverity, + /// Has this issue detail been marked as safe or unsafe? + pub status: DelphiStatus, } impl ReportIssueDetail { @@ -311,8 +236,8 @@ impl ReportIssueDetail { ) -> Result { Ok(DelphiReportIssueDetailsId(sqlx::query_scalar!( " - INSERT INTO delphi_report_issue_details (issue_id, key, file_path, decompiled_source, data, severity) - VALUES ($1, $2, $3, $4, $5, $6) + INSERT INTO delphi_report_issue_details (issue_id, key, file_path, decompiled_source, data, severity, status) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id ", self.issue_id as DelphiReportIssueId, @@ -321,6 +246,7 @@ impl ReportIssueDetail { self.decompiled_source, sqlx::types::Json(&self.data) as Json<&HashMap>, self.severity as DelphiSeverity, + self.status as _, ) .fetch_one(&mut **transaction) .await?)) diff --git a/apps/labrinth/src/routes/internal/delphi.rs b/apps/labrinth/src/routes/internal/delphi.rs index 0c690634ac..26ef746638 100644 --- a/apps/labrinth/src/routes/internal/delphi.rs +++ b/apps/labrinth/src/routes/internal/delphi.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, fmt::Write, sync::LazyLock, time::Instant}; -use actix_web::{HttpRequest, HttpResponse, get, post, put, web}; +use actix_web::{HttpRequest, HttpResponse, get, post, web}; use chrono::{DateTime, Utc}; use eyre::eyre; use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT}; @@ -16,8 +16,8 @@ use crate::{ DBFileId, DelphiReportId, DelphiReportIssueDetailsId, DelphiReportIssueId, delphi_report_item::{ - DBDelphiReport, DBDelphiReportIssue, DelphiReportIssueStatus, - DelphiReportListOrder, DelphiSeverity, ReportIssueDetail, + DBDelphiReport, DBDelphiReportIssue, DelphiSeverity, + DelphiStatus, ReportIssueDetail, }, }, redis::RedisPool, @@ -37,8 +37,6 @@ pub fn config(cfg: &mut web::ServiceConfig) { .service(ingest_report) .service(_run) .service(version) - .service(issues) - .service(update_issue) .service(issue_type_schema), ); } @@ -192,7 +190,6 @@ async fn ingest_report_deserialized( id: DelphiReportIssueId(0), // This will be set by the database report_id, issue_type, - status: DelphiReportIssueStatus::Pending, } .upsert(&mut transaction) .await?; @@ -213,6 +210,7 @@ async fn ingest_report_deserialized( decompiled_source: decompiled_source.cloned().flatten(), data: issue_detail.data, severity: issue_detail.severity, + status: DelphiStatus::Pending, } .insert(&mut transaction) .await?; @@ -308,93 +306,6 @@ async fn version( )) } -#[derive(Deserialize)] -struct DelphiIssuesSearchOptions { - #[serde(rename = "type")] - ty: Option, - status: Option, - order_by: Option, - count: Option, - offset: Option, -} - -#[get("issues")] -async fn issues( - req: HttpRequest, - pool: web::Data, - redis: web::Data, - session_queue: web::Data, - web::Query(search_options): web::Query, -) -> Result { - check_is_moderator_from_headers( - &req, - &**pool, - &redis, - &session_queue, - Scopes::PROJECT_READ, - ) - .await?; - - Ok(HttpResponse::Ok().json( - DBDelphiReportIssue::find_all_by( - search_options.ty, - search_options.status, - search_options.order_by, - search_options.count, - search_options - .offset - .map(|offset| offset.try_into()) - .transpose() - .map_err(|err| { - ApiError::InvalidInput(format!("Invalid offset: {err}")) - })?, - &**pool, - ) - .await?, - )) -} - -#[put("issue/{issue_id}")] -async fn update_issue( - req: HttpRequest, - pool: web::Data, - redis: web::Data, - session_queue: web::Data, - issue_id: web::Path, - web::Json(update_data): web::Json, -) -> Result { - check_is_moderator_from_headers( - &req, - &**pool, - &redis, - &session_queue, - Scopes::PROJECT_READ, - ) - .await?; - - let new_id = issue_id.into_inner(); - - let mut transaction = pool.begin().await?; - - let modified_same_issue = (DBDelphiReportIssue { - id: new_id, // Doesn't matter, upsert done for values of other fields - report_id: update_data.report_id, - issue_type: update_data.issue_type, - status: update_data.status, - }) - .upsert(&mut transaction) - .await? - == new_id; - - transaction.commit().await?; - - if modified_same_issue { - Ok(HttpResponse::NoContent().finish()) - } else { - Ok(HttpResponse::Created().finish()) - } -} - #[get("issue_type/schema")] async fn issue_type_schema( req: HttpRequest, diff --git a/apps/labrinth/src/routes/internal/moderation/mod.rs b/apps/labrinth/src/routes/internal/moderation/mod.rs index 29b7331253..700b511692 100644 --- a/apps/labrinth/src/routes/internal/moderation/mod.rs +++ b/apps/labrinth/src/routes/internal/moderation/mod.rs @@ -111,9 +111,18 @@ pub async fn get_projects_internal( let project_ids = sqlx::query!( " - SELECT id FROM mods - WHERE status = $1 - ORDER BY queued ASC + SELECT m.id FROM mods m + + -- exclude projects in tech review queue + LEFT JOIN versions v ON v.mod_id = m.id + LEFT JOIN files f ON f.version_id = v.id + LEFT JOIN delphi_reports dr ON dr.file_id = f.id + + WHERE + m.status = $1 + AND dr.file_id IS NULL + + ORDER BY m.queued ASC OFFSET $3 LIMIT $2 ", diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 7c7fa04af5..8596ba841b 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, fmt}; -use actix_web::{HttpRequest, get, post, web}; +use actix_web::{HttpRequest, get, patch, post, web}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; @@ -13,10 +13,9 @@ use crate::{ DBProject, models::{ DBProjectId, DBThread, DBThreadId, DBUser, DelphiReportId, - DelphiReportIssueId, ProjectTypeId, + DelphiReportIssueDetailsId, DelphiReportIssueId, ProjectTypeId, delphi_report_item::{ - DelphiReportIssueStatus, DelphiSeverity, DelphiVerdict, - ReportIssueDetail, + DelphiSeverity, DelphiStatus, DelphiVerdict, ReportIssueDetail, }, thread_item::ThreadMessageBuilder, }, @@ -38,7 +37,7 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { .service(get_report) .service(get_issue) .service(submit_report) - .service(update_issue); + .service(update_issue_detail); } /// Arguments for searching project technical reviews. @@ -133,8 +132,6 @@ pub struct FileIssue { /// Labrinth does not know the full set of kinds of issues, so this is kept /// as a string. pub issue_type: String, - /// Is this issue valid (malicious) or a false positive (safe)? - pub status: DelphiReportIssueStatus, /// Details of why this issue might have been raised, such as what file it /// was found in. pub details: Vec, @@ -405,7 +402,8 @@ async fn search_projects( 'file_path', drid.file_path, -- ignore `decompiled_source` 'data', drid.data, - 'severity', drid.severity + 'severity', drid.severity, + 'status', drid.status ) ) FROM delphi_report_issue_details drid @@ -427,7 +425,12 @@ async fn search_projects( INNER JOIN threads t ON t.mod_id = m.id INNER JOIN versions v ON v.mod_id = m.id INNER JOIN files f ON f.version_id = v.id + + -- only return projects with at least 1 pending drid INNER JOIN delphi_reports dr ON dr.file_id = f.id + INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id + INNER JOIN delphi_report_issue_details drid + ON drid.issue_id = dri.id AND drid.status = 'pending' -- filtering LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id @@ -436,7 +439,6 @@ async fn search_projects( -- project type (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[])) AND m.status NOT IN ('draft', 'rejected', 'withheld') - AND dr.status = 'pending' GROUP BY m.id, t.id ) t @@ -486,10 +488,17 @@ async fn search_projects( let db_threads = DBThread::get_many(&thread_ids, &**pool) .await .wrap_internal_err("failed to fetch threads")?; + tracing::info!("db threads = {db_threads:?}"); let thread_author_ids = db_threads .iter() - .flat_map(|thread| thread.members.clone()) + .flat_map(|thread| { + thread + .messages + .iter() + .filter_map(|message| message.author_id) + }) .collect::>(); + tracing::info!("thread author ids = {thread_author_ids:?}"); let thread_authors = DBUser::get_many_ids(&thread_author_ids, &**pool, &redis) .await @@ -538,66 +547,6 @@ async fn search_projects( })) } -// async fn maybe_reject_project( -// report_id: DelphiReportId, -// txn: &mut PgTransaction<'_>, -// update_req: &UpdateStatus, -// user: &User, -// pool: &PgPool, -// redis: &RedisPool, -// ) -> Result<(), ApiError> { -// if update_req.status != DelphiVerdict::Unsafe { -// return Ok(()); -// }; - -// let record = sqlx::query!( -// r#" -// UPDATE mods -// SET status = $1 -// FROM delphi_reports dr -// INNER JOIN files f ON f.id = dr.file_id -// INNER JOIN versions v ON v.id = f.version_id -// INNER JOIN mods m ON v.mod_id = m.id -// INNER JOIN threads t ON t.mod_id = m.id -// WHERE dr.id = $2 -// RETURNING -// m.id AS "project_id: DBProjectId", -// t.id AS "thread_id: DBThreadId", -// (SELECT status FROM mods WHERE id = m.id) AS "old_status!" -// "#, -// ProjectStatus::Rejected.as_str(), -// report_id as DelphiReportId, -// ) -// .fetch_one(&mut **txn) -// .await -// .wrap_internal_err("failed to mark project as rejected")?; - -// if let Some(body) = &update_req.message { -// thread_send_message_internal( -// user, -// record.thread_id.into(), -// pool, -// NewThreadMessage { -// body: MessageBody::Text { -// body: body.clone(), -// private: true, -// replying_to: None, -// associated_images: Vec::new(), -// }, -// }, -// redis, -// ) -// .await -// .wrap_internal_err("failed to add moderation thread message")?; -// } - -// // DBProject::clear_cache(record.project_id, None, None, redis) -// // .await -// // .wrap_internal_err("failed to clear project cache")?; - -// Ok(()) -// } - /// See [`submit_report`]. #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct SubmitReport { @@ -624,7 +573,7 @@ async fn submit_report( pool: web::Data, redis: web::Data, session_queue: web::Data, - submit_report: web::Json, + web::Json(submit_report): web::Json, path: web::Path<(ProjectId,)>, ) -> Result<(), ApiError> { let user = check_is_moderator_from_headers( @@ -643,19 +592,19 @@ async fn submit_report( .await .wrap_internal_err("failed to begin transaction")?; - let pending_issues = sqlx::query!( + let pending_issue_details = sqlx::query!( " SELECT - dri.id AS issue_id - FROM delphi_report_issues dri - INNER JOIN delphi_reports dr ON dr.id = dri.report_id - INNER JOIN files f ON f.id = dr.file_id - INNER JOIN versions v ON v.id = f.version_id - INNER JOIN mods m ON m.id = v.mod_id + drid.id AS issue_detail_id + FROM mods m + INNER JOIN versions v ON v.mod_id = m.id + INNER JOIN files f ON f.version_id = v.id + INNER JOIN delphi_reports dr ON dr.file_id = f.id + INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id + INNER JOIN delphi_report_issue_details drid ON drid.issue_id = dri.id WHERE m.id = $1 - AND dr.status = 'pending' - AND dri.status = 'pending' + AND drid.status = 'pending' ", project_id as _, ) @@ -663,41 +612,31 @@ async fn submit_report( .await .wrap_internal_err("failed to fetch pending issues")?; - if !pending_issues.is_empty() { - return Err(ApiError::TechReviewIssuesWithNoVerdict { - issues: pending_issues + if !pending_issue_details.is_empty() { + return Err(ApiError::TechReviewDetailsWithNoVerdict { + details: pending_issue_details .into_iter() - .map(|record| DelphiReportIssueId(record.issue_id)) + .map(|record| { + DelphiReportIssueDetailsId(record.issue_detail_id) + }) .collect(), }); } - let verdict = submit_report.verdict; - let status = match verdict { - DelphiVerdict::Unsafe => DelphiReportIssueStatus::Unsafe, - DelphiVerdict::Safe => DelphiReportIssueStatus::Safe, - }; let record = sqlx::query!( r#" - UPDATE delphi_reports - SET status = $1 - FROM delphi_reports dr - INNER JOIN files f ON f.id = dr.file_id - INNER JOIN versions v ON v.id = f.version_id - INNER JOIN mods m ON v.mod_id = m.id + SELECT t.id AS "thread_id: DBThreadId" + FROM mods m INNER JOIN threads t ON t.mod_id = m.id - WHERE m.id = $2 - RETURNING - t.id AS "thread_id: DBThreadId" + WHERE m.id = $1 "#, - status as _, project_id as _, ) .fetch_one(&mut *txn) .await .wrap_internal_err("failed to update reports")?; - if let Some(body) = submit_report.0.message { + if let Some(body) = submit_report.message { ThreadMessageBuilder { author_id: Some(user.id.into()), body: MessageBody::Text { @@ -714,6 +653,7 @@ async fn submit_report( .wrap_internal_err("failed to add moderator message")?; } + let verdict = submit_report.verdict; ThreadMessageBuilder { author_id: Some(user.id.into()), body: MessageBody::TechReview { verdict }, @@ -775,7 +715,7 @@ pub struct UpdateIssue { pub verdict: DelphiVerdict, } -/// Updates the state of a technical review issue. +/// Updates the state of a technical review issue detail. /// /// This will not automatically reject the project for malware, but just flag /// this issue with a verdict. @@ -783,16 +723,16 @@ pub struct UpdateIssue { security(("bearer_auth" = [])), responses((status = NO_CONTENT)) )] -#[post("/issue/{id}")] -async fn update_issue( +#[patch("/issue-detail/{id}")] +async fn update_issue_detail( req: HttpRequest, pool: web::Data, redis: web::Data, session_queue: web::Data, update_req: web::Json, - path: web::Path<(DelphiReportIssueId,)>, + path: web::Path<(DelphiReportIssueDetailsId,)>, ) -> Result<(), ApiError> { - let _user = check_is_moderator_from_headers( + check_is_moderator_from_headers( &req, &**pool, &redis, @@ -800,7 +740,7 @@ async fn update_issue( Scopes::PROJECT_WRITE, ) .await?; - let (issue_id,) = path.into_inner(); + let (issue_detail_id,) = path.into_inner(); let mut txn = pool .begin() @@ -808,23 +748,21 @@ async fn update_issue( .wrap_internal_err("failed to start transaction")?; let status = match update_req.verdict { - DelphiVerdict::Safe => DelphiReportIssueStatus::Safe, - DelphiVerdict::Unsafe => DelphiReportIssueStatus::Unsafe, + DelphiVerdict::Safe => DelphiStatus::Safe, + DelphiVerdict::Unsafe => DelphiStatus::Unsafe, }; - let _record = sqlx::query!( + sqlx::query!( r#" - UPDATE delphi_report_issues dri + UPDATE delphi_report_issue_details drid SET status = $1 - FROM delphi_reports dr - WHERE dri.id = $2 AND dr.id = dri.report_id - RETURNING dr.id AS "report_id: DelphiReportId" + WHERE drid.id = $2 "#, status as _, - issue_id as _, + issue_detail_id as _, ) - .fetch_one(&mut *txn) + .execute(&mut *txn) .await - .wrap_internal_err("failed to update issue")?; + .wrap_internal_err("failed to update issue detail")?; txn.commit() .await diff --git a/apps/labrinth/src/routes/mod.rs b/apps/labrinth/src/routes/mod.rs index f79a391a80..64b0c2a2f8 100644 --- a/apps/labrinth/src/routes/mod.rs +++ b/apps/labrinth/src/routes/mod.rs @@ -1,4 +1,4 @@ -use crate::database::models::DelphiReportIssueId; +use crate::database::models::DelphiReportIssueDetailsId; use crate::file_hosting::FileHostingError; use crate::routes::analytics::{page_view_ingest, playtime_ingest}; use crate::util::cors::default_cors; @@ -167,8 +167,10 @@ pub enum ApiError { Delphi(reqwest::Error), #[error(transparent)] Mural(#[from] Box), - #[error("report still has {} issues with no verdict", issues.len())] - TechReviewIssuesWithNoVerdict { issues: Vec }, + #[error("report still has {} issue detailss with no verdict", details.len())] + TechReviewDetailsWithNoVerdict { + details: Vec, + }, } impl ApiError { @@ -211,7 +213,7 @@ impl ApiError { Self::Slack(..) => "slack_error", Self::Delphi(..) => "delphi_error", Self::Mural(..) => "mural_error", - Self::TechReviewIssuesWithNoVerdict { .. } => { + Self::TechReviewDetailsWithNoVerdict { .. } => { "tech_review_issues_with_no_verdict" } }, @@ -223,11 +225,11 @@ impl ApiError { }, details: match self { Self::Mural(err) => serde_json::to_value(err.clone()).ok(), - Self::TechReviewIssuesWithNoVerdict { issues } => { - let issues = serde_json::to_value(issues) - .expect("issues should never fail to serialize"); + Self::TechReviewDetailsWithNoVerdict { details } => { + let details = serde_json::to_value(details) + .expect("details should never fail to serialize"); Some(json!({ - "issues": issues + "issue_details": details })) } _ => None, @@ -275,7 +277,7 @@ impl actix_web::ResponseError for ApiError { Self::Slack(..) => StatusCode::INTERNAL_SERVER_ERROR, Self::Delphi(..) => StatusCode::INTERNAL_SERVER_ERROR, Self::Mural(..) => StatusCode::BAD_REQUEST, - Self::TechReviewIssuesWithNoVerdict { .. } => { + Self::TechReviewDetailsWithNoVerdict { .. } => { StatusCode::BAD_REQUEST } } From 8a699f50ff8808afe1918accdb82fd7cdd6c94fb Mon Sep 17 00:00:00 2001 From: aecsocket Date: Mon, 15 Dec 2025 11:56:56 +0000 Subject: [PATCH 55/65] add default to drid status --- apps/labrinth/migrations/20251213141500_delphi_report_fixes.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/labrinth/migrations/20251213141500_delphi_report_fixes.sql b/apps/labrinth/migrations/20251213141500_delphi_report_fixes.sql index e5fa7189cd..df40552920 100644 --- a/apps/labrinth/migrations/20251213141500_delphi_report_fixes.sql +++ b/apps/labrinth/migrations/20251213141500_delphi_report_fixes.sql @@ -5,4 +5,4 @@ ALTER TABLE delphi_report_issues DROP COLUMN status; ALTER TABLE delphi_report_issue_details -ADD COLUMN status DELPHI_REPORT_ISSUE_STATUS NOT NULL; +ADD COLUMN status DELPHI_REPORT_ISSUE_STATUS NOT NULL DEFAULT 'pending'; From 1624186b3efda98b0da03b70069ff7a0b9be38a2 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Mon, 15 Dec 2025 20:03:39 +0000 Subject: [PATCH 56/65] dedup issue details --- ...72f9daba801e6c4e9fe47e6476bb994c87a336fb4b4193aa9c2.json} | 4 ++-- apps/labrinth/src/routes/internal/moderation/tech_review.rs | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) rename apps/labrinth/.sqlx/{query-b96e50f4324fda4adbe5742a356ed9d06749cea63ae8650d83506eb074688917.json => query-9fc141bf33d7d72f9daba801e6c4e9fe47e6476bb994c87a336fb4b4193aa9c2.json} (70%) diff --git a/apps/labrinth/.sqlx/query-b96e50f4324fda4adbe5742a356ed9d06749cea63ae8650d83506eb074688917.json b/apps/labrinth/.sqlx/query-9fc141bf33d7d72f9daba801e6c4e9fe47e6476bb994c87a336fb4b4193aa9c2.json similarity index 70% rename from apps/labrinth/.sqlx/query-b96e50f4324fda4adbe5742a356ed9d06749cea63ae8650d83506eb074688917.json rename to apps/labrinth/.sqlx/query-9fc141bf33d7d72f9daba801e6c4e9fe47e6476bb994c87a336fb4b4193aa9c2.json index d10966ef91..7a0d680aeb 100644 --- a/apps/labrinth/.sqlx/query-b96e50f4324fda4adbe5742a356ed9d06749cea63ae8650d83506eb074688917.json +++ b/apps/labrinth/.sqlx/query-9fc141bf33d7d72f9daba801e6c4e9fe47e6476bb994c87a336fb4b4193aa9c2.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (m.id)\n m.id AS project_id,\n t.id AS project_thread_id,\n MAX(dr.severity) AS severity,\n MIN(dr.created) AS earliest_report_created,\n MAX(dr.created) AS latest_report_created,\n\n jsonb_build_object(\n 'project_id', to_base62(m.id),\n 'max_severity', MAX(dr.severity),\n -- TODO: replace with `json_array` in Postgres 16\n 'versions', (\n SELECT json_agg(jsonb_build_object(\n 'version_id', to_base62(v.id),\n -- TODO: replace with `json_array` in Postgres 16\n 'files', (\n SELECT json_agg(jsonb_build_object(\n 'report_id', dr.id,\n 'file_id', to_base62(f.id),\n 'created', dr.created,\n 'flag_reason', 'delphi',\n 'severity', dr.severity,\n 'file_name', f.filename,\n 'file_size', f.size,\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT json_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT json_agg(\n jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity,\n 'status', drid.status\n )\n )\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ))\n FROM delphi_reports dr\n WHERE dr.file_id = f.id\n )\n ))\n )\n ) AS report\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n\n -- only return projects with at least 1 pending drid\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_report_issue_details drid\n ON drid.issue_id = dri.id AND drid.status = 'pending'\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[]))\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n\n GROUP BY m.id, t.id\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN t.earliest_report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN t.latest_report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN t.severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN t.severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $1\n OFFSET $2\n ", + "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (m.id)\n m.id AS project_id,\n t.id AS project_thread_id,\n MAX(dr.severity) AS severity,\n MIN(dr.created) AS earliest_report_created,\n MAX(dr.created) AS latest_report_created,\n\n jsonb_build_object(\n 'project_id', to_base62(m.id),\n 'max_severity', MAX(dr.severity),\n -- TODO: replace with `json_array` in Postgres 16\n 'versions', (\n SELECT json_agg(jsonb_build_object(\n 'version_id', to_base62(v.id),\n -- TODO: replace with `json_array` in Postgres 16\n 'files', (\n SELECT json_agg(jsonb_build_object(\n 'report_id', dr.id,\n 'file_id', to_base62(f.id),\n 'created', dr.created,\n 'flag_reason', 'delphi',\n 'severity', dr.severity,\n 'file_name', f.filename,\n 'file_size', f.size,\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT json_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT json_agg(\n jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity,\n 'status', drid.status\n )\n )\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ))\n FROM delphi_reports dr\n WHERE dr.file_id = f.id\n )\n ))\n FROM versions v\n INNER JOIN files f ON f.version_id = v.id\n WHERE v.mod_id = m.id\n )\n ) AS report\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n\n -- only return projects with at least 1 pending drid\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_report_issue_details drid\n ON drid.issue_id = dri.id AND drid.status = 'pending'\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[]))\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n\n GROUP BY m.id, t.id\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN t.earliest_report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN t.latest_report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN t.severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN t.severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $1\n OFFSET $2\n ", "describe": { "columns": [ { @@ -33,5 +33,5 @@ null ] }, - "hash": "b96e50f4324fda4adbe5742a356ed9d06749cea63ae8650d83506eb074688917" + "hash": "9fc141bf33d7d72f9daba801e6c4e9fe47e6476bb994c87a336fb4b4193aa9c2" } diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 8596ba841b..3c269bad05 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -419,6 +419,9 @@ async fn search_projects( WHERE dr.file_id = f.id ) )) + FROM versions v + INNER JOIN files f ON f.version_id = v.id + WHERE v.mod_id = m.id ) ) AS report FROM mods m @@ -488,7 +491,6 @@ async fn search_projects( let db_threads = DBThread::get_many(&thread_ids, &**pool) .await .wrap_internal_err("failed to fetch threads")?; - tracing::info!("db threads = {db_threads:?}"); let thread_author_ids = db_threads .iter() .flat_map(|thread| { @@ -498,7 +500,6 @@ async fn search_projects( .filter_map(|message| message.author_id) }) .collect::>(); - tracing::info!("thread author ids = {thread_author_ids:?}"); let thread_authors = DBUser::get_many_ids(&thread_author_ids, &**pool, &redis) .await From 35b977e4d173279fbc52be5724b9e48250c22b8e Mon Sep 17 00:00:00 2001 From: aecsocket Date: Mon, 15 Dec 2025 22:38:06 +0000 Subject: [PATCH 57/65] fix sqlx query on empty files --- ...55cee835771a32fd89e33b86506e172bc45db.json | 37 +++++++++++++++++++ ...4e9fe47e6476bb994c87a336fb4b4193aa9c2.json | 37 ------------------- .../routes/internal/moderation/tech_review.rs | 23 +++++++----- 3 files changed, 51 insertions(+), 46 deletions(-) create mode 100644 apps/labrinth/.sqlx/query-79123e0d616a23396a8284f363355cee835771a32fd89e33b86506e172bc45db.json delete mode 100644 apps/labrinth/.sqlx/query-9fc141bf33d7d72f9daba801e6c4e9fe47e6476bb994c87a336fb4b4193aa9c2.json diff --git a/apps/labrinth/.sqlx/query-79123e0d616a23396a8284f363355cee835771a32fd89e33b86506e172bc45db.json b/apps/labrinth/.sqlx/query-79123e0d616a23396a8284f363355cee835771a32fd89e33b86506e172bc45db.json new file mode 100644 index 0000000000..f097423f6e --- /dev/null +++ b/apps/labrinth/.sqlx/query-79123e0d616a23396a8284f363355cee835771a32fd89e33b86506e172bc45db.json @@ -0,0 +1,37 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (m.id)\n m.id AS project_id,\n t.id AS project_thread_id,\n MAX(dr.severity) AS severity,\n MIN(dr.created) AS earliest_report_created,\n MAX(dr.created) AS latest_report_created,\n\n jsonb_build_object(\n 'project_id', to_base62(m.id),\n 'max_severity', MAX(dr.severity),\n -- TODO: replace with `json_array` in Postgres 16\n 'versions', (\n SELECT coalesce(jsonb_agg(jsonb_build_object(\n 'version_id', to_base62(v.id),\n -- TODO: replace with `json_array` in Postgres 16\n 'files', (\n SELECT coalesce(jsonb_agg(jsonb_build_object(\n 'report_id', dr.id,\n 'file_id', to_base62(f.id),\n 'created', dr.created,\n 'flag_reason', 'delphi',\n 'severity', dr.severity,\n 'file_name', f.filename,\n 'file_size', f.size,\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT coalesce(jsonb_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT coalesce(jsonb_agg(\n jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity,\n 'status', drid.status\n )\n ), '[]'::jsonb)\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n ), '[]'::jsonb)\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n )), '[]'::jsonb)\n FROM delphi_reports dr\n WHERE dr.file_id = f.id\n )\n )), '[]'::jsonb)\n FROM versions v\n INNER JOIN files f ON f.version_id = v.id\n WHERE v.mod_id = m.id\n )\n ) AS report\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n\n -- only return projects with at least 1 pending drid\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_report_issue_details drid\n ON drid.issue_id = dri.id AND drid.status = 'pending'\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[]))\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n\n GROUP BY m.id, t.id\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN t.earliest_report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN t.latest_report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN t.severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN t.severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $1\n OFFSET $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "project_id: DBProjectId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "project_thread_id: DBThreadId", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "report!: sqlx::types::Json", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Text", + "Int4Array" + ] + }, + "nullable": [ + false, + false, + null + ] + }, + "hash": "79123e0d616a23396a8284f363355cee835771a32fd89e33b86506e172bc45db" +} diff --git a/apps/labrinth/.sqlx/query-9fc141bf33d7d72f9daba801e6c4e9fe47e6476bb994c87a336fb4b4193aa9c2.json b/apps/labrinth/.sqlx/query-9fc141bf33d7d72f9daba801e6c4e9fe47e6476bb994c87a336fb4b4193aa9c2.json deleted file mode 100644 index 7a0d680aeb..0000000000 --- a/apps/labrinth/.sqlx/query-9fc141bf33d7d72f9daba801e6c4e9fe47e6476bb994c87a336fb4b4193aa9c2.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (m.id)\n m.id AS project_id,\n t.id AS project_thread_id,\n MAX(dr.severity) AS severity,\n MIN(dr.created) AS earliest_report_created,\n MAX(dr.created) AS latest_report_created,\n\n jsonb_build_object(\n 'project_id', to_base62(m.id),\n 'max_severity', MAX(dr.severity),\n -- TODO: replace with `json_array` in Postgres 16\n 'versions', (\n SELECT json_agg(jsonb_build_object(\n 'version_id', to_base62(v.id),\n -- TODO: replace with `json_array` in Postgres 16\n 'files', (\n SELECT json_agg(jsonb_build_object(\n 'report_id', dr.id,\n 'file_id', to_base62(f.id),\n 'created', dr.created,\n 'flag_reason', 'delphi',\n 'severity', dr.severity,\n 'file_name', f.filename,\n 'file_size', f.size,\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT json_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT json_agg(\n jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity,\n 'status', drid.status\n )\n )\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ))\n FROM delphi_reports dr\n WHERE dr.file_id = f.id\n )\n ))\n FROM versions v\n INNER JOIN files f ON f.version_id = v.id\n WHERE v.mod_id = m.id\n )\n ) AS report\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n\n -- only return projects with at least 1 pending drid\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_report_issue_details drid\n ON drid.issue_id = dri.id AND drid.status = 'pending'\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[]))\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n\n GROUP BY m.id, t.id\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN t.earliest_report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN t.latest_report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN t.severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN t.severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $1\n OFFSET $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "project_id: DBProjectId", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "project_thread_id: DBThreadId", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "report!: sqlx::types::Json", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Text", - "Int4Array" - ] - }, - "nullable": [ - false, - false, - null - ] - }, - "hash": "9fc141bf33d7d72f9daba801e6c4e9fe47e6476bb994c87a336fb4b4193aa9c2" -} diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 3c269bad05..679d370244 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -114,6 +114,7 @@ pub struct FileReport { /// URL to download the flagged file. pub download_url: String, /// What issues appeared in the file. + #[serde(default)] pub issues: Vec, } @@ -134,6 +135,7 @@ pub struct FileIssue { pub issue_type: String, /// Details of why this issue might have been raised, such as what file it /// was found in. + #[serde(default)] pub details: Vec, } @@ -291,8 +293,9 @@ pub struct ProjectReport { pub project_id: ProjectId, /// Highest severity of any report of any file of any version under this /// project. - pub max_severity: DelphiSeverity, + pub max_severity: Option, /// Reports for this project's versions. + #[serde(default)] pub versions: Vec, } @@ -302,6 +305,7 @@ pub struct VersionReport { /// ID of the project version this report is for. pub version_id: VersionId, /// Reports for this version's files. + #[serde(default)] pub files: Vec, } @@ -315,6 +319,7 @@ pub struct ProjectModerationInfo { /// Project name. pub name: String, /// The aggregated project typos of the versions of this project + #[serde(default)] pub project_types: Vec, /// The URL of the icon of the project pub icon_url: Option, @@ -374,11 +379,11 @@ async fn search_projects( 'max_severity', MAX(dr.severity), -- TODO: replace with `json_array` in Postgres 16 'versions', ( - SELECT json_agg(jsonb_build_object( + SELECT coalesce(jsonb_agg(jsonb_build_object( 'version_id', to_base62(v.id), -- TODO: replace with `json_array` in Postgres 16 'files', ( - SELECT json_agg(jsonb_build_object( + SELECT coalesce(jsonb_agg(jsonb_build_object( 'report_id', dr.id, 'file_id', to_base62(f.id), 'created', dr.created, @@ -389,12 +394,12 @@ async fn search_projects( 'download_url', f.url, -- TODO: replace with `json_array` in Postgres 16 'issues', ( - SELECT json_agg( + SELECT coalesce(jsonb_agg( to_jsonb(dri) || jsonb_build_object( -- TODO: replace with `json_array` in Postgres 16 'details', ( - SELECT json_agg( + SELECT coalesce(jsonb_agg( jsonb_build_object( 'id', drid.id, 'issue_id', drid.issue_id, @@ -405,20 +410,20 @@ async fn search_projects( 'severity', drid.severity, 'status', drid.status ) - ) + ), '[]'::jsonb) FROM delphi_report_issue_details drid WHERE drid.issue_id = dri.id ) ) - ) + ), '[]'::jsonb) FROM delphi_report_issues dri WHERE dri.report_id = dr.id ) - )) + )), '[]'::jsonb) FROM delphi_reports dr WHERE dr.file_id = f.id ) - )) + )), '[]'::jsonb) FROM versions v INNER JOIN files f ON f.version_id = v.id WHERE v.mod_id = m.id From 10a222a92d693937ff8456be33abb3f3da942182 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Wed, 17 Dec 2025 20:13:42 +0000 Subject: [PATCH 58/65] fixes --- ...b02663526b5635cf58f4095efe8755c7d3786.json | 24 ------ ...2f09597e02752ef4176109ece44c793ec1f72.json | 24 ++++++ ...061d930ff06a8a928ff1cea6a723bb37c1b75.json | 28 ++++++ apps/labrinth/src/models/v3/threads.rs | 2 +- .../src/routes/internal/moderation/mod.rs | 35 +++++--- .../routes/internal/moderation/tech_review.rs | 85 +++++++++++++++++-- 6 files changed, 156 insertions(+), 42 deletions(-) delete mode 100644 apps/labrinth/.sqlx/query-5411cc0153106bfe95f2341fd85b02663526b5635cf58f4095efe8755c7d3786.json create mode 100644 apps/labrinth/.sqlx/query-95137df2c76294593cfa988d3ed2f09597e02752ef4176109ece44c793ec1f72.json create mode 100644 apps/labrinth/.sqlx/query-cd630ba950611b387fb5b04999a061d930ff06a8a928ff1cea6a723bb37c1b75.json diff --git a/apps/labrinth/.sqlx/query-5411cc0153106bfe95f2341fd85b02663526b5635cf58f4095efe8755c7d3786.json b/apps/labrinth/.sqlx/query-5411cc0153106bfe95f2341fd85b02663526b5635cf58f4095efe8755c7d3786.json deleted file mode 100644 index e359d92a4f..0000000000 --- a/apps/labrinth/.sqlx/query-5411cc0153106bfe95f2341fd85b02663526b5635cf58f4095efe8755c7d3786.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT m.id FROM mods m\n\n -- exclude projects in tech review queue\n LEFT JOIN versions v ON v.mod_id = m.id\n LEFT JOIN files f ON f.version_id = v.id\n LEFT JOIN delphi_reports dr ON dr.file_id = f.id\n\n WHERE\n m.status = $1\n AND dr.file_id IS NULL\n\n ORDER BY m.queued ASC\n OFFSET $3\n LIMIT $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - "Int8", - "Int8" - ] - }, - "nullable": [ - false - ] - }, - "hash": "5411cc0153106bfe95f2341fd85b02663526b5635cf58f4095efe8755c7d3786" -} diff --git a/apps/labrinth/.sqlx/query-95137df2c76294593cfa988d3ed2f09597e02752ef4176109ece44c793ec1f72.json b/apps/labrinth/.sqlx/query-95137df2c76294593cfa988d3ed2f09597e02752ef4176109ece44c793ec1f72.json new file mode 100644 index 0000000000..11d2d7d812 --- /dev/null +++ b/apps/labrinth/.sqlx/query-95137df2c76294593cfa988d3ed2f09597e02752ef4176109ece44c793ec1f72.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id\n FROM (\n SELECT DISTINCT ON (m.id)\n m.id,\n m.queued\n FROM mods m\n\n -- exclude projects in tech review queue\n LEFT JOIN versions v ON v.mod_id = m.id\n LEFT JOIN files f ON f.version_id = v.id\n LEFT JOIN delphi_reports dr ON dr.file_id = f.id\n LEFT JOIN delphi_report_issues dri ON dri.report_id = dr.id\n LEFT JOIN delphi_report_issue_details drid\n ON drid.issue_id = dri.id AND drid.status = 'pending'\n\n WHERE\n m.status = $1\n AND drid.status IS NULL\n\n GROUP BY m.id\n ) t\n\n ORDER BY queued ASC\n OFFSET $3\n LIMIT $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Int8", + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "95137df2c76294593cfa988d3ed2f09597e02752ef4176109ece44c793ec1f72" +} diff --git a/apps/labrinth/.sqlx/query-cd630ba950611b387fb5b04999a061d930ff06a8a928ff1cea6a723bb37c1b75.json b/apps/labrinth/.sqlx/query-cd630ba950611b387fb5b04999a061d930ff06a8a928ff1cea6a723bb37c1b75.json new file mode 100644 index 0000000000..aa2bec3121 --- /dev/null +++ b/apps/labrinth/.sqlx/query-cd630ba950611b387fb5b04999a061d930ff06a8a928ff1cea6a723bb37c1b75.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n f.url,\n COUNT(dr.id) AS \"report_count!\"\n FROM files f\n LEFT JOIN delphi_reports dr ON dr.file_id = f.id\n WHERE f.id = $1\n GROUP BY f.url\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "url", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "report_count!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + null + ] + }, + "hash": "cd630ba950611b387fb5b04999a061d930ff06a8a928ff1cea6a723bb37c1b75" +} diff --git a/apps/labrinth/src/models/v3/threads.rs b/apps/labrinth/src/models/v3/threads.rs index 4f371caab0..adfc21af50 100644 --- a/apps/labrinth/src/models/v3/threads.rs +++ b/apps/labrinth/src/models/v3/threads.rs @@ -118,7 +118,7 @@ impl Thread { messages: data .messages .into_iter() - .filter(|x| user.role.is_mod() || x.body.is_private()) + .filter(|x| user.role.is_mod() || !x.body.is_private()) .map(|x| ThreadMessage::from(x, user)) .collect(), members: users, diff --git a/apps/labrinth/src/routes/internal/moderation/mod.rs b/apps/labrinth/src/routes/internal/moderation/mod.rs index 700b511692..b719d688c3 100644 --- a/apps/labrinth/src/routes/internal/moderation/mod.rs +++ b/apps/labrinth/src/routes/internal/moderation/mod.rs @@ -111,18 +111,29 @@ pub async fn get_projects_internal( let project_ids = sqlx::query!( " - SELECT m.id FROM mods m - - -- exclude projects in tech review queue - LEFT JOIN versions v ON v.mod_id = m.id - LEFT JOIN files f ON f.version_id = v.id - LEFT JOIN delphi_reports dr ON dr.file_id = f.id - - WHERE - m.status = $1 - AND dr.file_id IS NULL - - ORDER BY m.queued ASC + SELECT id + FROM ( + SELECT DISTINCT ON (m.id) + m.id, + m.queued + FROM mods m + + -- exclude projects in tech review queue + LEFT JOIN versions v ON v.mod_id = m.id + LEFT JOIN files f ON f.version_id = v.id + LEFT JOIN delphi_reports dr ON dr.file_id = f.id + LEFT JOIN delphi_report_issues dri ON dri.report_id = dr.id + LEFT JOIN delphi_report_issue_details drid + ON drid.issue_id = dri.id AND drid.status = 'pending' + + WHERE + m.status = $1 + AND drid.status IS NULL + + GROUP BY m.id + ) t + + ORDER BY queued ASC OFFSET $3 LIMIT $2 ", diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 679d370244..e6b9735056 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, fmt}; -use actix_web::{HttpRequest, get, patch, post, web}; +use actix_web::{HttpRequest, get, patch, post, put, web}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; @@ -12,10 +12,12 @@ use crate::{ database::{ DBProject, models::{ - DBProjectId, DBThread, DBThreadId, DBUser, DelphiReportId, - DelphiReportIssueDetailsId, DelphiReportIssueId, ProjectTypeId, + DBFileId, DBProjectId, DBThread, DBThreadId, DBUser, + DelphiReportId, DelphiReportIssueDetailsId, DelphiReportIssueId, + ProjectTypeId, delphi_report_item::{ - DelphiSeverity, DelphiStatus, DelphiVerdict, ReportIssueDetail, + DBDelphiReport, DelphiSeverity, DelphiStatus, DelphiVerdict, + ReportIssueDetail, }, thread_item::ThreadMessageBuilder, }, @@ -31,13 +33,15 @@ use crate::{ routes::{ApiError, internal::moderation::Ownership}, util::error::Context, }; +use eyre::eyre; pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { cfg.service(search_projects) .service(get_report) .service(get_issue) .service(submit_report) - .service(update_issue_detail); + .service(update_issue_detail) + .service(add_report); } /// Arguments for searching project technical reviews. @@ -776,3 +780,74 @@ async fn update_issue_detail( Ok(()) } + +/// See [`add_report`]. +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct AddReport { + pub file_id: FileId, +} + +/// Adds a file to the technical review queue by adding an empty report, if one +/// does not already exist for it. +#[utoipa::path] +#[put("/report")] +async fn add_report( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, + web::Json(add_report): web::Json, +) -> Result, ApiError> { + check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_WRITE, + ) + .await?; + let file_id = add_report.file_id; + + let mut txn = pool + .begin() + .await + .wrap_internal_err("failed to begin transaction")?; + + let record = sqlx::query!( + r#" + SELECT + f.url, + COUNT(dr.id) AS "report_count!" + FROM files f + LEFT JOIN delphi_reports dr ON dr.file_id = f.id + WHERE f.id = $1 + GROUP BY f.url + "#, + DBFileId::from(file_id) as _, + ) + .fetch_one(&mut *txn) + .await + .wrap_internal_err("failed to fetch file")?; + + if record.report_count > 0 { + return Err(ApiError::Request(eyre!("file already has reports"))); + } + + let report_id = DBDelphiReport { + id: DelphiReportId(0), + file_id: Some(file_id.into()), + delphi_version: -1, // TODO + artifact_url: record.url, + created: Utc::now(), + severity: DelphiSeverity::Low, // TODO + } + .upsert(&mut txn) + .await + .wrap_internal_err("failed to insert report")?; + + txn.commit() + .await + .wrap_internal_err("failed to commit transaction")?; + + Ok(web::Json(report_id)) +} From ca6724f45c0bb0050c18d8b94303447ca8af4367 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Thu, 18 Dec 2025 01:02:30 +0000 Subject: [PATCH 59/65] Dedupe issue detail statuses and message on entering tech rev --- ...fac7d220fe043b7580d7b0cba74189688fad2.json | 26 +++++++++++ ...ebe1d03998be169a306b63a0ca1beaa07397f.json | 28 +++++++++++ ...6b1d26441bc2428f3f63d18f0ac5a2708caad.json | 22 +++++++++ ...39198f064dd0bfa53ad27db598928f62598f0.json | 22 --------- ...55cee835771a32fd89e33b86506e172bc45db.json | 37 --------------- ...2f09597e02752ef4176109ece44c793ec1f72.json | 24 ---------- ...efd9e422151b683d9897a071ee0c4bac1cd4.json} | 16 +------ ...df8c80613f5c51ed38a3c52e39e86d5e3044c.json | 37 +++++++++++++++ ...350233d40ad81ac2d16481a0e9b32424a999d.json | 24 ++++++++++ ...e19dffcfa710e5b7095cbe33ca58b000b310e.json | 26 ----------- ...47_delphi_dedupe_issue_detail_statuses.sql | 26 +++++++++++ .../src/database/models/delphi_report_item.rs | 5 +- apps/labrinth/src/models/v2/threads.rs | 16 +++++++ apps/labrinth/src/models/v3/threads.rs | 6 ++- apps/labrinth/src/routes/internal/delphi.rs | 41 ++++++++++++++++- .../src/routes/internal/moderation/mod.rs | 10 ++-- .../routes/internal/moderation/tech_review.rs | 46 +++++++++++-------- 17 files changed, 256 insertions(+), 156 deletions(-) create mode 100644 apps/labrinth/.sqlx/query-020bac2af0493e11368af8f60e1fac7d220fe043b7580d7b0cba74189688fad2.json create mode 100644 apps/labrinth/.sqlx/query-2d9e36c76a1e214c53d9dc2aa3debe1d03998be169a306b63a0ca1beaa07397f.json create mode 100644 apps/labrinth/.sqlx/query-46eb7d30bd83f3018677d9ffad26b1d26441bc2428f3f63d18f0ac5a2708caad.json delete mode 100644 apps/labrinth/.sqlx/query-506a7cb24b4ff9d0d3a49ef801039198f064dd0bfa53ad27db598928f62598f0.json delete mode 100644 apps/labrinth/.sqlx/query-79123e0d616a23396a8284f363355cee835771a32fd89e33b86506e172bc45db.json delete mode 100644 apps/labrinth/.sqlx/query-95137df2c76294593cfa988d3ed2f09597e02752ef4176109ece44c793ec1f72.json rename apps/labrinth/.sqlx/{query-1116dd6bc5024f0908147f820d76ee7a6510aef3c210ffbc18bb12e556f30b84.json => query-b65094517546487e43b65a76aa38efd9e422151b683d9897a071ee0c4bac1cd4.json} (57%) create mode 100644 apps/labrinth/.sqlx/query-bdb1b3de1a2a0af9e5cbc1412fbdf8c80613f5c51ed38a3c52e39e86d5e3044c.json create mode 100644 apps/labrinth/.sqlx/query-d30290c1b55d9fb0939d122a96f350233d40ad81ac2d16481a0e9b32424a999d.json delete mode 100644 apps/labrinth/.sqlx/query-e6c6348e0bc2bcbac825468a867e19dffcfa710e5b7095cbe33ca58b000b310e.json create mode 100644 apps/labrinth/migrations/20251217234047_delphi_dedupe_issue_detail_statuses.sql diff --git a/apps/labrinth/.sqlx/query-020bac2af0493e11368af8f60e1fac7d220fe043b7580d7b0cba74189688fad2.json b/apps/labrinth/.sqlx/query-020bac2af0493e11368af8f60e1fac7d220fe043b7580d7b0cba74189688fad2.json new file mode 100644 index 0000000000..e9ac50cfca --- /dev/null +++ b/apps/labrinth/.sqlx/query-020bac2af0493e11368af8f60e1fac7d220fe043b7580d7b0cba74189688fad2.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO delphi_issue_detail_verdicts (\n project_id,\n detail_key,\n verdict\n )\n VALUES (\n (SELECT project_id FROM delphi_issue_details_with_statuses WHERE id = $2),\n (SELECT key FROM delphi_report_issue_details WHERE id = $2),\n $1\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + { + "Custom": { + "name": "delphi_report_issue_status", + "kind": { + "Enum": [ + "pending", + "safe", + "unsafe" + ] + } + } + }, + "Int8" + ] + }, + "nullable": [] + }, + "hash": "020bac2af0493e11368af8f60e1fac7d220fe043b7580d7b0cba74189688fad2" +} diff --git a/apps/labrinth/.sqlx/query-2d9e36c76a1e214c53d9dc2aa3debe1d03998be169a306b63a0ca1beaa07397f.json b/apps/labrinth/.sqlx/query-2d9e36c76a1e214c53d9dc2aa3debe1d03998be169a306b63a0ca1beaa07397f.json new file mode 100644 index 0000000000..6e975b591a --- /dev/null +++ b/apps/labrinth/.sqlx/query-2d9e36c76a1e214c53d9dc2aa3debe1d03998be169a306b63a0ca1beaa07397f.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n EXISTS(\n SELECT 1 FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = $1 AND didws.status = 'pending'\n ) AS \"pending_issue_details_exist!\",\n t.id AS \"thread_id: DBThreadId\"\n FROM mods m\n INNER JOIN threads t ON t.mod_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "pending_issue_details_exist!", + "type_info": "Bool" + }, + { + "ordinal": 1, + "name": "thread_id: DBThreadId", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + null, + false + ] + }, + "hash": "2d9e36c76a1e214c53d9dc2aa3debe1d03998be169a306b63a0ca1beaa07397f" +} diff --git a/apps/labrinth/.sqlx/query-46eb7d30bd83f3018677d9ffad26b1d26441bc2428f3f63d18f0ac5a2708caad.json b/apps/labrinth/.sqlx/query-46eb7d30bd83f3018677d9ffad26b1d26441bc2428f3f63d18f0ac5a2708caad.json new file mode 100644 index 0000000000..cb691694b7 --- /dev/null +++ b/apps/labrinth/.sqlx/query-46eb7d30bd83f3018677d9ffad26b1d26441bc2428f3f63d18f0ac5a2708caad.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n didws.id AS \"issue_detail_id!\"\n FROM mods m\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_issue_details_with_statuses didws ON didws.issue_id = dri.id\n WHERE\n m.id = $1\n AND didws.status = 'pending'\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "issue_detail_id!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + true + ] + }, + "hash": "46eb7d30bd83f3018677d9ffad26b1d26441bc2428f3f63d18f0ac5a2708caad" +} diff --git a/apps/labrinth/.sqlx/query-506a7cb24b4ff9d0d3a49ef801039198f064dd0bfa53ad27db598928f62598f0.json b/apps/labrinth/.sqlx/query-506a7cb24b4ff9d0d3a49ef801039198f064dd0bfa53ad27db598928f62598f0.json deleted file mode 100644 index ff1d49e597..0000000000 --- a/apps/labrinth/.sqlx/query-506a7cb24b4ff9d0d3a49ef801039198f064dd0bfa53ad27db598928f62598f0.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n drid.id AS issue_detail_id\n FROM mods m\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_report_issue_details drid ON drid.issue_id = dri.id\n WHERE\n m.id = $1\n AND drid.status = 'pending'\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "issue_detail_id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false - ] - }, - "hash": "506a7cb24b4ff9d0d3a49ef801039198f064dd0bfa53ad27db598928f62598f0" -} diff --git a/apps/labrinth/.sqlx/query-79123e0d616a23396a8284f363355cee835771a32fd89e33b86506e172bc45db.json b/apps/labrinth/.sqlx/query-79123e0d616a23396a8284f363355cee835771a32fd89e33b86506e172bc45db.json deleted file mode 100644 index f097423f6e..0000000000 --- a/apps/labrinth/.sqlx/query-79123e0d616a23396a8284f363355cee835771a32fd89e33b86506e172bc45db.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (m.id)\n m.id AS project_id,\n t.id AS project_thread_id,\n MAX(dr.severity) AS severity,\n MIN(dr.created) AS earliest_report_created,\n MAX(dr.created) AS latest_report_created,\n\n jsonb_build_object(\n 'project_id', to_base62(m.id),\n 'max_severity', MAX(dr.severity),\n -- TODO: replace with `json_array` in Postgres 16\n 'versions', (\n SELECT coalesce(jsonb_agg(jsonb_build_object(\n 'version_id', to_base62(v.id),\n -- TODO: replace with `json_array` in Postgres 16\n 'files', (\n SELECT coalesce(jsonb_agg(jsonb_build_object(\n 'report_id', dr.id,\n 'file_id', to_base62(f.id),\n 'created', dr.created,\n 'flag_reason', 'delphi',\n 'severity', dr.severity,\n 'file_name', f.filename,\n 'file_size', f.size,\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT coalesce(jsonb_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT coalesce(jsonb_agg(\n jsonb_build_object(\n 'id', drid.id,\n 'issue_id', drid.issue_id,\n 'key', drid.key,\n 'file_path', drid.file_path,\n -- ignore `decompiled_source`\n 'data', drid.data,\n 'severity', drid.severity,\n 'status', drid.status\n )\n ), '[]'::jsonb)\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n ), '[]'::jsonb)\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n )), '[]'::jsonb)\n FROM delphi_reports dr\n WHERE dr.file_id = f.id\n )\n )), '[]'::jsonb)\n FROM versions v\n INNER JOIN files f ON f.version_id = v.id\n WHERE v.mod_id = m.id\n )\n ) AS report\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n\n -- only return projects with at least 1 pending drid\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_report_issue_details drid\n ON drid.issue_id = dri.id AND drid.status = 'pending'\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[]))\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n\n GROUP BY m.id, t.id\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN t.earliest_report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN t.latest_report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN t.severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN t.severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $1\n OFFSET $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "project_id: DBProjectId", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "project_thread_id: DBThreadId", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "report!: sqlx::types::Json", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Text", - "Int4Array" - ] - }, - "nullable": [ - false, - false, - null - ] - }, - "hash": "79123e0d616a23396a8284f363355cee835771a32fd89e33b86506e172bc45db" -} diff --git a/apps/labrinth/.sqlx/query-95137df2c76294593cfa988d3ed2f09597e02752ef4176109ece44c793ec1f72.json b/apps/labrinth/.sqlx/query-95137df2c76294593cfa988d3ed2f09597e02752ef4176109ece44c793ec1f72.json deleted file mode 100644 index 11d2d7d812..0000000000 --- a/apps/labrinth/.sqlx/query-95137df2c76294593cfa988d3ed2f09597e02752ef4176109ece44c793ec1f72.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT id\n FROM (\n SELECT DISTINCT ON (m.id)\n m.id,\n m.queued\n FROM mods m\n\n -- exclude projects in tech review queue\n LEFT JOIN versions v ON v.mod_id = m.id\n LEFT JOIN files f ON f.version_id = v.id\n LEFT JOIN delphi_reports dr ON dr.file_id = f.id\n LEFT JOIN delphi_report_issues dri ON dri.report_id = dr.id\n LEFT JOIN delphi_report_issue_details drid\n ON drid.issue_id = dri.id AND drid.status = 'pending'\n\n WHERE\n m.status = $1\n AND drid.status IS NULL\n\n GROUP BY m.id\n ) t\n\n ORDER BY queued ASC\n OFFSET $3\n LIMIT $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - "Int8", - "Int8" - ] - }, - "nullable": [ - false - ] - }, - "hash": "95137df2c76294593cfa988d3ed2f09597e02752ef4176109ece44c793ec1f72" -} diff --git a/apps/labrinth/.sqlx/query-1116dd6bc5024f0908147f820d76ee7a6510aef3c210ffbc18bb12e556f30b84.json b/apps/labrinth/.sqlx/query-b65094517546487e43b65a76aa38efd9e422151b683d9897a071ee0c4bac1cd4.json similarity index 57% rename from apps/labrinth/.sqlx/query-1116dd6bc5024f0908147f820d76ee7a6510aef3c210ffbc18bb12e556f30b84.json rename to apps/labrinth/.sqlx/query-b65094517546487e43b65a76aa38efd9e422151b683d9897a071ee0c4bac1cd4.json index d7d5856672..a0ea4442ee 100644 --- a/apps/labrinth/.sqlx/query-1116dd6bc5024f0908147f820d76ee7a6510aef3c210ffbc18bb12e556f30b84.json +++ b/apps/labrinth/.sqlx/query-b65094517546487e43b65a76aa38efd9e422151b683d9897a071ee0c4bac1cd4.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO delphi_report_issue_details (issue_id, key, file_path, decompiled_source, data, severity, status)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n RETURNING id\n ", + "query": "\n INSERT INTO delphi_report_issue_details (issue_id, key, file_path, decompiled_source, data, severity)\n VALUES ($1, $2, $3, $4, $5, $6)\n RETURNING id\n ", "describe": { "columns": [ { @@ -28,18 +28,6 @@ ] } } - }, - { - "Custom": { - "name": "delphi_report_issue_status", - "kind": { - "Enum": [ - "pending", - "safe", - "unsafe" - ] - } - } } ] }, @@ -47,5 +35,5 @@ false ] }, - "hash": "1116dd6bc5024f0908147f820d76ee7a6510aef3c210ffbc18bb12e556f30b84" + "hash": "b65094517546487e43b65a76aa38efd9e422151b683d9897a071ee0c4bac1cd4" } diff --git a/apps/labrinth/.sqlx/query-bdb1b3de1a2a0af9e5cbc1412fbdf8c80613f5c51ed38a3c52e39e86d5e3044c.json b/apps/labrinth/.sqlx/query-bdb1b3de1a2a0af9e5cbc1412fbdf8c80613f5c51ed38a3c52e39e86d5e3044c.json new file mode 100644 index 0000000000..d55acc8f1f --- /dev/null +++ b/apps/labrinth/.sqlx/query-bdb1b3de1a2a0af9e5cbc1412fbdf8c80613f5c51ed38a3c52e39e86d5e3044c.json @@ -0,0 +1,37 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (m.id)\n m.id AS project_id,\n t.id AS project_thread_id,\n MAX(dr.severity) AS severity,\n MIN(dr.created) AS earliest_report_created,\n MAX(dr.created) AS latest_report_created,\n\n jsonb_build_object(\n 'project_id', to_base62(m.id),\n 'max_severity', MAX(dr.severity),\n -- TODO: replace with `json_array` in Postgres 16\n 'versions', (\n SELECT coalesce(jsonb_agg(jsonb_build_object(\n 'version_id', to_base62(v.id),\n -- TODO: replace with `json_array` in Postgres 16\n 'files', (\n SELECT coalesce(jsonb_agg(jsonb_build_object(\n 'report_id', dr.id,\n 'file_id', to_base62(f.id),\n 'created', dr.created,\n 'flag_reason', 'delphi',\n 'severity', dr.severity,\n 'file_name', f.filename,\n 'file_size', f.size,\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT coalesce(jsonb_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT coalesce(jsonb_agg(\n jsonb_build_object(\n 'id', didws.id,\n 'issue_id', didws.issue_id,\n 'key', didws.key,\n 'file_path', didws.file_path,\n -- ignore `decompiled_source`\n 'data', didws.data,\n 'severity', didws.severity,\n 'status', didws.status\n )\n ), '[]'::jsonb)\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.issue_id = dri.id\n )\n )\n ), '[]'::jsonb)\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n )), '[]'::jsonb)\n FROM delphi_reports dr\n WHERE dr.file_id = f.id\n )\n )), '[]'::jsonb)\n FROM versions v\n INNER JOIN files f ON f.version_id = v.id\n WHERE v.mod_id = m.id\n )\n ) AS report\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n\n -- only return projects with at least 1 pending drid\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_issue_details_with_statuses didws\n ON didws.project_id = m.id AND didws.status = 'pending'\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[]))\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n\n GROUP BY m.id, t.id\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN t.earliest_report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN t.latest_report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN t.severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN t.severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $1\n OFFSET $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "project_id: DBProjectId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "project_thread_id: DBThreadId", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "report!: sqlx::types::Json", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Text", + "Int4Array" + ] + }, + "nullable": [ + false, + false, + null + ] + }, + "hash": "bdb1b3de1a2a0af9e5cbc1412fbdf8c80613f5c51ed38a3c52e39e86d5e3044c" +} diff --git a/apps/labrinth/.sqlx/query-d30290c1b55d9fb0939d122a96f350233d40ad81ac2d16481a0e9b32424a999d.json b/apps/labrinth/.sqlx/query-d30290c1b55d9fb0939d122a96f350233d40ad81ac2d16481a0e9b32424a999d.json new file mode 100644 index 0000000000..126f19363b --- /dev/null +++ b/apps/labrinth/.sqlx/query-d30290c1b55d9fb0939d122a96f350233d40ad81ac2d16481a0e9b32424a999d.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id\n FROM (\n SELECT DISTINCT ON (m.id)\n m.id,\n m.queued\n FROM mods m\n\n -- exclude projects in tech review queue\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.project_id = m.id AND didws.status = 'pending'\n\n WHERE\n m.status = $1\n AND didws.status IS NULL\n\n GROUP BY m.id\n ) t\n\n ORDER BY queued ASC\n OFFSET $3\n LIMIT $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Int8", + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "d30290c1b55d9fb0939d122a96f350233d40ad81ac2d16481a0e9b32424a999d" +} diff --git a/apps/labrinth/.sqlx/query-e6c6348e0bc2bcbac825468a867e19dffcfa710e5b7095cbe33ca58b000b310e.json b/apps/labrinth/.sqlx/query-e6c6348e0bc2bcbac825468a867e19dffcfa710e5b7095cbe33ca58b000b310e.json deleted file mode 100644 index 1cd41afdb1..0000000000 --- a/apps/labrinth/.sqlx/query-e6c6348e0bc2bcbac825468a867e19dffcfa710e5b7095cbe33ca58b000b310e.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE delphi_report_issue_details drid\n SET status = $1\n WHERE drid.id = $2\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - { - "Custom": { - "name": "delphi_report_issue_status", - "kind": { - "Enum": [ - "pending", - "safe", - "unsafe" - ] - } - } - }, - "Int8" - ] - }, - "nullable": [] - }, - "hash": "e6c6348e0bc2bcbac825468a867e19dffcfa710e5b7095cbe33ca58b000b310e" -} diff --git a/apps/labrinth/migrations/20251217234047_delphi_dedupe_issue_detail_statuses.sql b/apps/labrinth/migrations/20251217234047_delphi_dedupe_issue_detail_statuses.sql new file mode 100644 index 0000000000..cdaf439502 --- /dev/null +++ b/apps/labrinth/migrations/20251217234047_delphi_dedupe_issue_detail_statuses.sql @@ -0,0 +1,26 @@ +ALTER TABLE delphi_report_issue_details +DROP COLUMN status; + +CREATE TABLE delphi_issue_detail_verdicts ( + project_id BIGINT REFERENCES mods(id) + ON DELETE SET NULL + ON UPDATE CASCADE, + detail_key TEXT NOT NULL, + verdict delphi_report_issue_status NOT NULL, + PRIMARY KEY (project_id, detail_key) +); + +CREATE VIEW delphi_issue_details_with_statuses AS +SELECT + drid.*, + m.id AS project_id, + COALESCE(didv.verdict, 'pending') AS status +FROM delphi_report_issue_details drid +INNER JOIN delphi_report_issues dri ON dri.id = drid.issue_id +INNER JOIN delphi_reports dr ON dr.id = dri.report_id +INNER JOIN files f ON f.id = dr.file_id +INNER JOIN versions v ON v.id = f.version_id +INNER JOIN mods m ON m.id = v.mod_id +LEFT JOIN delphi_issue_detail_verdicts didv + ON m.id = didv.project_id + AND drid.key = didv.detail_key; diff --git a/apps/labrinth/src/database/models/delphi_report_item.rs b/apps/labrinth/src/database/models/delphi_report_item.rs index bf4fe53fc6..20878181d2 100644 --- a/apps/labrinth/src/database/models/delphi_report_item.rs +++ b/apps/labrinth/src/database/models/delphi_report_item.rs @@ -236,8 +236,8 @@ impl ReportIssueDetail { ) -> Result { Ok(DelphiReportIssueDetailsId(sqlx::query_scalar!( " - INSERT INTO delphi_report_issue_details (issue_id, key, file_path, decompiled_source, data, severity, status) - VALUES ($1, $2, $3, $4, $5, $6, $7) + INSERT INTO delphi_report_issue_details (issue_id, key, file_path, decompiled_source, data, severity) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING id ", self.issue_id as DelphiReportIssueId, @@ -246,7 +246,6 @@ impl ReportIssueDetail { self.decompiled_source, sqlx::types::Json(&self.data) as Json<&HashMap>, self.severity as DelphiSeverity, - self.status as _, ) .fetch_one(&mut **transaction) .await?)) diff --git a/apps/labrinth/src/models/v2/threads.rs b/apps/labrinth/src/models/v2/threads.rs index 969d35622d..9a041277b8 100644 --- a/apps/labrinth/src/models/v2/threads.rs +++ b/apps/labrinth/src/models/v2/threads.rs @@ -104,6 +104,22 @@ impl From for LegacyMessageBody { associated_images: Vec::new(), } } + crate::models::v3::threads::MessageBody::TechReviewEntered => { + LegacyMessageBody::Text { + body: "(legacy) Entered technical review".into(), + private: true, + replying_to: None, + associated_images: Vec::new(), + } + } + crate::models::v3::threads::MessageBody::TechReviewExitFileDeleted => { + LegacyMessageBody::Text { + body: "(legacy) Exited technical review because file was deleted".into(), + private: true, + replying_to: None, + associated_images: Vec::new(), + } + } crate::models::v3::threads::MessageBody::ThreadClosure => { LegacyMessageBody::ThreadClosure } diff --git a/apps/labrinth/src/models/v3/threads.rs b/apps/labrinth/src/models/v3/threads.rs index adfc21af50..c3ac604cb5 100644 --- a/apps/labrinth/src/models/v3/threads.rs +++ b/apps/labrinth/src/models/v3/threads.rs @@ -46,6 +46,8 @@ pub enum MessageBody { TechReview { verdict: DelphiVerdict, }, + TechReviewEntered, + TechReviewExitFileDeleted, ThreadClosure, ThreadReopen, Deleted { @@ -58,7 +60,9 @@ impl MessageBody { pub fn is_private(&self) -> bool { match self { Self::Text { private, .. } | Self::Deleted { private } => *private, - Self::TechReview { .. } => true, + Self::TechReview { .. } + | Self::TechReviewEntered + | Self::TechReviewExitFileDeleted => true, Self::StatusChange { .. } | Self::ThreadClosure | Self::ThreadReopen => false, diff --git a/apps/labrinth/src/routes/internal/delphi.rs b/apps/labrinth/src/routes/internal/delphi.rs index 26ef746638..5c6ca5205f 100644 --- a/apps/labrinth/src/routes/internal/delphi.rs +++ b/apps/labrinth/src/routes/internal/delphi.rs @@ -13,18 +13,20 @@ use crate::{ auth::check_is_moderator_from_headers, database::{ models::{ - DBFileId, DelphiReportId, DelphiReportIssueDetailsId, - DelphiReportIssueId, + DBFileId, DBProjectId, DBThreadId, DelphiReportId, + DelphiReportIssueDetailsId, DelphiReportIssueId, delphi_report_item::{ DBDelphiReport, DBDelphiReportIssue, DelphiSeverity, DelphiStatus, ReportIssueDetail, }, + thread_item::ThreadMessageBuilder, }, redis::RedisPool, }, models::{ ids::{ProjectId, VersionId}, pats::Scopes, + threads::MessageBody, }, queue::session::AuthQueue, routes::ApiError, @@ -185,6 +187,41 @@ async fn ingest_report_deserialized( "Delphi found issues in file", ); + let record = sqlx::query!( + r#" + SELECT + EXISTS( + SELECT 1 FROM delphi_issue_details_with_statuses didws + WHERE didws.project_id = $1 AND didws.status = 'pending' + ) AS "pending_issue_details_exist!", + t.id AS "thread_id: DBThreadId" + FROM mods m + INNER JOIN threads t ON t.mod_id = $1 + "#, + DBProjectId::from(report.project_id) as _, + ) + .fetch_one(&mut *transaction) + .await + .wrap_internal_err("failed to check if pending issue details exist")?; + + if record.pending_issue_details_exist { + info!( + "File's project already has pending issue details, is not entering tech review queue" + ); + } else { + info!("File's project is entering tech review queue"); + + ThreadMessageBuilder { + author_id: None, + body: MessageBody::TechReviewEntered, + thread_id: record.thread_id, + hide_identity: false, + } + .insert(&mut transaction) + .await + .wrap_internal_err("failed to add entering tech review message")?; + } + for (issue_type, issue_details) in report.issues { let issue_id = DBDelphiReportIssue { id: DelphiReportIssueId(0), // This will be set by the database diff --git a/apps/labrinth/src/routes/internal/moderation/mod.rs b/apps/labrinth/src/routes/internal/moderation/mod.rs index b719d688c3..7be20fc407 100644 --- a/apps/labrinth/src/routes/internal/moderation/mod.rs +++ b/apps/labrinth/src/routes/internal/moderation/mod.rs @@ -119,16 +119,12 @@ pub async fn get_projects_internal( FROM mods m -- exclude projects in tech review queue - LEFT JOIN versions v ON v.mod_id = m.id - LEFT JOIN files f ON f.version_id = v.id - LEFT JOIN delphi_reports dr ON dr.file_id = f.id - LEFT JOIN delphi_report_issues dri ON dri.report_id = dr.id - LEFT JOIN delphi_report_issue_details drid - ON drid.issue_id = dri.id AND drid.status = 'pending' + LEFT JOIN delphi_issue_details_with_statuses didws + ON didws.project_id = m.id AND didws.status = 'pending' WHERE m.status = $1 - AND drid.status IS NULL + AND didws.status IS NULL GROUP BY m.id ) t diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index e6b9735056..1869c1857e 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -405,18 +405,18 @@ async fn search_projects( 'details', ( SELECT coalesce(jsonb_agg( jsonb_build_object( - 'id', drid.id, - 'issue_id', drid.issue_id, - 'key', drid.key, - 'file_path', drid.file_path, + 'id', didws.id, + 'issue_id', didws.issue_id, + 'key', didws.key, + 'file_path', didws.file_path, -- ignore `decompiled_source` - 'data', drid.data, - 'severity', drid.severity, - 'status', drid.status + 'data', didws.data, + 'severity', didws.severity, + 'status', didws.status ) ), '[]'::jsonb) - FROM delphi_report_issue_details drid - WHERE drid.issue_id = dri.id + FROM delphi_issue_details_with_statuses didws + WHERE didws.issue_id = dri.id ) ) ), '[]'::jsonb) @@ -440,9 +440,8 @@ async fn search_projects( -- only return projects with at least 1 pending drid INNER JOIN delphi_reports dr ON dr.file_id = f.id - INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id - INNER JOIN delphi_report_issue_details drid - ON drid.issue_id = dri.id AND drid.status = 'pending' + INNER JOIN delphi_issue_details_with_statuses didws + ON didws.project_id = m.id AND didws.status = 'pending' -- filtering LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id @@ -603,19 +602,19 @@ async fn submit_report( .wrap_internal_err("failed to begin transaction")?; let pending_issue_details = sqlx::query!( - " + r#" SELECT - drid.id AS issue_detail_id + didws.id AS "issue_detail_id!" FROM mods m INNER JOIN versions v ON v.mod_id = m.id INNER JOIN files f ON f.version_id = v.id INNER JOIN delphi_reports dr ON dr.file_id = f.id INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id - INNER JOIN delphi_report_issue_details drid ON drid.issue_id = dri.id + INNER JOIN delphi_issue_details_with_statuses didws ON didws.issue_id = dri.id WHERE m.id = $1 - AND drid.status = 'pending' - ", + AND didws.status = 'pending' + "#, project_id as _, ) .fetch_all(&mut *txn) @@ -763,9 +762,16 @@ async fn update_issue_detail( }; sqlx::query!( r#" - UPDATE delphi_report_issue_details drid - SET status = $1 - WHERE drid.id = $2 + INSERT INTO delphi_issue_detail_verdicts ( + project_id, + detail_key, + verdict + ) + VALUES ( + (SELECT project_id FROM delphi_issue_details_with_statuses WHERE id = $2), + (SELECT key FROM delphi_report_issue_details WHERE id = $2), + $1 + ) "#, status as _, issue_detail_id as _, From 33c2b87a6bf94c82e1d2e6e6ffdd9b866dd2e36f Mon Sep 17 00:00:00 2001 From: aecsocket Date: Fri, 19 Dec 2025 21:08:58 +0000 Subject: [PATCH 60/65] Fix qa issues --- ...fe1472402338e0f0bb323b6147d2a0cc4eca.json} | 4 +-- ...a2eb115dc88da24735fffeca3eb1c269ad53.json} | 4 +-- ...2917689c31af7dd9a6baea4dbde99dc1a08e.json} | 4 +-- .../src/database/models/delphi_report_item.rs | 2 +- apps/labrinth/src/routes/internal/delphi.rs | 34 ++++++++++++++++++- .../routes/internal/moderation/tech_review.rs | 12 +++++-- 6 files changed, 50 insertions(+), 10 deletions(-) rename apps/labrinth/.sqlx/{query-47b4d61218e7d38d63a7f96efe152530ffe2a76b7bfbbd2053e87455be8772a1.json => query-3e2804a3443239104b2d8b095941fe1472402338e0f0bb323b6147d2a0cc4eca.json} (72%) rename apps/labrinth/.sqlx/{query-46eb7d30bd83f3018677d9ffad26b1d26441bc2428f3f63d18f0ac5a2708caad.json => query-52ef6d02f8d533fc4e4ceb141d07a2eb115dc88da24735fffeca3eb1c269ad53.json} (76%) rename apps/labrinth/.sqlx/{query-bdb1b3de1a2a0af9e5cbc1412fbdf8c80613f5c51ed38a3c52e39e86d5e3044c.json => query-6f5ec5cee9fc0007d11b4707b4442917689c31af7dd9a6baea4dbde99dc1a08e.json} (64%) diff --git a/apps/labrinth/.sqlx/query-47b4d61218e7d38d63a7f96efe152530ffe2a76b7bfbbd2053e87455be8772a1.json b/apps/labrinth/.sqlx/query-3e2804a3443239104b2d8b095941fe1472402338e0f0bb323b6147d2a0cc4eca.json similarity index 72% rename from apps/labrinth/.sqlx/query-47b4d61218e7d38d63a7f96efe152530ffe2a76b7bfbbd2053e87455be8772a1.json rename to apps/labrinth/.sqlx/query-3e2804a3443239104b2d8b095941fe1472402338e0f0bb323b6147d2a0cc4eca.json index 4bbdebb887..82ba66e5ee 100644 --- a/apps/labrinth/.sqlx/query-47b4d61218e7d38d63a7f96efe152530ffe2a76b7bfbbd2053e87455be8772a1.json +++ b/apps/labrinth/.sqlx/query-3e2804a3443239104b2d8b095941fe1472402338e0f0bb323b6147d2a0cc4eca.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT DISTINCT ON (dr.id)\n to_jsonb(dr)\n || jsonb_build_object(\n 'file_id', to_base62(f.id),\n 'version_id', to_base62(v.id),\n 'project_id', to_base62(v.mod_id),\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT json_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT json_agg(to_jsonb(drid))\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n )\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n ) AS \"data!: sqlx::types::Json\"\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n WHERE dr.id = $1\n ", + "query": "\n SELECT DISTINCT ON (dr.id)\n to_jsonb(dr)\n || jsonb_build_object(\n 'file_id', to_base62(f.id),\n 'version_id', to_base62(v.id),\n 'project_id', to_base62(v.mod_id),\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT json_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT json_agg(to_jsonb(drid))\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dri.id\n )\n )\n )\n FROM delphi_report_issues dri\n WHERE\n dri.report_id = dr.id\n -- see delphi.rs todo comment\n AND dri.issue_type != '__dummy'\n )\n ) AS \"data!: sqlx::types::Json\"\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n WHERE dr.id = $1\n ", "describe": { "columns": [ { @@ -18,5 +18,5 @@ null ] }, - "hash": "47b4d61218e7d38d63a7f96efe152530ffe2a76b7bfbbd2053e87455be8772a1" + "hash": "3e2804a3443239104b2d8b095941fe1472402338e0f0bb323b6147d2a0cc4eca" } diff --git a/apps/labrinth/.sqlx/query-46eb7d30bd83f3018677d9ffad26b1d26441bc2428f3f63d18f0ac5a2708caad.json b/apps/labrinth/.sqlx/query-52ef6d02f8d533fc4e4ceb141d07a2eb115dc88da24735fffeca3eb1c269ad53.json similarity index 76% rename from apps/labrinth/.sqlx/query-46eb7d30bd83f3018677d9ffad26b1d26441bc2428f3f63d18f0ac5a2708caad.json rename to apps/labrinth/.sqlx/query-52ef6d02f8d533fc4e4ceb141d07a2eb115dc88da24735fffeca3eb1c269ad53.json index cb691694b7..a0657d3e6d 100644 --- a/apps/labrinth/.sqlx/query-46eb7d30bd83f3018677d9ffad26b1d26441bc2428f3f63d18f0ac5a2708caad.json +++ b/apps/labrinth/.sqlx/query-52ef6d02f8d533fc4e4ceb141d07a2eb115dc88da24735fffeca3eb1c269ad53.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n didws.id AS \"issue_detail_id!\"\n FROM mods m\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_issue_details_with_statuses didws ON didws.issue_id = dri.id\n WHERE\n m.id = $1\n AND didws.status = 'pending'\n ", + "query": "\n SELECT\n didws.id AS \"issue_detail_id!\"\n FROM mods m\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_issue_details_with_statuses didws ON didws.issue_id = dri.id\n WHERE\n m.id = $1\n AND didws.status = 'pending'\n -- see delphi.rs todo comment\n AND dri.issue_type != '__dummy'\n ", "describe": { "columns": [ { @@ -18,5 +18,5 @@ true ] }, - "hash": "46eb7d30bd83f3018677d9ffad26b1d26441bc2428f3f63d18f0ac5a2708caad" + "hash": "52ef6d02f8d533fc4e4ceb141d07a2eb115dc88da24735fffeca3eb1c269ad53" } diff --git a/apps/labrinth/.sqlx/query-bdb1b3de1a2a0af9e5cbc1412fbdf8c80613f5c51ed38a3c52e39e86d5e3044c.json b/apps/labrinth/.sqlx/query-6f5ec5cee9fc0007d11b4707b4442917689c31af7dd9a6baea4dbde99dc1a08e.json similarity index 64% rename from apps/labrinth/.sqlx/query-bdb1b3de1a2a0af9e5cbc1412fbdf8c80613f5c51ed38a3c52e39e86d5e3044c.json rename to apps/labrinth/.sqlx/query-6f5ec5cee9fc0007d11b4707b4442917689c31af7dd9a6baea4dbde99dc1a08e.json index d55acc8f1f..010a32edd8 100644 --- a/apps/labrinth/.sqlx/query-bdb1b3de1a2a0af9e5cbc1412fbdf8c80613f5c51ed38a3c52e39e86d5e3044c.json +++ b/apps/labrinth/.sqlx/query-6f5ec5cee9fc0007d11b4707b4442917689c31af7dd9a6baea4dbde99dc1a08e.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (m.id)\n m.id AS project_id,\n t.id AS project_thread_id,\n MAX(dr.severity) AS severity,\n MIN(dr.created) AS earliest_report_created,\n MAX(dr.created) AS latest_report_created,\n\n jsonb_build_object(\n 'project_id', to_base62(m.id),\n 'max_severity', MAX(dr.severity),\n -- TODO: replace with `json_array` in Postgres 16\n 'versions', (\n SELECT coalesce(jsonb_agg(jsonb_build_object(\n 'version_id', to_base62(v.id),\n -- TODO: replace with `json_array` in Postgres 16\n 'files', (\n SELECT coalesce(jsonb_agg(jsonb_build_object(\n 'report_id', dr.id,\n 'file_id', to_base62(f.id),\n 'created', dr.created,\n 'flag_reason', 'delphi',\n 'severity', dr.severity,\n 'file_name', f.filename,\n 'file_size', f.size,\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT coalesce(jsonb_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT coalesce(jsonb_agg(\n jsonb_build_object(\n 'id', didws.id,\n 'issue_id', didws.issue_id,\n 'key', didws.key,\n 'file_path', didws.file_path,\n -- ignore `decompiled_source`\n 'data', didws.data,\n 'severity', didws.severity,\n 'status', didws.status\n )\n ), '[]'::jsonb)\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.issue_id = dri.id\n )\n )\n ), '[]'::jsonb)\n FROM delphi_report_issues dri\n WHERE dri.report_id = dr.id\n )\n )), '[]'::jsonb)\n FROM delphi_reports dr\n WHERE dr.file_id = f.id\n )\n )), '[]'::jsonb)\n FROM versions v\n INNER JOIN files f ON f.version_id = v.id\n WHERE v.mod_id = m.id\n )\n ) AS report\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n\n -- only return projects with at least 1 pending drid\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_issue_details_with_statuses didws\n ON didws.project_id = m.id AND didws.status = 'pending'\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[]))\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n\n GROUP BY m.id, t.id\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN t.earliest_report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN t.latest_report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN t.severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN t.severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $1\n OFFSET $2\n ", + "query": "\n SELECT\n project_id AS \"project_id: DBProjectId\",\n project_thread_id AS \"project_thread_id: DBThreadId\",\n report AS \"report!: sqlx::types::Json\"\n FROM (\n SELECT DISTINCT ON (m.id)\n m.id AS project_id,\n t.id AS project_thread_id,\n MAX(dr.severity) AS severity,\n MIN(dr.created) AS earliest_report_created,\n MAX(dr.created) AS latest_report_created,\n\n jsonb_build_object(\n 'project_id', to_base62(m.id),\n 'max_severity', MAX(dr.severity),\n -- TODO: replace with `json_array` in Postgres 16\n 'versions', (\n SELECT coalesce(jsonb_agg(jsonb_build_object(\n 'version_id', to_base62(v.id),\n -- TODO: replace with `json_array` in Postgres 16\n 'files', (\n SELECT coalesce(jsonb_agg(jsonb_build_object(\n 'report_id', dr.id,\n 'file_id', to_base62(f.id),\n 'created', dr.created,\n 'flag_reason', 'delphi',\n 'severity', dr.severity,\n 'file_name', f.filename,\n 'file_size', f.size,\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n 'issues', (\n SELECT coalesce(jsonb_agg(\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT coalesce(jsonb_agg(\n jsonb_build_object(\n 'id', didws.id,\n 'issue_id', didws.issue_id,\n 'key', didws.key,\n 'file_path', didws.file_path,\n -- ignore `decompiled_source`\n 'data', didws.data,\n 'severity', didws.severity,\n 'status', didws.status\n )\n ), '[]'::jsonb)\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.issue_id = dri.id\n )\n )\n ), '[]'::jsonb)\n FROM delphi_report_issues dri\n WHERE\n dri.report_id = dr.id\n -- see delphi.rs todo comment\n AND dri.issue_type != '__dummy'\n )\n )), '[]'::jsonb)\n FROM delphi_reports dr\n WHERE dr.file_id = f.id\n )\n )), '[]'::jsonb)\n FROM versions v\n INNER JOIN files f ON f.version_id = v.id\n WHERE v.mod_id = m.id\n )\n ) AS report\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n\n -- only return projects with at least 1 pending drid\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_issue_details_with_statuses didws\n ON didws.project_id = m.id AND didws.status = 'pending'\n\n -- filtering\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON c.id = mc.joining_category_id\n WHERE\n -- project type\n (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[]))\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n\n GROUP BY m.id, t.id\n ) t\n\n -- sorting\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN t.earliest_report_created ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN t.latest_report_created ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN t.severity ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN t.severity ELSE 'low'::delphi_severity END DESC\n\n -- pagination\n LIMIT $1\n OFFSET $2\n ", "describe": { "columns": [ { @@ -33,5 +33,5 @@ null ] }, - "hash": "bdb1b3de1a2a0af9e5cbc1412fbdf8c80613f5c51ed38a3c52e39e86d5e3044c" + "hash": "6f5ec5cee9fc0007d11b4707b4442917689c31af7dd9a6baea4dbde99dc1a08e" } diff --git a/apps/labrinth/src/database/models/delphi_report_item.rs b/apps/labrinth/src/database/models/delphi_report_item.rs index 20878181d2..540f3fb6b6 100644 --- a/apps/labrinth/src/database/models/delphi_report_item.rs +++ b/apps/labrinth/src/database/models/delphi_report_item.rs @@ -177,7 +177,7 @@ pub struct DelphiReportIssueResult { } impl DBDelphiReportIssue { - pub async fn upsert( + pub async fn insert( &self, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { diff --git a/apps/labrinth/src/routes/internal/delphi.rs b/apps/labrinth/src/routes/internal/delphi.rs index 5c6ca5205f..68b42ec729 100644 --- a/apps/labrinth/src/routes/internal/delphi.rs +++ b/apps/labrinth/src/routes/internal/delphi.rs @@ -222,13 +222,45 @@ async fn ingest_report_deserialized( .wrap_internal_err("failed to add entering tech review message")?; } + // TODO: Currently, the way we determine if an issue is in tech review or not + // is if it has any issue detials which are pending. + // If you mark all issue details are safe or not safe - even if you don't + // submit the final report - the project will be taken out of tech review + // queue, and into moderation queue. + // + // This is undesirable, but we can't rework the database schema to fix it + // right now. As a hack, we add a dummy report issue which blocks the + // project from exiting the tech review queue. + { + let dummy_issue_id = DBDelphiReportIssue { + id: DelphiReportIssueId(0), // This will be set by the database + report_id, + issue_type: "__dummy".into(), + } + .insert(&mut transaction) + .await?; + + ReportIssueDetail { + id: DelphiReportIssueDetailsId(0), // This will be set by the database + issue_id: dummy_issue_id, + key: "".into(), + file_path: "".into(), + decompiled_source: None, + data: HashMap::new(), + severity: DelphiSeverity::Low, + status: DelphiStatus::Pending, + } + .insert(&mut transaction) + .await?; + } + for (issue_type, issue_details) in report.issues { let issue_id = DBDelphiReportIssue { id: DelphiReportIssueId(0), // This will be set by the database report_id, issue_type, } - .upsert(&mut transaction) + .insert(&mut transaction) .await?; // This is required to handle the case where the same Delphi version is re-run on the same file diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 1869c1857e..b3709fa9b2 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -259,7 +259,10 @@ async fn get_report( ) ) FROM delphi_report_issues dri - WHERE dri.report_id = dr.id + WHERE + dri.report_id = dr.id + -- see delphi.rs todo comment + AND dri.issue_type != '__dummy' ) ) AS "data!: sqlx::types::Json" FROM delphi_reports dr @@ -421,7 +424,10 @@ async fn search_projects( ) ), '[]'::jsonb) FROM delphi_report_issues dri - WHERE dri.report_id = dr.id + WHERE + dri.report_id = dr.id + -- see delphi.rs todo comment + AND dri.issue_type != '__dummy' ) )), '[]'::jsonb) FROM delphi_reports dr @@ -614,6 +620,8 @@ async fn submit_report( WHERE m.id = $1 AND didws.status = 'pending' + -- see delphi.rs todo comment + AND dri.issue_type != '__dummy' "#, project_id as _, ) From 41ecb7790b8bfba33803e97d3fbd011e9183fc92 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Fri, 19 Dec 2025 21:39:30 +0000 Subject: [PATCH 61/65] Fix qa issues --- ...45c1fa3af3a899b232482aab9cc44b9336063.json | 14 +++++++ ...95f2732f1a7611ea6f7ce49cd7e077761ebf.json} | 4 +- .../routes/internal/moderation/tech_review.rs | 37 ++++++++++++++++--- apps/labrinth/src/routes/mod.rs | 2 +- 4 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 apps/labrinth/.sqlx/query-9ab1f07c2968b5d445752c1480345c1fa3af3a899b232482aab9cc44b9336063.json rename apps/labrinth/.sqlx/{query-020bac2af0493e11368af8f60e1fac7d220fe043b7580d7b0cba74189688fad2.json => query-cfe6c9e2abba8e9c1cd7aa799a6a95f2732f1a7611ea6f7ce49cd7e077761ebf.json} (54%) diff --git a/apps/labrinth/.sqlx/query-9ab1f07c2968b5d445752c1480345c1fa3af3a899b232482aab9cc44b9336063.json b/apps/labrinth/.sqlx/query-9ab1f07c2968b5d445752c1480345c1fa3af3a899b232482aab9cc44b9336063.json new file mode 100644 index 0000000000..83806baae2 --- /dev/null +++ b/apps/labrinth/.sqlx/query-9ab1f07c2968b5d445752c1480345c1fa3af3a899b232482aab9cc44b9336063.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM delphi_report_issue_details drid\n WHERE issue_id IN (\n SELECT dri.id\n FROM mods m\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n WHERE m.id = $1 AND dri.issue_type = '__dummy'\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "9ab1f07c2968b5d445752c1480345c1fa3af3a899b232482aab9cc44b9336063" +} diff --git a/apps/labrinth/.sqlx/query-020bac2af0493e11368af8f60e1fac7d220fe043b7580d7b0cba74189688fad2.json b/apps/labrinth/.sqlx/query-cfe6c9e2abba8e9c1cd7aa799a6a95f2732f1a7611ea6f7ce49cd7e077761ebf.json similarity index 54% rename from apps/labrinth/.sqlx/query-020bac2af0493e11368af8f60e1fac7d220fe043b7580d7b0cba74189688fad2.json rename to apps/labrinth/.sqlx/query-cfe6c9e2abba8e9c1cd7aa799a6a95f2732f1a7611ea6f7ce49cd7e077761ebf.json index e9ac50cfca..dc84840d5f 100644 --- a/apps/labrinth/.sqlx/query-020bac2af0493e11368af8f60e1fac7d220fe043b7580d7b0cba74189688fad2.json +++ b/apps/labrinth/.sqlx/query-cfe6c9e2abba8e9c1cd7aa799a6a95f2732f1a7611ea6f7ce49cd7e077761ebf.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO delphi_issue_detail_verdicts (\n project_id,\n detail_key,\n verdict\n )\n VALUES (\n (SELECT project_id FROM delphi_issue_details_with_statuses WHERE id = $2),\n (SELECT key FROM delphi_report_issue_details WHERE id = $2),\n $1\n )\n ", + "query": "\n INSERT INTO delphi_issue_detail_verdicts (\n project_id,\n detail_key,\n verdict\n )\n SELECT\n didws.project_id,\n didws.key,\n $1\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n WHERE\n didws.id = $2\n -- see delphi.rs todo comment\n AND dri.issue_type != '__dummy'\n ", "describe": { "columns": [], "parameters": { @@ -22,5 +22,5 @@ }, "nullable": [] }, - "hash": "020bac2af0493e11368af8f60e1fac7d220fe043b7580d7b0cba74189688fad2" + "hash": "cfe6c9e2abba8e9c1cd7aa799a6a95f2732f1a7611ea6f7ce49cd7e077761ebf" } diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index b3709fa9b2..602127a810 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -640,6 +640,25 @@ async fn submit_report( }); } + sqlx::query!( + " + DELETE FROM delphi_report_issue_details drid + WHERE issue_id IN ( + SELECT dri.id + FROM mods m + INNER JOIN versions v ON v.mod_id = m.id + INNER JOIN files f ON f.version_id = v.id + INNER JOIN delphi_reports dr ON dr.file_id = f.id + INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id + WHERE m.id = $1 AND dri.issue_type = '__dummy' + ) + ", + project_id as _, + ) + .execute(&mut *txn) + .await + .wrap_internal_err("failed to delete dummy issue")?; + let record = sqlx::query!( r#" SELECT t.id AS "thread_id: DBThreadId" @@ -768,18 +787,23 @@ async fn update_issue_detail( DelphiVerdict::Safe => DelphiStatus::Safe, DelphiVerdict::Unsafe => DelphiStatus::Unsafe, }; - sqlx::query!( + let results = sqlx::query!( r#" INSERT INTO delphi_issue_detail_verdicts ( project_id, detail_key, verdict ) - VALUES ( - (SELECT project_id FROM delphi_issue_details_with_statuses WHERE id = $2), - (SELECT key FROM delphi_report_issue_details WHERE id = $2), + SELECT + didws.project_id, + didws.key, $1 - ) + FROM delphi_issue_details_with_statuses didws + INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id + WHERE + didws.id = $2 + -- see delphi.rs todo comment + AND dri.issue_type != '__dummy' "#, status as _, issue_detail_id as _, @@ -787,6 +811,9 @@ async fn update_issue_detail( .execute(&mut *txn) .await .wrap_internal_err("failed to update issue detail")?; + if results.rows_affected() == 0 { + return Err(ApiError::Request(eyre!("issue detail does not exist"))); + } txn.commit() .await diff --git a/apps/labrinth/src/routes/mod.rs b/apps/labrinth/src/routes/mod.rs index 64b0c2a2f8..49ef754ff0 100644 --- a/apps/labrinth/src/routes/mod.rs +++ b/apps/labrinth/src/routes/mod.rs @@ -167,7 +167,7 @@ pub enum ApiError { Delphi(reqwest::Error), #[error(transparent)] Mural(#[from] Box), - #[error("report still has {} issue detailss with no verdict", details.len())] + #[error("report still has {} issue details with no verdict", details.len())] TechReviewDetailsWithNoVerdict { details: Vec, }, From c2d1f13d2097e9382a388caf3b98c98b1ff2161e Mon Sep 17 00:00:00 2001 From: aecsocket Date: Fri, 19 Dec 2025 21:51:47 +0000 Subject: [PATCH 62/65] fix review comments --- apps/labrinth/src/util/error.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/apps/labrinth/src/util/error.rs b/apps/labrinth/src/util/error.rs index cba9c21971..5f9ff343c2 100644 --- a/apps/labrinth/src/util/error.rs +++ b/apps/labrinth/src/util/error.rs @@ -19,7 +19,6 @@ pub trait Context: Sized { /// Maps the error variant into an [`eyre::Report`] with the given message. #[inline] - #[track_caller] fn wrap_err(self, msg: D) -> Result where D: Send + Sync + Debug + Display + 'static, @@ -29,7 +28,6 @@ pub trait Context: Sized { /// Maps the error variant into an [`ApiError::Internal`] using the closure to create the message. #[inline] - #[track_caller] fn wrap_internal_err_with( self, f: impl FnOnce() -> D, @@ -42,7 +40,6 @@ pub trait Context: Sized { /// Maps the error variant into an [`ApiError::Internal`] with the given message. #[inline] - #[track_caller] fn wrap_internal_err(self, msg: D) -> Result where D: Send + Sync + Debug + Display + 'static, @@ -52,7 +49,6 @@ pub trait Context: Sized { /// Maps the error variant into an [`ApiError::Request`] using the closure to create the message. #[inline] - #[track_caller] fn wrap_request_err_with( self, f: impl FnOnce() -> D, @@ -65,7 +61,6 @@ pub trait Context: Sized { /// Maps the error variant into an [`ApiError::Request`] with the given message. #[inline] - #[track_caller] fn wrap_request_err(self, msg: D) -> Result where D: Send + Sync + Debug + Display + 'static, @@ -75,7 +70,6 @@ pub trait Context: Sized { /// Maps the error variant into an [`ApiError::Auth`] using the closure to create the message. #[inline] - #[track_caller] fn wrap_auth_err_with(self, f: impl FnOnce() -> D) -> Result where D: Send + Sync + Debug + Display + 'static, @@ -85,7 +79,6 @@ pub trait Context: Sized { /// Maps the error variant into an [`ApiError::Auth`] with the given message. #[inline] - #[track_caller] fn wrap_auth_err(self, msg: D) -> Result where D: Send + Sync + Debug + Display + 'static, From 7f4ee166e603aaa9546a30a8745d447d0cee100e Mon Sep 17 00:00:00 2001 From: aecsocket Date: Fri, 19 Dec 2025 22:04:56 +0000 Subject: [PATCH 63/65] typos --- Cargo.lock | 1 + apps/labrinth/src/routes/internal/delphi.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 92cc9f3838..56731707eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10349,6 +10349,7 @@ dependencies = [ "quote", "regex", "syn 2.0.106", + "url", "uuid 1.18.1", ] diff --git a/apps/labrinth/src/routes/internal/delphi.rs b/apps/labrinth/src/routes/internal/delphi.rs index 68b42ec729..9af6797fa7 100644 --- a/apps/labrinth/src/routes/internal/delphi.rs +++ b/apps/labrinth/src/routes/internal/delphi.rs @@ -223,7 +223,7 @@ async fn ingest_report_deserialized( } // TODO: Currently, the way we determine if an issue is in tech review or not - // is if it has any issue detials which are pending. + // is if it has any issue details which are pending. // If you mark all issue details are safe or not safe - even if you don't // submit the final report - the project will be taken out of tech review // queue, and into moderation queue. From f9a6eb33fff3a1d36ad1f95b72e17c27db3d7bce Mon Sep 17 00:00:00 2001 From: aecsocket Date: Sat, 20 Dec 2025 10:07:16 +0000 Subject: [PATCH 64/65] fix ci --- apps/labrinth/src/routes/internal/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/labrinth/src/routes/internal/mod.rs b/apps/labrinth/src/routes/internal/mod.rs index a2d2644632..08ca8fa215 100644 --- a/apps/labrinth/src/routes/internal/mod.rs +++ b/apps/labrinth/src/routes/internal/mod.rs @@ -32,7 +32,6 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) { .configure(statuses::config) .configure(medal::config) .configure(external_notifications::config) - .configure(affiliate::config) .configure(mural::config) .configure(delphi::config), ); From ebc7b3d3044e5de3fb282289893e4f17c2f199ed Mon Sep 17 00:00:00 2001 From: "Calum H." Date: Sat, 20 Dec 2025 10:53:03 +0000 Subject: [PATCH 65/65] feat: tech review frontend (#4781) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: fix typo in status message * feat(labrinth): overhaul malware scanner report storage and routes * chore: address some review comments * feat: add Delphi to Docker Compose `with-delphi` profile * chore: fix unused import Clippy lint * feat(labrinth/delphi): use PAT token authorization with project read scopes * chore: expose file IDs in version queries * fix: accept null decompiled source payloads from Delphi * tweak(labrinth): expose base62 file IDs more consistently for Delphi * feat(labrinth/delphi): support new Delphi report severity field * chore(labrinth): run `cargo sqlx prepare` to fix Docker build errors * tweak: add route for fetching Delphi issue type schema, abstract Labrinth away from issue types * chore: run `cargo sqlx prepare` * chore: fix typo on frontend generated state file message * feat: update to use new Delphi issue schema * wip: tech review endpoints * wip: add ToSchema for dependent types * wip: report issues return * wip * wip: returning more data * wip * Fix up db query * Delphi configuration to talk to Labrinth * Get Delphi working with Labrinth * Add Delphi dummy fixture * Better Delphi logging * Improve utoipa for tech review routes * Add more sorting options for tech review queue * Oops join * New routes for fetching issues and reports * Fix which kind of ID is returned in tech review endpoints * Deduplicate tech review report rows * Reduce info sent for projects * Fetch more thread info * Address PR comments * fix ci * fix ci * fix postgres version mismatch * fix version creation * Implement routes * feat: batch scan alert * feat: layout * feat: introduce surface variables * fix: theme selector * feat: rough draft of tech review card * feat: tab switcher * feat: batch scan btn * feat: api-client module for tech review * draft: impl * feat: auto icons * fix: layout issues * feat: fixes to code blocks + flag labels * feat: temp remove mock data * fix: search sort types * fix: intl & lint * chore: re-enable mock data * fix: flag badges + auto open first issue in file tab * feat: update for new routes * fix: more qa issues * feat: lazy load sources * fix: re-enable auth middleware * feat: impl threads * fix: lint & severity * feat: download btn + switch to using NavTabs with new local mode option * feat: re-add toplevel btns * feat: reports page consistency * fix: consistency on project queue * fix: icons + sizing * fix: colors and gaps * fix: impl endpoints * feat: load all flags on file tab * feat: thread generics changes * feat: more qa * feat: fix collapse * fix: qa * feat: msg modal * fix: ISO import * feat: qa fixes * fix: empty state basic * fix: collapsible region * fix: collapse thread by default * feat: rough draft of new process/flow * fix labrinth build * fix thread message privacy * New tech review search route * feat: qa fixes * feat: QA changes * fix: verdict on detail not whole issue * fix: lint + intl * fix: lint * fix: thread message for tech rev verdict * feat: use anim frames * fix: exports + typecheck * polish: qa changes * feat: qa * feat: qa polish * feat: fix malic modal * fix: lint * fix: qa + lint * fix: pagination * fix: lint * fix: qa * intl extract * fix ci --------- Signed-off-by: Calum H. Co-authored-by: Alejandro González Co-authored-by: aecsocket --- apps/frontend/src/components/ui/NavTabs.vue | 169 ++- .../withdraw-stages/MuralpayDetailsStage.vue | 8 +- .../ui/moderation/BatchScanProgressAlert.vue | 39 + .../ui/moderation/MaliciousSummaryModal.vue | 168 +++ .../moderation/ModerationDelphiReportCard.vue | 182 --- .../ui/moderation/ModerationQueueCard.vue | 286 +++-- .../ui/moderation/ModerationReportCard.vue | 394 +++--- .../ui/moderation/ModerationTechRevCard.vue | 1138 +++++++++++++++++ .../src/components/ui/servers/FileItem.vue | 48 +- .../ui/servers/icons/LoaderIcon.vue | 236 +--- .../ui/thread/ConversationThread.vue | 24 + .../src/components/ui/thread/ReportThread.vue | 286 ----- .../components/ui/thread/ThreadMessage.vue | 49 +- .../src/components/ui/thread/ThreadView.vue | 227 ++++ apps/frontend/src/layouts/default.vue | 17 +- apps/frontend/src/locales/en-US/index.json | 10 +- apps/frontend/src/pages/moderation.vue | 11 +- apps/frontend/src/pages/moderation/index.vue | 84 +- .../src/pages/moderation/reports/index.vue | 70 +- .../moderation/technical-review-mockup.vue | 387 ------ .../src/pages/moderation/technical-review.vue | 571 ++++++++- .../src/pages/settings/billing/index.vue | 8 +- .../src/providers/creator-withdraw.ts | 2 +- apps/frontend/src/utils/finance-icons.ts | 34 - ...c71674941487c15be1e8ce0ebc78e7c26b34d.json | 15 + ...d3aaeb30a8da4721959fdee99cf649a8b29e3.json | 49 + ...d07b7d42c37e089403961ee16be0f99958ea0.json | 15 + ...c9e2bd27ac9a87987eafd79b06f1c4ecdb659.json | 26 + packages/api-client/src/modules/index.ts | 2 + .../api-client/src/modules/labrinth/index.ts | 1 + .../modules/labrinth/tech-review/internal.ts | 124 ++ .../api-client/src/modules/labrinth/types.ts | 187 ++- packages/assets/generated-icons.ts | 16 + packages/assets/icons/bug.svg | 14 + packages/assets/icons/chevron-down.svg | 4 + packages/assets/icons/file-code.svg | 8 + packages/assets/icons/file-image.svg | 9 + packages/assets/icons/folder.svg | 5 + packages/assets/icons/list-filter.svg | 6 + packages/assets/icons/shield-alert.svg | 18 + packages/assets/icons/shield-check.svg | 7 + packages/moderation/package.json | 1 + .../src/data/report-quick-replies.ts | 5 +- .../src/data/tech-review-quick-replies.ts | 11 + packages/moderation/src/index.ts | 5 + packages/moderation/src/types/quick-reply.ts | 6 + packages/moderation/src/types/reports.ts | 13 +- .../ui/src/components/base/Admonition.vue | 13 +- packages/ui/src/components/base/Badge.vue | 20 + .../src/components/base/CollapsibleRegion.vue | 98 +- packages/ui/src/components/base/Combobox.vue | 41 +- packages/ui/src/components/base/index.ts | 1 + .../billing/FormattedPaymentMethod.vue | 8 +- .../ui/src/components/nav/PagewideBanner.vue | 9 +- .../components/project/ProjectStatusBadge.vue | 27 +- packages/ui/src/locales/en-US/index.json | 6 + packages/ui/src/utils/auto-icons.ts | 201 +++ packages/ui/src/utils/index.ts | 1 + packages/utils/highlightjs/index.ts | 33 + packages/utils/utils.ts | 20 +- pnpm-lock.yaml | 3 + 61 files changed, 3756 insertions(+), 1720 deletions(-) create mode 100644 apps/frontend/src/components/ui/moderation/BatchScanProgressAlert.vue create mode 100644 apps/frontend/src/components/ui/moderation/MaliciousSummaryModal.vue delete mode 100644 apps/frontend/src/components/ui/moderation/ModerationDelphiReportCard.vue create mode 100644 apps/frontend/src/components/ui/moderation/ModerationTechRevCard.vue delete mode 100644 apps/frontend/src/components/ui/thread/ReportThread.vue create mode 100644 apps/frontend/src/components/ui/thread/ThreadView.vue delete mode 100644 apps/frontend/src/pages/moderation/technical-review-mockup.vue delete mode 100644 apps/frontend/src/utils/finance-icons.ts create mode 100644 apps/labrinth/.sqlx/query-3240e4b5abc9850b5d3c09fafcac71674941487c15be1e8ce0ebc78e7c26b34d.json create mode 100644 apps/labrinth/.sqlx/query-3961aa17ce3219c057c398dca0ed3aaeb30a8da4721959fdee99cf649a8b29e3.json create mode 100644 apps/labrinth/.sqlx/query-6cf1862b3c197d42f9183dcbbd3d07b7d42c37e089403961ee16be0f99958ea0.json create mode 100644 apps/labrinth/.sqlx/query-b1df83f4592701f8aa03f6d16bac9e2bd27ac9a87987eafd79b06f1c4ecdb659.json create mode 100644 packages/api-client/src/modules/labrinth/tech-review/internal.ts create mode 100644 packages/assets/icons/bug.svg create mode 100644 packages/assets/icons/chevron-down.svg create mode 100644 packages/assets/icons/file-code.svg create mode 100644 packages/assets/icons/file-image.svg create mode 100644 packages/assets/icons/folder.svg create mode 100644 packages/assets/icons/list-filter.svg create mode 100644 packages/assets/icons/shield-alert.svg create mode 100644 packages/assets/icons/shield-check.svg create mode 100644 packages/moderation/src/data/tech-review-quick-replies.ts create mode 100644 packages/moderation/src/types/quick-reply.ts create mode 100644 packages/ui/src/utils/auto-icons.ts diff --git a/apps/frontend/src/components/ui/NavTabs.vue b/apps/frontend/src/components/ui/NavTabs.vue index 5a2c0b788c..a84e186a23 100644 --- a/apps/frontend/src/components/ui/NavTabs.vue +++ b/apps/frontend/src/components/ui/NavTabs.vue @@ -1,23 +1,58 @@ diff --git a/apps/frontend/src/components/ui/dashboard/withdraw-stages/MuralpayDetailsStage.vue b/apps/frontend/src/components/ui/dashboard/withdraw-stages/MuralpayDetailsStage.vue index 2249cf6af3..b7447ee458 100644 --- a/apps/frontend/src/components/ui/dashboard/withdraw-stages/MuralpayDetailsStage.vue +++ b/apps/frontend/src/components/ui/dashboard/withdraw-stages/MuralpayDetailsStage.vue @@ -207,6 +207,8 @@ import { financialMessages, formFieldLabels, formFieldPlaceholders, + getBlockchainColor, + getBlockchainIcon, normalizeChildren, } from '@modrinth/ui' import { defineMessages, useVIntl } from '@vintl/vintl' @@ -218,12 +220,6 @@ import RevenueInputField from '@/components/ui/dashboard/RevenueInputField.vue' import WithdrawFeeBreakdown from '@/components/ui/dashboard/WithdrawFeeBreakdown.vue' import { useGeneratedState } from '@/composables/generated' import { useWithdrawContext } from '@/providers/creator-withdraw.ts' -import { - getBlockchainColor, - getBlockchainIcon, - getCurrencyColor, - getCurrencyIcon, -} from '@/utils/finance-icons.ts' import { getRailConfig } from '@/utils/muralpay-rails' const { withdrawData, maxWithdrawAmount, availableMethods, calculateFees } = useWithdrawContext() diff --git a/apps/frontend/src/components/ui/moderation/BatchScanProgressAlert.vue b/apps/frontend/src/components/ui/moderation/BatchScanProgressAlert.vue new file mode 100644 index 0000000000..a0cc693637 --- /dev/null +++ b/apps/frontend/src/components/ui/moderation/BatchScanProgressAlert.vue @@ -0,0 +1,39 @@ + + + diff --git a/apps/frontend/src/components/ui/moderation/MaliciousSummaryModal.vue b/apps/frontend/src/components/ui/moderation/MaliciousSummaryModal.vue new file mode 100644 index 0000000000..f5b1fea8a8 --- /dev/null +++ b/apps/frontend/src/components/ui/moderation/MaliciousSummaryModal.vue @@ -0,0 +1,168 @@ + + + diff --git a/apps/frontend/src/components/ui/moderation/ModerationDelphiReportCard.vue b/apps/frontend/src/components/ui/moderation/ModerationDelphiReportCard.vue deleted file mode 100644 index d374418ea7..0000000000 --- a/apps/frontend/src/components/ui/moderation/ModerationDelphiReportCard.vue +++ /dev/null @@ -1,182 +0,0 @@ - - - - - diff --git a/apps/frontend/src/components/ui/moderation/ModerationQueueCard.vue b/apps/frontend/src/components/ui/moderation/ModerationQueueCard.vue index f60740c95e..3d34ba13de 100644 --- a/apps/frontend/src/components/ui/moderation/ModerationQueueCard.vue +++ b/apps/frontend/src/components/ui/moderation/ModerationQueueCard.vue @@ -1,143 +1,127 @@ diff --git a/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue b/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue index 895099a780..3b81942c17 100644 --- a/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue +++ b/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue @@ -1,176 +1,287 @@ - - diff --git a/apps/frontend/src/components/ui/moderation/ModerationTechRevCard.vue b/apps/frontend/src/components/ui/moderation/ModerationTechRevCard.vue new file mode 100644 index 0000000000..4d9b12f2d3 --- /dev/null +++ b/apps/frontend/src/components/ui/moderation/ModerationTechRevCard.vue @@ -0,0 +1,1138 @@ + + + + + diff --git a/apps/frontend/src/components/ui/servers/FileItem.vue b/apps/frontend/src/components/ui/servers/FileItem.vue index ac4c426591..ae4b2cea9d 100644 --- a/apps/frontend/src/components/ui/servers/FileItem.vue +++ b/apps/frontend/src/components/ui/servers/FileItem.vue @@ -68,26 +68,18 @@ import { DownloadIcon, EditIcon, - FileArchiveIcon, - FileIcon, FolderOpenIcon, MoreHorizontalIcon, PackageOpenIcon, RightArrowIcon, TrashIcon, } from '@modrinth/assets' -import { ButtonStyled } from '@modrinth/ui' +import { ButtonStyled, getFileExtensionIcon } from '@modrinth/ui' import { computed, ref, shallowRef } from 'vue' import { renderToString } from 'vue/server-renderer' import { useRoute, useRouter } from 'vue-router' -import { - UiServersIconsCodeFileIcon, - UiServersIconsCogFolderIcon, - UiServersIconsEarthIcon, - UiServersIconsImageFileIcon, - UiServersIconsTextFileIcon, -} from '#components' +import { UiServersIconsCogFolderIcon, UiServersIconsEarthIcon } from '#components' import PaletteIcon from '~/assets/icons/palette.svg?component' import TeleportOverflowMenu from './TeleportOverflowMenu.vue' @@ -116,36 +108,7 @@ const emit = defineEmits<{ const isDragOver = ref(false) const isDragging = ref(false) -const codeExtensions = Object.freeze([ - 'json', - 'json5', - 'jsonc', - 'java', - 'kt', - 'kts', - 'sh', - 'bat', - 'ps1', - 'yml', - 'yaml', - 'toml', - 'js', - 'ts', - 'py', - 'rb', - 'php', - 'html', - 'css', - 'cpp', - 'c', - 'h', - 'rs', - 'go', -]) - const textExtensions = Object.freeze(['txt', 'md', 'log', 'cfg', 'conf', 'properties', 'ini', 'sk']) -const imageExtensions = Object.freeze(['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp']) -const supportedArchiveExtensions = Object.freeze(['zip']) const units = Object.freeze(['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB']) const route = shallowRef(useRoute()) @@ -199,12 +162,7 @@ const iconComponent = computed(() => { return FolderOpenIcon } - const ext = fileExtension.value - if (codeExtensions.includes(ext)) return UiServersIconsCodeFileIcon - if (textExtensions.includes(ext)) return UiServersIconsTextFileIcon - if (imageExtensions.includes(ext)) return UiServersIconsImageFileIcon - if (supportedArchiveExtensions.includes(ext)) return FileArchiveIcon - return FileIcon + return getFileExtensionIcon(fileExtension.value) }) const subText = computed(() => { diff --git a/apps/frontend/src/components/ui/servers/icons/LoaderIcon.vue b/apps/frontend/src/components/ui/servers/icons/LoaderIcon.vue index 02d0ae05d2..4bd494e49f 100644 --- a/apps/frontend/src/components/ui/servers/icons/LoaderIcon.vue +++ b/apps/frontend/src/components/ui/servers/icons/LoaderIcon.vue @@ -1,232 +1,22 @@ diff --git a/apps/frontend/src/components/ui/thread/ConversationThread.vue b/apps/frontend/src/components/ui/thread/ConversationThread.vue index 8fb3904538..b288daf781 100644 --- a/apps/frontend/src/components/ui/thread/ConversationThread.vue +++ b/apps/frontend/src/components/ui/thread/ConversationThread.vue @@ -217,6 +217,14 @@ hoverFilled: true, disabled: project.status === 'withheld', }, + { + id: 'send-to-review-reply', + action: () => { + sendReply('processing', true) + }, + hoverFilled: true, + disabled: project.status === 'processing', + }, ] : [ { @@ -228,6 +236,14 @@ hoverFilled: true, disabled: project.status === 'withheld', }, + { + id: 'send-to-review', + action: () => { + setStatus('processing') + }, + hoverFilled: true, + disabled: project.status === 'processing', + }, ] " > @@ -240,6 +256,14 @@