diff --git a/apps/labrinth/.sqlx/query-1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286.json b/apps/labrinth/.sqlx/query-1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286.json new file mode 100644 index 0000000000..921f7f92d9 --- /dev/null +++ b/apps/labrinth/.sqlx/query-1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id,\n status AS \"status: PayoutStatus\"\n FROM payouts\n ORDER BY id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "status: PayoutStatus", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false + ] + }, + "hash": "1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286" +} diff --git a/apps/labrinth/.sqlx/query-6f5ec5cee9fc0007d11b4707b4442917689c31af7dd9a6baea4dbde99dc1a08e.json b/apps/labrinth/.sqlx/query-b55d397b08c2aacd97f4d0dad4f716505dc7970832d244412756d97d2d86722a.json similarity index 70% rename from apps/labrinth/.sqlx/query-6f5ec5cee9fc0007d11b4707b4442917689c31af7dd9a6baea4dbde99dc1a08e.json rename to apps/labrinth/.sqlx/query-b55d397b08c2aacd97f4d0dad4f716505dc7970832d244412756d97d2d86722a.json index 010a32edd8..376bce64e0 100644 --- a/apps/labrinth/.sqlx/query-6f5ec5cee9fc0007d11b4707b4442917689c31af7dd9a6baea4dbde99dc1a08e.json +++ b/apps/labrinth/.sqlx/query-b55d397b08c2aacd97f4d0dad4f716505dc7970832d244412756d97d2d86722a.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\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 ", + "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\n -- by project type\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\n -- get last message in thread for replied/unreplied filtering\n LEFT JOIN threads_messages tm_last\n ON tm_last.thread_id = t.id\n AND tm_last.id = (\n SELECT id FROM threads_messages\n WHERE thread_id = t.id\n ORDER BY created DESC\n LIMIT 1\n )\n LEFT JOIN users u_last\n ON u_last.id = tm_last.author_id\n\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 -- project status\n AND (cardinality($6::text[]) = 0 OR m.status = ANY($6::text[]))\n -- replied/unreplied filter\n AND (\n $5::text IS NULL\n OR ($5::text = 'unreplied' AND (tm_last.id IS NULL OR u_last.role IS NULL OR u_last.role NOT IN ('moderator', 'admin')))\n OR ($5::text = 'replied' AND tm_last.id IS NOT NULL AND u_last.role IS NOT NULL AND u_last.role IN ('moderator', 'admin'))\n )\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": [ { @@ -24,7 +24,9 @@ "Int8", "Int8", "Text", - "Int4Array" + "Int4Array", + "Text", + "TextArray" ] }, "nullable": [ @@ -33,5 +35,5 @@ null ] }, - "hash": "6f5ec5cee9fc0007d11b4707b4442917689c31af7dd9a6baea4dbde99dc1a08e" + "hash": "b55d397b08c2aacd97f4d0dad4f716505dc7970832d244412756d97d2d86722a" } diff --git a/apps/labrinth/.sqlx/query-b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3.json b/apps/labrinth/.sqlx/query-b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3.json new file mode 100644 index 0000000000..89bd8147dc --- /dev/null +++ b/apps/labrinth/.sqlx/query-b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT status AS \"status: PayoutStatus\" FROM payouts WHERE id = 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "status: PayoutStatus", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3" +} diff --git a/apps/labrinth/.sqlx/query-cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02.json b/apps/labrinth/.sqlx/query-cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02.json new file mode 100644 index 0000000000..469c30168a --- /dev/null +++ b/apps/labrinth/.sqlx/query-cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO payouts (id, method, platform_id, status, user_id, amount, created)\n VALUES ($1, $2, $3, $4, $5, 10.0, NOW())\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Text", + "Text", + "Varchar", + "Int8" + ] + }, + "nullable": [] + }, + "hash": "cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02" +} diff --git a/apps/labrinth/.sqlx/query-cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606.json b/apps/labrinth/.sqlx/query-cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606.json new file mode 100644 index 0000000000..52e020ebf2 --- /dev/null +++ b/apps/labrinth/.sqlx/query-cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO payouts (id, method, platform_id, status, user_id, amount, created)\n VALUES ($1, $2, NULL, $3, $4, 10.00, NOW())\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Text", + "Varchar", + "Int8" + ] + }, + "nullable": [] + }, + "hash": "cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606" +} diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 52f29df373..684352675d 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -69,7 +69,34 @@ fn default_sort_by() -> SearchProjectsSort { #[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)] pub struct SearchProjectsFilter { + #[serde(default)] pub project_type: Vec, + #[serde(default)] + pub replied_to: Option, + #[serde(default)] + pub project_status: Vec, +} + +/// Filter by whether a moderator has replied to the last message in the +/// project's moderation thread. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + utoipa::ToSchema, +)] +#[serde(rename_all = "snake_case")] +pub enum RepliedTo { + /// Last message in the thread is from a moderator, indicating a moderator + /// has replied to it. + Replied, + /// Last message in the thread is not from a moderator. + Unreplied, } #[derive( @@ -386,6 +413,11 @@ async fn search_projects( let offset = i64::try_from(offset) .wrap_request_err("offset cannot fit into `i64`")?; + let replied_to_filter = search_req.filter.replied_to.map(|r| match r { + RepliedTo::Replied => "replied", + RepliedTo::Unreplied => "unreplied", + }); + let mut project_reports = Vec::::new(); let mut project_ids = Vec::::new(); let mut thread_ids = Vec::::new(); @@ -473,12 +505,35 @@ async fn search_projects( ON didws.project_id = m.id AND didws.status = 'pending' -- filtering + + -- by project type LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id LEFT JOIN categories c ON c.id = mc.joining_category_id + + -- get last message in thread for replied/unreplied filtering + LEFT JOIN threads_messages tm_last + ON tm_last.thread_id = t.id + AND tm_last.id = ( + SELECT id FROM threads_messages + WHERE thread_id = t.id + ORDER BY created DESC + LIMIT 1 + ) + LEFT JOIN users u_last + ON u_last.id = tm_last.author_id + WHERE -- project type (cardinality($4::int[]) = 0 OR c.project_type = ANY($4::int[])) AND m.status NOT IN ('draft', 'rejected', 'withheld') + -- project status + AND (cardinality($6::text[]) = 0 OR m.status = ANY($6::text[])) + -- replied/unreplied filter + AND ( + $5::text IS NULL + OR ($5::text = 'unreplied' AND (tm_last.id IS NULL OR u_last.role IS NULL OR u_last.role NOT IN ('moderator', 'admin'))) + OR ($5::text = 'replied' AND tm_last.id IS NOT NULL AND u_last.role IS NOT NULL AND u_last.role IN ('moderator', 'admin')) + ) GROUP BY m.id, t.id ) t @@ -503,6 +558,13 @@ async fn search_projects( .iter() .map(|ty| ty.0) .collect::>(), + replied_to_filter.as_deref(), + &search_req + .filter + .project_status + .iter() + .map(|status| status.to_string()) + .collect::>() ) .fetch(&**pool); diff --git a/scripts/i18n-import-check.ts b/scripts/i18n-import-check.ts index 4e29eb5a5a..c66f761bf9 100644 --- a/scripts/i18n-import-check.ts +++ b/scripts/i18n-import-check.ts @@ -298,7 +298,7 @@ function analyzeFile(filePath: string): FileIssue[] { } } } catch { - // Silent fail for unparseable files + // Silent fail for unparsable files } return issues