diff --git a/Cargo.lock b/Cargo.lock index 98c5336704..c03c8d0a29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -437,6 +437,23 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "antlr4rust" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093d520274bfff7278d776f7ea12981a0a0a6f96db90964658e0f38fc6e9a6a6" +dependencies = [ + "better_any", + "bit-set 0.8.0", + "byteorder", + "lazy_static", + "murmur3", + "once_cell", + "parking_lot", + "typed-arena", + "uuid 1.23.3", +] + [[package]] name = "anyhow" version = "1.0.100" @@ -1421,10 +1438,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b08e33815c87d8cadcddb1e74ac307368a3751fbe40c961538afa21a1899f21c" dependencies = [ "base64 0.21.7", - "pastey", + "pastey 0.1.1", "serde", ] +[[package]] +name = "better_any" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4372b9543397a4b86050cc5e7ee36953edf4bac9518e8a774c2da694977fb6e4" + [[package]] name = "bindgen" version = "0.72.1" @@ -1451,7 +1474,16 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ - "bit-vec", + "bit-vec 0.6.3", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", ] [[package]] @@ -1460,6 +1492,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bit_field" version = "0.10.3" @@ -1889,6 +1927,23 @@ dependencies = [ "shlex 2.0.1", ] +[[package]] +name = "cel" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed39583e427bf41d93c28c7f27c943a93bcb8697220ff3575fd53a9e13f3814" +dependencies = [ + "antlr4rust", + "base64 0.22.1", + "lazy_static", + "nom 7.1.3", + "pastey 0.2.3", + "regex", + "serde", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "censor" version = "0.3.0" @@ -3530,7 +3585,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" dependencies = [ - "bit-set", + "bit-set 0.5.3", "regex-automata", "regex-syntax", ] @@ -5483,6 +5538,7 @@ dependencies = [ "base64 0.22.1", "bitflags 2.9.4", "bytes", + "cel", "censor", "chrono", "chumsky", @@ -6170,6 +6226,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb585ade2549a017db2e35978b77c319214fa4b37cede841e27954dd6e8f3ca8" +[[package]] +name = "murmur3" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a198f9589efc03f544388dfc4a19fe8af4323662b62f598b8dcfdac62c14771c" +dependencies = [ + "byteorder", +] + [[package]] name = "mutually_exclusive_features" version = "0.1.0" @@ -7192,6 +7257,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[package]] name = "path-util" version = "0.0.0" @@ -11868,6 +11939,12 @@ dependencies = [ "utf-8", ] +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + [[package]] name = "typed-path" version = "0.12.0" diff --git a/Cargo.toml b/Cargo.toml index 1377c4b047..99b0c562a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ bitflags = "2.9.4" bon = "3.9.3" bytemuck = "1.24.0" bytes = "1.10.1" +cel = { version = "0.14.0", default-features = false, features = ["json", "regex"] } censor = "0.3.0" chardetng = "0.1.17" chrono = "0.4.42" diff --git a/apps/frontend/src/components/ui/moderation/GlobalDetailLocalTraceCard.vue b/apps/frontend/src/components/ui/moderation/GlobalDetailLocalTraceCard.vue index 4a368d2d77..62f2e8dfe8 100644 --- a/apps/frontend/src/components/ui/moderation/GlobalDetailLocalTraceCard.vue +++ b/apps/frontend/src/components/ui/moderation/GlobalDetailLocalTraceCard.vue @@ -5,14 +5,8 @@

{{ trace.project_name }}

-

- {{ trace.version_number }} -

+

@@ -31,9 +25,11 @@ diff --git a/apps/frontend/src/components/ui/moderation/GlobalDetailTracesList.vue b/apps/frontend/src/components/ui/moderation/GlobalDetailTracesList.vue index 1803ee2b73..dff82aaa5d 100644 --- a/apps/frontend/src/components/ui/moderation/GlobalDetailTracesList.vue +++ b/apps/frontend/src/components/ui/moderation/GlobalDetailTracesList.vue @@ -64,7 +64,7 @@

Path - {{ decodeTracePath(getLatestLocalTrace(trace)?.file_path ?? '') }} +

@@ -132,6 +132,7 @@ import { } from '@modrinth/ui' import GlobalDetailLocalTraceCard from '~/components/ui/moderation/GlobalDetailLocalTraceCard.vue' +import IssueDetailPath from '~/components/ui/moderation/IssueDetailPath.vue' const client = injectModrinthClient() const { addNotification } = injectNotificationManager() @@ -164,24 +165,19 @@ function getLatestLocalTrace(trace: Labrinth.TechReview.Internal.GlobalIssueDeta return trace.local_traces.at(-1) } -function decodeTracePath(path: string): string { - try { - return decodeURIComponent(path) - } catch { - return path - } -} - function getSeverityBadgeColor( severity: Labrinth.TechReview.Internal.DelphiSeverity | undefined, ): string { switch (severity) { + case 'malware': case 'severe': return 'border-red/60 bg-highlight-red text-red' case 'high': return 'border-orange/60 bg-highlight-orange text-orange' case 'medium': return 'border-green/60 bg-highlight-green text-green' + case 'hidden': + return 'border-divider bg-surface-2 text-secondary' case 'low': default: return 'border-blue/60 bg-highlight-blue text-blue' diff --git a/apps/frontend/src/components/ui/moderation/IssueDetailPath.vue b/apps/frontend/src/components/ui/moderation/IssueDetailPath.vue new file mode 100644 index 0000000000..6289a9cd56 --- /dev/null +++ b/apps/frontend/src/components/ui/moderation/IssueDetailPath.vue @@ -0,0 +1,74 @@ + + + diff --git a/apps/frontend/src/components/ui/moderation/ModerationTechRevCard.vue b/apps/frontend/src/components/ui/moderation/ModerationTechRevCard.vue index 0a56144bfb..40e62bf129 100644 --- a/apps/frontend/src/components/ui/moderation/ModerationTechRevCard.vue +++ b/apps/frontend/src/components/ui/moderation/ModerationTechRevCard.vue @@ -1,7 +1,14 @@ + + diff --git a/apps/frontend/src/public/news/feed/rss.xml b/apps/frontend/src/public/news/feed/rss.xml index abde87b8cb..b7da3c04d4 100644 --- a/apps/frontend/src/public/news/feed/rss.xml +++ b/apps/frontend/src/public/news/feed/rss.xml @@ -4,7 +4,7 @@ https://modrinth.com/news/ @modrinth/blog - Tue, 18 Aug 2026 05:31:31 GMT + Wed, 02 Sep 2026 18:25:22 GMT @@ -115,7 +115,7 @@ https://modrinth.com/news/article/streamlined-version-creation/ https://modrinth.com/news/article/streamlined-version-creation/ Thu, 18 Dec 2025 20:50:00 GMT - <![CDATA[<p>Hey everyone! As part of our ongoing work to improve the creator side of the platform, we’re shipping a new project version creation and editing today. This part of the product was showing its age, so we’ve overhauled it to set us up for the new project types we plan to ship in the new year!</p><h2>TL;DR</h2><ul><li>Multi-file uploads with primary file detection and new supplementary file types</li><li>Automatic detection of version number, subtitle, loaders, game versions, and environment bundled into a version summary</li><li>A new loader selector that groups loaders by project type</li><li>A new game version selector with search and selecting version ranges</li><li>Project environments moved to be on a per-version basis</li><li>A new dependency selector with search and smart suggestions</li><li>Project gallery, versions, and publishing checklist moved into project settings</li></ul><h2>File uploading</h2><p>For starters, we’ve been centralizing all project editing into Project Settings to make the experience clearer and more approachable for new creators. Editing project versions now happens directly within Project Settings and projects look slightly different if you’re the creator.</p><p><img src="/news/article/streamlined-version-creation/edit-button.webp" alt="Project page header showing the primary action as &quot;Edit project&quot; for the creator"></p><p>You can create a new version by drag and dropping files into the versions table or creating a new version and uploading them. Multiple files can be uploaded at once.</p><p>The primary file is explicitly listed at the top and separate from any supplementary files. From there, you can add additional supplementary files and assign their types. Newly supported types include sources jar, dev jar, javadoc jar, and signature file.</p><div class="video-wrapper mb-8"><video autoplay loop muted playsinline><source src="https://cdn-raw.modrinth.com/blog/streamlined-version-creation/vid1.mp4" type="video/mp4"></video></div><h2>Version summary</h2><p>Once you’ve uploaded your files, you’re taken to a summary page where we automatically detect the version number, subtitle, loaders, game versions, and environments based on the primary file and previous project versions.</p><p>Any field can be individually edited by clicking the edit button in the top right. For cases where we’re unable to detect something, that field simply won’t appear in the summary and will instead show up as an additional step in the modal flow.</p><p><img src="/news/article/streamlined-version-creation/details.webp" alt="Add details stage of the upload modal, where the user selects version type, number, subtitle, and can edit loaders, game versions, and environment metadata."></p><h2>Loader selector</h2><p>We’ve added a refreshed loader selection screen that groups loaders by project type. You can click any loader tag to add it.</p><div class="video-wrapper mb-8"><video autoplay loop muted playsinline><source src="https://cdn-raw.modrinth.com/blog/streamlined-version-creation/vid2.mp4" type="video/mp4"></video></div><h2>Game version selector</h2><p>Game versions now have their own dedicated step. This was a major pain point for projects that support a wide range of game versions. You can search for versions or toggle between releases and snapshots. Select individual versions with a click, or use shift-click to select a range.</p><div class="video-wrapper mb-8"><video autoplay loop muted playsinline><source src="https://cdn-raw.modrinth.com/blog/streamlined-version-creation/vid3.mp4" type="video/mp4"></video></div><h2>Environment selector</h2><p>Project environments were released earlier this year, and we heard feedback that some projects need them configured at the version level. We’ve moved environments out of project settings and into versions. For the vast majority of projects environments rarely change, so we automatically carry them over from a previous version that uses the same loader. You can always edit this if needed.</p><p><img src="/news/article/streamlined-version-creation/environments.webp" alt="Edit environment screen, showing a bunch of options to select such as client-side only, server-side only, and more."></p><h2>Dependency selector</h2><p>Dependencies were another pain point, so we’ve added the ability to search projects and versions directly, no more copying IDs. We also suggest dependencies from the other versions you’ve uploaded with the same loader, making them easy to add with a single click.</p><div class="video-wrapper mb-8"><video autoplay loop muted playsinline><source src="https://cdn-raw.modrinth.com/blog/streamlined-version-creation/vid4.mp4" type="video/mp4"></video></div><h2>Misc</h2><ul><li>Gallery management has now also been moved into Project Settings</li><li>The project publishing checklist now lives in Project Settings</li></ul><hr><p>Thank you all for your continued support. We hope you have a great holiday and get some well-earned time with your families! 🎅</p>]]> + <![CDATA[<p>Hey everyone! As part of our ongoing work to improve the creator side of the platform, we’re shipping a new project version creation and editing today. This part of the product was showing its age, so we’ve overhauled it to set us up for the new project types we plan to ship in the new year!</p><h2>TL;DR</h2><ul><li>Multi-file uploads with primary file detection and new supplementary file types</li><li>Automatic detection of version number, subtitle, loaders, game versions, and environment bundled into a version summary</li><li>A new loader selector that groups loaders by project type</li><li>A new game version selector with search and selecting version ranges</li><li>Project environments moved to be on a per-version basis</li><li>A new dependency selector with search and smart suggestions</li><li>Project gallery, versions, and publishing checklist moved into project settings</li></ul><h2>File uploading</h2><p>For starters, we’ve been centralizing all project editing into Project Settings to make the experience clearer and more approachable for new creators. Editing project versions now happens directly within Project Settings and projects look slightly different if you’re the creator.</p><p><img src="/news/article/streamlined-version-creation/edit-button.webp" alt="Project page header showing the primary action as &quot;Edit project&quot; for the creator"></p><p>You can create a new version by drag and dropping files into the versions table or creating a new version and uploading them. Multiple files can be uploaded at once.</p><p>The primary file is explicitly listed at the top and separate from any supplementary files. From there, you can add additional supplementary files and assign their types. Newly supported types include sources jar, dev jar, javadoc jar, and signature file.</p><div class="video-wrapper mb-8"><video autoplay loop muted playsinline><source src="https://cdn.modrinth.com/blog/streamlined-version-creation/vid1.mp4" type="video/mp4"></video></div><h2>Version summary</h2><p>Once you’ve uploaded your files, you’re taken to a summary page where we automatically detect the version number, subtitle, loaders, game versions, and environments based on the primary file and previous project versions.</p><p>Any field can be individually edited by clicking the edit button in the top right. For cases where we’re unable to detect something, that field simply won’t appear in the summary and will instead show up as an additional step in the modal flow.</p><p><img src="/news/article/streamlined-version-creation/details.webp" alt="Add details stage of the upload modal, where the user selects version type, number, subtitle, and can edit loaders, game versions, and environment metadata."></p><h2>Loader selector</h2><p>We’ve added a refreshed loader selection screen that groups loaders by project type. You can click any loader tag to add it.</p><div class="video-wrapper mb-8"><video autoplay loop muted playsinline><source src="https://cdn.modrinth.com/blog/streamlined-version-creation/vid2.mp4" type="video/mp4"></video></div><h2>Game version selector</h2><p>Game versions now have their own dedicated step. This was a major pain point for projects that support a wide range of game versions. You can search for versions or toggle between releases and snapshots. Select individual versions with a click, or use shift-click to select a range.</p><div class="video-wrapper mb-8"><video autoplay loop muted playsinline><source src="https://cdn.modrinth.com/blog/streamlined-version-creation/vid3.mp4" type="video/mp4"></video></div><h2>Environment selector</h2><p>Project environments were released earlier this year, and we heard feedback that some projects need them configured at the version level. We’ve moved environments out of project settings and into versions. For the vast majority of projects environments rarely change, so we automatically carry them over from a previous version that uses the same loader. You can always edit this if needed.</p><p><img src="/news/article/streamlined-version-creation/environments.webp" alt="Edit environment screen, showing a bunch of options to select such as client-side only, server-side only, and more."></p><h2>Dependency selector</h2><p>Dependencies were another pain point, so we’ve added the ability to search projects and versions directly, no more copying IDs. We also suggest dependencies from the other versions you’ve uploaded with the same loader, making them easy to add with a single click.</p><div class="video-wrapper mb-8"><video autoplay loop muted playsinline><source src="https://cdn.modrinth.com/blog/streamlined-version-creation/vid4.mp4" type="video/mp4"></video></div><h2>Misc</h2><ul><li>Gallery management has now also been moved into Project Settings</li><li>The project publishing checklist now lives in Project Settings</li></ul><hr><p>Thank you all for your continued support. We hope you have a great holiday and get some well-earned time with your families! 🎅</p>]]> <![CDATA[More Ways to Withdraw]]> diff --git a/apps/labrinth/.sqlx/query-c3598ed9f64f7151b83d47a15fabfdcf015bda1b3c0a7f2358c7f284feb532c3.json b/apps/labrinth/.sqlx/query-0082cce2f6ee34b338fa1dcd2143a35adc01109b87eedd245ba90b38f96bb972.json similarity index 74% rename from apps/labrinth/.sqlx/query-c3598ed9f64f7151b83d47a15fabfdcf015bda1b3c0a7f2358c7f284feb532c3.json rename to apps/labrinth/.sqlx/query-0082cce2f6ee34b338fa1dcd2143a35adc01109b87eedd245ba90b38f96bb972.json index c27569000b..e05b8b2734 100644 --- a/apps/labrinth/.sqlx/query-c3598ed9f64f7151b83d47a15fabfdcf015bda1b3c0a7f2358c7f284feb532c3.json +++ b/apps/labrinth/.sqlx/query-0082cce2f6ee34b338fa1dcd2143a35adc01109b87eedd245ba90b38f96bb972.json @@ -1,6 +1,6 @@ { "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 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 'decompiled_source', didws.decompiled_source,\n 'data', didws.data,\n 'severity', didws.severity,\n 'local_status', didws.local_status,\n 'global_status', didws.global_status,\n 'status', didws.status\n )\n ), '[]'::jsonb)\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.issue_id = dri.id\n )\n ) AS \"data!: sqlx::types::Json\"\n FROM delphi_report_issues dri\n WHERE dri.id = $1\n ", + "query": "\n SELECT\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 'decompiled_source', didws.decompiled_source,\n 'data', didws.data,\n 'severity', didws.severity,\n 'local_status', didws.local_status,\n 'global_status', didws.global_status,\n 'status', didws.status\n )\n ), '[]'::jsonb)\n FROM delphi_issue_details_with_statuses didws\n WHERE\n didws.issue_id = dri.id\n AND ($2 OR didws.severity != 'hidden')\n )\n ) AS \"data!: sqlx::types::Json\"\n FROM delphi_report_issues dri\n WHERE dri.id = $1\n ", "describe": { "columns": [ { @@ -11,12 +11,13 @@ ], "parameters": { "Left": [ - "Int8" + "Int8", + "Bool" ] }, "nullable": [ null ] }, - "hash": "c3598ed9f64f7151b83d47a15fabfdcf015bda1b3c0a7f2358c7f284feb532c3" + "hash": "0082cce2f6ee34b338fa1dcd2143a35adc01109b87eedd245ba90b38f96bb972" } diff --git a/apps/labrinth/.sqlx/query-03e377401b1a9bab44f3ff6451d3c1474a73fbce86ee25a3dab2320d59fc72b9.json b/apps/labrinth/.sqlx/query-03e377401b1a9bab44f3ff6451d3c1474a73fbce86ee25a3dab2320d59fc72b9.json new file mode 100644 index 0000000000..e929d42e00 --- /dev/null +++ b/apps/labrinth/.sqlx/query-03e377401b1a9bab44f3ff6451d3c1474a73fbce86ee25a3dab2320d59fc72b9.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n WITH project_membership AS (\n SELECT\n detail.project_id,\n BOOL_OR(\n detail.status IN ('pending', 'unsafe')\n AND detail.severity != 'hidden'\n ) AS old_needs_review,\n BOOL_OR(\n detail.status IN ('pending', 'unsafe')\n AND COALESCE(\n new_effect.severity,\n detail.original_severity\n ) != 'hidden'\n ) AS new_needs_review\n FROM delphi_issue_details_with_statuses detail\n LEFT JOIN delphi_rule_effects new_effect\n ON new_effect.revision = $1\n AND new_effect.detail_id = detail.id\n GROUP BY detail.project_id\n )\n SELECT\n project_id AS \"project_id!: DBProjectId\",\n new_needs_review AS \"new_needs_review!\"\n FROM project_membership\n WHERE old_needs_review IS DISTINCT FROM new_needs_review\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "project_id!: DBProjectId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "new_needs_review!", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + true, + null + ] + }, + "hash": "03e377401b1a9bab44f3ff6451d3c1474a73fbce86ee25a3dab2320d59fc72b9" +} diff --git a/apps/labrinth/.sqlx/query-080e2f0068c0c1c248dcfacf39798aefed70d9a61610fc7f4f1c8f291706fa4a.json b/apps/labrinth/.sqlx/query-080e2f0068c0c1c248dcfacf39798aefed70d9a61610fc7f4f1c8f291706fa4a.json deleted file mode 100644 index a80d2649ca..0000000000 --- a/apps/labrinth/.sqlx/query-080e2f0068c0c1c248dcfacf39798aefed70d9a61610fc7f4f1c8f291706fa4a.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n (\n SELECT t.id\n FROM threads t\n WHERE t.mod_id = $1\n ORDER BY t.id\n LIMIT 1\n ) AS \"thread_id: DBThreadId\",\n (\n SELECT tm.body->>'type'\n FROM threads t\n INNER JOIN threads_messages tm ON tm.thread_id = t.id\n WHERE\n t.mod_id = $1\n AND tm.body->>'type' = ANY($2::text[])\n ORDER BY tm.created DESC, tm.id DESC\n LIMIT 1\n ) AS \"last_tech_review_message_type\"\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "thread_id: DBThreadId", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "last_tech_review_message_type", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Int8", - "TextArray" - ] - }, - "nullable": [ - null, - null - ] - }, - "hash": "080e2f0068c0c1c248dcfacf39798aefed70d9a61610fc7f4f1c8f291706fa4a" -} diff --git a/apps/labrinth/.sqlx/query-0a541871989f0139a7f6f52e8cbae110b88a9cc05985c6cddc325ab913c9731a.json b/apps/labrinth/.sqlx/query-0a541871989f0139a7f6f52e8cbae110b88a9cc05985c6cddc325ab913c9731a.json new file mode 100644 index 0000000000..20e84c3874 --- /dev/null +++ b/apps/labrinth/.sqlx/query-0a541871989f0139a7f6f52e8cbae110b88a9cc05985c6cddc325ab913c9731a.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT revision FROM delphi_rule_revisions LIMIT 1 FOR UPDATE", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "revision", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "0a541871989f0139a7f6f52e8cbae110b88a9cc05985c6cddc325ab913c9731a" +} diff --git a/apps/labrinth/.sqlx/query-0b94bdb7bf42fdfd0fb1b015fe582bfe8c43582acd17e726616da863e35742c5.json b/apps/labrinth/.sqlx/query-0b94bdb7bf42fdfd0fb1b015fe582bfe8c43582acd17e726616da863e35742c5.json new file mode 100644 index 0000000000..3263182843 --- /dev/null +++ b/apps/labrinth/.sqlx/query-0b94bdb7bf42fdfd0fb1b015fe582bfe8c43582acd17e726616da863e35742c5.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n WITH inserted AS (\n INSERT INTO delphi_tech_review_queue (project_id)\n SELECT unnest($1::bigint[])\n ON CONFLICT (project_id) DO NOTHING\n RETURNING project_id\n )\n SELECT\n inserted.project_id AS \"project_id!: DBProjectId\",\n (\n SELECT thread.id\n FROM threads thread\n WHERE thread.mod_id = inserted.project_id\n ORDER BY thread.id\n LIMIT 1\n ) AS \"thread_id!: DBThreadId\"\n FROM inserted\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "project_id!: DBProjectId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "thread_id!: DBThreadId", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + null + ] + }, + "hash": "0b94bdb7bf42fdfd0fb1b015fe582bfe8c43582acd17e726616da863e35742c5" +} diff --git a/apps/labrinth/.sqlx/query-0cf618abf01774d1af5c9a7f23fe515f204468685333be953b746da8620af127.json b/apps/labrinth/.sqlx/query-0cf618abf01774d1af5c9a7f23fe515f204468685333be953b746da8620af127.json new file mode 100644 index 0000000000..78c0636f0e --- /dev/null +++ b/apps/labrinth/.sqlx/query-0cf618abf01774d1af5c9a7f23fe515f204468685333be953b746da8620af127.json @@ -0,0 +1,81 @@ +{ + "db_name": "PostgreSQL", + "query": "\n\t\tUPDATE delphi_rules\n\t\tSET\n\t\t\tname = $2,\n\t\t\trule = $3,\n\t\t\tpriority = $4,\n\t\t\ton_issue_types = $5,\n\t\t\trevision = (\n\t\t\t\tSELECT revision + 1 FROM delphi_rule_revisions LIMIT 1\n\t\t\t),\n\t\t\tupdated_at = CURRENT_TIMESTAMP,\n\t\t\tupdated_by = $6\n\t\tWHERE id = $1 AND NOT delete_on_next_revision\n\t\tRETURNING\n\t\t\tid AS \"id!: DelphiRuleId\",\n\t\t\tname,\n\t\t\trule,\n\t\t\tpriority,\n\t\t\ton_issue_types,\n\t\t\trevision,\n\t\t\tcreated_at,\n\t\t\tupdated_at,\n\t\t\tcreated_by,\n\t\t\tupdated_by\n\t\t", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id!: DelphiRuleId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "rule", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "priority", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "on_issue_types", + "type_info": "TextArray" + }, + { + "ordinal": 5, + "name": "revision", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 7, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "created_by", + "type_info": "Int8" + }, + { + "ordinal": 9, + "name": "updated_by", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Varchar", + "Varchar", + "Int4", + "TextArray", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + true + ] + }, + "hash": "0cf618abf01774d1af5c9a7f23fe515f204468685333be953b746da8620af127" +} diff --git a/apps/labrinth/.sqlx/query-b3aaa0b2f38950057d845b3d4dc8103c009077768b33c17e22dcef4774ae5814.json b/apps/labrinth/.sqlx/query-0dfa4c0fc061ceb908e012867a0a0ff8a77fc1f1a7181bee8992981747b69592.json similarity index 98% rename from apps/labrinth/.sqlx/query-b3aaa0b2f38950057d845b3d4dc8103c009077768b33c17e22dcef4774ae5814.json rename to apps/labrinth/.sqlx/query-0dfa4c0fc061ceb908e012867a0a0ff8a77fc1f1a7181bee8992981747b69592.json index e54ff20ebf..d4c5656dd6 100644 --- a/apps/labrinth/.sqlx/query-b3aaa0b2f38950057d845b3d4dc8103c009077768b33c17e22dcef4774ae5814.json +++ b/apps/labrinth/.sqlx/query-0dfa4c0fc061ceb908e012867a0a0ff8a77fc1f1a7181bee8992981747b69592.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n WITH moderation_projects AS (\n SELECT\n m.id,\n m.slug,\n m.name,\n m.summary,\n m.description,\n m.queued,\n m.published,\n m.organization_id,\n m.team_id,\n m.components\n FROM mods m\n WHERE\n m.status = $1\n AND (\n $9::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = m.id\n AND didws.status = 'pending'\n )\n )\n ),\n external_dependencies AS (\n SELECT\n v.mod_id,\n COUNT(*) AS external_dependencies_count\n FROM versions v\n INNER JOIN moderation_projects mp ON mp.id = v.mod_id\n INNER JOIN dependencies d ON d.dependent_id = v.id\n WHERE d.dependency_file_name IS NOT NULL\n GROUP BY v.mod_id\n ),\n version_project_types AS (\n SELECT\n v.mod_id,\n ARRAY_AGG(DISTINCT pt.name::text) FILTER (WHERE pt.name IS NOT NULL) AS project_types\n FROM versions v\n INNER JOIN moderation_projects mp ON mp.id = v.mod_id\n INNER JOIN loaders_versions lv ON v.id = lv.version_id\n INNER JOIN loaders l ON lv.loader_id = l.id\n INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id\n INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n WHERE v.status = ANY($2)\n GROUP BY v.mod_id\n ),\n queue_projects AS (\n SELECT\n mp.id,\n mp.slug,\n mp.name,\n mp.summary,\n mp.description,\n mp.queued,\n mp.published,\n search_organization.name AS organization_name,\n search_owner.username AS owner_name,\n CASE\n WHEN jsonb_typeof(mp.components -> 'minecraft_server') = 'object'\n THEN ARRAY_APPEND(\n ARRAY_REMOVE(\n COALESCE(vpt.project_types::text[], ARRAY[]::text[]),\n 'modpack'\n ),\n 'minecraft_java_server'\n )\n ELSE COALESCE(vpt.project_types::text[], ARRAY[]::text[])\n END AS project_types,\n COALESCE(ed.external_dependencies_count, 0) AS external_dependencies_count\n FROM moderation_projects mp\n LEFT JOIN organizations search_organization\n ON search_organization.id = mp.organization_id\n AND $3::text IS NOT NULL\n LEFT JOIN LATERAL (\n SELECT\n u.username\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = mp.team_id\n AND tm.is_owner\n ORDER BY tm.ordering ASC\n LIMIT 1\n ) search_owner ON $3::text IS NOT NULL\n AND mp.organization_id IS NULL\n LEFT JOIN external_dependencies ed ON ed.mod_id = mp.id\n LEFT JOIN version_project_types vpt ON vpt.mod_id = mp.id\n ),\n filtered_projects AS (\n SELECT\n id,\n queued,\n published,\n project_types,\n external_dependencies_count\n FROM queue_projects\n WHERE\n (\n $3::text IS NULL\n OR name ILIKE '%' || $3 || '%'\n OR slug ILIKE '%' || $3 || '%'\n OR summary ILIKE '%' || $3 || '%'\n OR description ILIKE '%' || $3 || '%'\n OR owner_name ILIKE '%' || $3 || '%'\n OR organization_name ILIKE '%' || $3 || '%'\n OR EXISTS (\n SELECT 1\n FROM UNNEST(project_types) AS searched_project_type(project_type)\n WHERE searched_project_type.project_type ILIKE '%' || $3 || '%'\n )\n )\n AND (\n $4::text IS NULL\n OR ($4 = 'none' AND CARDINALITY(project_types) = 0)\n OR ($4 = 'minecraft_java_server' AND project_types @> ARRAY['minecraft_java_server']::text[])\n OR ($4 <> 'none' AND $4 <> 'minecraft_java_server' AND\n project_types[1] = $4\n )\n )\n AND ($5::boolean IS NULL OR (external_dependencies_count > 0) = $5)\n ),\n total AS (\n SELECT COUNT(*) AS total_count FROM filtered_projects\n ),\n page_ids AS (\n SELECT\n id,\n queued,\n published,\n project_types,\n external_dependencies_count\n FROM filtered_projects\n ORDER BY\n CASE WHEN $8 = 'most_external_deps' THEN external_dependencies_count END DESC,\n CASE WHEN $8 = 'least_external_deps' THEN external_dependencies_count END ASC,\n CASE WHEN $8 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $8 IN ('oldest', 'most_external_deps', 'least_external_deps') THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n OFFSET $7\n LIMIT $6\n ),\n page_projects AS (\n SELECT\n m.id,\n m.slug,\n m.name,\n m.summary,\n m.icon_url,\n m.status,\n m.requested_status,\n m.queued,\n m.published,\n m.updated,\n m.organization_id,\n m.team_id,\n o.name AS organization_name,\n o.icon_url AS organization_icon_url,\n owner.user_id AS owner_id,\n owner.username AS owner_name,\n owner.avatar_url AS owner_icon_url,\n page_ids.project_types,\n page_ids.external_dependencies_count\n FROM page_ids\n INNER JOIN mods m ON m.id = page_ids.id\n LEFT JOIN organizations o ON o.id = m.organization_id\n LEFT JOIN LATERAL (\n SELECT\n tm.user_id,\n u.username,\n u.avatar_url\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = m.team_id\n AND tm.is_owner\n ORDER BY tm.ordering ASC\n LIMIT 1\n ) owner ON m.organization_id IS NULL\n )\n SELECT\n total.total_count AS \"total_count!\",\n page_projects.id AS \"id?\",\n page_projects.slug AS \"slug?\",\n page_projects.name AS \"name?\",\n page_projects.summary AS \"summary?\",\n page_projects.icon_url AS \"icon_url?\",\n page_projects.status AS \"status?\",\n page_projects.requested_status AS \"requested_status?\",\n page_projects.queued AS \"queued?\",\n page_projects.published AS \"published?\",\n page_projects.updated AS \"updated?\",\n page_projects.organization_id AS \"organization_id?\",\n page_projects.organization_name AS \"organization_name?\",\n page_projects.organization_icon_url AS \"organization_icon_url?\",\n page_projects.owner_id AS \"owner_id?\",\n page_projects.owner_name AS \"owner_name?\",\n page_projects.owner_icon_url AS \"owner_icon_url?\",\n page_projects.project_types AS \"project_types?: Vec\",\n page_projects.external_dependencies_count AS \"external_dependencies_count?\"\n FROM total\n LEFT JOIN page_projects ON true\n ORDER BY\n CASE WHEN $8 = 'most_external_deps' THEN page_projects.external_dependencies_count END DESC,\n CASE WHEN $8 = 'least_external_deps' THEN page_projects.external_dependencies_count END ASC,\n CASE WHEN $8 = 'newest' THEN COALESCE(page_projects.queued, page_projects.published) END DESC NULLS LAST,\n CASE WHEN $8 IN ('oldest', 'most_external_deps', 'least_external_deps') THEN COALESCE(page_projects.queued, page_projects.published) END ASC NULLS LAST,\n page_projects.id ASC\n ", + "query": "\n WITH moderation_projects AS (\n SELECT\n m.id,\n m.slug,\n m.name,\n m.summary,\n m.description,\n m.queued,\n m.published,\n m.organization_id,\n m.team_id,\n m.components\n FROM mods m\n WHERE\n m.status = $1\n AND (\n $9::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = m.id\n AND didws.status = 'pending'\n AND didws.severity != 'hidden'\n )\n )\n ),\n external_dependencies AS (\n SELECT\n v.mod_id,\n COUNT(*) AS external_dependencies_count\n FROM versions v\n INNER JOIN moderation_projects mp ON mp.id = v.mod_id\n INNER JOIN dependencies d ON d.dependent_id = v.id\n WHERE d.dependency_file_name IS NOT NULL\n GROUP BY v.mod_id\n ),\n version_project_types AS (\n SELECT\n v.mod_id,\n ARRAY_AGG(DISTINCT pt.name::text) FILTER (WHERE pt.name IS NOT NULL) AS project_types\n FROM versions v\n INNER JOIN moderation_projects mp ON mp.id = v.mod_id\n INNER JOIN loaders_versions lv ON v.id = lv.version_id\n INNER JOIN loaders l ON lv.loader_id = l.id\n INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id\n INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n WHERE v.status = ANY($2)\n GROUP BY v.mod_id\n ),\n queue_projects AS (\n SELECT\n mp.id,\n mp.slug,\n mp.name,\n mp.summary,\n mp.description,\n mp.queued,\n mp.published,\n search_organization.name AS organization_name,\n search_owner.username AS owner_name,\n CASE\n WHEN jsonb_typeof(mp.components -> 'minecraft_server') = 'object'\n THEN ARRAY_APPEND(\n ARRAY_REMOVE(\n COALESCE(vpt.project_types::text[], ARRAY[]::text[]),\n 'modpack'\n ),\n 'minecraft_java_server'\n )\n ELSE COALESCE(vpt.project_types::text[], ARRAY[]::text[])\n END AS project_types,\n COALESCE(ed.external_dependencies_count, 0) AS external_dependencies_count\n FROM moderation_projects mp\n LEFT JOIN organizations search_organization\n ON search_organization.id = mp.organization_id\n AND $3::text IS NOT NULL\n LEFT JOIN LATERAL (\n SELECT\n u.username\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = mp.team_id\n AND tm.is_owner\n ORDER BY tm.ordering ASC\n LIMIT 1\n ) search_owner ON $3::text IS NOT NULL\n AND mp.organization_id IS NULL\n LEFT JOIN external_dependencies ed ON ed.mod_id = mp.id\n LEFT JOIN version_project_types vpt ON vpt.mod_id = mp.id\n ),\n filtered_projects AS (\n SELECT\n id,\n queued,\n published,\n project_types,\n external_dependencies_count\n FROM queue_projects\n WHERE\n (\n $3::text IS NULL\n OR name ILIKE '%' || $3 || '%'\n OR slug ILIKE '%' || $3 || '%'\n OR summary ILIKE '%' || $3 || '%'\n OR description ILIKE '%' || $3 || '%'\n OR owner_name ILIKE '%' || $3 || '%'\n OR organization_name ILIKE '%' || $3 || '%'\n OR EXISTS (\n SELECT 1\n FROM UNNEST(project_types) AS searched_project_type(project_type)\n WHERE searched_project_type.project_type ILIKE '%' || $3 || '%'\n )\n )\n AND (\n $4::text IS NULL\n OR ($4 = 'none' AND CARDINALITY(project_types) = 0)\n OR ($4 = 'minecraft_java_server' AND project_types @> ARRAY['minecraft_java_server']::text[])\n OR ($4 <> 'none' AND $4 <> 'minecraft_java_server' AND\n project_types[1] = $4\n )\n )\n AND ($5::boolean IS NULL OR (external_dependencies_count > 0) = $5)\n ),\n total AS (\n SELECT COUNT(*) AS total_count FROM filtered_projects\n ),\n page_ids AS (\n SELECT\n id,\n queued,\n published,\n project_types,\n external_dependencies_count\n FROM filtered_projects\n ORDER BY\n CASE WHEN $8 = 'most_external_deps' THEN external_dependencies_count END DESC,\n CASE WHEN $8 = 'least_external_deps' THEN external_dependencies_count END ASC,\n CASE WHEN $8 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $8 IN ('oldest', 'most_external_deps', 'least_external_deps') THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n OFFSET $7\n LIMIT $6\n ),\n page_projects AS (\n SELECT\n m.id,\n m.slug,\n m.name,\n m.summary,\n m.icon_url,\n m.status,\n m.requested_status,\n m.queued,\n m.published,\n m.updated,\n m.organization_id,\n m.team_id,\n o.name AS organization_name,\n o.icon_url AS organization_icon_url,\n owner.user_id AS owner_id,\n owner.username AS owner_name,\n owner.avatar_url AS owner_icon_url,\n page_ids.project_types,\n page_ids.external_dependencies_count\n FROM page_ids\n INNER JOIN mods m ON m.id = page_ids.id\n LEFT JOIN organizations o ON o.id = m.organization_id\n LEFT JOIN LATERAL (\n SELECT\n tm.user_id,\n u.username,\n u.avatar_url\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = m.team_id\n AND tm.is_owner\n ORDER BY tm.ordering ASC\n LIMIT 1\n ) owner ON m.organization_id IS NULL\n )\n SELECT\n total.total_count AS \"total_count!\",\n page_projects.id AS \"id?\",\n page_projects.slug AS \"slug?\",\n page_projects.name AS \"name?\",\n page_projects.summary AS \"summary?\",\n page_projects.icon_url AS \"icon_url?\",\n page_projects.status AS \"status?\",\n page_projects.requested_status AS \"requested_status?\",\n page_projects.queued AS \"queued?\",\n page_projects.published AS \"published?\",\n page_projects.updated AS \"updated?\",\n page_projects.organization_id AS \"organization_id?\",\n page_projects.organization_name AS \"organization_name?\",\n page_projects.organization_icon_url AS \"organization_icon_url?\",\n page_projects.owner_id AS \"owner_id?\",\n page_projects.owner_name AS \"owner_name?\",\n page_projects.owner_icon_url AS \"owner_icon_url?\",\n page_projects.project_types AS \"project_types?: Vec\",\n page_projects.external_dependencies_count AS \"external_dependencies_count?\"\n FROM total\n LEFT JOIN page_projects ON true\n ORDER BY\n CASE WHEN $8 = 'most_external_deps' THEN page_projects.external_dependencies_count END DESC,\n CASE WHEN $8 = 'least_external_deps' THEN page_projects.external_dependencies_count END ASC,\n CASE WHEN $8 = 'newest' THEN COALESCE(page_projects.queued, page_projects.published) END DESC NULLS LAST,\n CASE WHEN $8 IN ('oldest', 'most_external_deps', 'least_external_deps') THEN COALESCE(page_projects.queued, page_projects.published) END ASC NULLS LAST,\n page_projects.id ASC\n ", "describe": { "columns": [ { @@ -134,5 +134,5 @@ null ] }, - "hash": "b3aaa0b2f38950057d845b3d4dc8103c009077768b33c17e22dcef4774ae5814" + "hash": "0dfa4c0fc061ceb908e012867a0a0ff8a77fc1f1a7181bee8992981747b69592" } diff --git a/apps/labrinth/.sqlx/query-1697315d3414e911c9be30fa16164f884d1941120499ba076c7393c942131848.json b/apps/labrinth/.sqlx/query-1697315d3414e911c9be30fa16164f884d1941120499ba076c7393c942131848.json new file mode 100644 index 0000000000..f643ed3d22 --- /dev/null +++ b/apps/labrinth/.sqlx/query-1697315d3414e911c9be30fa16164f884d1941120499ba076c7393c942131848.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE delphi_rules SET revision = $1 WHERE NOT delete_on_next_revision", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "1697315d3414e911c9be30fa16164f884d1941120499ba076c7393c942131848" +} diff --git a/apps/labrinth/.sqlx/query-1a6d4ac11af078439cff5c772f4dc2392729f99ba1f8c7892831235f341fb276.json b/apps/labrinth/.sqlx/query-1a6d4ac11af078439cff5c772f4dc2392729f99ba1f8c7892831235f341fb276.json deleted file mode 100644 index 4db30b1153..0000000000 --- a/apps/labrinth/.sqlx/query-1a6d4ac11af078439cff5c772f4dc2392729f99ba1f8c7892831235f341fb276.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n m.id AS \"project_id: DBProjectId\",\n MIN(t.id) AS \"thread_id!: DBThreadId\"\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 INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_issue_details_with_statuses didws\n ON didws.issue_id = dri.id\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 WHERE\n (\n cardinality($4::text[]) = 0\n OR (\n 'minecraft_java_server' = ANY($4::text[])\n AND (\n m.components ? 'minecraft_server'\n OR m.components ? 'minecraft_java_server'\n )\n )\n OR EXISTS (\n SELECT 1\n FROM versions type_v\n INNER JOIN loaders_versions type_lv\n ON type_lv.version_id = type_v.id\n INNER JOIN loaders_project_types type_lpt\n ON type_lpt.joining_loader_id = type_lv.loader_id\n INNER JOIN project_types type_pt\n ON type_pt.id = type_lpt.joining_project_type_id\n WHERE\n type_v.mod_id = m.id\n AND type_pt.name = ANY($4::text[])\n AND (\n type_pt.name != 'modpack'\n OR NOT (\n m.components ? 'minecraft_server'\n OR m.components ? 'minecraft_java_server'\n )\n )\n )\n )\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n AND (cardinality($6::text[]) = 0 OR m.status = ANY($6::text[]))\n AND (cardinality($7::text[]) = 0 OR dri.issue_type = ANY($7::text[]))\n AND didws.status = 'pending'\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 GROUP BY m.id\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END DESC,\n -- tie-breaker: oldest reports\n MIN(dr.created) ASC\n LIMIT $1 OFFSET $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "project_id: DBProjectId", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "thread_id!: DBThreadId", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Text", - "TextArray", - "Text", - "TextArray", - "TextArray" - ] - }, - "nullable": [ - false, - null - ] - }, - "hash": "1a6d4ac11af078439cff5c772f4dc2392729f99ba1f8c7892831235f341fb276" -} diff --git a/apps/labrinth/.sqlx/query-20a88febcaf673264ff7f00a11848f97f334779cd871caa93a6de25c6c871e61.json b/apps/labrinth/.sqlx/query-20a88febcaf673264ff7f00a11848f97f334779cd871caa93a6de25c6c871e61.json new file mode 100644 index 0000000000..f918bf11f0 --- /dev/null +++ b/apps/labrinth/.sqlx/query-20a88febcaf673264ff7f00a11848f97f334779cd871caa93a6de25c6c871e61.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM delphi_rules WHERE delete_on_next_revision", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "20a88febcaf673264ff7f00a11848f97f334779cd871caa93a6de25c6c871e61" +} diff --git a/apps/labrinth/.sqlx/query-285c66152a506e79c3c8996b4f44b8f63dcf79a2d02dcfeac9655d89bdb2750e.json b/apps/labrinth/.sqlx/query-285c66152a506e79c3c8996b4f44b8f63dcf79a2d02dcfeac9655d89bdb2750e.json new file mode 100644 index 0000000000..5d0c482906 --- /dev/null +++ b/apps/labrinth/.sqlx/query-285c66152a506e79c3c8996b4f44b8f63dcf79a2d02dcfeac9655d89bdb2750e.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM delphi_tech_review_queue\n WHERE project_id = $1\n RETURNING project_id AS \"project_id: DBProjectId\"\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "project_id: DBProjectId", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "285c66152a506e79c3c8996b4f44b8f63dcf79a2d02dcfeac9655d89bdb2750e" +} diff --git a/apps/labrinth/.sqlx/query-33181ac6fe503d4ca6b396d6ddd7070d2abac466048c870b2f23497848cf55b3.json b/apps/labrinth/.sqlx/query-33181ac6fe503d4ca6b396d6ddd7070d2abac466048c870b2f23497848cf55b3.json deleted file mode 100644 index b30c98b1bb..0000000000 --- a/apps/labrinth/.sqlx/query-33181ac6fe503d4ca6b396d6ddd7070d2abac466048c870b2f23497848cf55b3.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT mod_id, SUM(amount) amount_sum, DATE_BIN($4::interval, created, TIMESTAMP '2001-01-01') AS interval_start\n FROM payouts_values\n WHERE user_id = $1 AND created >= $2 AND created < $3\n GROUP by mod_id, interval_start ORDER BY interval_start\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "mod_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "amount_sum", - "type_info": "Numeric" - }, - { - "ordinal": 2, - "name": "interval_start", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Int8", - "Timestamptz", - "Timestamptz", - "Interval" - ] - }, - "nullable": [ - true, - null, - null - ] - }, - "hash": "33181ac6fe503d4ca6b396d6ddd7070d2abac466048c870b2f23497848cf55b3" -} diff --git a/apps/labrinth/.sqlx/query-348f71a746d67cbbbcaf48c42f2228eff41da7c2846d0f2be5c06830a1189ad4.json b/apps/labrinth/.sqlx/query-348f71a746d67cbbbcaf48c42f2228eff41da7c2846d0f2be5c06830a1189ad4.json new file mode 100644 index 0000000000..c6a72838f0 --- /dev/null +++ b/apps/labrinth/.sqlx/query-348f71a746d67cbbbcaf48c42f2228eff41da7c2846d0f2be5c06830a1189ad4.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n\t\tSELECT COUNT(*) AS \"count!\"\n\t\tFROM delphi_rule_effects effect\n\t\tINNER JOIN delphi_rule_revisions published\n\t\t\tON published.revision = effect.revision\n\t\tWHERE effect.rule_id = $1\n\t\t", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + null + ] + }, + "hash": "348f71a746d67cbbbcaf48c42f2228eff41da7c2846d0f2be5c06830a1189ad4" +} diff --git a/apps/labrinth/.sqlx/query-055f71ec6d193c5f5259cafc989a04f3f791205e6c2df15c5bf9d5afcf71a76a.json b/apps/labrinth/.sqlx/query-36ffcc9010d1f70deba53483cc2bcbc274f4b55db499bf83b990634b9a805575.json similarity index 94% rename from apps/labrinth/.sqlx/query-055f71ec6d193c5f5259cafc989a04f3f791205e6c2df15c5bf9d5afcf71a76a.json rename to apps/labrinth/.sqlx/query-36ffcc9010d1f70deba53483cc2bcbc274f4b55db499bf83b990634b9a805575.json index cf686ee65a..8e9078348f 100644 --- a/apps/labrinth/.sqlx/query-055f71ec6d193c5f5259cafc989a04f3f791205e6c2df15c5bf9d5afcf71a76a.json +++ b/apps/labrinth/.sqlx/query-36ffcc9010d1f70deba53483cc2bcbc274f4b55db499bf83b990634b9a805575.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n didws.key AS \"detail_key!\",\n didws.id AS \"detail_id!: DelphiReportIssueDetailsId\",\n didws.issue_id AS \"issue_id!: DelphiReportIssueId\",\n dri.issue_type,\n m.id AS \"project_id!: DBProjectId\",\n m.slug AS \"project_slug?\",\n m.name AS \"project_name!\",\n v.id AS \"version_id!: DBVersionId\",\n v.version_number,\n f.id AS \"file_id!: DBFileId\",\n f.filename AS \"file_name!\",\n didws.jar AS \"jar?\",\n didws.file_path AS \"file_path!\",\n didws.severity AS \"severity!: DelphiSeverity\",\n COALESCE(didv.verdict, 'pending'::delphi_report_issue_status)\n AS \"local_status!: DelphiStatus\",\n didws.status AS \"effective_status!: DelphiStatus\"\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\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 LEFT JOIN delphi_issue_detail_verdicts didv\n ON didv.project_id = didws.project_id\n AND didv.detail_key = didws.key\n WHERE\n didws.key = $1\n AND ($2::bigint IS NULL OR didws.id > $2)\n AND dri.issue_type != '__dummy'\n ORDER BY didws.id\n LIMIT $3\n ", + "query": "\n SELECT\n didws.key AS \"detail_key!\",\n didws.id AS \"detail_id!: DelphiReportIssueDetailsId\",\n didws.issue_id AS \"issue_id!: DelphiReportIssueId\",\n dri.issue_type,\n m.id AS \"project_id!: DBProjectId\",\n m.slug AS \"project_slug?\",\n m.name AS \"project_name!\",\n v.id AS \"version_id!: DBVersionId\",\n v.version_number,\n f.id AS \"file_id!: DBFileId\",\n f.filename AS \"file_name!\",\n didws.jar AS \"jar?\",\n didws.file_path AS \"file_path!\",\n didws.severity AS \"severity!: DelphiSeverity\",\n COALESCE(didv.verdict, 'pending'::delphi_report_issue_status)\n AS \"local_status!: DelphiStatus\",\n didws.status AS \"effective_status!: DelphiStatus\"\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\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 LEFT JOIN delphi_issue_detail_verdicts didv\n ON didv.project_id = didws.project_id\n AND didv.detail_key = didws.key\n WHERE\n didws.key = $1\n AND ($2::bigint IS NULL OR didws.id > $2)\n AND didws.severity != 'hidden'\n ORDER BY didws.id\n LIMIT $3\n ", "describe": { "columns": [ { @@ -76,10 +76,12 @@ "name": "delphi_severity", "kind": { "Enum": [ + "hidden", "low", "medium", "high", - "severe" + "severe", + "malware" ] } } @@ -144,5 +146,5 @@ true ] }, - "hash": "055f71ec6d193c5f5259cafc989a04f3f791205e6c2df15c5bf9d5afcf71a76a" + "hash": "36ffcc9010d1f70deba53483cc2bcbc274f4b55db499bf83b990634b9a805575" } diff --git a/apps/labrinth/.sqlx/query-1551b022217df05490a01e715b3584d26550d01a1917d1af64fe236d63dd5005.json b/apps/labrinth/.sqlx/query-3b0d78d18738798fd6be69c040bcb1a9986509413b2115de367a82eb58bc62f3.json similarity index 75% rename from apps/labrinth/.sqlx/query-1551b022217df05490a01e715b3584d26550d01a1917d1af64fe236d63dd5005.json rename to apps/labrinth/.sqlx/query-3b0d78d18738798fd6be69c040bcb1a9986509413b2115de367a82eb58bc62f3.json index db45e607d0..434a2f596b 100644 --- a/apps/labrinth/.sqlx/query-1551b022217df05490a01e715b3584d26550d01a1917d1af64fe236d63dd5005.json +++ b/apps/labrinth/.sqlx/query-3b0d78d18738798fd6be69c040bcb1a9986509413b2115de367a82eb58bc62f3.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n WITH ranked_local_traces AS (\n SELECT\n didws.key AS detail_key,\n didws.id AS detail_id,\n didws.issue_id,\n dri.issue_type,\n m.id AS project_id,\n m.slug AS project_slug,\n m.name AS project_name,\n v.id AS version_id,\n v.version_number,\n f.id AS file_id,\n f.filename AS file_name,\n didws.jar,\n didws.file_path,\n didws.severity,\n COALESCE(didv.verdict, 'pending'::delphi_report_issue_status)\n AS local_status,\n didws.status AS effective_status,\n ROW_NUMBER() OVER (\n PARTITION BY didws.key\n ORDER BY didws.id\n ) AS row_num\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\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 LEFT JOIN delphi_issue_detail_verdicts didv\n ON didv.project_id = didws.project_id\n AND didv.detail_key = didws.key\n WHERE\n didws.key = ANY($1::text[])\n AND dri.issue_type != '__dummy'\n AND (\n $3::text IS NULL\n OR didws.key ILIKE '%' || $3 || '%'\n OR didws.file_path ILIKE '%' || $3 || '%'\n )\n )\n SELECT\n detail_key AS \"detail_key!\",\n detail_id AS \"detail_id!: DelphiReportIssueDetailsId\",\n issue_id AS \"issue_id!: DelphiReportIssueId\",\n issue_type,\n project_id AS \"project_id!: DBProjectId\",\n project_slug AS \"project_slug?\",\n project_name AS \"project_name!\",\n version_id AS \"version_id!: DBVersionId\",\n v.version_number,\n file_id AS \"file_id!: DBFileId\",\n file_name AS \"file_name!\",\n jar AS \"jar?\",\n file_path AS \"file_path!\",\n severity AS \"severity!: DelphiSeverity\",\n local_status AS \"local_status!: DelphiStatus\",\n effective_status AS \"effective_status!: DelphiStatus\"\n FROM ranked_local_traces v\n WHERE row_num <= $2\n ORDER BY detail_key, detail_id\n ", + "query": "\n WITH ranked_local_traces AS (\n SELECT\n didws.key AS detail_key,\n didws.id AS detail_id,\n didws.issue_id,\n dri.issue_type,\n m.id AS project_id,\n m.slug AS project_slug,\n m.name AS project_name,\n v.id AS version_id,\n v.version_number,\n f.id AS file_id,\n f.filename AS file_name,\n didws.jar,\n didws.file_path,\n didws.severity,\n COALESCE(didv.verdict, 'pending'::delphi_report_issue_status)\n AS local_status,\n didws.status AS effective_status,\n ROW_NUMBER() OVER (\n PARTITION BY didws.key\n ORDER BY didws.id\n ) AS row_num\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\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 LEFT JOIN delphi_issue_detail_verdicts didv\n ON didv.project_id = didws.project_id\n AND didv.detail_key = didws.key\n WHERE\n didws.key = ANY($1::text[])\n AND didws.severity != 'hidden'\n AND dri.issue_type != '__dummy'\n AND (\n $3::text IS NULL\n OR didws.key ILIKE '%' || $3 || '%'\n OR didws.file_path ILIKE '%' || $3 || '%'\n )\n )\n SELECT\n detail_key AS \"detail_key!\",\n detail_id AS \"detail_id!: DelphiReportIssueDetailsId\",\n issue_id AS \"issue_id!: DelphiReportIssueId\",\n issue_type,\n project_id AS \"project_id!: DBProjectId\",\n project_slug AS \"project_slug?\",\n project_name AS \"project_name!\",\n version_id AS \"version_id!: DBVersionId\",\n v.version_number,\n file_id AS \"file_id!: DBFileId\",\n file_name AS \"file_name!\",\n jar AS \"jar?\",\n file_path AS \"file_path!\",\n severity AS \"severity!: DelphiSeverity\",\n local_status AS \"local_status!: DelphiStatus\",\n effective_status AS \"effective_status!: DelphiStatus\"\n FROM ranked_local_traces v\n WHERE row_num <= $2\n ORDER BY detail_key, detail_id\n ", "describe": { "columns": [ { @@ -76,10 +76,12 @@ "name": "delphi_severity", "kind": { "Enum": [ + "hidden", "low", "medium", "high", - "severe" + "severe", + "malware" ] } } @@ -144,5 +146,5 @@ true ] }, - "hash": "1551b022217df05490a01e715b3584d26550d01a1917d1af64fe236d63dd5005" + "hash": "3b0d78d18738798fd6be69c040bcb1a9986509413b2115de367a82eb58bc62f3" } diff --git a/apps/labrinth/.sqlx/query-41da353ec0bfb3209f095e1e4f932beedc4b3b1c6de8365b7b523b9d917977b2.json b/apps/labrinth/.sqlx/query-41da353ec0bfb3209f095e1e4f932beedc4b3b1c6de8365b7b523b9d917977b2.json deleted file mode 100644 index e4c019979a..0000000000 --- a/apps/labrinth/.sqlx/query-41da353ec0bfb3209f095e1e4f932beedc4b3b1c6de8365b7b523b9d917977b2.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT mod_id, SUM(amount) amount_sum, DATE_BIN($4::interval, created, TIMESTAMP '2001-01-01') AS interval_start\n FROM payouts_values\n WHERE mod_id = ANY($1) AND created >= $2 AND created < $3\n GROUP by mod_id, interval_start ORDER BY interval_start\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "mod_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "amount_sum", - "type_info": "Numeric" - }, - { - "ordinal": 2, - "name": "interval_start", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Int8Array", - "Timestamptz", - "Timestamptz", - "Interval" - ] - }, - "nullable": [ - true, - null, - null - ] - }, - "hash": "41da353ec0bfb3209f095e1e4f932beedc4b3b1c6de8365b7b523b9d917977b2" -} diff --git a/apps/labrinth/.sqlx/query-42579ab8ccb58b9b2553787327115af8dbb2a85db0affc67eac74cfb48fbaa3e.json b/apps/labrinth/.sqlx/query-42579ab8ccb58b9b2553787327115af8dbb2a85db0affc67eac74cfb48fbaa3e.json new file mode 100644 index 0000000000..c04d8b8fea --- /dev/null +++ b/apps/labrinth/.sqlx/query-42579ab8ccb58b9b2553787327115af8dbb2a85db0affc67eac74cfb48fbaa3e.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT COUNT(*) AS \"count!\"\n FROM delphi_report_issue_details\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "42579ab8ccb58b9b2553787327115af8dbb2a85db0affc67eac74cfb48fbaa3e" +} diff --git a/apps/labrinth/.sqlx/query-48dfc2f2bcf8917f110b7b2b142167f1525cdea2ace9e6165e5c5f596cbd9fb3.json b/apps/labrinth/.sqlx/query-48dfc2f2bcf8917f110b7b2b142167f1525cdea2ace9e6165e5c5f596cbd9fb3.json deleted file mode 100644 index fa0165cc60..0000000000 --- a/apps/labrinth/.sqlx/query-48dfc2f2bcf8917f110b7b2b142167f1525cdea2ace9e6165e5c5f596cbd9fb3.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 'report_id', dr.id,\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\t\t\t\t'issues', (\n\t\t\t\t\tSELECT coalesce(json_agg(\n\t\t\t\t\t\tto_jsonb(dri)\n\t\t\t\t\t\t|| jsonb_build_object(\n\t\t\t\t\t\t\t-- TODO: replace with `json_array` in Postgres 16\n\t\t\t\t\t\t\t'details', (\n\t\t\t\t\t\t\t\tSELECT 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 'decompiled_source', didws.decompiled_source,\n 'data', didws.data,\n 'severity', didws.severity,\n 'local_status', didws.local_status,\n 'global_status', didws.global_status,\n 'status', didws.status\n )\n ), '[]'::jsonb)\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.issue_id = dri.id\n )\n\t\t\t\t\t\t)\n\t\t\t\t\t), '[]'::json)\n\t\t\t\t\tFROM delphi_report_issues dri\n\t\t\t\t\tWHERE\n\t\t\t\t\t\tdri.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": [ - { - "ordinal": 0, - "name": "data!: sqlx::types::Json", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - null - ] - }, - "hash": "48dfc2f2bcf8917f110b7b2b142167f1525cdea2ace9e6165e5c5f596cbd9fb3" -} diff --git a/apps/labrinth/.sqlx/query-4e30093d30c2ee3c39e6e3f2d9ffdc2ba5455d2213e06bc3e273f250ef97a525.json b/apps/labrinth/.sqlx/query-4e30093d30c2ee3c39e6e3f2d9ffdc2ba5455d2213e06bc3e273f250ef97a525.json new file mode 100644 index 0000000000..fcafa64eb2 --- /dev/null +++ b/apps/labrinth/.sqlx/query-4e30093d30c2ee3c39e6e3f2d9ffdc2ba5455d2213e06bc3e273f250ef97a525.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT requested.project_id AS \"project_id!: DBProjectId\"\n FROM unnest($1::bigint[]) AS requested(project_id)\n WHERE NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses detail\n WHERE detail.project_id = requested.project_id\n )\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "project_id!: DBProjectId", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + null + ] + }, + "hash": "4e30093d30c2ee3c39e6e3f2d9ffdc2ba5455d2213e06bc3e273f250ef97a525" +} diff --git a/apps/labrinth/.sqlx/query-50a5f3a116ebe5cf88452a9aac89f73135491f170e3d619a93144b36ef429705.json b/apps/labrinth/.sqlx/query-50a5f3a116ebe5cf88452a9aac89f73135491f170e3d619a93144b36ef429705.json new file mode 100644 index 0000000000..ec24566ebe --- /dev/null +++ b/apps/labrinth/.sqlx/query-50a5f3a116ebe5cf88452a9aac89f73135491f170e3d619a93144b36ef429705.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM delphi_rule_effects WHERE revision <> $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "50a5f3a116ebe5cf88452a9aac89f73135491f170e3d619a93144b36ef429705" +} diff --git a/apps/labrinth/.sqlx/query-52ef6d02f8d533fc4e4ceb141d07a2eb115dc88da24735fffeca3eb1c269ad53.json b/apps/labrinth/.sqlx/query-563d4d8ff958b3a915e6e20b0654c9f189f1c34a94c27a2b894e7eade286b8a3.json similarity index 76% rename from apps/labrinth/.sqlx/query-52ef6d02f8d533fc4e4ceb141d07a2eb115dc88da24735fffeca3eb1c269ad53.json rename to apps/labrinth/.sqlx/query-563d4d8ff958b3a915e6e20b0654c9f189f1c34a94c27a2b894e7eade286b8a3.json index a0657d3e6d..d9091ae767 100644 --- a/apps/labrinth/.sqlx/query-52ef6d02f8d533fc4e4ceb141d07a2eb115dc88da24735fffeca3eb1c269ad53.json +++ b/apps/labrinth/.sqlx/query-563d4d8ff958b3a915e6e20b0654c9f189f1c34a94c27a2b894e7eade286b8a3.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 -- see delphi.rs todo comment\n AND dri.issue_type != '__dummy'\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 AND didws.severity != 'hidden'\n ", "describe": { "columns": [ { @@ -18,5 +18,5 @@ true ] }, - "hash": "52ef6d02f8d533fc4e4ceb141d07a2eb115dc88da24735fffeca3eb1c269ad53" + "hash": "563d4d8ff958b3a915e6e20b0654c9f189f1c34a94c27a2b894e7eade286b8a3" } diff --git a/apps/labrinth/.sqlx/query-1bff1c5714dd039814d7a9d9f25f4ceca0e84a42b3a4c414210739fec6308e2d.json b/apps/labrinth/.sqlx/query-5d5024681546a90c9fb18dae934a0f99a63c3f075cbad8545000a03a5a030323.json similarity index 64% rename from apps/labrinth/.sqlx/query-1bff1c5714dd039814d7a9d9f25f4ceca0e84a42b3a4c414210739fec6308e2d.json rename to apps/labrinth/.sqlx/query-5d5024681546a90c9fb18dae934a0f99a63c3f075cbad8545000a03a5a030323.json index c55b8c6c7c..b1341067bb 100644 --- a/apps/labrinth/.sqlx/query-1bff1c5714dd039814d7a9d9f25f4ceca0e84a42b3a4c414210739fec6308e2d.json +++ b/apps/labrinth/.sqlx/query-5d5024681546a90c9fb18dae934a0f99a63c3f075cbad8545000a03a5a030323.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT id\n FROM mods\n WHERE\n status = $1\n AND (\n $3::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = mods.id\n AND didws.status = 'pending'\n )\n )\n ORDER BY\n CASE WHEN $2 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $2 = 'oldest' THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n ", + "query": "\n SELECT id\n FROM mods\n WHERE\n status = $1\n AND (\n $3::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = mods.id\n AND didws.status = 'pending'\n AND didws.severity != 'hidden'\n )\n )\n ORDER BY\n CASE WHEN $2 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $2 = 'oldest' THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n ", "describe": { "columns": [ { @@ -20,5 +20,5 @@ false ] }, - "hash": "1bff1c5714dd039814d7a9d9f25f4ceca0e84a42b3a4c414210739fec6308e2d" + "hash": "5d5024681546a90c9fb18dae934a0f99a63c3f075cbad8545000a03a5a030323" } diff --git a/apps/labrinth/.sqlx/query-6241bc72a80de01fd8e5506442da48057c24dece008dbf2c00c6ec135d8cef41.json b/apps/labrinth/.sqlx/query-6241bc72a80de01fd8e5506442da48057c24dece008dbf2c00c6ec135d8cef41.json deleted file mode 100644 index 665cd936e4..0000000000 --- a/apps/labrinth/.sqlx/query-6241bc72a80de01fd8e5506442da48057c24dece008dbf2c00c6ec135d8cef41.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n WITH project_ids AS (\n SELECT unnest($1::bigint[]) AS project_id\n ),\n detail_states AS (\n SELECT\n p.project_id,\n COALESCE(\n BOOL_OR(\n didws.status = 'pending'\n AND dri.issue_type != $3\n ),\n FALSE\n ) AS has_pending_detail,\n COALESCE(\n BOOL_OR(\n didws.status = 'unsafe'\n AND dri.issue_type != $3\n ),\n FALSE\n ) AS has_unsafe_detail,\n COALESCE(\n BOOL_OR(\n didws.status = 'pending'\n AND dri.issue_type = $3\n ),\n FALSE\n ) AS has_dummy\n FROM project_ids p\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.project_id = p.project_id\n LEFT JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n GROUP BY p.project_id\n )\n SELECT\n p.project_id AS \"project_id!: DBProjectId\",\n p.has_pending_detail AS \"has_pending_detail!\",\n p.has_unsafe_detail AS \"has_unsafe_detail!\",\n p.has_dummy AS \"has_dummy!\",\n (\n SELECT t.id\n FROM threads t\n WHERE t.mod_id = p.project_id\n ORDER BY t.id\n LIMIT 1\n ) AS \"thread_id: DBThreadId\",\n (\n SELECT tm.body->>'type'\n FROM threads t\n INNER JOIN threads_messages tm ON tm.thread_id = t.id\n WHERE\n t.mod_id = p.project_id\n AND tm.body->>'type' = ANY($2::text[])\n ORDER BY tm.created DESC, tm.id DESC\n LIMIT 1\n ) AS \"last_tech_review_message_type\",\n (\n SELECT dr.id\n FROM versions v\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n WHERE v.mod_id = p.project_id\n ORDER BY dr.created DESC, dr.id DESC\n LIMIT 1\n ) AS \"report_id: DelphiReportId\"\n FROM detail_states p\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "project_id!: DBProjectId", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "has_pending_detail!", - "type_info": "Bool" - }, - { - "ordinal": 2, - "name": "has_unsafe_detail!", - "type_info": "Bool" - }, - { - "ordinal": 3, - "name": "has_dummy!", - "type_info": "Bool" - }, - { - "ordinal": 4, - "name": "thread_id: DBThreadId", - "type_info": "Int8" - }, - { - "ordinal": 5, - "name": "last_tech_review_message_type", - "type_info": "Text" - }, - { - "ordinal": 6, - "name": "report_id: DelphiReportId", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8Array", - "TextArray", - "Text" - ] - }, - "nullable": [ - null, - null, - null, - null, - null, - null, - null - ] - }, - "hash": "6241bc72a80de01fd8e5506442da48057c24dece008dbf2c00c6ec135d8cef41" -} diff --git a/apps/labrinth/.sqlx/query-628d178e568998627de7e7cdba3b5c83300b5649f2ccc215f20fd82e258e76f4.json b/apps/labrinth/.sqlx/query-628d178e568998627de7e7cdba3b5c83300b5649f2ccc215f20fd82e258e76f4.json new file mode 100644 index 0000000000..05ff0c342a --- /dev/null +++ b/apps/labrinth/.sqlx/query-628d178e568998627de7e7cdba3b5c83300b5649f2ccc215f20fd82e258e76f4.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n m.id AS \"project_id: DBProjectId\",\n MIN(t.id) AS \"thread_id!: DBThreadId\"\n FROM mods m\n INNER JOIN delphi_tech_review_queue trq ON trq.project_id = m.id\n INNER JOIN threads t ON t.mod_id = m.id\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_issue_details_with_statuses didws\n ON didws.issue_id = dri.id\n AND didws.severity != 'hidden'\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 WHERE\n (\n cardinality($4::text[]) = 0\n OR (\n 'minecraft_java_server' = ANY($4::text[])\n AND (\n m.components ? 'minecraft_server'\n OR m.components ? 'minecraft_java_server'\n )\n )\n OR EXISTS (\n SELECT 1\n FROM versions type_v\n INNER JOIN loaders_versions type_lv\n ON type_lv.version_id = type_v.id\n INNER JOIN loaders_project_types type_lpt\n ON type_lpt.joining_loader_id = type_lv.loader_id\n INNER JOIN project_types type_pt\n ON type_pt.id = type_lpt.joining_project_type_id\n WHERE\n type_v.mod_id = m.id\n AND type_pt.name = ANY($4::text[])\n AND (\n type_pt.name != 'modpack'\n OR NOT (\n m.components ? 'minecraft_server'\n OR m.components ? 'minecraft_java_server'\n )\n )\n )\n )\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n AND (cardinality($6::text[]) = 0 OR m.status = ANY($6::text[]))\n AND (\n cardinality($7::text[]) = 0\n OR EXISTS (\n SELECT 1\n FROM versions issue_version\n INNER JOIN files issue_file\n ON issue_file.version_id = issue_version.id\n INNER JOIN delphi_reports issue_report\n ON issue_report.file_id = issue_file.id\n INNER JOIN delphi_report_issues issue\n ON issue.report_id = issue_report.id\n INNER JOIN delphi_issue_details_with_statuses detail\n ON detail.issue_id = issue.id\n WHERE\n issue_version.mod_id = m.id\n AND issue.issue_type = ANY($7::text[])\n AND detail.severity != 'hidden'\n )\n )\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 GROUP BY m.id\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN COALESCE(MAX(didws.severity), 'low'::delphi_severity) ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN COALESCE(MAX(didws.severity), 'low'::delphi_severity) ELSE 'low'::delphi_severity END DESC,\n -- tie-breaker: oldest reports\n MIN(dr.created) ASC\n LIMIT $1 OFFSET $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "project_id: DBProjectId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "thread_id!: DBThreadId", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Text", + "TextArray", + "Text", + "TextArray", + "TextArray" + ] + }, + "nullable": [ + false, + null + ] + }, + "hash": "628d178e568998627de7e7cdba3b5c83300b5649f2ccc215f20fd82e258e76f4" +} diff --git a/apps/labrinth/.sqlx/query-62d548639ed397f3619a8c3a4503f472fceb6ee0e120b511c1ed64cb200e0e9c.json b/apps/labrinth/.sqlx/query-62d548639ed397f3619a8c3a4503f472fceb6ee0e120b511c1ed64cb200e0e9c.json new file mode 100644 index 0000000000..e2a4cfb50f --- /dev/null +++ b/apps/labrinth/.sqlx/query-62d548639ed397f3619a8c3a4503f472fceb6ee0e120b511c1ed64cb200e0e9c.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "LOCK TABLE delphi_report_issue_details IN SHARE MODE", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "62d548639ed397f3619a8c3a4503f472fceb6ee0e120b511c1ed64cb200e0e9c" +} diff --git a/apps/labrinth/.sqlx/query-6776dc50f184188756ad7fe263b0304333536768527525a43bdd45aedffa3c4f.json b/apps/labrinth/.sqlx/query-6776dc50f184188756ad7fe263b0304333536768527525a43bdd45aedffa3c4f.json new file mode 100644 index 0000000000..4187b628b7 --- /dev/null +++ b/apps/labrinth/.sqlx/query-6776dc50f184188756ad7fe263b0304333536768527525a43bdd45aedffa3c4f.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT pg_try_advisory_xact_lock($1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "pg_try_advisory_xact_lock", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + null + ] + }, + "hash": "6776dc50f184188756ad7fe263b0304333536768527525a43bdd45aedffa3c4f" +} diff --git a/apps/labrinth/.sqlx/query-ba7585f55df2a4596caae265ac956eb23268f26aef664234b898b17d13c09f91.json b/apps/labrinth/.sqlx/query-6fa83172261e64445f2fa18d2c98eb68f2617f6f57a31edda36c2f861e7da7f7.json similarity index 70% rename from apps/labrinth/.sqlx/query-ba7585f55df2a4596caae265ac956eb23268f26aef664234b898b17d13c09f91.json rename to apps/labrinth/.sqlx/query-6fa83172261e64445f2fa18d2c98eb68f2617f6f57a31edda36c2f861e7da7f7.json index 20a2667bb3..34f1afab32 100644 --- a/apps/labrinth/.sqlx/query-ba7585f55df2a4596caae265ac956eb23268f26aef664234b898b17d13c09f91.json +++ b/apps/labrinth/.sqlx/query-6fa83172261e64445f2fa18d2c98eb68f2617f6f57a31edda36c2f861e7da7f7.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT DISTINCT didws.project_id AS \"project_id!: DBProjectId\"\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.key = ANY($1::text[])\n ", + "query": "\n SELECT DISTINCT didws.project_id AS \"project_id!: DBProjectId\"\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.id = ANY($1::bigint[])\n ", "describe": { "columns": [ { @@ -11,12 +11,12 @@ ], "parameters": { "Left": [ - "TextArray" + "Int8Array" ] }, "nullable": [ true ] }, - "hash": "ba7585f55df2a4596caae265ac956eb23268f26aef664234b898b17d13c09f91" + "hash": "6fa83172261e64445f2fa18d2c98eb68f2617f6f57a31edda36c2f861e7da7f7" } diff --git a/apps/labrinth/.sqlx/query-820810b83ce47f32bf5b0dc20e5fc3223f66e83a2b5f6efbf10eec3a927f31b2.json b/apps/labrinth/.sqlx/query-820810b83ce47f32bf5b0dc20e5fc3223f66e83a2b5f6efbf10eec3a927f31b2.json new file mode 100644 index 0000000000..24b8b0d507 --- /dev/null +++ b/apps/labrinth/.sqlx/query-820810b83ce47f32bf5b0dc20e5fc3223f66e83a2b5f6efbf10eec3a927f31b2.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "LOCK TABLE delphi_rules IN SHARE MODE", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "820810b83ce47f32bf5b0dc20e5fc3223f66e83a2b5f6efbf10eec3a927f31b2" +} diff --git a/apps/labrinth/.sqlx/query-852eff37ff36e1e87fcbc6c50f27bab003a40537c191eb6cb64a01d92b57e25f.json b/apps/labrinth/.sqlx/query-852eff37ff36e1e87fcbc6c50f27bab003a40537c191eb6cb64a01d92b57e25f.json new file mode 100644 index 0000000000..374b61603c --- /dev/null +++ b/apps/labrinth/.sqlx/query-852eff37ff36e1e87fcbc6c50f27bab003a40537c191eb6cb64a01d92b57e25f.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT DISTINCT ON (dr.id)\n to_jsonb(dr)\n || jsonb_build_object(\n 'report_id', dr.id,\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 'severity', COALESCE((\n SELECT MAX(didws.severity)\n FROM delphi_report_issues severity_issue\n INNER JOIN delphi_issue_details_with_statuses didws\n ON didws.issue_id = severity_issue.id\n WHERE\n severity_issue.report_id = dr.id\n AND didws.severity != 'hidden'\n ), 'low'::delphi_severity),\n -- TODO: replace with `json_array` in Postgres 16\n\t\t\t\t'issues', (\n\t\t\t\t\tSELECT coalesce(json_agg(\n\t\t\t\t\t\tto_jsonb(dri)\n\t\t\t\t\t\t|| jsonb_build_object(\n\t\t\t\t\t\t\t-- TODO: replace with `json_array` in Postgres 16\n\t\t\t\t\t\t\t'details', (\n\t\t\t\t\t\t\t\tSELECT 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 'decompiled_source', didws.decompiled_source,\n 'data', didws.data,\n 'severity', didws.severity,\n 'local_status', didws.local_status,\n 'global_status', didws.global_status,\n 'status', didws.status\n )\n ), '[]'::jsonb)\n FROM delphi_issue_details_with_statuses didws\n WHERE\n didws.issue_id = dri.id\n AND didws.severity != 'hidden'\n )\n\t\t\t\t\t\t)\n\t\t\t\t\t), '[]'::json)\n\t\t\t\t\tFROM delphi_report_issues dri\n\t\t\t\t\tWHERE\n dri.report_id = dr.id\n AND EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses visible_detail\n WHERE\n visible_detail.issue_id = dri.id\n AND visible_detail.severity != 'hidden'\n )\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": "852eff37ff36e1e87fcbc6c50f27bab003a40537c191eb6cb64a01d92b57e25f" +} diff --git a/apps/labrinth/.sqlx/query-8c7f17ad005b42b4c6cac5ddb6a1f2d13592cc6995d1b87e79397ab592ad7675.json b/apps/labrinth/.sqlx/query-8c7f17ad005b42b4c6cac5ddb6a1f2d13592cc6995d1b87e79397ab592ad7675.json new file mode 100644 index 0000000000..4ed9dbbf05 --- /dev/null +++ b/apps/labrinth/.sqlx/query-8c7f17ad005b42b4c6cac5ddb6a1f2d13592cc6995d1b87e79397ab592ad7675.json @@ -0,0 +1,38 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO delphi_rule_effects (\n revision,\n detail_id,\n rule_id,\n severity\n )\n SELECT $1, effect.*\n FROM UNNEST(\n $2::BIGINT[],\n $3::BIGINT[],\n $4::delphi_severity[]\n ) AS effect(detail_id, rule_id, severity)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8Array", + "Int8Array", + { + "Custom": { + "name": "delphi_severity[]", + "kind": { + "Array": { + "Custom": { + "name": "delphi_severity", + "kind": { + "Enum": [ + "hidden", + "low", + "medium", + "high", + "severe", + "malware" + ] + } + } + } + } + } + } + ] + }, + "nullable": [] + }, + "hash": "8c7f17ad005b42b4c6cac5ddb6a1f2d13592cc6995d1b87e79397ab592ad7675" +} diff --git a/apps/labrinth/.sqlx/query-8d37c8c2633b7d601c7ba74d63608ff40af3b6b56f43540ec3798ef30e5c0b9d.json b/apps/labrinth/.sqlx/query-8d37c8c2633b7d601c7ba74d63608ff40af3b6b56f43540ec3798ef30e5c0b9d.json index 475dba1b98..cf1d9b23c6 100644 --- a/apps/labrinth/.sqlx/query-8d37c8c2633b7d601c7ba74d63608ff40af3b6b56f43540ec3798ef30e5c0b9d.json +++ b/apps/labrinth/.sqlx/query-8d37c8c2633b7d601c7ba74d63608ff40af3b6b56f43540ec3798ef30e5c0b9d.json @@ -26,10 +26,12 @@ "name": "delphi_severity", "kind": { "Enum": [ + "hidden", "low", "medium", "high", - "severe" + "severe", + "malware" ] } } diff --git a/apps/labrinth/.sqlx/query-9369f0659c5fbd08463923a9b2bba49f4963315fd7667c6db96e6153e54a2fd2.json b/apps/labrinth/.sqlx/query-9369f0659c5fbd08463923a9b2bba49f4963315fd7667c6db96e6153e54a2fd2.json index 8ca4b69491..f3609a305c 100644 --- a/apps/labrinth/.sqlx/query-9369f0659c5fbd08463923a9b2bba49f4963315fd7667c6db96e6153e54a2fd2.json +++ b/apps/labrinth/.sqlx/query-9369f0659c5fbd08463923a9b2bba49f4963315fd7667c6db96e6153e54a2fd2.json @@ -22,10 +22,12 @@ "name": "delphi_severity", "kind": { "Enum": [ + "hidden", "low", "medium", "high", - "severe" + "severe", + "malware" ] } } diff --git a/apps/labrinth/.sqlx/query-99985f5d2dbb61dae4794b8546063661efa32b4bef83cff364940b2fcb5908ee.json b/apps/labrinth/.sqlx/query-99985f5d2dbb61dae4794b8546063661efa32b4bef83cff364940b2fcb5908ee.json new file mode 100644 index 0000000000..dcfbb9cbe9 --- /dev/null +++ b/apps/labrinth/.sqlx/query-99985f5d2dbb61dae4794b8546063661efa32b4bef83cff364940b2fcb5908ee.json @@ -0,0 +1,120 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n detail.key,\n issue.issue_type,\n detail.severity AS \"severity: DelphiSeverity\",\n detail.jar,\n detail.file_path,\n detail.data AS \"data: Json>\",\n COALESCE(file_traces.traces, '[]'::jsonb)\n AS \"file_traces!: Json>\",\n report.delphi_version,\n file.size AS \"size?\",\n file.id AS \"file_id?\",\n version.id AS \"version_id?\",\n version.mod_id AS \"project_id?\",\n COALESCE(version_metadata.project_types, ARRAY[]::text[])\n AS \"project_types!: Vec\",\n COALESCE(version_metadata.loaders, ARRAY[]::text[])\n AS \"loaders!: Vec\",\n COALESCE(file_hashes.hashes, '{}'::jsonb)\n AS \"hashes!: Json>\"\n FROM delphi_report_issue_details detail\n INNER JOIN delphi_report_issues issue ON issue.id = detail.issue_id\n INNER JOIN delphi_reports report ON report.id = issue.report_id\n LEFT JOIN LATERAL (\n SELECT\n jsonb_agg(\n jsonb_build_object(\n 'key', file_detail.key,\n 'issue_type', file_issue.issue_type,\n 'severity', file_detail.severity,\n 'jar', file_detail.jar,\n 'file_path', file_detail.file_path,\n 'data', file_detail.data\n )\n ORDER BY file_detail.id\n ) AS traces\n FROM delphi_report_issues file_issue\n INNER JOIN delphi_report_issue_details file_detail\n ON file_detail.issue_id = file_issue.id\n WHERE file_issue.report_id = issue.report_id\n AND file_detail.file_path = detail.file_path\n ) file_traces ON TRUE\n LEFT JOIN files file ON file.id = report.file_id\n LEFT JOIN versions version ON version.id = file.version_id\n LEFT JOIN LATERAL (\n SELECT\n ARRAY_AGG(\n DISTINCT project_type.name::text\n ORDER BY project_type.name::text\n ) FILTER (WHERE project_type.name IS NOT NULL)\n AS project_types,\n ARRAY_AGG(\n DISTINCT loader.loader\n ORDER BY loader.loader\n ) AS loaders\n FROM loaders_versions loader_version\n INNER JOIN loaders loader\n ON loader.id = loader_version.loader_id\n LEFT JOIN loaders_project_types loader_project_type\n ON loader_project_type.joining_loader_id = loader_version.loader_id\n LEFT JOIN project_types project_type\n ON project_type.id = loader_project_type.joining_project_type_id\n WHERE loader_version.version_id = version.id\n ) version_metadata ON TRUE\n LEFT JOIN LATERAL (\n SELECT\n jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes\n FROM hashes\n WHERE hashes.file_id = file.id\n ) file_hashes ON TRUE\n WHERE detail.id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "key", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "issue_type", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "severity: DelphiSeverity", + "type_info": { + "Custom": { + "name": "delphi_severity", + "kind": { + "Enum": [ + "hidden", + "low", + "medium", + "high", + "severe", + "malware" + ] + } + } + } + }, + { + "ordinal": 3, + "name": "jar", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "file_path", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "data: Json>", + "type_info": "Jsonb" + }, + { + "ordinal": 6, + "name": "file_traces!: Json>", + "type_info": "Jsonb" + }, + { + "ordinal": 7, + "name": "delphi_version", + "type_info": "Int4" + }, + { + "ordinal": 8, + "name": "size?", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "file_id?", + "type_info": "Int8" + }, + { + "ordinal": 10, + "name": "version_id?", + "type_info": "Int8" + }, + { + "ordinal": 11, + "name": "project_id?", + "type_info": "Int8" + }, + { + "ordinal": 12, + "name": "project_types!: Vec", + "type_info": "TextArray" + }, + { + "ordinal": 13, + "name": "loaders!: Vec", + "type_info": "VarcharArray" + }, + { + "ordinal": 14, + "name": "hashes!: Json>", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + null, + false, + false, + false, + false, + false, + null, + null, + null + ] + }, + "hash": "99985f5d2dbb61dae4794b8546063661efa32b4bef83cff364940b2fcb5908ee" +} diff --git a/apps/labrinth/.sqlx/query-9ab1f07c2968b5d445752c1480345c1fa3af3a899b232482aab9cc44b9336063.json b/apps/labrinth/.sqlx/query-9ab1f07c2968b5d445752c1480345c1fa3af3a899b232482aab9cc44b9336063.json deleted file mode 100644 index 83806baae2..0000000000 --- a/apps/labrinth/.sqlx/query-9ab1f07c2968b5d445752c1480345c1fa3af3a899b232482aab9cc44b9336063.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "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-9b0edb6399bc5f38ee96677cb2539476ecf08549b914f96f0c23fbfbe3f17f2e.json b/apps/labrinth/.sqlx/query-9b0edb6399bc5f38ee96677cb2539476ecf08549b914f96f0c23fbfbe3f17f2e.json deleted file mode 100644 index c8aad7deda..0000000000 --- a/apps/labrinth/.sqlx/query-9b0edb6399bc5f38ee96677cb2539476ecf08549b914f96f0c23fbfbe3f17f2e.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT DISTINCT m.id\n FROM mods m\n WHERE\n EXISTS(\n SELECT 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.project_id = m.id\n AND didws.status = 'pending'\n -- see delphi.rs todo comment\n AND dri.issue_type != '__dummy'\n )\n AND NOT EXISTS(\n SELECT 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.project_id = m.id\n AND didws.status IN ('safe', 'unsafe')\n -- see delphi.rs todo comment\n AND dri.issue_type != '__dummy'\n )\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false - ] - }, - "hash": "9b0edb6399bc5f38ee96677cb2539476ecf08549b914f96f0c23fbfbe3f17f2e" -} diff --git a/apps/labrinth/.sqlx/query-a1f38a33cc3159108dd7deda48a60ebdae8d60a6d555a625ee2ec5780ab78fc3.json b/apps/labrinth/.sqlx/query-a1f38a33cc3159108dd7deda48a60ebdae8d60a6d555a625ee2ec5780ab78fc3.json new file mode 100644 index 0000000000..d43d795d65 --- /dev/null +++ b/apps/labrinth/.sqlx/query-a1f38a33cc3159108dd7deda48a60ebdae8d60a6d555a625ee2ec5780ab78fc3.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT DISTINCT detail.project_id AS \"project_id!: DBProjectId\"\n FROM delphi_issue_details_with_statuses detail\n WHERE detail.key = ANY($1::text[])\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "project_id!: DBProjectId", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "TextArray" + ] + }, + "nullable": [ + true + ] + }, + "hash": "a1f38a33cc3159108dd7deda48a60ebdae8d60a6d555a625ee2ec5780ab78fc3" +} diff --git a/apps/labrinth/.sqlx/query-a26673f34772ab181adda35117c7b68bd36d1267caa8a59facbafe8ed3760f84.json b/apps/labrinth/.sqlx/query-a26673f34772ab181adda35117c7b68bd36d1267caa8a59facbafe8ed3760f84.json new file mode 100644 index 0000000000..13f2bc0af1 --- /dev/null +++ b/apps/labrinth/.sqlx/query-a26673f34772ab181adda35117c7b68bd36d1267caa8a59facbafe8ed3760f84.json @@ -0,0 +1,124 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n detail.id,\n detail.key,\n issue.issue_type,\n detail.severity AS \"severity: DelphiSeverity\",\n detail.jar,\n detail.file_path,\n detail.data AS \"data: Json>\",\n COALESCE(file_traces.traces, '[]'::jsonb)\n AS \"file_traces!: Json>\",\n report.delphi_version,\n file.size AS \"size?\",\n file.id AS \"file_id?\",\n version.id AS \"version_id?\",\n version.mod_id AS \"project_id?\",\n COALESCE(version_metadata.project_types, ARRAY[]::text[])\n AS \"project_types!: Vec\",\n COALESCE(version_metadata.loaders, ARRAY[]::text[])\n AS \"loaders!: Vec\",\n COALESCE(file_hashes.hashes, '{}'::jsonb)\n AS \"hashes!: Json>\"\n FROM delphi_report_issue_details detail\n INNER JOIN delphi_report_issues issue ON issue.id = detail.issue_id\n INNER JOIN delphi_reports report ON report.id = issue.report_id\n LEFT JOIN LATERAL (\n SELECT\n jsonb_agg(\n jsonb_build_object(\n 'key', file_detail.key,\n 'issue_type', file_issue.issue_type,\n 'severity', file_detail.severity,\n 'jar', file_detail.jar,\n 'file_path', file_detail.file_path,\n 'data', file_detail.data\n )\n ORDER BY file_detail.id\n ) AS traces\n FROM delphi_report_issues file_issue\n INNER JOIN delphi_report_issue_details file_detail\n ON file_detail.issue_id = file_issue.id\n WHERE file_issue.report_id = issue.report_id\n AND file_detail.file_path = detail.file_path\n ) file_traces ON TRUE\n LEFT JOIN files file ON file.id = report.file_id\n LEFT JOIN versions version ON version.id = file.version_id\n LEFT JOIN (\n SELECT\n loader_version.version_id,\n ARRAY_AGG(\n DISTINCT project_type.name::text\n ORDER BY project_type.name::text\n ) FILTER (WHERE project_type.name IS NOT NULL)\n AS project_types,\n ARRAY_AGG(\n DISTINCT loader.loader\n ORDER BY loader.loader\n ) AS loaders\n FROM loaders_versions loader_version\n INNER JOIN loaders loader\n ON loader.id = loader_version.loader_id\n LEFT JOIN loaders_project_types loader_project_type\n ON loader_project_type.joining_loader_id = loader_version.loader_id\n LEFT JOIN project_types project_type\n ON project_type.id = loader_project_type.joining_project_type_id\n GROUP BY loader_version.version_id\n ) version_metadata\n ON version_metadata.version_id = version.id\n LEFT JOIN (\n SELECT\n file_id,\n jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes\n FROM hashes\n GROUP BY file_id\n ) file_hashes ON file_hashes.file_id = file.id\n ORDER BY detail.id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "key", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "issue_type", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "severity: DelphiSeverity", + "type_info": { + "Custom": { + "name": "delphi_severity", + "kind": { + "Enum": [ + "hidden", + "low", + "medium", + "high", + "severe", + "malware" + ] + } + } + } + }, + { + "ordinal": 4, + "name": "jar", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "file_path", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "data: Json>", + "type_info": "Jsonb" + }, + { + "ordinal": 7, + "name": "file_traces!: Json>", + "type_info": "Jsonb" + }, + { + "ordinal": 8, + "name": "delphi_version", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "size?", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "file_id?", + "type_info": "Int8" + }, + { + "ordinal": 11, + "name": "version_id?", + "type_info": "Int8" + }, + { + "ordinal": 12, + "name": "project_id?", + "type_info": "Int8" + }, + { + "ordinal": 13, + "name": "project_types!: Vec", + "type_info": "TextArray" + }, + { + "ordinal": 14, + "name": "loaders!: Vec", + "type_info": "VarcharArray" + }, + { + "ordinal": 15, + "name": "hashes!: Json>", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + null, + false, + false, + false, + false, + false, + null, + null, + null + ] + }, + "hash": "a26673f34772ab181adda35117c7b68bd36d1267caa8a59facbafe8ed3760f84" +} diff --git a/apps/labrinth/.sqlx/query-17dc154d8cf4d857d2611c21352b206d70238d0132dad1f8974ae1ac346d2bd8.json b/apps/labrinth/.sqlx/query-a4b177d0d334c20fd2058ffcd4c245a2cd0f0504a156ac61f2405d2eb38b5672.json similarity index 96% rename from apps/labrinth/.sqlx/query-17dc154d8cf4d857d2611c21352b206d70238d0132dad1f8974ae1ac346d2bd8.json rename to apps/labrinth/.sqlx/query-a4b177d0d334c20fd2058ffcd4c245a2cd0f0504a156ac61f2405d2eb38b5672.json index b3a813243e..297250ca83 100644 --- a/apps/labrinth/.sqlx/query-17dc154d8cf4d857d2611c21352b206d70238d0132dad1f8974ae1ac346d2bd8.json +++ b/apps/labrinth/.sqlx/query-a4b177d0d334c20fd2058ffcd4c245a2cd0f0504a156ac61f2405d2eb38b5672.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n WITH moderation_projects AS (\n SELECT\n m.id,\n m.slug,\n m.name,\n m.summary,\n m.description,\n m.queued,\n m.published,\n m.organization_id,\n m.team_id,\n m.components\n FROM mods m\n WHERE\n m.status = $1\n AND (\n $7::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = m.id\n AND didws.status = 'pending'\n )\n )\n ),\n external_dependencies AS (\n SELECT\n v.mod_id,\n COUNT(*) AS external_dependencies_count\n FROM versions v\n INNER JOIN moderation_projects mp ON mp.id = v.mod_id\n INNER JOIN dependencies d ON d.dependent_id = v.id\n WHERE d.dependency_file_name IS NOT NULL\n GROUP BY v.mod_id\n ),\n version_project_types AS (\n SELECT\n v.mod_id,\n ARRAY_AGG(DISTINCT pt.name::text) FILTER (WHERE pt.name IS NOT NULL) AS project_types\n FROM versions v\n INNER JOIN moderation_projects mp ON mp.id = v.mod_id\n INNER JOIN loaders_versions lv ON v.id = lv.version_id\n INNER JOIN loaders l ON lv.loader_id = l.id\n INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id\n INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n WHERE v.status = ANY($2)\n GROUP BY v.mod_id\n ),\n queue_projects AS (\n SELECT\n mp.id,\n mp.slug,\n mp.name,\n mp.summary,\n mp.description,\n mp.queued,\n mp.published,\n search_organization.name AS organization_name,\n search_owner.username AS owner_name,\n CASE\n WHEN jsonb_typeof(mp.components -> 'minecraft_server') = 'object'\n THEN ARRAY_APPEND(\n ARRAY_REMOVE(\n COALESCE(vpt.project_types::text[], ARRAY[]::text[]),\n 'modpack'\n ),\n 'minecraft_java_server'\n )\n ELSE COALESCE(vpt.project_types::text[], ARRAY[]::text[])\n END AS project_types,\n COALESCE(ed.external_dependencies_count, 0) AS external_dependencies_count\n FROM moderation_projects mp\n LEFT JOIN organizations search_organization\n ON search_organization.id = mp.organization_id\n AND $3::text IS NOT NULL\n LEFT JOIN LATERAL (\n SELECT\n u.username\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = mp.team_id\n AND tm.is_owner\n ORDER BY tm.ordering ASC\n LIMIT 1\n ) search_owner ON $3::text IS NOT NULL\n AND mp.organization_id IS NULL\n LEFT JOIN external_dependencies ed ON ed.mod_id = mp.id\n LEFT JOIN version_project_types vpt ON vpt.mod_id = mp.id\n )\n SELECT id\n FROM queue_projects\n WHERE\n (\n $3::text IS NULL\n OR name ILIKE '%' || $3 || '%'\n OR slug ILIKE '%' || $3 || '%'\n OR summary ILIKE '%' || $3 || '%'\n OR description ILIKE '%' || $3 || '%'\n OR owner_name ILIKE '%' || $3 || '%'\n OR organization_name ILIKE '%' || $3 || '%'\n OR EXISTS (\n SELECT 1\n FROM UNNEST(project_types) AS searched_project_type(project_type)\n WHERE searched_project_type.project_type ILIKE '%' || $3 || '%'\n )\n )\n AND (\n $4::text IS NULL\n OR ($4 = 'none' AND CARDINALITY(project_types) = 0)\n OR ($4 = 'minecraft_java_server' AND project_types @> ARRAY['minecraft_java_server']::text[])\n OR ($4 <> 'none' AND $4 <> 'minecraft_java_server' AND project_types[1] = $4)\n )\n AND ($5::boolean IS NULL OR (external_dependencies_count > 0) = $5)\n ORDER BY\n CASE WHEN $6 = 'most_external_deps' THEN external_dependencies_count END DESC,\n CASE WHEN $6 = 'least_external_deps' THEN external_dependencies_count END ASC,\n CASE WHEN $6 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $6 IN ('oldest', 'most_external_deps', 'least_external_deps') THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n ", + "query": "\n WITH moderation_projects AS (\n SELECT\n m.id,\n m.slug,\n m.name,\n m.summary,\n m.description,\n m.queued,\n m.published,\n m.organization_id,\n m.team_id,\n m.components\n FROM mods m\n WHERE\n m.status = $1\n AND (\n $7::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = m.id\n AND didws.status = 'pending'\n AND didws.severity != 'hidden'\n )\n )\n ),\n external_dependencies AS (\n SELECT\n v.mod_id,\n COUNT(*) AS external_dependencies_count\n FROM versions v\n INNER JOIN moderation_projects mp ON mp.id = v.mod_id\n INNER JOIN dependencies d ON d.dependent_id = v.id\n WHERE d.dependency_file_name IS NOT NULL\n GROUP BY v.mod_id\n ),\n version_project_types AS (\n SELECT\n v.mod_id,\n ARRAY_AGG(DISTINCT pt.name::text) FILTER (WHERE pt.name IS NOT NULL) AS project_types\n FROM versions v\n INNER JOIN moderation_projects mp ON mp.id = v.mod_id\n INNER JOIN loaders_versions lv ON v.id = lv.version_id\n INNER JOIN loaders l ON lv.loader_id = l.id\n INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id\n INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n WHERE v.status = ANY($2)\n GROUP BY v.mod_id\n ),\n queue_projects AS (\n SELECT\n mp.id,\n mp.slug,\n mp.name,\n mp.summary,\n mp.description,\n mp.queued,\n mp.published,\n search_organization.name AS organization_name,\n search_owner.username AS owner_name,\n CASE\n WHEN jsonb_typeof(mp.components -> 'minecraft_server') = 'object'\n THEN ARRAY_APPEND(\n ARRAY_REMOVE(\n COALESCE(vpt.project_types::text[], ARRAY[]::text[]),\n 'modpack'\n ),\n 'minecraft_java_server'\n )\n ELSE COALESCE(vpt.project_types::text[], ARRAY[]::text[])\n END AS project_types,\n COALESCE(ed.external_dependencies_count, 0) AS external_dependencies_count\n FROM moderation_projects mp\n LEFT JOIN organizations search_organization\n ON search_organization.id = mp.organization_id\n AND $3::text IS NOT NULL\n LEFT JOIN LATERAL (\n SELECT\n u.username\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = mp.team_id\n AND tm.is_owner\n ORDER BY tm.ordering ASC\n LIMIT 1\n ) search_owner ON $3::text IS NOT NULL\n AND mp.organization_id IS NULL\n LEFT JOIN external_dependencies ed ON ed.mod_id = mp.id\n LEFT JOIN version_project_types vpt ON vpt.mod_id = mp.id\n )\n SELECT id\n FROM queue_projects\n WHERE\n (\n $3::text IS NULL\n OR name ILIKE '%' || $3 || '%'\n OR slug ILIKE '%' || $3 || '%'\n OR summary ILIKE '%' || $3 || '%'\n OR description ILIKE '%' || $3 || '%'\n OR owner_name ILIKE '%' || $3 || '%'\n OR organization_name ILIKE '%' || $3 || '%'\n OR EXISTS (\n SELECT 1\n FROM UNNEST(project_types) AS searched_project_type(project_type)\n WHERE searched_project_type.project_type ILIKE '%' || $3 || '%'\n )\n )\n AND (\n $4::text IS NULL\n OR ($4 = 'none' AND CARDINALITY(project_types) = 0)\n OR ($4 = 'minecraft_java_server' AND project_types @> ARRAY['minecraft_java_server']::text[])\n OR ($4 <> 'none' AND $4 <> 'minecraft_java_server' AND project_types[1] = $4)\n )\n AND ($5::boolean IS NULL OR (external_dependencies_count > 0) = $5)\n ORDER BY\n CASE WHEN $6 = 'most_external_deps' THEN external_dependencies_count END DESC,\n CASE WHEN $6 = 'least_external_deps' THEN external_dependencies_count END ASC,\n CASE WHEN $6 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $6 IN ('oldest', 'most_external_deps', 'least_external_deps') THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n ", "describe": { "columns": [ { @@ -24,5 +24,5 @@ false ] }, - "hash": "17dc154d8cf4d857d2611c21352b206d70238d0132dad1f8974ae1ac346d2bd8" + "hash": "a4b177d0d334c20fd2058ffcd4c245a2cd0f0504a156ac61f2405d2eb38b5672" } diff --git a/apps/labrinth/.sqlx/query-a58e701218270abb1c5b177d7ab12a8d3fce511a3d03e883de8ce925f3332957.json b/apps/labrinth/.sqlx/query-a58e701218270abb1c5b177d7ab12a8d3fce511a3d03e883de8ce925f3332957.json new file mode 100644 index 0000000000..d747bd944a --- /dev/null +++ b/apps/labrinth/.sqlx/query-a58e701218270abb1c5b177d7ab12a8d3fce511a3d03e883de8ce925f3332957.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT DISTINCT m.id\n FROM mods m\n INNER JOIN delphi_tech_review_queue queue ON queue.project_id = m.id\n WHERE\n EXISTS(\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE\n didws.project_id = m.id\n AND didws.status = 'pending'\n AND didws.severity != 'hidden'\n )\n AND NOT EXISTS(\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE\n didws.project_id = m.id\n AND didws.status IN ('safe', 'unsafe')\n AND didws.severity != 'hidden'\n )\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "a58e701218270abb1c5b177d7ab12a8d3fce511a3d03e883de8ce925f3332957" +} diff --git a/apps/labrinth/.sqlx/query-ab87c1771628979a051e4b30cd6afa32576d95e5861cc857276e099b18a5e1f4.json b/apps/labrinth/.sqlx/query-ab87c1771628979a051e4b30cd6afa32576d95e5861cc857276e099b18a5e1f4.json new file mode 100644 index 0000000000..b8322723a3 --- /dev/null +++ b/apps/labrinth/.sqlx/query-ab87c1771628979a051e4b30cd6afa32576d95e5861cc857276e099b18a5e1f4.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE delphi_rule_revisions SET revision = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "ab87c1771628979a051e4b30cd6afa32576d95e5861cc857276e099b18a5e1f4" +} diff --git a/apps/labrinth/.sqlx/query-abef4603febf18dae62ca424b0433f2bacc2555433277ee195ed6fbedb7be537.json b/apps/labrinth/.sqlx/query-abef4603febf18dae62ca424b0433f2bacc2555433277ee195ed6fbedb7be537.json new file mode 100644 index 0000000000..155fd19fbd --- /dev/null +++ b/apps/labrinth/.sqlx/query-abef4603febf18dae62ca424b0433f2bacc2555433277ee195ed6fbedb7be537.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT revision FROM delphi_rule_revisions LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "revision", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "abef4603febf18dae62ca424b0433f2bacc2555433277ee195ed6fbedb7be537" +} diff --git a/apps/labrinth/.sqlx/query-b5233d5b729fae87fdf24e7d5f9ee6c96fe5a33015ba94bc2c606c31f87af9c9.json b/apps/labrinth/.sqlx/query-b5233d5b729fae87fdf24e7d5f9ee6c96fe5a33015ba94bc2c606c31f87af9c9.json new file mode 100644 index 0000000000..c26c415abd --- /dev/null +++ b/apps/labrinth/.sqlx/query-b5233d5b729fae87fdf24e7d5f9ee6c96fe5a33015ba94bc2c606c31f87af9c9.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n\t\tUPDATE delphi_rules\n\t\tSET\n\t\t\tdelete_on_next_revision = TRUE,\n\t\t\trevision = (\n\t\t\t\tSELECT revision + 1 FROM delphi_rule_revisions LIMIT 1\n\t\t\t),\n\t\t\tupdated_at = CURRENT_TIMESTAMP,\n\t\t\tupdated_by = $2\n\t\tWHERE id = $1 AND NOT delete_on_next_revision\n\t\tRETURNING id\n\t\t", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "b5233d5b729fae87fdf24e7d5f9ee6c96fe5a33015ba94bc2c606c31f87af9c9" +} diff --git a/apps/labrinth/.sqlx/query-c268a2a256f11a65449bd132a14f931c2e9776ffed87f4406bb5c2bc30087f5b.json b/apps/labrinth/.sqlx/query-b60fde3984aec9e4b8e1135490933aa08f1f6fae366925449ea8bbc8dbc98377.json similarity index 97% rename from apps/labrinth/.sqlx/query-c268a2a256f11a65449bd132a14f931c2e9776ffed87f4406bb5c2bc30087f5b.json rename to apps/labrinth/.sqlx/query-b60fde3984aec9e4b8e1135490933aa08f1f6fae366925449ea8bbc8dbc98377.json index bf61274204..911f2d39e6 100644 --- a/apps/labrinth/.sqlx/query-c268a2a256f11a65449bd132a14f931c2e9776ffed87f4406bb5c2bc30087f5b.json +++ b/apps/labrinth/.sqlx/query-b60fde3984aec9e4b8e1135490933aa08f1f6fae366925449ea8bbc8dbc98377.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n WITH filtered_projects AS (\n SELECT\n m.id,\n m.queued,\n m.published\n FROM mods m\n WHERE\n m.status = $1\n AND (\n $6::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = m.id\n AND didws.status = 'pending'\n )\n )\n ),\n total AS (\n SELECT COUNT(*) AS total_count FROM filtered_projects\n ),\n page_ids AS (\n SELECT\n id,\n queued,\n published\n FROM filtered_projects\n ORDER BY\n CASE WHEN $5 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $5 = 'oldest' THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n OFFSET $4\n LIMIT $3\n ),\n page_project_types AS (\n SELECT\n v.mod_id,\n ARRAY_AGG(DISTINCT pt.name::text) FILTER (WHERE pt.name IS NOT NULL) AS project_types\n FROM versions v\n INNER JOIN page_ids page ON page.id = v.mod_id\n INNER JOIN loaders_versions lv ON v.id = lv.version_id\n INNER JOIN loaders l ON lv.loader_id = l.id\n INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id\n INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n WHERE v.status = ANY($2)\n GROUP BY v.mod_id\n ),\n page_external_dependencies AS (\n SELECT\n v.mod_id,\n COUNT(*) AS external_dependencies_count\n FROM versions v\n INNER JOIN page_ids page ON page.id = v.mod_id\n INNER JOIN dependencies d ON d.dependent_id = v.id\n WHERE d.dependency_file_name IS NOT NULL\n GROUP BY v.mod_id\n ),\n page_projects AS (\n SELECT\n m.id,\n m.slug,\n m.name,\n m.summary,\n m.icon_url,\n m.status,\n m.requested_status,\n m.queued,\n m.published,\n m.updated,\n m.organization_id,\n o.name AS organization_name,\n o.icon_url AS organization_icon_url,\n owner.user_id AS owner_id,\n owner.username AS owner_name,\n owner.avatar_url AS owner_icon_url,\n CASE\n WHEN jsonb_typeof(m.components -> 'minecraft_server') = 'object'\n THEN ARRAY_APPEND(\n ARRAY_REMOVE(\n COALESCE(ppt.project_types::text[], ARRAY[]::text[]),\n 'modpack'\n ),\n 'minecraft_java_server'\n )\n ELSE COALESCE(ppt.project_types::text[], ARRAY[]::text[])\n END AS project_types,\n COALESCE(ped.external_dependencies_count, 0) AS external_dependencies_count\n FROM page_ids page\n INNER JOIN mods m ON m.id = page.id\n LEFT JOIN organizations o ON o.id = m.organization_id\n LEFT JOIN LATERAL (\n SELECT\n tm.user_id,\n u.username,\n u.avatar_url\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = m.team_id\n AND tm.is_owner\n ORDER BY tm.ordering ASC\n LIMIT 1\n ) owner ON m.organization_id IS NULL\n LEFT JOIN page_project_types ppt ON ppt.mod_id = m.id\n LEFT JOIN page_external_dependencies ped ON ped.mod_id = m.id\n )\n SELECT\n total.total_count AS \"total_count!\",\n page_projects.id AS \"id?\",\n page_projects.slug AS \"slug?\",\n page_projects.name AS \"name?\",\n page_projects.summary AS \"summary?\",\n page_projects.icon_url AS \"icon_url?\",\n page_projects.status AS \"status?\",\n page_projects.requested_status AS \"requested_status?\",\n page_projects.queued AS \"queued?\",\n page_projects.published AS \"published?\",\n page_projects.updated AS \"updated?\",\n page_projects.organization_id AS \"organization_id?\",\n page_projects.organization_name AS \"organization_name?\",\n page_projects.organization_icon_url AS \"organization_icon_url?\",\n page_projects.owner_id AS \"owner_id?\",\n page_projects.owner_name AS \"owner_name?\",\n page_projects.owner_icon_url AS \"owner_icon_url?\",\n page_projects.project_types AS \"project_types?: Vec\",\n page_projects.external_dependencies_count AS \"external_dependencies_count?\"\n FROM total\n LEFT JOIN page_projects ON true\n ORDER BY\n CASE WHEN $5 = 'newest' THEN COALESCE(page_projects.queued, page_projects.published) END DESC NULLS LAST,\n CASE WHEN $5 = 'oldest' THEN COALESCE(page_projects.queued, page_projects.published) END ASC NULLS LAST,\n page_projects.id ASC\n ", + "query": "\n WITH filtered_projects AS (\n SELECT\n m.id,\n m.queued,\n m.published\n FROM mods m\n WHERE\n m.status = $1\n AND (\n $6::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = m.id\n AND didws.status = 'pending'\n AND didws.severity != 'hidden'\n )\n )\n ),\n total AS (\n SELECT COUNT(*) AS total_count FROM filtered_projects\n ),\n page_ids AS (\n SELECT\n id,\n queued,\n published\n FROM filtered_projects\n ORDER BY\n CASE WHEN $5 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $5 = 'oldest' THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n OFFSET $4\n LIMIT $3\n ),\n page_project_types AS (\n SELECT\n v.mod_id,\n ARRAY_AGG(DISTINCT pt.name::text) FILTER (WHERE pt.name IS NOT NULL) AS project_types\n FROM versions v\n INNER JOIN page_ids page ON page.id = v.mod_id\n INNER JOIN loaders_versions lv ON v.id = lv.version_id\n INNER JOIN loaders l ON lv.loader_id = l.id\n INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id\n INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n WHERE v.status = ANY($2)\n GROUP BY v.mod_id\n ),\n page_external_dependencies AS (\n SELECT\n v.mod_id,\n COUNT(*) AS external_dependencies_count\n FROM versions v\n INNER JOIN page_ids page ON page.id = v.mod_id\n INNER JOIN dependencies d ON d.dependent_id = v.id\n WHERE d.dependency_file_name IS NOT NULL\n GROUP BY v.mod_id\n ),\n page_projects AS (\n SELECT\n m.id,\n m.slug,\n m.name,\n m.summary,\n m.icon_url,\n m.status,\n m.requested_status,\n m.queued,\n m.published,\n m.updated,\n m.organization_id,\n o.name AS organization_name,\n o.icon_url AS organization_icon_url,\n owner.user_id AS owner_id,\n owner.username AS owner_name,\n owner.avatar_url AS owner_icon_url,\n CASE\n WHEN jsonb_typeof(m.components -> 'minecraft_server') = 'object'\n THEN ARRAY_APPEND(\n ARRAY_REMOVE(\n COALESCE(ppt.project_types::text[], ARRAY[]::text[]),\n 'modpack'\n ),\n 'minecraft_java_server'\n )\n ELSE COALESCE(ppt.project_types::text[], ARRAY[]::text[])\n END AS project_types,\n COALESCE(ped.external_dependencies_count, 0) AS external_dependencies_count\n FROM page_ids page\n INNER JOIN mods m ON m.id = page.id\n LEFT JOIN organizations o ON o.id = m.organization_id\n LEFT JOIN LATERAL (\n SELECT\n tm.user_id,\n u.username,\n u.avatar_url\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = m.team_id\n AND tm.is_owner\n ORDER BY tm.ordering ASC\n LIMIT 1\n ) owner ON m.organization_id IS NULL\n LEFT JOIN page_project_types ppt ON ppt.mod_id = m.id\n LEFT JOIN page_external_dependencies ped ON ped.mod_id = m.id\n )\n SELECT\n total.total_count AS \"total_count!\",\n page_projects.id AS \"id?\",\n page_projects.slug AS \"slug?\",\n page_projects.name AS \"name?\",\n page_projects.summary AS \"summary?\",\n page_projects.icon_url AS \"icon_url?\",\n page_projects.status AS \"status?\",\n page_projects.requested_status AS \"requested_status?\",\n page_projects.queued AS \"queued?\",\n page_projects.published AS \"published?\",\n page_projects.updated AS \"updated?\",\n page_projects.organization_id AS \"organization_id?\",\n page_projects.organization_name AS \"organization_name?\",\n page_projects.organization_icon_url AS \"organization_icon_url?\",\n page_projects.owner_id AS \"owner_id?\",\n page_projects.owner_name AS \"owner_name?\",\n page_projects.owner_icon_url AS \"owner_icon_url?\",\n page_projects.project_types AS \"project_types?: Vec\",\n page_projects.external_dependencies_count AS \"external_dependencies_count?\"\n FROM total\n LEFT JOIN page_projects ON true\n ORDER BY\n CASE WHEN $5 = 'newest' THEN COALESCE(page_projects.queued, page_projects.published) END DESC NULLS LAST,\n CASE WHEN $5 = 'oldest' THEN COALESCE(page_projects.queued, page_projects.published) END ASC NULLS LAST,\n page_projects.id ASC\n ", "describe": { "columns": [ { @@ -131,5 +131,5 @@ null ] }, - "hash": "c268a2a256f11a65449bd132a14f931c2e9776ffed87f4406bb5c2bc30087f5b" + "hash": "b60fde3984aec9e4b8e1135490933aa08f1f6fae366925449ea8bbc8dbc98377" } diff --git a/apps/labrinth/.sqlx/query-bb08891e6ad48863335244539a87a7f1235db5a21e02ef9f2e267c02cf3fe3a5.json b/apps/labrinth/.sqlx/query-bb08891e6ad48863335244539a87a7f1235db5a21e02ef9f2e267c02cf3fe3a5.json new file mode 100644 index 0000000000..df9b6d7406 --- /dev/null +++ b/apps/labrinth/.sqlx/query-bb08891e6ad48863335244539a87a7f1235db5a21e02ef9f2e267c02cf3fe3a5.json @@ -0,0 +1,126 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n detail.id,\n detail.key,\n issue.issue_type,\n detail.severity AS \"severity: DelphiSeverity\",\n detail.jar,\n detail.file_path,\n detail.data AS \"data: Json>\",\n COALESCE(file_traces.traces, '[]'::jsonb)\n AS \"file_traces!: Json>\",\n report.delphi_version,\n file.size AS \"size?\",\n file.id AS \"file_id?\",\n version.id AS \"version_id?\",\n version.mod_id AS \"project_id?\",\n COALESCE(version_metadata.project_types, ARRAY[]::text[])\n AS \"project_types!: Vec\",\n COALESCE(version_metadata.loaders, ARRAY[]::text[])\n AS \"loaders!: Vec\",\n COALESCE(file_hashes.hashes, '{}'::jsonb)\n AS \"hashes!: Json>\"\n FROM delphi_report_issue_details detail\n INNER JOIN delphi_report_issues issue ON issue.id = detail.issue_id\n INNER JOIN delphi_reports report ON report.id = issue.report_id\n LEFT JOIN LATERAL (\n SELECT\n jsonb_agg(\n jsonb_build_object(\n 'key', file_detail.key,\n 'issue_type', file_issue.issue_type,\n 'severity', file_detail.severity,\n 'jar', file_detail.jar,\n 'file_path', file_detail.file_path,\n 'data', file_detail.data\n )\n ORDER BY file_detail.id\n ) AS traces\n FROM delphi_report_issues file_issue\n INNER JOIN delphi_report_issue_details file_detail\n ON file_detail.issue_id = file_issue.id\n WHERE file_issue.report_id = issue.report_id\n AND file_detail.file_path = detail.file_path\n ) file_traces ON TRUE\n LEFT JOIN files file ON file.id = report.file_id\n LEFT JOIN versions version ON version.id = file.version_id\n LEFT JOIN LATERAL (\n SELECT\n ARRAY_AGG(\n DISTINCT project_type.name::text\n ORDER BY project_type.name::text\n ) FILTER (WHERE project_type.name IS NOT NULL)\n AS project_types,\n ARRAY_AGG(\n DISTINCT loader.loader\n ORDER BY loader.loader\n ) AS loaders\n FROM loaders_versions loader_version\n INNER JOIN loaders loader\n ON loader.id = loader_version.loader_id\n LEFT JOIN loaders_project_types loader_project_type\n ON loader_project_type.joining_loader_id = loader_version.loader_id\n LEFT JOIN project_types project_type\n ON project_type.id = loader_project_type.joining_project_type_id\n WHERE loader_version.version_id = version.id\n ) version_metadata ON TRUE\n LEFT JOIN LATERAL (\n SELECT\n jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes\n FROM hashes\n WHERE hashes.file_id = file.id\n ) file_hashes ON TRUE\n WHERE detail.id = ANY($1::bigint[])\n ORDER BY detail.id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "key", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "issue_type", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "severity: DelphiSeverity", + "type_info": { + "Custom": { + "name": "delphi_severity", + "kind": { + "Enum": [ + "hidden", + "low", + "medium", + "high", + "severe", + "malware" + ] + } + } + } + }, + { + "ordinal": 4, + "name": "jar", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "file_path", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "data: Json>", + "type_info": "Jsonb" + }, + { + "ordinal": 7, + "name": "file_traces!: Json>", + "type_info": "Jsonb" + }, + { + "ordinal": 8, + "name": "delphi_version", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "size?", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "file_id?", + "type_info": "Int8" + }, + { + "ordinal": 11, + "name": "version_id?", + "type_info": "Int8" + }, + { + "ordinal": 12, + "name": "project_id?", + "type_info": "Int8" + }, + { + "ordinal": 13, + "name": "project_types!: Vec", + "type_info": "TextArray" + }, + { + "ordinal": 14, + "name": "loaders!: Vec", + "type_info": "VarcharArray" + }, + { + "ordinal": 15, + "name": "hashes!: Json>", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + null, + false, + true, + true, + true, + true, + null, + null, + null + ] + }, + "hash": "bb08891e6ad48863335244539a87a7f1235db5a21e02ef9f2e267c02cf3fe3a5" +} diff --git a/apps/labrinth/.sqlx/query-beac242c6a4f0f8502dcba5078d7aff5475e1b6e8f1cc63c8ea08c602efe0f12.json b/apps/labrinth/.sqlx/query-beac242c6a4f0f8502dcba5078d7aff5475e1b6e8f1cc63c8ea08c602efe0f12.json new file mode 100644 index 0000000000..1040130035 --- /dev/null +++ b/apps/labrinth/.sqlx/query-beac242c6a4f0f8502dcba5078d7aff5475e1b6e8f1cc63c8ea08c602efe0f12.json @@ -0,0 +1,80 @@ +{ + "db_name": "PostgreSQL", + "query": "\n\t\tINSERT INTO delphi_rules (\n\t\t\tname,\n\t\t\trule,\n\t\t\tpriority,\n\t\t\ton_issue_types,\n\t\t\trevision,\n\t\t\tcreated_by,\n\t\t\tupdated_by\n\t\t)\n\t\tVALUES (\n\t\t\t$1,\n\t\t\t$2,\n\t\t\t$3,\n\t\t\t$4,\n\t\t\t(SELECT revision + 1 FROM delphi_rule_revisions LIMIT 1),\n\t\t\t$5,\n\t\t\t$5\n\t\t)\n\t\tRETURNING\n\t\t\tid AS \"id!: DelphiRuleId\",\n\t\t\tname,\n\t\t\trule,\n\t\t\tpriority,\n\t\t\ton_issue_types,\n\t\t\trevision,\n\t\t\tcreated_at,\n\t\t\tupdated_at,\n\t\t\tcreated_by,\n\t\t\tupdated_by\n\t\t", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id!: DelphiRuleId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "rule", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "priority", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "on_issue_types", + "type_info": "TextArray" + }, + { + "ordinal": 5, + "name": "revision", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 7, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "created_by", + "type_info": "Int8" + }, + { + "ordinal": 9, + "name": "updated_by", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Int4", + "TextArray", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + true + ] + }, + "hash": "beac242c6a4f0f8502dcba5078d7aff5475e1b6e8f1cc63c8ea08c602efe0f12" +} diff --git a/apps/labrinth/.sqlx/query-bfc21c97440520573234e266dd28b6007451b8c2d993a33b90941aec06136486.json b/apps/labrinth/.sqlx/query-bfc21c97440520573234e266dd28b6007451b8c2d993a33b90941aec06136486.json new file mode 100644 index 0000000000..9abbc799a3 --- /dev/null +++ b/apps/labrinth/.sqlx/query-bfc21c97440520573234e266dd28b6007451b8c2d993a33b90941aec06136486.json @@ -0,0 +1,198 @@ +{ + "db_name": "PostgreSQL", + "query": "\n\t\tSELECT\n\t\t\tdelphi_rule.id AS \"id!: DelphiRuleId\",\n\t\t\tdelphi_rule.name,\n\t\t\tdelphi_rule.rule,\n\t\t\tdelphi_rule.priority,\n\t\t\tdelphi_rule.on_issue_types,\n\t\t\tdelphi_rule.revision,\n\t\t\t(\n\t\t\t\tSELECT revision FROM delphi_rule_revisions LIMIT 1\n\t\t\t) AS \"current_revision!\",\n\t\t\tdelphi_rule.created_at,\n\t\t\tdelphi_rule.updated_at,\n\t\t\tdelphi_rule.created_by,\n\t\t\tdelphi_rule.updated_by,\n\t\t\tCOALESCE(preview.affected_details_count, 0)\n\t\t\t\tAS \"affected_details_count!\",\n\t\t\tpreview.detail_id AS \"detail_id?: DelphiReportIssueDetailsId\",\n\t\t\tpreview.issue_id AS \"issue_id?: DelphiReportIssueId\",\n\t\t\tpreview.project_id AS \"project_id?: DBProjectId\",\n\t\t\tpreview.project_name AS \"project_name?\",\n\t\t\tpreview.project_icon_url AS \"project_icon_url?\",\n\t\t\tpreview.version_id AS \"version_id?: DBVersionId\",\n\t\t\tpreview.version_name AS \"version_name?\",\n\t\t\tpreview.version_number AS \"version_number?\",\n\t\t\tpreview.issue_type AS \"issue_type?\",\n\t\t\tpreview.key AS \"key?\",\n\t\t\tpreview.jar AS \"jar?\",\n\t\t\tpreview.file_path AS \"file_path?\",\n\t\t\tpreview.original_severity AS \"original_severity?: DelphiSeverity\",\n\t\t\tpreview.severity AS \"effect_severity?: DelphiSeverity\"\n\t\tFROM delphi_rules delphi_rule\n\t\tLEFT JOIN LATERAL (\n\t\t\tSELECT\n\t\t\t\teffect.detail_id,\n\t\t\t\tdetail.issue_id,\n\t\t\t\tversion.mod_id AS project_id,\n\t\t\t\tproject.name AS project_name,\n\t\t\t\tproject.icon_url AS project_icon_url,\n\t\t\t\tversion.id AS version_id,\n\t\t\t\tversion.name AS version_name,\n\t\t\t\tversion.version_number,\n\t\t\t\tissue.issue_type,\n\t\t\t\tdetail.key,\n\t\t\t\tdetail.jar,\n\t\t\t\tdetail.file_path,\n\t\t\t\tdetail.severity AS original_severity,\n\t\t\t\teffect.severity,\n\t\t\t\tCOUNT(*) OVER () AS affected_details_count\n\t\t\tFROM delphi_rule_effects effect\n\t\t\tINNER JOIN delphi_rule_revisions published\n\t\t\t\tON published.revision = effect.revision\n\t\t\tINNER JOIN delphi_report_issue_details detail\n\t\t\t\tON detail.id = effect.detail_id\n\t\t\tINNER JOIN delphi_report_issues issue\n\t\t\t\tON issue.id = detail.issue_id\n\t\t\tINNER JOIN delphi_reports report\n\t\t\t\tON report.id = issue.report_id\n\t\t\tLEFT JOIN files file ON file.id = report.file_id\n\t\t\tLEFT JOIN versions version ON version.id = file.version_id\n\t\t\tLEFT JOIN mods project ON project.id = version.mod_id\n\t\t\tWHERE effect.rule_id = delphi_rule.id\n\t\t\tORDER BY effect.detail_id DESC\n\t\t\tLIMIT 3\n\t\t) preview ON TRUE\n\t\tWHERE NOT delphi_rule.delete_on_next_revision\n\t\tORDER BY\n\t\t\tdelphi_rule.priority DESC,\n\t\t\tdelphi_rule.id,\n\t\t\tpreview.detail_id DESC\n\t\t", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id!: DelphiRuleId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "rule", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "priority", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "on_issue_types", + "type_info": "TextArray" + }, + { + "ordinal": 5, + "name": "revision", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "current_revision!", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "created_by", + "type_info": "Int8" + }, + { + "ordinal": 10, + "name": "updated_by", + "type_info": "Int8" + }, + { + "ordinal": 11, + "name": "affected_details_count!", + "type_info": "Int8" + }, + { + "ordinal": 12, + "name": "detail_id?: DelphiReportIssueDetailsId", + "type_info": "Int8" + }, + { + "ordinal": 13, + "name": "issue_id?: DelphiReportIssueId", + "type_info": "Int8" + }, + { + "ordinal": 14, + "name": "project_id?: DBProjectId", + "type_info": "Int8" + }, + { + "ordinal": 15, + "name": "project_name?", + "type_info": "Varchar" + }, + { + "ordinal": 16, + "name": "project_icon_url?", + "type_info": "Varchar" + }, + { + "ordinal": 17, + "name": "version_id?: DBVersionId", + "type_info": "Int8" + }, + { + "ordinal": 18, + "name": "version_name?", + "type_info": "Varchar" + }, + { + "ordinal": 19, + "name": "version_number?", + "type_info": "Varchar" + }, + { + "ordinal": 20, + "name": "issue_type?", + "type_info": "Text" + }, + { + "ordinal": 21, + "name": "key?", + "type_info": "Text" + }, + { + "ordinal": 22, + "name": "jar?", + "type_info": "Text" + }, + { + "ordinal": 23, + "name": "file_path?", + "type_info": "Text" + }, + { + "ordinal": 24, + "name": "original_severity?: DelphiSeverity", + "type_info": { + "Custom": { + "name": "delphi_severity", + "kind": { + "Enum": [ + "hidden", + "low", + "medium", + "high", + "severe", + "malware" + ] + } + } + } + }, + { + "ordinal": 25, + "name": "effect_severity?: DelphiSeverity", + "type_info": { + "Custom": { + "name": "delphi_severity", + "kind": { + "Enum": [ + "hidden", + "low", + "medium", + "high", + "severe", + "malware" + ] + } + } + } + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + null, + false, + false, + true, + true, + null, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + true, + false, + false, + false + ] + }, + "hash": "bfc21c97440520573234e266dd28b6007451b8c2d993a33b90941aec06136486" +} diff --git a/apps/labrinth/.sqlx/query-c40c2085202bc4486568fb24db9201239cefbe174f4a5cc4b8eb21a10268cd73.json b/apps/labrinth/.sqlx/query-c40c2085202bc4486568fb24db9201239cefbe174f4a5cc4b8eb21a10268cd73.json deleted file mode 100644 index 81c343b584..0000000000 --- a/apps/labrinth/.sqlx/query-c40c2085202bc4486568fb24db9201239cefbe174f4a5cc4b8eb21a10268cd73.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n WITH incoming AS (\n SELECT *\n FROM unnest($1::bigint[], $2::text[]) WITH ORDINALITY\n AS u(detail_id, verdict, ord)\n ),\n resolved AS (\n SELECT\n i.ord,\n didws.project_id,\n didws.key AS detail_key,\n i.verdict\n FROM incoming i\n INNER JOIN delphi_issue_details_with_statuses didws ON didws.id = i.detail_id\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n WHERE\n -- see delphi.rs todo comment\n dri.issue_type != '__dummy'\n ),\n validated AS (\n SELECT\n (SELECT COUNT(*) FROM incoming) AS incoming_count,\n (SELECT COUNT(*) FROM resolved) AS resolved_count\n ),\n latest AS (\n SELECT DISTINCT ON (project_id, detail_key)\n project_id,\n detail_key,\n verdict\n FROM resolved\n ORDER BY project_id, detail_key, ord DESC\n ),\n deleted AS (\n DELETE FROM delphi_issue_detail_verdicts didv\n USING latest\n WHERE\n didv.project_id = latest.project_id\n AND didv.detail_key = latest.detail_key\n AND latest.verdict = 'pending'\n RETURNING 1\n ),\n upserted AS (\n INSERT INTO delphi_issue_detail_verdicts (\n project_id,\n detail_key,\n verdict,\n updated_at\n )\n SELECT\n project_id,\n detail_key,\n verdict::delphi_report_issue_status,\n NOW()\n FROM latest\n WHERE verdict != 'pending'\n ON CONFLICT (project_id, detail_key)\n DO UPDATE SET\n verdict = EXCLUDED.verdict,\n updated_at = EXCLUDED.updated_at\n RETURNING 1\n )\n SELECT\n (v.incoming_count = v.resolved_count) AS \"all_found!\",\n (SELECT COUNT(*) FROM upserted) AS \"upserted_count!\"\n FROM validated v\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "all_found!", - "type_info": "Bool" - }, - { - "ordinal": 1, - "name": "upserted_count!", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8Array", - "TextArray" - ] - }, - "nullable": [ - null, - null - ] - }, - "hash": "c40c2085202bc4486568fb24db9201239cefbe174f4a5cc4b8eb21a10268cd73" -} diff --git a/apps/labrinth/.sqlx/query-cb983aed301378550831204ff7f88e95e5db9cabd8e1fc177cdfdaf0951e58e4.json b/apps/labrinth/.sqlx/query-cb983aed301378550831204ff7f88e95e5db9cabd8e1fc177cdfdaf0951e58e4.json new file mode 100644 index 0000000000..16a0b80bde --- /dev/null +++ b/apps/labrinth/.sqlx/query-cb983aed301378550831204ff7f88e95e5db9cabd8e1fc177cdfdaf0951e58e4.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT DISTINCT detail.project_id AS \"project_id!: DBProjectId\"\n FROM delphi_issue_details_with_statuses detail\n WHERE\n detail.project_id = ANY($1::bigint[])\n AND detail.status IN ('pending', 'unsafe')\n AND detail.severity != 'hidden'\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "project_id!: DBProjectId", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + true + ] + }, + "hash": "cb983aed301378550831204ff7f88e95e5db9cabd8e1fc177cdfdaf0951e58e4" +} diff --git a/apps/labrinth/.sqlx/query-d4d04f7cfd6efa582970ec0636f16df646df14ec647f7f2017d59fe786b9ad5b.json b/apps/labrinth/.sqlx/query-d4d04f7cfd6efa582970ec0636f16df646df14ec647f7f2017d59fe786b9ad5b.json new file mode 100644 index 0000000000..1760686240 --- /dev/null +++ b/apps/labrinth/.sqlx/query-d4d04f7cfd6efa582970ec0636f16df646df14ec647f7f2017d59fe786b9ad5b.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n WITH removed AS (\n DELETE FROM delphi_tech_review_queue\n WHERE project_id = ANY($1::bigint[])\n RETURNING project_id\n )\n SELECT\n removed.project_id AS \"project_id!: DBProjectId\",\n (\n SELECT thread.id\n FROM threads thread\n WHERE thread.mod_id = removed.project_id\n ORDER BY thread.id\n LIMIT 1\n ) AS \"thread_id!: DBThreadId\"\n FROM removed\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "project_id!: DBProjectId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "thread_id!: DBThreadId", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + null + ] + }, + "hash": "d4d04f7cfd6efa582970ec0636f16df646df14ec647f7f2017d59fe786b9ad5b" +} diff --git a/apps/labrinth/.sqlx/query-d7dd31de696eeb696a692787c3ee17d6bc343a6537944a82fdff4f94d82cdc3e.json b/apps/labrinth/.sqlx/query-d7dd31de696eeb696a692787c3ee17d6bc343a6537944a82fdff4f94d82cdc3e.json new file mode 100644 index 0000000000..f441374fb9 --- /dev/null +++ b/apps/labrinth/.sqlx/query-d7dd31de696eeb696a692787c3ee17d6bc343a6537944a82fdff4f94d82cdc3e.json @@ -0,0 +1,29 @@ +{ + "db_name": "PostgreSQL", + "query": "\n WITH incoming AS (\n SELECT *\n FROM unnest($1::bigint[], $2::text[]) WITH ORDINALITY\n AS u(detail_id, verdict, ord)\n ),\n resolved AS (\n SELECT\n i.ord,\n didws.project_id,\n didws.key AS detail_key,\n i.verdict\n FROM incoming i\n INNER JOIN delphi_issue_details_with_statuses didws ON didws.id = i.detail_id\n ),\n validated AS (\n SELECT\n (SELECT COUNT(*) FROM incoming) AS incoming_count,\n (SELECT COUNT(*) FROM resolved) AS resolved_count\n ),\n latest AS (\n SELECT DISTINCT ON (project_id, detail_key)\n project_id,\n detail_key,\n verdict\n FROM resolved\n ORDER BY project_id, detail_key, ord DESC\n ),\n deleted AS (\n DELETE FROM delphi_issue_detail_verdicts didv\n USING latest\n WHERE\n didv.project_id = latest.project_id\n AND didv.detail_key = latest.detail_key\n AND latest.verdict = 'pending'\n RETURNING 1\n ),\n upserted AS (\n INSERT INTO delphi_issue_detail_verdicts (\n project_id,\n detail_key,\n verdict,\n updated_at\n )\n SELECT\n project_id,\n detail_key,\n verdict::delphi_report_issue_status,\n NOW()\n FROM latest\n WHERE verdict != 'pending'\n ON CONFLICT (project_id, detail_key)\n DO UPDATE SET\n verdict = EXCLUDED.verdict,\n updated_at = EXCLUDED.updated_at\n RETURNING 1\n )\n SELECT\n (v.incoming_count = v.resolved_count) AS \"all_found!\",\n (SELECT COUNT(*) FROM upserted) AS \"upserted_count!\"\n FROM validated v\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "all_found!", + "type_info": "Bool" + }, + { + "ordinal": 1, + "name": "upserted_count!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8Array", + "TextArray" + ] + }, + "nullable": [ + null, + null + ] + }, + "hash": "d7dd31de696eeb696a692787c3ee17d6bc343a6537944a82fdff4f94d82cdc3e" +} diff --git a/apps/labrinth/.sqlx/query-e08df13a2829ce06d826dc05bf8ff386ba20e6443dde34efa1ea4ff1e38ca677.json b/apps/labrinth/.sqlx/query-e08df13a2829ce06d826dc05bf8ff386ba20e6443dde34efa1ea4ff1e38ca677.json new file mode 100644 index 0000000000..3233ce4740 --- /dev/null +++ b/apps/labrinth/.sqlx/query-e08df13a2829ce06d826dc05bf8ff386ba20e6443dde34efa1ea4ff1e38ca677.json @@ -0,0 +1,38 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id AS \"id!: DelphiRuleId\", name, rule, on_issue_types\n FROM delphi_rules\n WHERE NOT delete_on_next_revision\n ORDER BY priority DESC, id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id!: DelphiRuleId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "rule", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "on_issue_types", + "type_info": "TextArray" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "e08df13a2829ce06d826dc05bf8ff386ba20e6443dde34efa1ea4ff1e38ca677" +} diff --git a/apps/labrinth/.sqlx/query-e3c356bd41074ba4b3474dfab5cc0349636abea5b21faf5fc18d9f69fd21db1d.json b/apps/labrinth/.sqlx/query-e3c356bd41074ba4b3474dfab5cc0349636abea5b21faf5fc18d9f69fd21db1d.json deleted file mode 100644 index cfb283ab10..0000000000 --- a/apps/labrinth/.sqlx/query-e3c356bd41074ba4b3474dfab5cc0349636abea5b21faf5fc18d9f69fd21db1d.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT DISTINCT didws.project_id AS \"project_id!: DBProjectId\"\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 = ANY($1::bigint[])\n AND dri.issue_type != '__dummy'\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "project_id!: DBProjectId", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8Array" - ] - }, - "nullable": [ - true - ] - }, - "hash": "e3c356bd41074ba4b3474dfab5cc0349636abea5b21faf5fc18d9f69fd21db1d" -} diff --git a/apps/labrinth/.sqlx/query-e67fda05dacea7a0b6290e8b69932ad27e5a0dd128af9273d1d6179e60f9ea0b.json b/apps/labrinth/.sqlx/query-e67fda05dacea7a0b6290e8b69932ad27e5a0dd128af9273d1d6179e60f9ea0b.json new file mode 100644 index 0000000000..82f38da2f6 --- /dev/null +++ b/apps/labrinth/.sqlx/query-e67fda05dacea7a0b6290e8b69932ad27e5a0dd128af9273d1d6179e60f9ea0b.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "e67fda05dacea7a0b6290e8b69932ad27e5a0dd128af9273d1d6179e60f9ea0b" +} diff --git a/apps/labrinth/.sqlx/query-63a9a89443566baa880ddbe05f741e2a83b50d89f5954ab8d951a090e702de73.json b/apps/labrinth/.sqlx/query-e9e3b660bde18834f77aba2d4c3615ed333ab037dfc450c1715b31952dba43f9.json similarity index 89% rename from apps/labrinth/.sqlx/query-63a9a89443566baa880ddbe05f741e2a83b50d89f5954ab8d951a090e702de73.json rename to apps/labrinth/.sqlx/query-e9e3b660bde18834f77aba2d4c3615ed333ab037dfc450c1715b31952dba43f9.json index b9c6d3543c..ad332fa962 100644 --- a/apps/labrinth/.sqlx/query-63a9a89443566baa880ddbe05f741e2a83b50d89f5954ab8d951a090e702de73.json +++ b/apps/labrinth/.sqlx/query-e9e3b660bde18834f77aba2d4c3615ed333ab037dfc450c1715b31952dba43f9.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n didws.id AS \"id!: DelphiReportIssueDetailsId\",\n didws.issue_id AS \"issue_id!: DelphiReportIssueId\",\n didws.key AS \"key!: String\",\n didws.jar AS \"jar?: String\",\n didws.file_path AS \"file_path!: String\",\n didws.data AS \"data!: sqlx::types::Json>\",\n didws.severity AS \"severity!: DelphiSeverity\",\n didws.local_status AS \"local_status?: DelphiStatus\",\n didws.global_status AS \"global_status?: DelphiStatus\",\n didws.status AS \"status!: DelphiStatus\"\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.issue_id = ANY($1::bigint[])\n ORDER BY didws.issue_id, didws.id\n ", + "query": "\n SELECT\n didws.id AS \"id!: DelphiReportIssueDetailsId\",\n didws.issue_id AS \"issue_id!: DelphiReportIssueId\",\n didws.key AS \"key!: String\",\n didws.jar AS \"jar?: String\",\n didws.file_path AS \"file_path!: String\",\n didws.data AS \"data!: sqlx::types::Json>\",\n didws.severity AS \"severity!: DelphiSeverity\",\n didws.local_status AS \"local_status?: DelphiStatus\",\n didws.global_status AS \"global_status?: DelphiStatus\",\n didws.status AS \"status!: DelphiStatus\"\n FROM delphi_issue_details_with_statuses didws\n WHERE\n didws.issue_id = ANY($1::bigint[])\n AND ($2 OR didws.severity != 'hidden')\n ORDER BY didws.issue_id, didws.id\n ", "describe": { "columns": [ { @@ -41,10 +41,12 @@ "name": "delphi_severity", "kind": { "Enum": [ + "hidden", "low", "medium", "high", - "severe" + "severe", + "malware" ] } } @@ -101,7 +103,8 @@ ], "parameters": { "Left": [ - "Int8Array" + "Int8Array", + "Bool" ] }, "nullable": [ @@ -117,5 +120,5 @@ true ] }, - "hash": "63a9a89443566baa880ddbe05f741e2a83b50d89f5954ab8d951a090e702de73" + "hash": "e9e3b660bde18834f77aba2d4c3615ed333ab037dfc450c1715b31952dba43f9" } diff --git a/apps/labrinth/.sqlx/query-f2054ae7dcc89b21ed6b2f04526de1e7cddd68ac956143bef994104280a8dc07.json b/apps/labrinth/.sqlx/query-f2054ae7dcc89b21ed6b2f04526de1e7cddd68ac956143bef994104280a8dc07.json index 8cbe94abd5..1e2ab737e9 100644 --- a/apps/labrinth/.sqlx/query-f2054ae7dcc89b21ed6b2f04526de1e7cddd68ac956143bef994104280a8dc07.json +++ b/apps/labrinth/.sqlx/query-f2054ae7dcc89b21ed6b2f04526de1e7cddd68ac956143bef994104280a8dc07.json @@ -19,10 +19,12 @@ "name": "delphi_severity", "kind": { "Enum": [ + "hidden", "low", "medium", "high", - "severe" + "severe", + "malware" ] } } diff --git a/apps/labrinth/.sqlx/query-f40f23ca7f9d3c42a6f4c505afc7dd596c9829b5f0b42ca1b2830b0f68e33882.json b/apps/labrinth/.sqlx/query-f40f23ca7f9d3c42a6f4c505afc7dd596c9829b5f0b42ca1b2830b0f68e33882.json new file mode 100644 index 0000000000..bc9858831e --- /dev/null +++ b/apps/labrinth/.sqlx/query-f40f23ca7f9d3c42a6f4c505afc7dd596c9829b5f0b42ca1b2830b0f68e33882.json @@ -0,0 +1,130 @@ +{ + "db_name": "PostgreSQL", + "query": "\n\t\tSELECT\n\t\t\teffect.detail_id AS \"detail_id!: DelphiReportIssueDetailsId\",\n\t\t\tdetail.issue_id AS \"issue_id!: DelphiReportIssueId\",\n\t\t\tversion.mod_id AS \"project_id?: DBProjectId\",\n\t\t\tproject.name AS \"project_name?\",\n\t\t\tproject.icon_url AS \"project_icon_url?\",\n\t\t\tversion.id AS \"version_id?: DBVersionId\",\n\t\t\tversion.name AS \"version_name?\",\n\t\t\tversion.version_number AS \"version_number?\",\n\t\t\tissue.issue_type,\n\t\t\tdetail.key,\n\t\t\tdetail.jar,\n\t\t\tdetail.file_path,\n\t\t\tdetail.severity AS \"original_severity!: DelphiSeverity\",\n\t\t\teffect.severity AS \"effect_severity!: DelphiSeverity\"\n\t\tFROM delphi_rule_effects effect\n\t\tINNER JOIN delphi_rule_revisions published\n\t\t\tON published.revision = effect.revision\n\t\tINNER JOIN delphi_report_issue_details detail\n\t\t\tON detail.id = effect.detail_id\n\t\tINNER JOIN delphi_report_issues issue ON issue.id = detail.issue_id\n\t\tINNER JOIN delphi_reports report ON report.id = issue.report_id\n\t\tLEFT JOIN files file ON file.id = report.file_id\n\t\tLEFT JOIN versions version ON version.id = file.version_id\n\t\tLEFT JOIN mods project ON project.id = version.mod_id\n\t\tWHERE effect.rule_id = $1\n\t\tORDER BY effect.detail_id DESC\n\t\tLIMIT $2 OFFSET $3\n\t\t", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "detail_id!: DelphiReportIssueDetailsId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "issue_id!: DelphiReportIssueId", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "project_id?: DBProjectId", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "project_name?", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "project_icon_url?", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "version_id?: DBVersionId", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "version_name?", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "version_number?", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "issue_type", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "key", + "type_info": "Text" + }, + { + "ordinal": 10, + "name": "jar", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "file_path", + "type_info": "Text" + }, + { + "ordinal": 12, + "name": "original_severity!: DelphiSeverity", + "type_info": { + "Custom": { + "name": "delphi_severity", + "kind": { + "Enum": [ + "hidden", + "low", + "medium", + "high", + "severe", + "malware" + ] + } + } + } + }, + { + "ordinal": 13, + "name": "effect_severity!: DelphiSeverity", + "type_info": { + "Custom": { + "name": "delphi_severity", + "kind": { + "Enum": [ + "hidden", + "low", + "medium", + "high", + "severe", + "malware" + ] + } + } + } + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + true, + false, + false, + false + ] + }, + "hash": "f40f23ca7f9d3c42a6f4c505afc7dd596c9829b5f0b42ca1b2830b0f68e33882" +} diff --git a/apps/labrinth/.sqlx/query-5e5fcfa6ac68f296ba5238e8211f8e206473f11fe1796fc2feda15f8d0ee7f41.json b/apps/labrinth/.sqlx/query-f952ab269d0aa1b98bb86b58debfd4ab14cea8575e4a15cf8c1c57e9c1e367a1.json similarity index 77% rename from apps/labrinth/.sqlx/query-5e5fcfa6ac68f296ba5238e8211f8e206473f11fe1796fc2feda15f8d0ee7f41.json rename to apps/labrinth/.sqlx/query-f952ab269d0aa1b98bb86b58debfd4ab14cea8575e4a15cf8c1c57e9c1e367a1.json index 99e2d4391d..06674db8d5 100644 --- a/apps/labrinth/.sqlx/query-5e5fcfa6ac68f296ba5238e8211f8e206473f11fe1796fc2feda15f8d0ee7f41.json +++ b/apps/labrinth/.sqlx/query-f952ab269d0aa1b98bb86b58debfd4ab14cea8575e4a15cf8c1c57e9c1e367a1.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n dgdv.detail_key,\n dgdv.verdict AS \"verdict!: DelphiStatus\",\n COUNT(dri.id) AS \"local_trace_count!\"\n FROM delphi_global_detail_verdicts dgdv\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.key = dgdv.detail_key\n LEFT JOIN delphi_report_issues dri\n ON dri.id = didws.issue_id\n AND dri.issue_type != '__dummy'\n WHERE dgdv.detail_key = $1\n GROUP BY dgdv.detail_key, dgdv.verdict\n ", + "query": "\n SELECT\n dgdv.detail_key,\n dgdv.verdict AS \"verdict!: DelphiStatus\",\n COUNT(dri.id) AS \"local_trace_count!\"\n FROM delphi_global_detail_verdicts dgdv\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.key = dgdv.detail_key\n AND didws.severity != 'hidden'\n LEFT JOIN delphi_report_issues dri\n ON dri.id = didws.issue_id\n WHERE dgdv.detail_key = $1\n GROUP BY dgdv.detail_key, dgdv.verdict\n ", "describe": { "columns": [ { @@ -41,5 +41,5 @@ null ] }, - "hash": "5e5fcfa6ac68f296ba5238e8211f8e206473f11fe1796fc2feda15f8d0ee7f41" + "hash": "f952ab269d0aa1b98bb86b58debfd4ab14cea8575e4a15cf8c1c57e9c1e367a1" } diff --git a/apps/labrinth/.sqlx/query-fc6c20348de487e4039ee7d53212ecd1856bde7aa4be6e366c98cef1321a7279.json b/apps/labrinth/.sqlx/query-fc6c20348de487e4039ee7d53212ecd1856bde7aa4be6e366c98cef1321a7279.json deleted file mode 100644 index 16a500d5ac..0000000000 --- a/apps/labrinth/.sqlx/query-fc6c20348de487e4039ee7d53212ecd1856bde7aa4be6e366c98cef1321a7279.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n WITH dummy_issue AS (\n INSERT INTO delphi_report_issues (report_id, issue_type)\n VALUES ($1, $2)\n ON CONFLICT (report_id, issue_type)\n DO UPDATE SET issue_type = EXCLUDED.issue_type\n RETURNING id\n )\n INSERT INTO delphi_report_issue_details (\n issue_id,\n key,\n jar,\n file_path,\n decompiled_source,\n data,\n severity\n )\n SELECT\n id,\n '',\n NULL,\n '',\n NULL,\n '{}'::jsonb,\n 'low'::delphi_severity\n FROM dummy_issue\n WHERE NOT EXISTS (\n SELECT 1\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dummy_issue.id\n )\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Text" - ] - }, - "nullable": [] - }, - "hash": "fc6c20348de487e4039ee7d53212ecd1856bde7aa4be6e366c98cef1321a7279" -} diff --git a/apps/labrinth/.sqlx/query-33d4e31565ece4a99832cbfd0a39bc49666e9fc8a400a5c41660504205e7cac5.json b/apps/labrinth/.sqlx/query-ffca905cc54942581efa461115fac2260bbc8d593aab70a2510491724981d56d.json similarity index 54% rename from apps/labrinth/.sqlx/query-33d4e31565ece4a99832cbfd0a39bc49666e9fc8a400a5c41660504205e7cac5.json rename to apps/labrinth/.sqlx/query-ffca905cc54942581efa461115fac2260bbc8d593aab70a2510491724981d56d.json index 509e4cc4dd..7766aaefde 100644 --- a/apps/labrinth/.sqlx/query-33d4e31565ece4a99832cbfd0a39bc49666e9fc8a400a5c41660504205e7cac5.json +++ b/apps/labrinth/.sqlx/query-ffca905cc54942581efa461115fac2260bbc8d593aab70a2510491724981d56d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n dgdv.detail_key,\n dgdv.verdict AS \"verdict!: DelphiStatus\",\n COUNT(dri.id) AS \"local_trace_count!\"\n FROM delphi_global_detail_verdicts dgdv\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.key = dgdv.detail_key\n LEFT JOIN delphi_report_issues dri\n ON dri.id = didws.issue_id\n AND dri.issue_type != '__dummy'\n WHERE (\n $1::text IS NULL\n OR dgdv.detail_key ILIKE '%' || $1 || '%'\n OR EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses matching_didws\n INNER JOIN delphi_report_issues matching_dri\n ON matching_dri.id = matching_didws.issue_id\n WHERE\n matching_didws.key = dgdv.detail_key\n AND matching_dri.issue_type != '__dummy'\n AND matching_didws.file_path ILIKE '%' || $1 || '%'\n )\n )\n GROUP BY dgdv.detail_key, dgdv.verdict\n ORDER BY dgdv.detail_key\n LIMIT $2 OFFSET $3\n ", + "query": "\n SELECT\n dgdv.detail_key,\n dgdv.verdict AS \"verdict!: DelphiStatus\",\n COUNT(dri.id) AS \"local_trace_count!\"\n FROM delphi_global_detail_verdicts dgdv\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.key = dgdv.detail_key\n AND didws.severity != 'hidden'\n LEFT JOIN delphi_report_issues dri\n ON dri.id = didws.issue_id\n WHERE (\n $1::text IS NULL\n OR dgdv.detail_key ILIKE '%' || $1 || '%'\n OR EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses matching_didws\n INNER JOIN delphi_report_issues matching_dri\n ON matching_dri.id = matching_didws.issue_id\n WHERE\n matching_didws.key = dgdv.detail_key\n AND matching_dri.issue_type != '__dummy'\n AND matching_didws.file_path ILIKE '%' || $1 || '%'\n )\n )\n GROUP BY dgdv.detail_key, dgdv.verdict\n ORDER BY dgdv.detail_key\n LIMIT $2 OFFSET $3\n ", "describe": { "columns": [ { @@ -43,5 +43,5 @@ null ] }, - "hash": "33d4e31565ece4a99832cbfd0a39bc49666e9fc8a400a5c41660504205e7cac5" + "hash": "ffca905cc54942581efa461115fac2260bbc8d593aab70a2510491724981d56d" } diff --git a/apps/labrinth/Cargo.toml b/apps/labrinth/Cargo.toml index 03eb3ecae6..d94e808ed4 100644 --- a/apps/labrinth/Cargo.toml +++ b/apps/labrinth/Cargo.toml @@ -28,6 +28,7 @@ aws-sdk-s3 = { workspace = true } base64 = { workspace = true } bitflags = { workspace = true } bytes = { workspace = true } +cel = { workspace = true } censor = { workspace = true } chrono = { workspace = true, features = ["serde"] } chumsky = { workspace = true } diff --git a/apps/labrinth/fixtures/delphi-trace-rules.sql b/apps/labrinth/fixtures/delphi-trace-rules.sql new file mode 100644 index 0000000000..4e9baa13cb --- /dev/null +++ b/apps/labrinth/fixtures/delphi-trace-rules.sql @@ -0,0 +1,342 @@ +-- Development fixtures for the Delphi trace-rule editor and scanner. +-- This file is idempotent and only replaces rows using the fixture namespace. + +BEGIN; + +DELETE FROM delphi_rule_effects +WHERE rule_id IN ( + SELECT id + FROM delphi_rules + WHERE name LIKE '[DEV trace-rule fixture]%' +); + +DELETE FROM delphi_reports +WHERE artifact_url = 'https://example.invalid/fixtures/delphi-trace-rules.json'; + +DELETE FROM delphi_rules +WHERE name LIKE '[DEV trace-rule fixture]%'; + +WITH fixture_report AS ( + INSERT INTO delphi_reports ( + file_id, + delphi_version, + artifact_url, + severity + ) + VALUES ( + NULL, + 18, + 'https://example.invalid/fixtures/delphi-trace-rules.json', + 'malware' + ) + RETURNING id +), fixture_issues AS ( + INSERT INTO delphi_report_issues (report_id, issue_type) + SELECT fixture_report.id, fixture_issue.issue_type + FROM fixture_report + CROSS JOIN ( + VALUES + ('OBFUSCATED_NAMES'), + ('SUSPICIOUS_NETWORK_ACCESS'), + ('RUNTIME_EXEC_USAGE'), + ('BUNDLED_LIBRARY'), + ('HARDCODED_URL'), + ('CRYPTO_MINING_SIGNATURE'), + ('CLASSLOADER_USAGE'), + ('NATIVE_LIBRARY_LOAD') + ) AS fixture_issue(issue_type) + RETURNING id, issue_type +) +INSERT INTO delphi_report_issue_details ( + issue_id, + key, + jar, + file_path, + decompiled_source, + data, + severity +) +SELECT + fixture_issues.id, + fixture_detail.key, + fixture_detail.jar, + fixture_detail.file_path, + fixture_detail.decompiled_source, + fixture_detail.data, + fixture_detail.severity::delphi_severity +FROM fixture_issues +INNER JOIN ( + VALUES + ( + 'OBFUSCATED_NAMES', + 'dev-trace-rules/obfuscation/known-bootstrap', + 'META-INF/jars/bootstrap.jar', + 'com/example/bootstrap/Bootstrap.class', + 'class Bootstrap { /* generated fixture */ }', + '{"confidence": 0.99, "symbol_count": 58}'::jsonb, + 'high' + ), + ( + 'OBFUSCATED_NAMES', + 'dev-trace-rules/obfuscation/unknown-library', + NULL, + 'com/example/internal/A.class', + 'class A { /* generated fixture */ }', + '{"confidence": 0.71, "symbol_count": 240}'::jsonb, + 'high' + ), + ( + 'SUSPICIOUS_NETWORK_ACCESS', + 'dev-trace-rules/network/malware-host', + NULL, + 'com/example/network/Backdoor.class', + 'class Backdoor { /* generated fixture */ }', + '{"host": "evil.example", "port": 4444, "protocol": "tcp"}'::jsonb, + 'medium' + ), + ( + 'SUSPICIOUS_NETWORK_ACCESS', + 'dev-trace-rules/network/telemetry-host', + NULL, + 'com/example/telemetry/TelemetryClient.class', + 'class TelemetryClient { /* generated fixture */ }', + '{"host": "telemetry.example.com", "port": 443, "protocol": "https"}'::jsonb, + 'medium' + ), + ( + 'SUSPICIOUS_NETWORK_ACCESS', + 'dev-trace-rules/network/unmatched-host', + NULL, + 'com/example/update/UpdateChecker.class', + 'class UpdateChecker { /* generated fixture */ }', + '{"host": "api.github.com", "port": 443, "protocol": "https"}'::jsonb, + 'low' + ), + ( + 'RUNTIME_EXEC_USAGE', + 'dev-trace-rules/runtime/powershell', + NULL, + 'com/example/platform/WindowsHelper.class', + 'class WindowsHelper { /* generated fixture */ }', + '{"command": "powershell.exe", "arguments": ["-EncodedCommand", "fixture"]}'::jsonb, + 'high' + ), + ( + 'RUNTIME_EXEC_USAGE', + 'dev-trace-rules/runtime/desktop-opener', + NULL, + 'com/example/platform/DesktopBrowser.class', + 'class DesktopBrowser { /* generated fixture */ }', + '{"command": "xdg-open", "arguments": ["https://example.com/docs"]}'::jsonb, + 'medium' + ), + ( + 'RUNTIME_EXEC_USAGE', + 'dev-trace-rules/runtime/unmatched-java', + NULL, + 'com/example/launcher/JavaLauncher.class', + 'class JavaLauncher { /* generated fixture */ }', + '{"command": "java", "arguments": ["-version"]}'::jsonb, + 'low' + ), + ( + 'BUNDLED_LIBRARY', + 'dev-trace-rules/library/example-helper', + 'META-INF/jars/example-helper.jar', + 'com/example/helper/Helper.class', + NULL, + '{"library": "example-helper", "version": "1.0.0"}'::jsonb, + 'high' + ), + ( + 'BUNDLED_LIBRARY', + 'dev-trace-rules/library/kotlin-runtime', + 'META-INF/jars/kotlin-stdlib.jar', + 'kotlin/collections/CollectionsKt.class', + NULL, + '{"library": "kotlin-stdlib", "version": "2.1.0"}'::jsonb, + 'medium' + ), + ( + 'HARDCODED_URL', + 'dev-trace-rules/url/known-documentation', + NULL, + 'com/example/help/DocumentationLink.class', + 'class DocumentationLink { /* generated fixture */ }', + '{"url": "https://docs.example.com/help"}'::jsonb, + 'low' + ), + ( + 'HARDCODED_URL', + 'dev-trace-rules/url/unmatched-webhook', + NULL, + 'com/example/integration/WebhookClient.class', + 'class WebhookClient { /* generated fixture */ }', + '{"url": "https://hooks.example.net/incoming"}'::jsonb, + 'medium' + ), + ( + 'CRYPTO_MINING_SIGNATURE', + 'dev-trace-rules/crypto/randomx-miner', + NULL, + 'com/example/worker/NativeWorker.class', + 'class NativeWorker { /* generated fixture */ }', + '{"algorithm": "randomx", "pool": "pool.example"}'::jsonb, + 'severe' + ), + ( + 'CLASSLOADER_USAGE', + 'dev-trace-rules/classloader/plugin-loader', + 'META-INF/jars/plugin-api.jar', + 'com/example/plugin/PluginLoader.class', + 'class PluginLoader { /* generated fixture */ }', + '{"purpose": "plugin-discovery", "parent_first": true}'::jsonb, + 'high' + ), + ( + 'NATIVE_LIBRARY_LOAD', + 'dev-trace-rules/native/unmatched-jna', + 'META-INF/jars/jna.jar', + 'com/sun/jna/Native.class', + NULL, + '{"library": "jnidispatch", "platform": "linux-x86_64"}'::jsonb, + 'high' + ) +) AS fixture_detail( + issue_type, + key, + jar, + file_path, + decompiled_source, + data, + severity +) USING (issue_type); + +INSERT INTO delphi_rules (name, rule, priority, revision) +VALUES + ( + '[DEV trace-rule fixture] Escalate known malware host', + 'trace.issue_type == "SUSPICIOUS_NETWORK_ACCESS" && trace.data.host == "evil.example" ? "malware" : null', + 1000, + (SELECT revision FROM delphi_rule_revisions LIMIT 1) + ), + ( + '[DEV trace-rule fixture] Escalate crypto-mining signatures', + 'trace.issue_type == "CRYPTO_MINING_SIGNATURE" && trace.data.algorithm == "randomx" ? {"severity": "malware"} : null', + 900, + (SELECT revision FROM delphi_rule_revisions LIMIT 1) + ), + ( + '[DEV trace-rule fixture] Escalate encoded PowerShell', + 'trace.issue_type == "RUNTIME_EXEC_USAGE" && trace.data.command == "powershell.exe" ? {"severity": "severe"} : null', + 800, + (SELECT revision FROM delphi_rule_revisions LIMIT 1) + ), + ( + '[DEV trace-rule fixture] Hide known-safe obfuscation', + 'trace.issue_type == "OBFUSCATED_NAMES" && trace.data.confidence >= 0.95 ? "hidden" : null', + 700, + (SELECT revision FROM delphi_rule_revisions LIMIT 1) + ), + ( + '[DEV trace-rule fixture] Downgrade known telemetry', + 'trace.issue_type == "SUSPICIOUS_NETWORK_ACCESS" && trace.data.host == "telemetry.example.com" ? {"severity": "low"} : null', + 600, + (SELECT revision FROM delphi_rule_revisions LIMIT 1) + ), + ( + '[DEV trace-rule fixture] Downgrade desktop URL openers', + 'trace.issue_type == "RUNTIME_EXEC_USAGE" && trace.data.command == "xdg-open" ? {"severity": "low"} : null', + 500, + (SELECT revision FROM delphi_rule_revisions LIMIT 1) + ), + ( + '[DEV trace-rule fixture] Downgrade bundled libraries', + 'trace.issue_type == "BUNDLED_LIBRARY" ? "low" : null', + 400, + (SELECT revision FROM delphi_rule_revisions LIMIT 1) + ), + ( + '[DEV trace-rule fixture] Hide documentation URLs', + 'trace.issue_type == "HARDCODED_URL" && trace.data.url == "https://docs.example.com/help" ? {"severity": "hidden"} : null', + 300, + (SELECT revision FROM delphi_rule_revisions LIMIT 1) + ), + ( + '[DEV trace-rule fixture] Downgrade plugin classloaders', + 'trace.issue_type == "CLASSLOADER_USAGE" && trace.data.purpose == "plugin-discovery" ? {"severity": "low"} : null', + 200, + (SELECT revision FROM delphi_rule_revisions LIMIT 1) + ), + ( + '[DEV trace-rule fixture] Pending native-library exception', + 'trace.issue_type == "NATIVE_LIBRARY_LOAD" && trace.data.library == "jnidispatch" ? {"severity": "low"} : null', + 100, + (SELECT revision + 1 FROM delphi_rule_revisions LIMIT 1) + ); + +INSERT INTO delphi_rule_effects (revision, detail_id, rule_id, severity) +SELECT + (SELECT revision FROM delphi_rule_revisions LIMIT 1), + detail.id, + rule.id, + fixture_effect.severity::delphi_severity +FROM ( + VALUES + ( + 'dev-trace-rules/network/malware-host', + '[DEV trace-rule fixture] Escalate known malware host', + 'malware' + ), + ( + 'dev-trace-rules/crypto/randomx-miner', + '[DEV trace-rule fixture] Escalate crypto-mining signatures', + 'malware' + ), + ( + 'dev-trace-rules/runtime/powershell', + '[DEV trace-rule fixture] Escalate encoded PowerShell', + 'severe' + ), + ( + 'dev-trace-rules/obfuscation/known-bootstrap', + '[DEV trace-rule fixture] Hide known-safe obfuscation', + 'hidden' + ), + ( + 'dev-trace-rules/network/telemetry-host', + '[DEV trace-rule fixture] Downgrade known telemetry', + 'low' + ), + ( + 'dev-trace-rules/runtime/desktop-opener', + '[DEV trace-rule fixture] Downgrade desktop URL openers', + 'low' + ), + ( + 'dev-trace-rules/library/example-helper', + '[DEV trace-rule fixture] Downgrade bundled libraries', + 'low' + ), + ( + 'dev-trace-rules/library/kotlin-runtime', + '[DEV trace-rule fixture] Downgrade bundled libraries', + 'low' + ), + ( + 'dev-trace-rules/url/known-documentation', + '[DEV trace-rule fixture] Hide documentation URLs', + 'hidden' + ), + ( + 'dev-trace-rules/classloader/plugin-loader', + '[DEV trace-rule fixture] Downgrade plugin classloaders', + 'low' + ) +) AS fixture_effect(detail_key, rule_name, severity) +INNER JOIN delphi_report_issue_details detail + ON detail.key = fixture_effect.detail_key +INNER JOIN delphi_rules rule + ON rule.name = fixture_effect.rule_name; + +COMMIT; diff --git a/apps/labrinth/migrations/20260720105358_delphi_trace_rules.sql b/apps/labrinth/migrations/20260720105358_delphi_trace_rules.sql new file mode 100644 index 0000000000..666a8863d9 --- /dev/null +++ b/apps/labrinth/migrations/20260720105358_delphi_trace_rules.sql @@ -0,0 +1,103 @@ +ALTER TYPE delphi_severity ADD VALUE IF NOT EXISTS 'hidden' BEFORE 'low'; +ALTER TYPE delphi_severity ADD VALUE IF NOT EXISTS 'malware' AFTER 'severe'; + +CREATE TABLE delphi_rules ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_by BIGINT REFERENCES users(id) + ON DELETE SET NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_by BIGINT REFERENCES users(id) + ON DELETE SET NULL, + name VARCHAR(256) NOT NULL, + rule VARCHAR(65536) NOT NULL, + priority INTEGER NOT NULL DEFAULT 0, + revision BIGINT NOT NULL DEFAULT 0, + delete_on_next_revision BOOL NOT NULL DEFAULT FALSE +); + +CREATE TABLE delphi_rule_revisions ( + revision BIGINT PRIMARY KEY +); + +INSERT INTO delphi_rule_revisions (revision) +VALUES (1); + +CREATE TABLE delphi_rule_effects ( + revision BIGINT NOT NULL, + detail_id BIGINT NOT NULL REFERENCES delphi_report_issue_details(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + rule_id BIGINT NOT NULL REFERENCES delphi_rules(id) + ON UPDATE CASCADE, + severity delphi_severity NOT NULL, + PRIMARY KEY (revision, detail_id) +); + +CREATE INDEX delphi_rule_effects_rule_id ON delphi_rule_effects(rule_id); + +DROP VIEW delphi_issue_details_with_statuses; + +CREATE VIEW delphi_issue_details_with_statuses AS +SELECT + drid.id, + drid.issue_id, + drid.key, + drid.jar, + drid.file_path, + drid.decompiled_source, + drid.data, + drid.severity AS original_severity, + COALESCE(dre.severity, drid.severity) AS severity, + m.id AS project_id, + didv.verdict AS local_status, + dgdv.verdict AS global_status, + COALESCE(dgdv.verdict, 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_global_detail_verdicts dgdv + ON drid.key = dgdv.detail_key +LEFT JOIN delphi_issue_detail_verdicts didv + ON m.id = didv.project_id + AND drid.key = didv.detail_key +LEFT JOIN ( + SELECT revision + FROM delphi_rule_revisions + LIMIT 1 +) drr ON TRUE +LEFT JOIN delphi_rule_effects dre + ON dre.revision = drr.revision + AND dre.detail_id = drid.id; + +CREATE TABLE delphi_tech_review_queue ( + project_id BIGINT PRIMARY KEY REFERENCES mods(id) + ON DELETE CASCADE +); + +INSERT INTO delphi_tech_review_queue (project_id) +SELECT DISTINCT didws.project_id +FROM delphi_issue_details_with_statuses didws +INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id +WHERE + ( + dri.issue_type = '__dummy' + AND didws.status = 'pending' + ) + OR ( + dri.issue_type != '__dummy' + AND didws.status IN ('pending', 'unsafe') + AND didws.severity::text != 'hidden' + ); + +DELETE FROM delphi_report_issue_details detail +USING delphi_report_issues issue +WHERE + detail.issue_id = issue.id + AND issue.issue_type = '__dummy'; + +DELETE FROM delphi_report_issues +WHERE issue_type = '__dummy'; diff --git a/apps/labrinth/migrations/20260830120001_delphi_rule_issue_types.sql b/apps/labrinth/migrations/20260830120001_delphi_rule_issue_types.sql new file mode 100644 index 0000000000..5177123813 --- /dev/null +++ b/apps/labrinth/migrations/20260830120001_delphi_rule_issue_types.sql @@ -0,0 +1,2 @@ +ALTER TABLE delphi_rules + ADD COLUMN on_issue_types TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[]; diff --git a/apps/labrinth/src/database/models/delphi_report_item.rs b/apps/labrinth/src/database/models/delphi_report_item.rs index 41bfa6b782..92cf1000aa 100644 --- a/apps/labrinth/src/database/models/delphi_report_item.rs +++ b/apps/labrinth/src/database/models/delphi_report_item.rs @@ -55,7 +55,7 @@ impl DBDelphiReport { } } -/// A severity level reported by Delphi. +/// A severity level for a Delphi report or transformed issue detail. #[derive( Deserialize, Serialize, @@ -76,6 +76,8 @@ impl DBDelphiReport { #[serde(rename_all = "snake_case")] #[sqlx(type_name = "delphi_severity", rename_all = "snake_case")] pub enum DelphiSeverity { + #[serde(alias = "HIDDEN")] + Hidden, #[serde(alias = "LOW")] Low, #[serde(alias = "MEDIUM")] diff --git a/apps/labrinth/src/database/models/ids.rs b/apps/labrinth/src/database/models/ids.rs index 5f92ece21d..bd953e7e2d 100644 --- a/apps/labrinth/src/database/models/ids.rs +++ b/apps/labrinth/src/database/models/ids.rs @@ -292,4 +292,5 @@ id_type!(StatusId as i32); id_type!(DelphiReportId as i64); id_type!(DelphiReportIssueId as i64); id_type!(DelphiReportIssueDetailsId as i64); +id_type!(DelphiRuleId as i64); id_type!(SharedInstanceId as i64); diff --git a/apps/labrinth/src/routes/internal/delphi/mod.rs b/apps/labrinth/src/routes/internal/delphi/mod.rs index ef265171d2..87acfbef58 100644 --- a/apps/labrinth/src/routes/internal/delphi/mod.rs +++ b/apps/labrinth/src/routes/internal/delphi/mod.rs @@ -31,7 +31,7 @@ use crate::{ }; pub mod rescan; -pub mod tech_review_sync; +pub mod tech_review_queue; pub fn config(cfg: &mut actix_web::web::ServiceConfig) { cfg.service( @@ -209,6 +209,7 @@ async fn ingest_report_deserialized( "Delphi found issues in file", ); + let mut inserted_detail_ids = Vec::new(); for (issue_type, issue_details) in report.issues { let issue_id = DBDelphiReportIssue { id: DelphiReportIssueId(0), // This will be set by the database @@ -228,7 +229,7 @@ async fn ingest_report_deserialized( let decompiled_source = report.decompiled_sources.get(&issue_detail.file); - ReportIssueDetail { + let detail_id = ReportIssueDetail { id: DelphiReportIssueDetailsId(0), // This will be set by the database issue_id, key: issue_detail.key.0, @@ -244,12 +245,19 @@ async fn ingest_report_deserialized( .insert(&mut transaction) .await .wrap_internal_err("failed to insert Delphi issue detail")?; + inserted_detail_ids.push(detail_id); } } - tech_review_sync::sync_project_tech_review_state( + crate::routes::internal::moderation::tech_review::rules_scan::materialize_current_rule_effects( + &inserted_detail_ids, + &mut transaction, + ) + .await + .wrap_internal_err("failed to apply delphi rules to new issue details")?; + + tech_review_queue::add_projects_with_review_details( &[DBProjectId::from(report.project_id)], - tech_review_sync::TechReviewExitReason::Resolved, &mut transaction, ) .await diff --git a/apps/labrinth/src/routes/internal/delphi/rescan.rs b/apps/labrinth/src/routes/internal/delphi/rescan.rs index c7d7fa297b..71b47d9cee 100644 --- a/apps/labrinth/src/routes/internal/delphi/rescan.rs +++ b/apps/labrinth/src/routes/internal/delphi/rescan.rs @@ -97,26 +97,23 @@ async fn fetch_unreviewed_tech_review_project_ids( r#" SELECT DISTINCT m.id FROM mods m + INNER JOIN delphi_tech_review_queue queue ON queue.project_id = m.id WHERE EXISTS( SELECT 1 FROM delphi_issue_details_with_statuses didws - INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id WHERE didws.project_id = m.id AND didws.status = 'pending' - -- see delphi.rs todo comment - AND dri.issue_type != '__dummy' + AND didws.severity != 'hidden' ) AND NOT EXISTS( SELECT 1 FROM delphi_issue_details_with_statuses didws - INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id WHERE didws.project_id = m.id AND didws.status IN ('safe', 'unsafe') - -- see delphi.rs todo comment - AND dri.issue_type != '__dummy' + AND didws.severity != 'hidden' ) "#, ) diff --git a/apps/labrinth/src/routes/internal/delphi/tech_review_queue.rs b/apps/labrinth/src/routes/internal/delphi/tech_review_queue.rs new file mode 100644 index 0000000000..de55b758df --- /dev/null +++ b/apps/labrinth/src/routes/internal/delphi/tech_review_queue.rs @@ -0,0 +1,203 @@ +//! Maintains explicit project membership in the technical review queue. +//! +//! Queue membership is represented by a row in `delphi_tech_review_queue`. +//! Enter and exit thread messages are emitted only when an insert or delete +//! actually changes that membership, in the same transaction. + +use itertools::Itertools; + +use crate::{ + database::{ + PgTransaction, + models::{DBProjectId, DBThreadId, thread_item::ThreadMessageBuilder}, + }, + models::threads::MessageBody, + routes::ApiError, + util::error::Context, +}; + +#[derive(Debug, Clone, Copy)] +pub enum TechReviewRemovalReason { + RulesChanged, + FileDeleted, +} + +pub async fn add_projects( + project_ids: &[DBProjectId], + txn: &mut PgTransaction<'_>, +) -> Result<(), ApiError> { + let project_ids = project_ids.iter().copied().unique().collect::>(); + if project_ids.is_empty() { + return Ok(()); + } + + let rows = sqlx::query!( + r#" + WITH inserted AS ( + INSERT INTO delphi_tech_review_queue (project_id) + SELECT unnest($1::bigint[]) + ON CONFLICT (project_id) DO NOTHING + RETURNING project_id + ) + SELECT + inserted.project_id AS "project_id!: DBProjectId", + ( + SELECT thread.id + FROM threads thread + WHERE thread.mod_id = inserted.project_id + ORDER BY thread.id + LIMIT 1 + ) AS "thread_id!: DBThreadId" + FROM inserted + "#, + &project_ids.iter().map(|id| id.0).collect::>(), + ) + .fetch_all(&mut *txn) + .await + .wrap_internal_err("failed to add projects to technical review queue")?; + + for row in rows { + ThreadMessageBuilder { + author_id: None, + body: MessageBody::TechReviewEntered, + thread_id: row.thread_id, + hide_identity: false, + } + .insert(txn) + .await + .wrap_internal_err("failed to add entering technical review message")?; + } + + Ok(()) +} + +pub async fn remove_projects( + project_ids: &[DBProjectId], + reason: TechReviewRemovalReason, + txn: &mut PgTransaction<'_>, +) -> Result<(), ApiError> { + let project_ids = project_ids.iter().copied().unique().collect::>(); + if project_ids.is_empty() { + return Ok(()); + } + + let rows = sqlx::query!( + r#" + WITH removed AS ( + DELETE FROM delphi_tech_review_queue + WHERE project_id = ANY($1::bigint[]) + RETURNING project_id + ) + SELECT + removed.project_id AS "project_id!: DBProjectId", + ( + SELECT thread.id + FROM threads thread + WHERE thread.mod_id = removed.project_id + ORDER BY thread.id + LIMIT 1 + ) AS "thread_id!: DBThreadId" + FROM removed + "#, + &project_ids.iter().map(|id| id.0).collect::>(), + ) + .fetch_all(&mut *txn) + .await + .wrap_internal_err( + "failed to remove projects from technical review queue", + )?; + + let body = match reason { + TechReviewRemovalReason::RulesChanged => MessageBody::TechReviewExited, + TechReviewRemovalReason::FileDeleted => { + MessageBody::TechReviewExitFileDeleted + } + }; + + for row in rows { + ThreadMessageBuilder { + author_id: None, + body: body.clone(), + thread_id: row.thread_id, + hide_identity: false, + } + .insert(txn) + .await + .wrap_internal_err("failed to add exiting technical review message")?; + } + + Ok(()) +} + +pub async fn add_projects_with_review_details( + project_ids: &[DBProjectId], + txn: &mut PgTransaction<'_>, +) -> Result<(), ApiError> { + let project_ids = project_ids.iter().copied().unique().collect::>(); + if project_ids.is_empty() { + return Ok(()); + } + + let rows = sqlx::query!( + r#" + SELECT DISTINCT detail.project_id AS "project_id!: DBProjectId" + FROM delphi_issue_details_with_statuses detail + WHERE + detail.project_id = ANY($1::bigint[]) + AND detail.status IN ('pending', 'unsafe') + AND detail.severity != 'hidden' + "#, + &project_ids.iter().map(|id| id.0).collect::>(), + ) + .fetch_all(&mut *txn) + .await + .wrap_internal_err("failed to find projects requiring technical review")?; + + add_projects( + &rows + .into_iter() + .map(|row| row.project_id) + .collect::>(), + txn, + ) + .await +} + +pub async fn remove_projects_without_details( + project_ids: &[DBProjectId], + reason: TechReviewRemovalReason, + txn: &mut PgTransaction<'_>, +) -> Result<(), ApiError> { + let project_ids = project_ids.iter().copied().unique().collect::>(); + if project_ids.is_empty() { + return Ok(()); + } + + let rows = sqlx::query!( + r#" + SELECT requested.project_id AS "project_id!: DBProjectId" + FROM unnest($1::bigint[]) AS requested(project_id) + WHERE NOT EXISTS ( + SELECT 1 + FROM delphi_issue_details_with_statuses detail + WHERE detail.project_id = requested.project_id + ) + "#, + &project_ids.iter().map(|id| id.0).collect::>(), + ) + .fetch_all(&mut *txn) + .await + .wrap_internal_err( + "failed to find projects without technical review details", + )?; + + remove_projects( + &rows + .into_iter() + .map(|row| row.project_id) + .collect::>(), + reason, + txn, + ) + .await +} diff --git a/apps/labrinth/src/routes/internal/delphi/tech_review_sync.rs b/apps/labrinth/src/routes/internal/delphi/tech_review_sync.rs deleted file mode 100644 index ca80c6e9e9..0000000000 --- a/apps/labrinth/src/routes/internal/delphi/tech_review_sync.rs +++ /dev/null @@ -1,427 +0,0 @@ -//! Synchronizes moderation thread messages and dummy queue blockers with the -//! current computed tech review state of affected projects. -//! -//! When a project has a Delphi report submitted for it, or when a moderator -//! updates one of its issue details' rows (like flagging a detail as *globally* -//! safe or unsafe), we will need to recheck if the project still belongs in the -//! tech review queue or if it needs to exit now. -//! -//! Side-note: "entering the queue" or "exiting the queue" right now just means -//! adding a new message to the project's moderation thread which indicates if -//! it entered/exited. In the future this should be replaced with a more proper -//! audit log table, or "is project currently in tech review" table. -//! -//! A project is considered to need tech review when it has at least one -//! non-dummy issue detail whose effective status is pending or unsafe, or when -//! it already has a dummy pending detail blocking the final review submission. -//! Effective status is just the local detail's verdict (from -//! `delphi_issue_detail_verdicts`), or if it's null then the global verdict for -//! the same `drid.key` (from `delphi_global_detail_verdicts`). -//! -//! Some examples of how this behavior manifests: let's assume you have projects -//! _A_ and _B_ currently in tech review. They each have one (unresolved) issue -//! detail with key _K_. -//! - If you mark _K_ on _A_ as locally safe/unsafe, then _A_ is fully resolved, -//! but we still have the `__dummy` detail, which means it's still in the -//! queue until the moderator submits the actual report. _B_ is entirely -//! unaffected. -//! - If you mark _K_ on _A_ as globally safe, then _A_ and _B_ both get fully -//! resolved, but both still have the `__dummy` detail, so they also still -//! need the final report to be submitted by the moderator. -//! -//! In practice, this means that some projects may have e.g. "100/100 traces -//! are safe" reported, but they will just be waiting for final moderator -//! approval. -//! -//! The logic for checking whether a project is now in tech review or not, and -//! sending the appropriate message, is complex! That's why this module exists: -//! to act as a single chokepoint which (correctly) syncs all the state, instead -//! of having each mutation run its own ad-hoc update logic. - -use crate::util::error::ApiContext as _; -use itertools::Itertools; - -use crate::{ - database::{ - PgTransaction, - models::{ - DBProjectId, DBThreadId, DelphiReportId, - delphi_report_item::DelphiVerdict, - thread_item::ThreadMessageBuilder, - }, - }, - models::threads::MessageBody, - routes::ApiError, - util::error::Context, -}; - -const DUMMY_ISSUE_TYPE: &str = "__dummy"; - -#[derive(Debug, Clone, Copy)] -pub enum TechReviewExitReason { - Resolved, - FileDeleted, -} - -struct ProjectTechReviewState { - has_pending_detail: bool, - has_unsafe_detail: bool, - has_dummy: bool, - thread_id: Option, - report_id: Option, - last_tech_review_message_type: Option, -} - -pub async fn sync_project_tech_review_state( - project_ids: &[DBProjectId], - exit_reason: TechReviewExitReason, - txn: &mut PgTransaction<'_>, -) -> Result<(), ApiError> { - let project_ids = project_ids.iter().copied().unique().collect::>(); - if project_ids.is_empty() { - return Ok(()); - } - - let project_ids_raw = project_ids.iter().map(|id| id.0).collect::>(); - let tech_review_message_types = tech_review_message_types(); - - let rows = sqlx::query!( - r#" - WITH project_ids AS ( - SELECT unnest($1::bigint[]) AS project_id - ), - detail_states AS ( - SELECT - p.project_id, - COALESCE( - BOOL_OR( - didws.status = 'pending' - AND dri.issue_type != $3 - ), - FALSE - ) AS has_pending_detail, - COALESCE( - BOOL_OR( - didws.status = 'unsafe' - AND dri.issue_type != $3 - ), - FALSE - ) AS has_unsafe_detail, - COALESCE( - BOOL_OR( - didws.status = 'pending' - AND dri.issue_type = $3 - ), - FALSE - ) AS has_dummy - FROM project_ids p - LEFT JOIN delphi_issue_details_with_statuses didws - ON didws.project_id = p.project_id - LEFT JOIN delphi_report_issues dri ON dri.id = didws.issue_id - GROUP BY p.project_id - ) - SELECT - p.project_id AS "project_id!: DBProjectId", - p.has_pending_detail AS "has_pending_detail!", - p.has_unsafe_detail AS "has_unsafe_detail!", - p.has_dummy AS "has_dummy!", - ( - SELECT t.id - FROM threads t - WHERE t.mod_id = p.project_id - ORDER BY t.id - LIMIT 1 - ) AS "thread_id: DBThreadId", - ( - SELECT tm.body->>'type' - FROM threads t - INNER JOIN threads_messages tm ON tm.thread_id = t.id - WHERE - t.mod_id = p.project_id - AND tm.body->>'type' = ANY($2::text[]) - ORDER BY tm.created DESC, tm.id DESC - LIMIT 1 - ) AS "last_tech_review_message_type", - ( - SELECT dr.id - FROM versions v - INNER JOIN files f ON f.version_id = v.id - INNER JOIN delphi_reports dr ON dr.file_id = f.id - WHERE v.mod_id = p.project_id - ORDER BY dr.created DESC, dr.id DESC - LIMIT 1 - ) AS "report_id: DelphiReportId" - FROM detail_states p - "#, - &project_ids_raw, - &tech_review_message_types, - DUMMY_ISSUE_TYPE, - ) - .fetch_all(&mut *txn) - .await - .wrap_internal_err("failed to fetch project tech review state")?; - - for row in rows { - let state = ProjectTechReviewState { - has_pending_detail: row.has_pending_detail, - has_unsafe_detail: row.has_unsafe_detail, - has_dummy: row.has_dummy, - thread_id: row.thread_id, - report_id: row.report_id, - last_tech_review_message_type: row.last_tech_review_message_type, - }; - - sync_one_project_tech_review_state(state, exit_reason, txn) - .await - .wrap_api_err("executing `sync_one_project_tech_review_state`")?; - } - - Ok(()) -} - -pub async fn sync_detail_key_tech_review_state( - detail_keys: &[String], - exit_reason: TechReviewExitReason, - txn: &mut PgTransaction<'_>, -) -> Result<(), ApiError> { - let detail_keys = detail_keys.iter().cloned().unique().collect::>(); - - if detail_keys.is_empty() { - return Ok(()); - } - - let rows = sqlx::query!( - r#" - SELECT DISTINCT didws.project_id AS "project_id!: DBProjectId" - FROM delphi_issue_details_with_statuses didws - WHERE didws.key = ANY($1::text[]) - "#, - &detail_keys, - ) - .fetch_all(&mut *txn) - .await - .wrap_internal_err("failed to fetch projects affected by detail keys")?; - - let project_ids = rows - .into_iter() - .map(|row| row.project_id) - .collect::>(); - - sync_project_tech_review_state(&project_ids, exit_reason, txn).await -} - -pub async fn sync_deleted_project_tech_review_exit( - project_id: DBProjectId, - txn: &mut PgTransaction<'_>, -) -> Result<(), ApiError> { - let tech_review_message_types = tech_review_message_types(); - - let row = sqlx::query!( - r#" - SELECT - ( - SELECT t.id - FROM threads t - WHERE t.mod_id = $1 - ORDER BY t.id - LIMIT 1 - ) AS "thread_id: DBThreadId", - ( - SELECT tm.body->>'type' - FROM threads t - INNER JOIN threads_messages tm ON tm.thread_id = t.id - WHERE - t.mod_id = $1 - AND tm.body->>'type' = ANY($2::text[]) - ORDER BY tm.created DESC, tm.id DESC - LIMIT 1 - ) AS "last_tech_review_message_type" - "#, - project_id as DBProjectId, - &tech_review_message_types, - ) - .fetch_one(&mut *txn) - .await - .wrap_internal_err("failed to fetch deleted project tech review state")?; - - if let Some(thread_id) = row.thread_id - && should_send_exit(row.last_tech_review_message_type.as_deref()) - { - insert_exit_message(thread_id, TechReviewExitReason::FileDeleted, txn) - .await - .wrap_api_err("executing `insert_exit_message`")?; - } - - Ok(()) -} - -async fn sync_one_project_tech_review_state( - state: ProjectTechReviewState, - exit_reason: TechReviewExitReason, - txn: &mut PgTransaction<'_>, -) -> Result<(), ApiError> { - let needs_tech_review = - state.has_pending_detail || state.has_unsafe_detail || state.has_dummy; - - if needs_tech_review { - if (state.has_pending_detail || state.has_unsafe_detail) - && !state.has_dummy - && let Some(report_id) = state.report_id - { - // TODO: Currently, the queue query determines whether a project is - // in tech review by checking whether it has any pending issue - // details. If all visible issue details are marked safe or unsafe - // before the final report is submitted, the project would otherwise - // leave the tech review queue without a final tech review verdict - // message. - // - // This should be replaced with explicit tech review state, such as - // an append-only project tech review event table where the latest - // enter/exit event is the current state. Until then, this dummy - // issue detail acts as the pending queue blocker. - ensure_dummy_issue_detail(report_id, txn) - .await - .wrap_api_err("validating dummy issue detail")?; - } - - if let Some(thread_id) = state.thread_id - && state.last_tech_review_message_type.as_deref() - != Some(MessageBody::TechReviewEntered.as_ref()) - { - ThreadMessageBuilder { - author_id: None, - body: MessageBody::TechReviewEntered, - thread_id, - hide_identity: false, - } - .insert(txn) - .await - .wrap_internal_err("failed to add entering tech review message")?; - } - - return Ok(()); - } - - if matches!(exit_reason, TechReviewExitReason::Resolved) - && state.last_tech_review_message_type.as_deref() - == Some(MessageBody::TechReviewEntered.as_ref()) - { - if let Some(report_id) = state.report_id { - ensure_dummy_issue_detail(report_id, txn) - .await - .wrap_api_err("validating dummy issue detail")?; - } - - return Ok(()); - } - - if let Some(thread_id) = state.thread_id - && should_send_exit(state.last_tech_review_message_type.as_deref()) - { - insert_exit_message(thread_id, exit_reason, txn) - .await - .wrap_api_err("executing `insert_exit_message`")?; - } - - Ok(()) -} - -fn should_send_exit(last_tech_review_message_type: Option<&str>) -> bool { - matches!(last_tech_review_message_type, Some(message_type) if !matches!( - message_type, - message_type if message_type == MessageBody::TechReviewExited.as_ref() - || message_type == MessageBody::TechReviewExitFileDeleted.as_ref() - || message_type == tech_review_completed_message_type() - )) -} - -async fn insert_exit_message( - thread_id: DBThreadId, - exit_reason: TechReviewExitReason, - txn: &mut PgTransaction<'_>, -) -> Result<(), ApiError> { - let body = match exit_reason { - TechReviewExitReason::Resolved => MessageBody::TechReviewExited, - TechReviewExitReason::FileDeleted => { - MessageBody::TechReviewExitFileDeleted - } - }; - - ThreadMessageBuilder { - author_id: None, - body, - thread_id, - hide_identity: false, - } - .insert(txn) - .await - .wrap_internal_err("failed to add exiting tech review message")?; - - Ok(()) -} - -async fn ensure_dummy_issue_detail( - report_id: DelphiReportId, - txn: &mut PgTransaction<'_>, -) -> Result<(), ApiError> { - sqlx::query!( - r#" - WITH dummy_issue AS ( - INSERT INTO delphi_report_issues (report_id, issue_type) - VALUES ($1, $2) - ON CONFLICT (report_id, issue_type) - DO UPDATE SET issue_type = EXCLUDED.issue_type - RETURNING id - ) - INSERT INTO delphi_report_issue_details ( - issue_id, - key, - jar, - file_path, - decompiled_source, - data, - severity - ) - SELECT - id, - '', - NULL, - '', - NULL, - '{}'::jsonb, - 'low'::delphi_severity - FROM dummy_issue - WHERE NOT EXISTS ( - SELECT 1 - FROM delphi_report_issue_details drid - WHERE drid.issue_id = dummy_issue.id - ) - "#, - report_id as DelphiReportId, - DUMMY_ISSUE_TYPE, - ) - .execute(&mut *txn) - .await - .wrap_internal_err("failed to ensure dummy Delphi report issue detail")?; - - Ok(()) -} - -fn tech_review_message_types() -> Vec { - [ - MessageBody::TechReviewEntered.as_ref(), - MessageBody::TechReviewExited.as_ref(), - MessageBody::TechReviewExitFileDeleted.as_ref(), - tech_review_completed_message_type(), - ] - .into_iter() - .map(|message_type| message_type.to_string()) - .collect() -} - -fn tech_review_completed_message_type() -> &'static str { - MessageBody::TechReview { - verdict: DelphiVerdict::Safe, - } - .as_ref() -} diff --git a/apps/labrinth/src/routes/internal/mod.rs b/apps/labrinth/src/routes/internal/mod.rs index d7cfa39d96..44932a2e6f 100644 --- a/apps/labrinth/src/routes/internal/mod.rs +++ b/apps/labrinth/src/routes/internal/mod.rs @@ -121,6 +121,15 @@ pub fn config(cfg: &mut web::ServiceConfig) { moderation::tech_review::search_projects, moderation::tech_review::global::search_global_issue_details, moderation::tech_review::global::get_global_issue_detail, + moderation::tech_review::rules::get_rules, + moderation::tech_review::rules::test_rule, + moderation::tech_review::rules::get_rule_affected_details, + moderation::tech_review::rules::create_rule, + moderation::tech_review::rules::update_rule, + moderation::tech_review::rules::delete_rule, + moderation::tech_review::rules_scan::get_rule_schema, + moderation::tech_review::rules_scan::get_detail_rule_input, + moderation::tech_review::rules_scan::scan_rules, moderation::tech_review::get_project_report, moderation::tech_review::submit_report, moderation::tech_review::update_issue_details, diff --git a/apps/labrinth/src/routes/internal/moderation/mod.rs b/apps/labrinth/src/routes/internal/moderation/mod.rs index b6d28c760a..31e807d7d8 100644 --- a/apps/labrinth/src/routes/internal/moderation/mod.rs +++ b/apps/labrinth/src/routes/internal/moderation/mod.rs @@ -297,6 +297,7 @@ pub async fn get_projects_internal( FROM delphi_issue_details_with_statuses didws WHERE didws.project_id = m.id AND didws.status = 'pending' + AND didws.severity != 'hidden' ) ) ), @@ -546,6 +547,7 @@ pub async fn get_projects_internal( FROM delphi_issue_details_with_statuses didws WHERE didws.project_id = m.id AND didws.status = 'pending' + AND didws.severity != 'hidden' ) ) ), @@ -777,6 +779,7 @@ pub async fn get_project_ids( FROM delphi_issue_details_with_statuses didws WHERE didws.project_id = m.id AND didws.status = 'pending' + AND didws.severity != 'hidden' ) ) ), @@ -905,6 +908,7 @@ pub async fn get_project_ids( FROM delphi_issue_details_with_statuses didws WHERE didws.project_id = mods.id AND didws.status = 'pending' + AND didws.severity != 'hidden' ) ) ORDER BY diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index f41f116d1e..9e583a5599 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -34,10 +34,7 @@ use crate::{ queue::session::AuthQueue, routes::{ ApiError, - internal::{ - delphi::tech_review_sync::{self, TechReviewExitReason}, - moderation::Ownership, - }, + internal::{delphi::tech_review_queue, moderation::Ownership}, }, search::SearchState, util::error::Context, @@ -45,10 +42,14 @@ use crate::{ use eyre::eyre; pub mod global; +pub mod rules; +pub mod rules_scan; pub fn config(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(search_projects) .configure(global::config) + .configure(rules::config) + .configure(rules_scan::config) .service(get_project_report) .service(get_report) .service(get_issue) @@ -204,11 +205,18 @@ pub enum FlagReason { Delphi, } +#[derive(Debug, Deserialize, utoipa::IntoParams)] +pub struct GetIssue { + #[serde(default)] + pub include_hidden: bool, +} + /// Get a Delphi report issue. #[utoipa::path( context_path = "/moderation/tech-review", tag = "moderation", security(("bearer_auth" = [])), + params(GetIssue), responses((status = OK, body = inline(FileIssue))) )] #[get("/issue/{issue_id}")] @@ -218,6 +226,7 @@ pub async fn get_issue( redis: web::Data, session_queue: web::Data, path: web::Path<(DelphiReportIssueId,)>, + query: web::Query, ) -> Result, ApiError> { check_is_moderator_from_headers( &req, @@ -252,13 +261,16 @@ pub async fn get_issue( ) ), '[]'::jsonb) FROM delphi_issue_details_with_statuses didws - WHERE didws.issue_id = dri.id + WHERE + didws.issue_id = dri.id + AND ($2 OR didws.severity != 'hidden') ) ) AS "data!: sqlx::types::Json" FROM delphi_report_issues dri WHERE dri.id = $1 "#, issue_id as DelphiReportIssueId, + query.include_hidden, ) .fetch_optional(&**pool) .await @@ -308,6 +320,15 @@ pub async fn get_report( 'file_size', f.size, 'flag_reason', 'delphi', 'download_url', f.url, + 'severity', COALESCE(( + SELECT MAX(didws.severity) + FROM delphi_report_issues severity_issue + INNER JOIN delphi_issue_details_with_statuses didws + ON didws.issue_id = severity_issue.id + WHERE + severity_issue.report_id = dr.id + AND didws.severity != 'hidden' + ), 'low'::delphi_severity), -- TODO: replace with `json_array` in Postgres 16 'issues', ( SELECT coalesce(json_agg( @@ -330,15 +351,22 @@ pub async fn get_report( ) ), '[]'::jsonb) FROM delphi_issue_details_with_statuses didws - WHERE didws.issue_id = dri.id + WHERE + didws.issue_id = dri.id + AND didws.severity != 'hidden' ) ) ), '[]'::json) FROM delphi_report_issues dri WHERE - dri.report_id = dr.id - -- see delphi.rs todo comment - AND dri.issue_type != '__dummy' + dri.report_id = dr.id + AND EXISTS ( + SELECT 1 + FROM delphi_issue_details_with_statuses visible_detail + WHERE + visible_detail.issue_id = dri.id + AND visible_detail.severity != 'hidden' + ) ) ) AS "data!: sqlx::types::Json" FROM delphi_reports dr @@ -425,6 +453,7 @@ async fn fetch_project_reports( project_ids: &[DBProjectId], pool: &PgPool, redis: &RedisPool, + include_hidden: bool, ) -> Result, ApiError> { struct FileRow { file_id: DBFileId, @@ -535,10 +564,13 @@ async fn fetch_project_reports( didws.global_status AS "global_status?: DelphiStatus", didws.status AS "status!: DelphiStatus" FROM delphi_issue_details_with_statuses didws - WHERE didws.issue_id = ANY($1::bigint[]) + WHERE + didws.issue_id = ANY($1::bigint[]) + AND ($2 OR didws.severity != 'hidden') ORDER BY didws.issue_id, didws.id "#, - &issue_ids.iter().map(|i| i.0).collect::>() + &issue_ids.iter().map(|i| i.0).collect::>(), + include_hidden ) .fetch_all(pool) .await @@ -633,14 +665,14 @@ async fn fetch_project_reports( let mut file_issues = Vec::new(); for issue_row in report_issues { - if issue_row.issue_type == "__dummy" { - continue; - } - let issue_details = details_by_issue .get(&issue_row.id) .unwrap_or(&empty_details); + if issue_details.is_empty() { + continue; + } + file_issues.push(FileIssue { id: issue_row.id, report_id: issue_row.report_id, @@ -649,12 +681,23 @@ async fn fetch_project_reports( }); } + if file_issues.is_empty() { + continue; + } + + let severity = file_issues + .iter() + .flat_map(|issue| issue.details.iter()) + .map(|detail| detail.severity) + .max() + .unwrap_or(report_row.severity); + file_reports.push(FileReport { report_id: report_row.report_id, file_id: FileId::from(file_row.file_id), created: report_row.created, flag_reason: FlagReason::Delphi, - severity: report_row.severity, + severity, file_name: file_row.filename.clone(), file_size: file_row.size, download_url: file_row.url.clone(), @@ -735,13 +778,15 @@ pub async fn search_projects( m.id AS "project_id: DBProjectId", MIN(t.id) AS "thread_id!: DBThreadId" FROM mods m + INNER JOIN delphi_tech_review_queue trq ON trq.project_id = m.id 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 - INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id - INNER JOIN delphi_issue_details_with_statuses didws + 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_issue_details_with_statuses didws ON didws.issue_id = dri.id + AND didws.severity != 'hidden' LEFT JOIN threads_messages tm_last ON tm_last.thread_id = t.id AND tm_last.id = ( @@ -785,8 +830,25 @@ pub async fn search_projects( ) AND m.status NOT IN ('draft', 'rejected', 'withheld') AND (cardinality($6::text[]) = 0 OR m.status = ANY($6::text[])) - AND (cardinality($7::text[]) = 0 OR dri.issue_type = ANY($7::text[])) - AND didws.status = 'pending' + AND ( + cardinality($7::text[]) = 0 + OR EXISTS ( + SELECT 1 + FROM versions issue_version + INNER JOIN files issue_file + ON issue_file.version_id = issue_version.id + INNER JOIN delphi_reports issue_report + ON issue_report.file_id = issue_file.id + INNER JOIN delphi_report_issues issue + ON issue.report_id = issue_report.id + INNER JOIN delphi_issue_details_with_statuses detail + ON detail.issue_id = issue.id + WHERE + issue_version.mod_id = m.id + AND issue.issue_type = ANY($7::text[]) + AND detail.severity != 'hidden' + ) + ) 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'))) @@ -796,8 +858,8 @@ pub async fn search_projects( ORDER BY CASE WHEN $3 = 'created_asc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END ASC, CASE WHEN $3 = 'created_desc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END DESC, - CASE WHEN $3 = 'severity_asc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END ASC, - CASE WHEN $3 = 'severity_desc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END DESC, + CASE WHEN $3 = 'severity_asc' THEN COALESCE(MAX(didws.severity), 'low'::delphi_severity) ELSE 'low'::delphi_severity END ASC, + CASE WHEN $3 = 'severity_desc' THEN COALESCE(MAX(didws.severity), 'low'::delphi_severity) ELSE 'low'::delphi_severity END DESC, -- tie-breaker: oldest reports MIN(dr.created) ASC LIMIT $1 OFFSET $2 @@ -826,9 +888,10 @@ pub async fn search_projects( thread_ids.push(row.thread_id); } - let project_reports = fetch_project_reports(&project_ids, &pool, &redis) - .await - .wrap_api_err("fetching project reports")?; + let project_reports = + fetch_project_reports(&project_ids, &pool, &redis, false) + .await + .wrap_api_err("fetching project reports")?; let projects = DBProject::get_many_ids(&project_ids, &**pool, &redis) .await @@ -941,7 +1004,7 @@ pub async fn get_project_report( .wrap_not_found_err("resource not found")?; let project_reports = - fetch_project_reports(&[db_project_id], &pool, &redis) + fetch_project_reports(&[db_project_id], &pool, &redis, true) .await .wrap_api_err("fetching project reports")?; @@ -1047,8 +1110,7 @@ pub async fn submit_report( WHERE m.id = $1 AND didws.status = 'pending' - -- see delphi.rs todo comment - AND dri.issue_type != '__dummy' + AND didws.severity != 'hidden' "#, project_id as _, ) @@ -1066,24 +1128,18 @@ pub 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 _, + sqlx::query_scalar!( + r#" + DELETE FROM delphi_tech_review_queue + WHERE project_id = $1 + RETURNING project_id AS "project_id: DBProjectId" + "#, + project_id as DBProjectId, ) - .execute(&mut txn) + .fetch_optional(&mut txn) .await - .wrap_internal_err("failed to delete dummy issue")?; + .wrap_internal_err("failed to remove project from technical review queue")? + .wrap_not_found_err("project not found in technical review queue")?; let record = sqlx::query!( r#" @@ -1272,10 +1328,6 @@ pub async fn update_issue_details( i.verdict FROM incoming i INNER JOIN delphi_issue_details_with_statuses didws ON didws.id = i.detail_id - INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id - WHERE - -- see delphi.rs todo comment - dri.issue_type != '__dummy' ), validated AS ( SELECT @@ -1339,10 +1391,7 @@ pub async fn update_issue_details( r#" SELECT DISTINCT didws.project_id AS "project_id!: DBProjectId" FROM delphi_issue_details_with_statuses didws - INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id - WHERE - didws.id = ANY($1::bigint[]) - AND dri.issue_type != '__dummy' + WHERE didws.id = ANY($1::bigint[]) "#, &detail_ids, ) @@ -1357,9 +1406,8 @@ pub async fn update_issue_details( .map(|row| row.project_id) .collect::>(); - tech_review_sync::sync_project_tech_review_state( + tech_review_queue::add_projects_with_review_details( &affected_project_ids, - TechReviewExitReason::Resolved, &mut txn, ) .await @@ -1384,7 +1432,7 @@ pub async fn update_issue_details( security(("bearer_auth" = [])), responses((status = NO_CONTENT)) )] -#[post("/global-issue-detail")] +#[post("/global-traces")] pub async fn update_global_issue_details( req: HttpRequest, pool: web::Data, @@ -1477,9 +1525,25 @@ pub async fn update_global_issue_details( .await .wrap_internal_err("failed to update global issue details")?; - tech_review_sync::sync_detail_key_tech_review_state( + let affected_projects = sqlx::query!( + r#" + SELECT DISTINCT detail.project_id AS "project_id!: DBProjectId" + FROM delphi_issue_details_with_statuses detail + WHERE detail.key = ANY($1::text[]) + "#, &detail_keys, - TechReviewExitReason::Resolved, + ) + .fetch_all(&mut txn) + .await + .wrap_internal_err( + "failed to fetch projects affected by global detail updates", + )?; + + tech_review_queue::add_projects_with_review_details( + &affected_projects + .into_iter() + .map(|row| row.project_id) + .collect::>(), &mut txn, ) .await diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review/global.rs b/apps/labrinth/src/routes/internal/moderation/tech_review/global.rs index 84fbe00fe3..3ca452aea9 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review/global.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review/global.rs @@ -133,7 +133,7 @@ pub struct GlobalIssueDetailTrace { security(("bearer_auth" = [])), responses((status = OK, body = SearchGlobalIssueDetailsResponse)) )] -#[post("/global-issue-detail/search")] +#[post("/global-traces/search")] pub async fn search_global_issue_details( req: HttpRequest, pool: web::Data, @@ -199,9 +199,9 @@ pub async fn search_global_issue_details( FROM delphi_global_detail_verdicts dgdv LEFT JOIN delphi_issue_details_with_statuses didws ON didws.key = dgdv.detail_key + AND didws.severity != 'hidden' LEFT JOIN delphi_report_issues dri ON dri.id = didws.issue_id - AND dri.issue_type != '__dummy' WHERE ( $1::text IS NULL OR dgdv.detail_key ILIKE '%' || $1 || '%' @@ -269,6 +269,7 @@ pub async fn search_global_issue_details( AND didv.detail_key = didws.key WHERE didws.key = ANY($1::text[]) + AND didws.severity != 'hidden' AND dri.issue_type != '__dummy' AND ( $3::text IS NULL @@ -356,7 +357,7 @@ pub async fn search_global_issue_details( security(("bearer_auth" = [])), responses((status = OK, body = GetGlobalIssueDetailResponse)) )] -#[post("/global-issue-detail/local-traces")] +#[post("/global-traces/local-traces")] pub async fn get_global_issue_detail( req: HttpRequest, pool: web::Data, @@ -395,9 +396,9 @@ pub async fn get_global_issue_detail( FROM delphi_global_detail_verdicts dgdv LEFT JOIN delphi_issue_details_with_statuses didws ON didws.key = dgdv.detail_key + AND didws.severity != 'hidden' LEFT JOIN delphi_report_issues dri ON dri.id = didws.issue_id - AND dri.issue_type != '__dummy' WHERE dgdv.detail_key = $1 GROUP BY dgdv.detail_key, dgdv.verdict "#, @@ -440,7 +441,7 @@ pub async fn get_global_issue_detail( WHERE didws.key = $1 AND ($2::bigint IS NULL OR didws.id > $2) - AND dri.issue_type != '__dummy' + AND didws.severity != 'hidden' ORDER BY didws.id LIMIT $3 "#, diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review/rules.rs b/apps/labrinth/src/routes/internal/moderation/tech_review/rules.rs new file mode 100644 index 0000000000..0848065045 --- /dev/null +++ b/apps/labrinth/src/routes/internal/moderation/tech_review/rules.rs @@ -0,0 +1,740 @@ +use actix_web::{HttpRequest, delete, get, post, put, web}; +use chrono::{DateTime, Utc}; +use eyre::eyre; +use serde::{Deserialize, Serialize}; +use validator::Validate; +use xredis::RedisPool; + +use super::rules_scan::RuleInput; +use crate::{ + auth::check_is_moderator_from_headers, + database::{ + PgPool, ReadOnlyPgPool, + models::{ + DBProjectId, DBVersionId, DelphiReportIssueDetailsId, + DelphiReportIssueId, DelphiRuleId, + delphi_report_item::DelphiSeverity, + }, + }, + models::{ + ids::{ProjectId, VersionId}, + pats::Scopes, + }, + queue::session::AuthQueue, + routes::ApiError, + util::{cel, error::Context, validate::validation_errors_to_string}, +}; + +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(get_rules) + .service(test_rule) + .service(get_rule_affected_details) + .service(create_rule) + .service(update_rule) + .service(delete_rule); +} + +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct DelphiRule { + pub id: DelphiRuleId, + pub name: String, + pub rule: String, + pub priority: i32, + /// Issue types this rule applies to. An empty list means all issue types. + pub on_issue_types: Vec, + pub revision: i64, + pub current_revision: i64, + pub created_at: DateTime, + pub updated_at: DateTime, + pub created_by: Option, + pub updated_by: Option, + pub affected_details_count: i64, + pub affected_details: Vec, +} + +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct DelphiRuleAffectedDetail { + pub detail_id: DelphiReportIssueDetailsId, + pub issue_id: DelphiReportIssueId, + pub project_id: Option, + pub project_name: Option, + pub project_icon_url: Option, + pub version_id: Option, + pub version_name: Option, + pub version_number: Option, + pub issue_type: String, + pub key: String, + pub jar: Option, + pub file_path: String, + pub original_severity: DelphiSeverity, + pub severity: DelphiSeverity, +} + +fn default_affected_details_limit() -> u64 { + 5 +} + +#[derive(Debug, Deserialize, utoipa::ToSchema)] +pub struct GetRuleAffectedDetailsRequest { + #[serde(default = "default_affected_details_limit")] + #[schema(default = 3)] + pub limit: u64, + #[serde(default)] + #[schema(default = 0)] + pub page: u64, +} + +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct GetRuleAffectedDetailsResponse { + pub total: i64, + pub details: Vec, +} + +#[derive(Debug, Deserialize, Validate, utoipa::ToSchema)] +pub struct WriteDelphiRule { + #[validate(length(min = 1, max = 256))] + pub name: String, + #[validate(length(min = 1, max = 65536))] + pub rule: String, + #[serde(default)] + pub priority: i32, + /// Issue types this rule applies to. An empty list means all issue types. + #[serde(default)] + pub on_issue_types: Vec, +} + +#[derive(Deserialize, Validate, utoipa::ToSchema)] +pub struct TestDelphiRule { + #[validate(length(min = 1, max = 65536))] + pub rule: String, + #[validate(length(max = 10))] + pub inputs: Vec, +} + +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct TestDelphiRuleResponse { + pub effects: Vec>, +} + +#[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)] +#[serde(deny_unknown_fields)] +pub struct DelphiRuleEffect { + pub severity: DelphiSeverity, +} + +#[derive(Serialize, utoipa::ToSchema)] +#[serde(untagged)] +pub enum DelphiRuleOutput { + Severity(DelphiSeverity), + Effect(DelphiRuleEffect), +} + +struct ValidatedRule { + name: String, + rule: String, + priority: i32, + on_issue_types: Vec, +} + +impl WriteDelphiRule { + async fn validate(mut self) -> Result { + self.name = self.name.trim().to_string(); + self.rule = self.rule.trim().to_string(); + self.on_issue_types = self + .on_issue_types + .into_iter() + .map(|issue_type| issue_type.trim().to_string()) + .filter(|issue_type| !issue_type.is_empty()) + .collect(); + self.on_issue_types.sort(); + self.on_issue_types.dedup(); + Validate::validate(&self).map_err(|error| { + ApiError::Request(eyre!(validation_errors_to_string(error, None))) + })?; + + let expression = self.rule.clone(); + tokio::task::spawn_blocking(move || cel::Program::compile(&expression)) + .await + .wrap_internal_err("failed to join cel compilation task")? + .map_err(|error| { + ApiError::Request( + eyre!(error).wrap_err("invalid cel expression"), + ) + })?; + + Ok(ValidatedRule { + name: self.name, + rule: self.rule, + priority: self.priority, + on_issue_types: self.on_issue_types, + }) + } +} + +/// Evaluate a CEL rule against caller-provided issue traces without saving it. +#[utoipa::path( + context_path = "/moderation/tech-review", + tag = "moderation", + security(("bearer_auth" = [])), + request_body = TestDelphiRule, + responses((status = OK, body = TestDelphiRuleResponse)) +)] +#[post("/rules/test")] +pub async fn test_rule( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, + body: web::Json, +) -> Result, ApiError> { + check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_READ, + ) + .await + .wrap_auth_err("authenticating API request")?; + + let mut request = body.into_inner(); + request.rule = request.rule.trim().to_string(); + request.validate().map_err(|error| { + ApiError::Request(eyre!(validation_errors_to_string(error, None))) + })?; + + let rule = request.rule; + let expression = rule.clone(); + let program = + tokio::task::spawn_blocking(move || cel::Program::compile(&expression)) + .await + .wrap_internal_err("failed to join cel compilation task")? + .map_err(|error| { + ApiError::Request( + eyre!(error).wrap_err("invalid cel expression"), + ) + })?; + let mut effects = Vec::with_capacity(request.inputs.len()); + + for (index, input) in request.inputs.iter().enumerate() { + let evaluation = + super::rules_scan::evaluate_rule(&program, &rule, input); + let effect = evaluation.wrap_request_err(format!( + "failed to evaluate test input {index}" + ))?; + effects.push(effect); + } + + Ok(web::Json(TestDelphiRuleResponse { effects })) +} + +/// List all Delphi rules that are not pending deletion. +#[utoipa::path( + context_path = "/moderation/tech-review", + tag = "moderation", + security(("bearer_auth" = [])), + responses((status = OK, body = Vec)) +)] +#[get("/rules")] +pub async fn get_rules( + req: HttpRequest, + pool: web::Data, + ro_pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result>, ApiError> { + check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_READ, + ) + .await + .wrap_auth_err("authenticating API request")?; + + let rules = sqlx::query!( + r#" + SELECT + delphi_rule.id AS "id!: DelphiRuleId", + delphi_rule.name, + delphi_rule.rule, + delphi_rule.priority, + delphi_rule.on_issue_types, + delphi_rule.revision, + ( + SELECT revision FROM delphi_rule_revisions LIMIT 1 + ) AS "current_revision!", + delphi_rule.created_at, + delphi_rule.updated_at, + delphi_rule.created_by, + delphi_rule.updated_by, + COALESCE(preview.affected_details_count, 0) + AS "affected_details_count!", + preview.detail_id AS "detail_id?: DelphiReportIssueDetailsId", + preview.issue_id AS "issue_id?: DelphiReportIssueId", + preview.project_id AS "project_id?: DBProjectId", + preview.project_name AS "project_name?", + preview.project_icon_url AS "project_icon_url?", + preview.version_id AS "version_id?: DBVersionId", + preview.version_name AS "version_name?", + preview.version_number AS "version_number?", + preview.issue_type AS "issue_type?", + preview.key AS "key?", + preview.jar AS "jar?", + preview.file_path AS "file_path?", + preview.original_severity AS "original_severity?: DelphiSeverity", + preview.severity AS "effect_severity?: DelphiSeverity" + FROM delphi_rules delphi_rule + LEFT JOIN LATERAL ( + SELECT + effect.detail_id, + detail.issue_id, + version.mod_id AS project_id, + project.name AS project_name, + project.icon_url AS project_icon_url, + version.id AS version_id, + version.name AS version_name, + version.version_number, + issue.issue_type, + detail.key, + detail.jar, + detail.file_path, + detail.severity AS original_severity, + effect.severity, + COUNT(*) OVER () AS affected_details_count + FROM delphi_rule_effects effect + INNER JOIN delphi_rule_revisions published + ON published.revision = effect.revision + INNER JOIN delphi_report_issue_details detail + ON detail.id = effect.detail_id + INNER JOIN delphi_report_issues issue + ON issue.id = detail.issue_id + INNER JOIN delphi_reports report + ON report.id = issue.report_id + LEFT JOIN files file ON file.id = report.file_id + LEFT JOIN versions version ON version.id = file.version_id + LEFT JOIN mods project ON project.id = version.mod_id + WHERE effect.rule_id = delphi_rule.id + ORDER BY effect.detail_id DESC + LIMIT 3 + ) preview ON TRUE + WHERE NOT delphi_rule.delete_on_next_revision + ORDER BY + delphi_rule.priority DESC, + delphi_rule.id, + preview.detail_id DESC + "#, + ) + .fetch_all(&***ro_pool) + .await + .wrap_internal_err("failed to fetch delphi rules")?; + + let mut response = Vec::::new(); + for rule in rules { + let affected_detail = if let ( + Some(detail_id), + Some(issue_id), + Some(issue_type), + Some(key), + Some(file_path), + Some(original_severity), + Some(severity), + ) = ( + rule.detail_id, + rule.issue_id, + rule.issue_type, + rule.key, + rule.file_path, + rule.original_severity, + rule.effect_severity, + ) { + Some(DelphiRuleAffectedDetail { + detail_id, + issue_id, + project_id: rule.project_id.map(ProjectId::from), + project_name: rule.project_name, + project_icon_url: rule.project_icon_url, + version_id: rule.version_id.map(VersionId::from), + version_name: rule.version_name, + version_number: rule.version_number, + issue_type, + key, + jar: rule.jar, + file_path, + original_severity, + severity, + }) + } else { + None + }; + + if let Some(existing) = response.last_mut() + && existing.id == rule.id + { + if let Some(affected_detail) = affected_detail { + existing.affected_details.push(affected_detail); + } + continue; + } + + response.push(DelphiRule { + id: rule.id, + name: rule.name, + rule: rule.rule, + priority: rule.priority, + on_issue_types: rule.on_issue_types, + revision: rule.revision, + current_revision: rule.current_revision, + created_at: rule.created_at, + updated_at: rule.updated_at, + created_by: rule.created_by, + updated_by: rule.updated_by, + affected_details_count: rule.affected_details_count, + affected_details: affected_detail.into_iter().collect(), + }); + } + + Ok(web::Json(response)) +} + +/// List all details affected by a Delphi rule in the published revision. +#[utoipa::path( + context_path = "/moderation/tech-review", + tag = "moderation", + security(("bearer_auth" = [])), + params( + ("id" = DelphiRuleId, Path), + ("limit" = Option, Query), + ("page" = Option, Query) + ), + responses((status = OK, body = GetRuleAffectedDetailsResponse)) +)] +#[get("/rules/{id}/effects")] +pub async fn get_rule_affected_details( + req: HttpRequest, + pool: web::Data, + ro_pool: web::Data, + redis: web::Data, + session_queue: web::Data, + path: web::Path<(DelphiRuleId,)>, + query: web::Query, +) -> Result, ApiError> { + check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_READ, + ) + .await + .wrap_auth_err("authenticating API request")?; + let (rule_id,) = path.into_inner(); + let limit = query.limit.clamp(1, 100); + let offset = limit.saturating_mul(query.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 total = sqlx::query_scalar!( + r#" + SELECT COUNT(*) AS "count!" + FROM delphi_rule_effects effect + INNER JOIN delphi_rule_revisions published + ON published.revision = effect.revision + WHERE effect.rule_id = $1 + "#, + rule_id as DelphiRuleId, + ) + .fetch_one(&***ro_pool) + .await + .wrap_internal_err("failed to count details affected by delphi rule")?; + + let details = sqlx::query!( + r#" + SELECT + effect.detail_id AS "detail_id!: DelphiReportIssueDetailsId", + detail.issue_id AS "issue_id!: DelphiReportIssueId", + version.mod_id AS "project_id?: DBProjectId", + project.name AS "project_name?", + project.icon_url AS "project_icon_url?", + version.id AS "version_id?: DBVersionId", + version.name AS "version_name?", + version.version_number AS "version_number?", + issue.issue_type, + detail.key, + detail.jar, + detail.file_path, + detail.severity AS "original_severity!: DelphiSeverity", + effect.severity AS "effect_severity!: DelphiSeverity" + FROM delphi_rule_effects effect + INNER JOIN delphi_rule_revisions published + ON published.revision = effect.revision + INNER JOIN delphi_report_issue_details detail + ON detail.id = effect.detail_id + INNER JOIN delphi_report_issues issue ON issue.id = detail.issue_id + INNER JOIN delphi_reports report ON report.id = issue.report_id + LEFT JOIN files file ON file.id = report.file_id + LEFT JOIN versions version ON version.id = file.version_id + LEFT JOIN mods project ON project.id = version.mod_id + WHERE effect.rule_id = $1 + ORDER BY effect.detail_id DESC + LIMIT $2 OFFSET $3 + "#, + rule_id as DelphiRuleId, + limit, + offset, + ) + .fetch_all(&***ro_pool) + .await + .wrap_internal_err("failed to fetch details affected by delphi rule")?; + + Ok(web::Json(GetRuleAffectedDetailsResponse { + total, + details: details + .into_iter() + .map(|detail| DelphiRuleAffectedDetail { + detail_id: detail.detail_id, + issue_id: detail.issue_id, + project_id: detail.project_id.map(ProjectId::from), + project_name: detail.project_name, + project_icon_url: detail.project_icon_url, + version_id: detail.version_id.map(VersionId::from), + version_name: detail.version_name, + version_number: detail.version_number, + issue_type: detail.issue_type, + key: detail.key, + jar: detail.jar, + file_path: detail.file_path, + original_severity: detail.original_severity, + severity: detail.effect_severity, + }) + .collect(), + })) +} + +/// Create a Delphi rule. It will be applied by the next manual rule scan. +#[utoipa::path( + context_path = "/moderation/tech-review", + tag = "moderation", + security(("bearer_auth" = [])), + request_body = WriteDelphiRule, + responses((status = OK, body = DelphiRule)) +)] +#[post("/rules")] +pub async fn create_rule( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, + body: web::Json, +) -> Result, ApiError> { + let user = check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_WRITE, + ) + .await + .wrap_auth_err("authenticating API request")?; + let rule = body.into_inner().validate().await?; + let user_id = user.id.0 as i64; + + let rule = sqlx::query!( + r#" + INSERT INTO delphi_rules ( + name, + rule, + priority, + on_issue_types, + revision, + created_by, + updated_by + ) + VALUES ( + $1, + $2, + $3, + $4, + (SELECT revision + 1 FROM delphi_rule_revisions LIMIT 1), + $5, + $5 + ) + RETURNING + id AS "id!: DelphiRuleId", + name, + rule, + priority, + on_issue_types, + revision, + created_at, + updated_at, + created_by, + updated_by + "#, + rule.name, + rule.rule, + rule.priority, + &rule.on_issue_types, + user_id, + ) + .fetch_one(&**pool) + .await + .wrap_internal_err("failed to create delphi rule")?; + + Ok(web::Json(DelphiRule { + id: rule.id, + name: rule.name, + rule: rule.rule, + priority: rule.priority, + on_issue_types: rule.on_issue_types, + revision: rule.revision, + current_revision: rule.revision - 1, + created_at: rule.created_at, + updated_at: rule.updated_at, + created_by: rule.created_by, + updated_by: rule.updated_by, + affected_details_count: 0, + affected_details: Vec::new(), + })) +} + +/// Update a Delphi rule. Its materialized effects remain unchanged until the next scan. +#[utoipa::path( + context_path = "/moderation/tech-review", + tag = "moderation", + security(("bearer_auth" = [])), + request_body = WriteDelphiRule, + responses((status = OK, body = DelphiRule), (status = NOT_FOUND)) +)] +#[put("/rules/{id}")] +pub async fn update_rule( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, + path: web::Path<(DelphiRuleId,)>, + body: web::Json, +) -> Result, ApiError> { + let user = check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_WRITE, + ) + .await + .wrap_auth_err("authenticating API request")?; + let (id,) = path.into_inner(); + let rule = body.into_inner().validate().await?; + let user_id = user.id.0 as i64; + + let rule = sqlx::query!( + r#" + UPDATE delphi_rules + SET + name = $2, + rule = $3, + priority = $4, + on_issue_types = $5, + revision = ( + SELECT revision + 1 FROM delphi_rule_revisions LIMIT 1 + ), + updated_at = CURRENT_TIMESTAMP, + updated_by = $6 + WHERE id = $1 AND NOT delete_on_next_revision + RETURNING + id AS "id!: DelphiRuleId", + name, + rule, + priority, + on_issue_types, + revision, + created_at, + updated_at, + created_by, + updated_by + "#, + id as DelphiRuleId, + rule.name, + rule.rule, + rule.priority, + &rule.on_issue_types, + user_id, + ) + .fetch_optional(&**pool) + .await + .wrap_internal_err("failed to update delphi rule")? + .wrap_not_found_err("delphi rule not found")?; + + Ok(web::Json(DelphiRule { + id: rule.id, + name: rule.name, + rule: rule.rule, + priority: rule.priority, + on_issue_types: rule.on_issue_types, + revision: rule.revision, + current_revision: rule.revision - 1, + created_at: rule.created_at, + updated_at: rule.updated_at, + created_by: rule.created_by, + updated_by: rule.updated_by, + affected_details_count: 0, + affected_details: Vec::new(), + })) +} + +/// Mark a Delphi rule for deletion when the next rule scan is published. +#[utoipa::path( + context_path = "/moderation/tech-review", + tag = "moderation", + security(("bearer_auth" = [])), + responses((status = OK), (status = NOT_FOUND)) +)] +#[delete("/rules/{id}")] +pub async fn delete_rule( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, + path: web::Path<(DelphiRuleId,)>, +) -> Result<(), ApiError> { + let user = check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_WRITE, + ) + .await + .wrap_auth_err("authenticating API request")?; + let (id,) = path.into_inner(); + + let deleted = sqlx::query!( + r#" + UPDATE delphi_rules + SET + delete_on_next_revision = TRUE, + revision = ( + SELECT revision + 1 FROM delphi_rule_revisions LIMIT 1 + ), + updated_at = CURRENT_TIMESTAMP, + updated_by = $2 + WHERE id = $1 AND NOT delete_on_next_revision + RETURNING id + "#, + id as DelphiRuleId, + user.id.0 as i64, + ) + .fetch_optional(&**pool) + .await + .wrap_internal_err("failed to mark delphi rule for deletion")?; + + if deleted.is_none() { + return Err(ApiError::NotFound(eyre!("delphi rule not found"))); + } + + Ok(()) +} diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review/rules_scan.rs b/apps/labrinth/src/routes/internal/moderation/tech_review/rules_scan.rs new file mode 100644 index 0000000000..facbe622ce --- /dev/null +++ b/apps/labrinth/src/routes/internal/moderation/tech_review/rules_scan.rs @@ -0,0 +1,1066 @@ +use std::collections::{BTreeMap, HashMap}; + +use actix_web::{HttpRequest, HttpResponse, get, post, web}; +use ariadne::ids::base62_impl::to_base62; +use bytes::Bytes; +use eyre::{Result, eyre}; +use futures_util::{StreamExt, TryStreamExt}; +use serde::{Deserialize, Serialize}; +use sqlx::types::Json; +use tokio::sync::mpsc; +use tokio_stream::wrappers::UnboundedReceiverStream; +use utoipa::{PartialSchema, ToSchema}; +use xredis::RedisPool; + +use super::rules::{DelphiRuleEffect, DelphiRuleOutput}; +use crate::routes::internal::delphi::tech_review_queue::{ + self, TechReviewRemovalReason, +}; +use crate::{ + auth::check_is_moderator_from_headers, + database::{ + PgPool, PgTransaction, ReadOnlyPgPool, + models::{ + DBProjectId, DelphiReportIssueDetailsId, DelphiRuleId, + delphi_report_item::DelphiSeverity, + }, + }, + models::pats::Scopes, + queue::session::AuthQueue, + routes::ApiError, + util::{cel, error::Context}, +}; + +const RULE_SCAN_LOCK_ID: i64 = 0x6465_6c70_6869_7275; +const PROGRESS_INTERVAL: usize = 50; +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(get_rule_schema) + .service(get_detail_rule_input) + .service(scan_rules); +} + +#[derive(Serialize)] +struct RuleScanEvent<'a> { + phase: &'a str, + revision: i64, + scanned: usize, + total: usize, + effects: usize, +} + +#[derive(Serialize)] +struct RuleScanErrorEvent<'a> { + message: &'a str, +} + +#[derive(Deserialize, Serialize, utoipa::ToSchema)] +pub struct RuleInput { + pub schema_version: u32, + pub trace: RuleTrace, + pub file_traces: Vec, + pub scan: RuleScan, + pub artifact: RuleArtifact, + pub project: RuleProject, + pub version: RuleVersion, + pub file: RuleFile, +} + +#[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)] +pub struct RuleTrace { + pub key: String, + pub issue_type: String, + pub severity: DelphiSeverity, + pub jar: Option, + pub file_path: String, + pub data: HashMap, +} + +#[derive(Deserialize, Serialize, utoipa::ToSchema)] +pub struct RuleScan { + pub delphi_version: i32, +} + +#[derive(Deserialize, Serialize, utoipa::ToSchema)] +pub struct RuleArtifact { + pub size: Option, + pub hashes: BTreeMap, +} + +#[derive(Deserialize, Serialize, utoipa::ToSchema)] +pub struct RuleProject { + pub id: Option, + pub types: Vec, +} + +#[derive(Deserialize, Serialize, utoipa::ToSchema)] +pub struct RuleVersion { + pub id: Option, + pub loaders: Vec, +} + +#[derive(Deserialize, Serialize, utoipa::ToSchema)] +pub struct RuleFile { + pub id: Option, +} + +struct CompiledRule { + id: DelphiRuleId, + name: String, + expression: String, + on_issue_types: Vec, + program: cel::Program, +} + +impl CompiledRule { + fn applies_to_issue_type(&self, issue_type: &str) -> bool { + self.on_issue_types.is_empty() + || self + .on_issue_types + .iter() + .any(|rule_issue_type| rule_issue_type == issue_type) + } +} + +struct MaterializedEffect { + detail_id: i64, + rule_id: DelphiRuleId, + effect: DelphiRuleEffect, +} + +struct ScanSummary { + revision: i64, + scanned: usize, + total: usize, + effects: usize, +} + +#[derive(Serialize, utoipa::ToSchema)] +pub struct DelphiRuleSchemaResponse { + pub input: serde_json::Value, + pub output: serde_json::Value, + pub components: BTreeMap, +} + +/// Get the schemas for the CEL context and output values. +#[utoipa::path( + context_path = "/moderation/tech-review", + tag = "moderation", + security(("bearer_auth" = [])), + responses((status = OK, body = DelphiRuleSchemaResponse)) +)] +#[get("/rules/schema")] +pub async fn get_rule_schema( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result, ApiError> { + check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_READ, + ) + .await + .wrap_auth_err("authenticating API request")?; + + let mut schemas = Vec::new(); + ::schemas(&mut schemas); + as ToSchema>::schemas(&mut schemas); + + Ok(web::Json(DelphiRuleSchemaResponse { + input: schema_to_value(::schema())?, + output: schema_to_value( + as PartialSchema>::schema(), + )?, + components: schemas + .into_iter() + .map(|(name, schema)| Ok((name, schema_to_value(schema)?))) + .collect::>()?, + })) +} + +/// Get the exact CEL input for a Delphi issue detail. +#[utoipa::path( + context_path = "/moderation/tech-review", + tag = "moderation", + security(("bearer_auth" = [])), + responses( + (status = OK, body = RuleInput), + (status = NOT_FOUND, description = "Detail not found") + ) +)] +#[get("/rules/details/{detail_id}/input")] +pub async fn get_detail_rule_input( + req: HttpRequest, + pool: web::Data, + ro_pool: web::Data, + redis: web::Data, + session_queue: web::Data, + path: web::Path<(DelphiReportIssueDetailsId,)>, +) -> Result, ApiError> { + check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_READ, + ) + .await + .wrap_auth_err("authenticating API request")?; + + let (detail_id,) = path.into_inner(); + let detail = sqlx::query!( + r#" + SELECT + detail.key, + issue.issue_type, + detail.severity AS "severity: DelphiSeverity", + detail.jar, + detail.file_path, + detail.data AS "data: Json>", + COALESCE(file_traces.traces, '[]'::jsonb) + AS "file_traces!: Json>", + report.delphi_version, + file.size AS "size?", + file.id AS "file_id?", + version.id AS "version_id?", + version.mod_id AS "project_id?", + COALESCE(version_metadata.project_types, ARRAY[]::text[]) + AS "project_types!: Vec", + COALESCE(version_metadata.loaders, ARRAY[]::text[]) + AS "loaders!: Vec", + COALESCE(file_hashes.hashes, '{}'::jsonb) + AS "hashes!: Json>" + FROM delphi_report_issue_details detail + INNER JOIN delphi_report_issues issue ON issue.id = detail.issue_id + INNER JOIN delphi_reports report ON report.id = issue.report_id + LEFT JOIN LATERAL ( + SELECT + jsonb_agg( + jsonb_build_object( + 'key', file_detail.key, + 'issue_type', file_issue.issue_type, + 'severity', file_detail.severity, + 'jar', file_detail.jar, + 'file_path', file_detail.file_path, + 'data', file_detail.data + ) + ORDER BY file_detail.id + ) AS traces + FROM delphi_report_issues file_issue + INNER JOIN delphi_report_issue_details file_detail + ON file_detail.issue_id = file_issue.id + WHERE file_issue.report_id = issue.report_id + AND file_detail.file_path = detail.file_path + ) file_traces ON TRUE + LEFT JOIN files file ON file.id = report.file_id + LEFT JOIN versions version ON version.id = file.version_id + LEFT JOIN LATERAL ( + SELECT + ARRAY_AGG( + DISTINCT project_type.name::text + ORDER BY project_type.name::text + ) FILTER (WHERE project_type.name IS NOT NULL) + AS project_types, + ARRAY_AGG( + DISTINCT loader.loader + ORDER BY loader.loader + ) AS loaders + FROM loaders_versions loader_version + INNER JOIN loaders loader + ON loader.id = loader_version.loader_id + LEFT JOIN loaders_project_types loader_project_type + ON loader_project_type.joining_loader_id = loader_version.loader_id + LEFT JOIN project_types project_type + ON project_type.id = loader_project_type.joining_project_type_id + WHERE loader_version.version_id = version.id + ) version_metadata ON TRUE + LEFT JOIN LATERAL ( + SELECT + jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes + FROM hashes + WHERE hashes.file_id = file.id + ) file_hashes ON TRUE + WHERE detail.id = $1 + "#, + detail_id as DelphiReportIssueDetailsId, + ) + .fetch_optional(&***ro_pool) + .await + .wrap_internal_err("failed to fetch delphi rule input")? + .wrap_not_found_err("delphi rule input not found")?; + + Ok(web::Json(RuleInput { + schema_version: 1, + trace: RuleTrace { + key: detail.key, + issue_type: detail.issue_type, + severity: detail.severity, + jar: detail.jar, + file_path: detail.file_path, + data: detail.data.0, + }, + file_traces: detail.file_traces.0, + scan: RuleScan { + delphi_version: detail.delphi_version, + }, + artifact: RuleArtifact { + size: detail.size, + hashes: detail.hashes.0, + }, + project: RuleProject { + id: detail.project_id.map(|id| to_base62(id as u64)), + types: detail.project_types, + }, + version: RuleVersion { + id: detail.version_id.map(|id| to_base62(id as u64)), + loaders: detail.loaders, + }, + file: RuleFile { + id: detail.file_id.map(|id| to_base62(id as u64)), + }, + })) +} + +fn schema_to_value( + schema: T, +) -> Result { + serde_json::to_value(schema).map_err(|error| { + ApiError::Internal( + eyre!(error).wrap_err("failed to serialize Delphi rule schema"), + ) + }) +} + +/// Re-evaluate every Delphi issue detail and atomically publish a new rule revision. +#[utoipa::path( + context_path = "/moderation/tech-review", + tag = "moderation", + security(("bearer_auth" = [])), + responses((status = OK), (status = CONFLICT)) +)] +#[post("/rules/scan")] +pub async fn scan_rules( + 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_WRITE, + ) + .await + .wrap_auth_err("authenticating API request")?; + + let mut transaction = pool + .begin() + .await + .wrap_internal_err("failed to begin delphi rule scan")?; + + sqlx::query!("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ") + .execute(&mut transaction) + .await + .map_err(|error| { + ApiError::Internal( + eyre!(error) + .wrap_err("failed to set delphi rule scan isolation"), + ) + })?; + + let acquired = sqlx::query_scalar!( + "SELECT pg_try_advisory_xact_lock($1)", + RULE_SCAN_LOCK_ID, + ) + .fetch_one(&mut transaction) + .await + .map_err(|error| { + ApiError::Internal( + eyre!(error).wrap_err("failed to acquire delphi rule scan lock"), + ) + })? + .unwrap_or(false); + + if !acquired { + return Err(ApiError::Conflict(eyre!( + "a delphi rule scan is already running" + ))); + } + + let (sender, receiver) = mpsc::unbounded_channel(); + actix_web::rt::spawn(async move { + match run_scan(transaction, &sender).await { + Ok(summary) => { + let event = RuleScanEvent { + phase: "complete", + revision: summary.revision, + scanned: summary.scanned, + total: summary.total, + effects: summary.effects, + }; + if let Ok(data) = serde_json::to_string(&event) { + let _ = sender.send(Bytes::from(format!( + "event: complete\ndata: {data}\n\n" + ))); + } + } + Err(error) => { + tracing::error!(error = ?error, "delphi rule scan failed"); + let message = format!("{error:#}"); + let event = RuleScanErrorEvent { message: &message }; + if let Ok(data) = serde_json::to_string(&event) { + let _ = sender.send(Bytes::from(format!( + "event: failed\ndata: {data}\n\n" + ))); + } + } + } + }); + + let stream = + UnboundedReceiverStream::new(receiver).map(Ok::<_, std::io::Error>); + + Ok(HttpResponse::Ok() + .insert_header(("Content-Type", "text/event-stream")) + .insert_header(("Cache-Control", "no-cache")) + .insert_header(("X-Accel-Buffering", "no")) + .streaming(stream)) +} + +async fn run_scan( + mut transaction: PgTransaction<'static>, + sender: &mpsc::UnboundedSender, +) -> Result { + sqlx::query!("LOCK TABLE delphi_rules IN SHARE MODE") + .execute(&mut transaction) + .await + .wrap_err("failed to lock delphi rules")?; + sqlx::query!("LOCK TABLE delphi_report_issue_details IN SHARE MODE") + .execute(&mut transaction) + .await + .wrap_err("failed to lock delphi issue details")?; + + let current_revision = sqlx::query_scalar!( + "SELECT revision FROM delphi_rule_revisions LIMIT 1 FOR UPDATE", + ) + .fetch_one(&mut transaction) + .await + .wrap_err("failed to fetch the current delphi rule revision")?; + let revision = current_revision + .checked_add(1) + .ok_or_else(|| eyre!("delphi rule revision overflowed"))?; + + let rules = fetch_compiled_rules(&mut transaction).await?; + + let total = sqlx::query_scalar!( + r#" + SELECT COUNT(*) AS "count!" + FROM delphi_report_issue_details + "#, + ) + .fetch_one(&mut transaction) + .await + .wrap_err("failed to count delphi issue details")? as usize; + + let mut details = sqlx::query!( + r#" + SELECT + detail.id, + detail.key, + issue.issue_type, + detail.severity AS "severity: DelphiSeverity", + detail.jar, + detail.file_path, + detail.data AS "data: Json>", + COALESCE(file_traces.traces, '[]'::jsonb) + AS "file_traces!: Json>", + report.delphi_version, + file.size AS "size?", + file.id AS "file_id?", + version.id AS "version_id?", + version.mod_id AS "project_id?", + COALESCE(version_metadata.project_types, ARRAY[]::text[]) + AS "project_types!: Vec", + COALESCE(version_metadata.loaders, ARRAY[]::text[]) + AS "loaders!: Vec", + COALESCE(file_hashes.hashes, '{}'::jsonb) + AS "hashes!: Json>" + FROM delphi_report_issue_details detail + INNER JOIN delphi_report_issues issue ON issue.id = detail.issue_id + INNER JOIN delphi_reports report ON report.id = issue.report_id + LEFT JOIN LATERAL ( + SELECT + jsonb_agg( + jsonb_build_object( + 'key', file_detail.key, + 'issue_type', file_issue.issue_type, + 'severity', file_detail.severity, + 'jar', file_detail.jar, + 'file_path', file_detail.file_path, + 'data', file_detail.data + ) + ORDER BY file_detail.id + ) AS traces + FROM delphi_report_issues file_issue + INNER JOIN delphi_report_issue_details file_detail + ON file_detail.issue_id = file_issue.id + WHERE file_issue.report_id = issue.report_id + AND file_detail.file_path = detail.file_path + ) file_traces ON TRUE + LEFT JOIN files file ON file.id = report.file_id + LEFT JOIN versions version ON version.id = file.version_id + LEFT JOIN ( + SELECT + loader_version.version_id, + ARRAY_AGG( + DISTINCT project_type.name::text + ORDER BY project_type.name::text + ) FILTER (WHERE project_type.name IS NOT NULL) + AS project_types, + ARRAY_AGG( + DISTINCT loader.loader + ORDER BY loader.loader + ) AS loaders + FROM loaders_versions loader_version + INNER JOIN loaders loader + ON loader.id = loader_version.loader_id + LEFT JOIN loaders_project_types loader_project_type + ON loader_project_type.joining_loader_id = loader_version.loader_id + LEFT JOIN project_types project_type + ON project_type.id = loader_project_type.joining_project_type_id + GROUP BY loader_version.version_id + ) version_metadata + ON version_metadata.version_id = version.id + LEFT JOIN ( + SELECT + file_id, + jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes + FROM hashes + GROUP BY file_id + ) file_hashes ON file_hashes.file_id = file.id + ORDER BY detail.id + "#, + ) + .fetch(&mut transaction); + + let mut effects = Vec::new(); + let mut scanned = 0; + let event = RuleScanEvent { + phase: "scanning", + revision, + scanned: 0, + total, + effects: 0, + }; + if let Ok(data) = serde_json::to_string(&event) { + let _ = sender + .send(Bytes::from(format!("event: progress\ndata: {data}\n\n"))); + } + + while let Some(detail) = details + .try_next() + .await + .wrap_err("failed to fetch a delphi issue detail")? + { + let detail_id = detail.id; + let input = RuleInput { + schema_version: 1, + trace: RuleTrace { + key: detail.key, + issue_type: detail.issue_type, + severity: detail.severity, + jar: detail.jar, + file_path: detail.file_path, + data: detail.data.0, + }, + file_traces: detail.file_traces.0, + scan: RuleScan { + delphi_version: detail.delphi_version, + }, + artifact: RuleArtifact { + size: detail.size, + hashes: detail.hashes.0, + }, + project: RuleProject { + id: detail.project_id.map(|id| to_base62(id as u64)), + types: detail.project_types, + }, + version: RuleVersion { + id: detail.version_id.map(|id| to_base62(id as u64)), + loaders: detail.loaders, + }, + file: RuleFile { + id: detail.file_id.map(|id| to_base62(id as u64)), + }, + }; + + for rule in &rules { + if !rule.applies_to_issue_type(&input.trace.issue_type) { + continue; + } + + let effect = evaluate_rule( + &rule.program, + &rule.expression, + &input, + ) + .wrap_err_with(|| { + format!( + "failed to evaluate delphi rule '{}' for detail {detail_id}", + rule.name + ) + })?; + if let Some(effect) = effect { + effects.push(MaterializedEffect { + detail_id, + rule_id: rule.id, + effect, + }); + break; + } + } + + scanned += 1; + if scanned % PROGRESS_INTERVAL == 0 || scanned == total { + let event = RuleScanEvent { + phase: "scanning", + revision, + scanned, + total, + effects: effects.len(), + }; + if let Ok(data) = serde_json::to_string(&event) { + let _ = sender.send(Bytes::from(format!( + "event: progress\ndata: {data}\n\n" + ))); + } + tokio::task::yield_now().await; + } + } + drop(details); + + let event = RuleScanEvent { + phase: "publishing", + revision, + scanned: total, + total, + effects: effects.len(), + }; + if let Ok(data) = serde_json::to_string(&event) { + let _ = sender + .send(Bytes::from(format!("event: progress\ndata: {data}\n\n"))); + } + + insert_materialized_effects(revision, &effects, &mut transaction).await?; + + let affected_projects = sqlx::query!( + r#" + WITH project_membership AS ( + SELECT + detail.project_id, + BOOL_OR( + detail.status IN ('pending', 'unsafe') + AND detail.severity != 'hidden' + ) AS old_needs_review, + BOOL_OR( + detail.status IN ('pending', 'unsafe') + AND COALESCE( + new_effect.severity, + detail.original_severity + ) != 'hidden' + ) AS new_needs_review + FROM delphi_issue_details_with_statuses detail + LEFT JOIN delphi_rule_effects new_effect + ON new_effect.revision = $1 + AND new_effect.detail_id = detail.id + GROUP BY detail.project_id + ) + SELECT + project_id AS "project_id!: DBProjectId", + new_needs_review AS "new_needs_review!" + FROM project_membership + WHERE old_needs_review IS DISTINCT FROM new_needs_review + "#, + revision, + ) + .fetch_all(&mut transaction) + .await + .wrap_err("failed to fetch projects affected by delphi rule changes")?; + + sqlx::query!( + "UPDATE delphi_rules SET revision = $1 WHERE NOT delete_on_next_revision", + revision, + ) + .execute(&mut transaction) + .await + .wrap_err("failed to update delphi rule revisions")?; + sqlx::query!("UPDATE delphi_rule_revisions SET revision = $1", revision) + .execute(&mut transaction) + .await + .wrap_err("failed to publish the delphi rule revision")?; + + tech_review_queue::add_projects( + &affected_projects + .iter() + .filter(|project| project.new_needs_review) + .map(|project| project.project_id) + .collect::>(), + &mut transaction, + ) + .await?; + tech_review_queue::remove_projects( + &affected_projects + .iter() + .filter(|project| !project.new_needs_review) + .map(|project| project.project_id) + .collect::>(), + TechReviewRemovalReason::RulesChanged, + &mut transaction, + ) + .await?; + + sqlx::query!( + "DELETE FROM delphi_rule_effects WHERE revision <> $1", + revision, + ) + .execute(&mut transaction) + .await + .wrap_err("failed to delete old delphi rule effects")?; + sqlx::query!("DELETE FROM delphi_rules WHERE delete_on_next_revision") + .execute(&mut transaction) + .await + .wrap_err("failed to delete retired delphi rules")?; + + transaction + .commit() + .await + .wrap_err("failed to commit the delphi rule scan")?; + + Ok(ScanSummary { + revision, + scanned: total, + total, + effects: effects.len(), + }) +} + +pub(crate) async fn materialize_current_rule_effects( + detail_ids: &[DelphiReportIssueDetailsId], + transaction: &mut PgTransaction<'_>, +) -> Result<()> { + if detail_ids.is_empty() { + return Ok(()); + } + + let revision = sqlx::query_scalar!( + "SELECT revision FROM delphi_rule_revisions LIMIT 1", + ) + .fetch_one(&mut *transaction) + .await + .wrap_err("failed to fetch the current delphi rule revision")?; + let rules = fetch_compiled_rules(transaction).await?; + + if rules.is_empty() { + return Ok(()); + } + + let details = sqlx::query!( + r#" + SELECT + detail.id, + detail.key, + issue.issue_type, + detail.severity AS "severity: DelphiSeverity", + detail.jar, + detail.file_path, + detail.data AS "data: Json>", + COALESCE(file_traces.traces, '[]'::jsonb) + AS "file_traces!: Json>", + report.delphi_version, + file.size AS "size?", + file.id AS "file_id?", + version.id AS "version_id?", + version.mod_id AS "project_id?", + COALESCE(version_metadata.project_types, ARRAY[]::text[]) + AS "project_types!: Vec", + COALESCE(version_metadata.loaders, ARRAY[]::text[]) + AS "loaders!: Vec", + COALESCE(file_hashes.hashes, '{}'::jsonb) + AS "hashes!: Json>" + FROM delphi_report_issue_details detail + INNER JOIN delphi_report_issues issue ON issue.id = detail.issue_id + INNER JOIN delphi_reports report ON report.id = issue.report_id + LEFT JOIN LATERAL ( + SELECT + jsonb_agg( + jsonb_build_object( + 'key', file_detail.key, + 'issue_type', file_issue.issue_type, + 'severity', file_detail.severity, + 'jar', file_detail.jar, + 'file_path', file_detail.file_path, + 'data', file_detail.data + ) + ORDER BY file_detail.id + ) AS traces + FROM delphi_report_issues file_issue + INNER JOIN delphi_report_issue_details file_detail + ON file_detail.issue_id = file_issue.id + WHERE file_issue.report_id = issue.report_id + AND file_detail.file_path = detail.file_path + ) file_traces ON TRUE + LEFT JOIN files file ON file.id = report.file_id + LEFT JOIN versions version ON version.id = file.version_id + LEFT JOIN LATERAL ( + SELECT + ARRAY_AGG( + DISTINCT project_type.name::text + ORDER BY project_type.name::text + ) FILTER (WHERE project_type.name IS NOT NULL) + AS project_types, + ARRAY_AGG( + DISTINCT loader.loader + ORDER BY loader.loader + ) AS loaders + FROM loaders_versions loader_version + INNER JOIN loaders loader + ON loader.id = loader_version.loader_id + LEFT JOIN loaders_project_types loader_project_type + ON loader_project_type.joining_loader_id = loader_version.loader_id + LEFT JOIN project_types project_type + ON project_type.id = loader_project_type.joining_project_type_id + WHERE loader_version.version_id = version.id + ) version_metadata ON TRUE + LEFT JOIN LATERAL ( + SELECT + jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes + FROM hashes + WHERE hashes.file_id = file.id + ) file_hashes ON TRUE + WHERE detail.id = ANY($1::bigint[]) + ORDER BY detail.id + "#, + &detail_ids.iter().map(|id| id.0).collect::>(), + ) + .fetch_all(&mut *transaction) + .await + .wrap_err("failed to fetch new delphi issue details")?; + + let mut effects = Vec::new(); + for detail in details { + let input = RuleInput { + schema_version: 1, + trace: RuleTrace { + key: detail.key, + issue_type: detail.issue_type, + severity: detail.severity, + jar: detail.jar, + file_path: detail.file_path, + data: detail.data.0, + }, + file_traces: detail.file_traces.0, + scan: RuleScan { + delphi_version: detail.delphi_version, + }, + artifact: RuleArtifact { + size: detail.size, + hashes: detail.hashes.0, + }, + project: RuleProject { + id: detail.project_id.map(|id| to_base62(id as u64)), + types: detail.project_types, + }, + version: RuleVersion { + id: detail.version_id.map(|id| to_base62(id as u64)), + loaders: detail.loaders, + }, + file: RuleFile { + id: detail.file_id.map(|id| to_base62(id as u64)), + }, + }; + + for rule in &rules { + if !rule.applies_to_issue_type(&input.trace.issue_type) { + continue; + } + + let effect = evaluate_rule(&rule.program, &rule.expression, &input) + .wrap_err_with(|| { + format!( + "failed to evaluate delphi rule '{}' for detail {}", + rule.name, detail.id + ) + })?; + if let Some(effect) = effect { + effects.push(MaterializedEffect { + detail_id: detail.id, + rule_id: rule.id, + effect, + }); + break; + } + } + } + + insert_materialized_effects(revision, &effects, transaction).await +} + +async fn fetch_compiled_rules( + transaction: &mut PgTransaction<'_>, +) -> Result> { + let rules = sqlx::query!( + r#" + SELECT id AS "id!: DelphiRuleId", name, rule, on_issue_types + FROM delphi_rules + WHERE NOT delete_on_next_revision + ORDER BY priority DESC, id + "#, + ) + .fetch_all(&mut *transaction) + .await + .wrap_err("failed to fetch delphi rules")?; + + tokio::task::spawn_blocking(move || { + rules + .into_iter() + .map(|rule| { + let program = + cel::Program::compile(&rule.rule).map_err(|error| { + eyre!( + "failed to compile delphi rule '{}': {error}", + rule.name + ) + })?; + Ok(CompiledRule { + id: rule.id, + name: rule.name, + expression: rule.rule, + on_issue_types: rule.on_issue_types, + program, + }) + }) + .collect() + }) + .await + .wrap_err("failed to join cel compilation task")? +} + +async fn insert_materialized_effects( + revision: i64, + effects: &[MaterializedEffect], + transaction: &mut PgTransaction<'_>, +) -> Result<()> { + if effects.is_empty() { + return Ok(()); + } + + let detail_ids = effects + .iter() + .map(|effect| effect.detail_id) + .collect::>(); + let rule_ids = effects + .iter() + .map(|effect| effect.rule_id.0) + .collect::>(); + let severities = effects + .iter() + .map(|effect| effect.effect.severity) + .collect::>(); + + sqlx::query!( + r#" + INSERT INTO delphi_rule_effects ( + revision, + detail_id, + rule_id, + severity + ) + SELECT $1, effect.* + FROM UNNEST( + $2::BIGINT[], + $3::BIGINT[], + $4::delphi_severity[] + ) AS effect(detail_id, rule_id, severity) + "#, + revision, + &detail_ids, + &rule_ids, + &severities as &[DelphiSeverity], + ) + .execute(&mut *transaction) + .await + .wrap_err("failed to insert delphi rule effects")?; + + Ok(()) +} + +pub(super) fn evaluate_rule( + program: &cel::Program, + expression: &str, + input: &RuleInput, +) -> Result> { + evaluate_rule_inner(program, input) + .wrap_err_with(|| { + let input = serde_json::to_string(input).unwrap_or_else(|error| { + format!("") + }); + format!("CEL input: {input}") + }) + .wrap_err_with(|| format!("CEL expression: {expression}")) +} + +fn evaluate_rule_inner( + program: &cel::Program, + input: &RuleInput, +) -> Result> { + let mut context = cel::Context::default(); + context + .add_variable("schema_version", input.schema_version) + .wrap_err("failed to add `schema_version` to cel context")?; + context + .add_variable("trace", &input.trace) + .wrap_err("failed to add `trace` to cel context")?; + context + .add_variable("file_traces", &input.file_traces) + .wrap_err("failed to add `file_traces` to cel context")?; + context + .add_variable("scan", &input.scan) + .wrap_err("failed to add `scan` to cel context")?; + context + .add_variable("artifact", &input.artifact) + .wrap_err("failed to add `artifact` to cel context")?; + context + .add_variable("project", &input.project) + .wrap_err("failed to add `project` to cel context")?; + context + .add_variable("version", &input.version) + .wrap_err("failed to add `version` to cel context")?; + context + .add_variable("file", &input.file) + .wrap_err("failed to add `file` to cel context")?; + + let value = program + .execute(&context) + .wrap_err("failed to execute cel expression")?; + let value = value.json().map_err(|error| { + eyre!("failed to convert cel result to json: {error}") + })?; + + match value { + serde_json::Value::Null => Ok(None), + serde_json::Value::String(severity) => { + let severity = + serde_json::from_value(serde_json::Value::String(severity)) + .wrap_err("cel expression returned an invalid severity")?; + Ok(Some(DelphiRuleEffect { severity })) + } + value => serde_json::from_value(value) + .map(Some) + .wrap_err("cel expression returned an invalid rule effect"), + } +} diff --git a/apps/labrinth/src/routes/mod.rs b/apps/labrinth/src/routes/mod.rs index fc5f72e75c..3713066342 100644 --- a/apps/labrinth/src/routes/mod.rs +++ b/apps/labrinth/src/routes/mod.rs @@ -235,6 +235,23 @@ impl ApiError { } pub fn as_api_error<'a>(&self) -> crate::models::error::ApiError<'a> { + let report = match self { + Self::Internal(report) + | Self::Request(report) + | Self::Auth(report) + | Self::NotFound(report) + | Self::Conflict(report) + | Self::FailedDependency(report) + | Self::PreconditionRequired(report) + | Self::PreconditionFailed(report) + | Self::RateLimit(report) => report, + }; + let details = report + .chain() + .skip(1) + .map(ToString::to_string) + .collect::>(); + crate::models::error::ApiError { error: match self { Self::Internal(..) => "internal_error", @@ -247,8 +264,8 @@ impl ApiError { Self::PreconditionFailed(..) => "precondition_failed", Self::RateLimit(..) => "ratelimit_error", }, - description: format!("{self:#}"), - details: None, + description: report.to_string(), + details: (!details.is_empty()).then(|| serde_json::json!(details)), } } } @@ -272,3 +289,25 @@ impl actix_web::ResponseError for ApiError { HttpResponse::build(self.status_code()).json(self.as_api_error()) } } + +#[cfg(test)] +mod tests { + use super::ApiError; + + #[test] + fn api_error_serializes_source_chain_as_details() { + let error = ApiError::Request( + eyre::eyre!("root cause") + .wrap_err("intermediate context") + .wrap_err("request failed"), + ); + + let response = error.as_api_error(); + + assert_eq!(response.description, "request failed"); + assert_eq!( + response.details, + Some(serde_json::json!(["intermediate context", "root cause"])), + ); + } +} diff --git a/apps/labrinth/src/routes/v3/projects.rs b/apps/labrinth/src/routes/v3/projects.rs index 5e00ab17f1..a919536bee 100644 --- a/apps/labrinth/src/routes/v3/projects.rs +++ b/apps/labrinth/src/routes/v3/projects.rs @@ -3053,6 +3053,13 @@ pub async fn project_delete_internal( .begin() .await .wrap_internal_err("failed to start transaction")?; + delphi::tech_review_queue::remove_projects( + &[project.inner.id], + delphi::tech_review_queue::TechReviewRemovalReason::FileDeleted, + &mut transaction, + ) + .await + .wrap_internal_err("removing project from technical review queue")?; // rejected & withheld projects are transferred to ghost so moderation data is preserved if matches!( @@ -3216,15 +3223,6 @@ pub async fn project_delete_internal( return Ok(()); } - delphi::tech_review_sync::sync_deleted_project_tech_review_exit( - project.inner.id, - &mut transaction, - ) - .await - .wrap_api_err( - "executing `tech_review_sync::sync_deleted_project_tech_review_exit`", - )?; - let context = ImageContext::Project { project_id: Some(project.inner.id.into()), }; diff --git a/apps/labrinth/src/routes/v3/version_file.rs b/apps/labrinth/src/routes/v3/version_file.rs index 929416b0c5..79eeb462eb 100644 --- a/apps/labrinth/src/routes/v3/version_file.rs +++ b/apps/labrinth/src/routes/v3/version_file.rs @@ -970,9 +970,9 @@ pub async fn delete_file( database::models::version_item::cleanup_unused_attribution_files_and_groups(&mut transaction) .await.wrap_internal_err("deleting version item from database")?; - delphi::tech_review_sync::sync_project_tech_review_state( + delphi::tech_review_queue::remove_projects_without_details( &[row.project_id], - delphi::tech_review_sync::TechReviewExitReason::FileDeleted, + delphi::tech_review_queue::TechReviewRemovalReason::FileDeleted, &mut transaction, ) .await diff --git a/apps/labrinth/src/routes/v3/versions.rs b/apps/labrinth/src/routes/v3/versions.rs index 377071142c..99eb84f19b 100644 --- a/apps/labrinth/src/routes/v3/versions.rs +++ b/apps/labrinth/src/routes/v3/versions.rs @@ -1317,9 +1317,9 @@ pub async fn version_delete( .await .wrap_internal_err("deleting version from database")?; - delphi::tech_review_sync::sync_project_tech_review_state( + delphi::tech_review_queue::remove_projects_without_details( &[version.inner.project_id], - delphi::tech_review_sync::TechReviewExitReason::FileDeleted, + delphi::tech_review_queue::TechReviewRemovalReason::FileDeleted, &mut transaction, ) .await diff --git a/apps/labrinth/src/util/cel.rs b/apps/labrinth/src/util/cel.rs new file mode 100644 index 0000000000..63da48bcfb --- /dev/null +++ b/apps/labrinth/src/util/cel.rs @@ -0,0 +1,882 @@ +use std::{ + collections::BTreeMap, + ops::{Deref, DerefMut}, + sync::Arc, +}; + +use ::cel::{ + Context as CelContext, ExecutionError, Program as CelProgram, Value, +}; +use chumsky::{Parser, prelude::*}; +use eyre::{Result, WrapErr, eyre}; +use serde::Serialize; +use thiserror::Error; +use url::{Host, Url}; + +const MAX_PREPROCESSED_SIZE: usize = 1_048_576; + +pub struct Context<'a> { + inner: CelContext<'a>, +} + +impl Default for Context<'_> { + fn default() -> Self { + let mut inner = CelContext::default(); + inner.add_function("url.parse", parse_url); + inner.add_function("url.is_valid", is_valid_url); + + Self { inner } + } +} + +impl<'a> Deref for Context<'a> { + type Target = CelContext<'a>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Context<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +#[derive(Serialize)] +struct ParsedUrl<'a> { + href: &'a str, + scheme: &'a str, + has_authority: bool, + cannot_be_a_base: bool, + username: &'a str, + password: Option<&'a str>, + host: Option<&'a str>, + host_type: Option<&'static str>, + domain: Option<&'a str>, + port: Option, + port_or_known_default: Option, + path: &'a str, + path_segments: Option>, + query: Option<&'a str>, + query_pairs: Vec, + fragment: Option<&'a str>, + origin: String, + origin_is_tuple: bool, +} + +#[derive(Serialize)] +struct QueryPair { + key: String, + value: String, +} + +fn parse_url(value: Arc) -> std::result::Result { + let parsed = Url::parse(&value) + .map_err(|error| ExecutionError::function_error("url.parse", error))?; + let origin = parsed.origin(); + let host_type = parsed.host().map(|host| match host { + Host::Domain(_) => "domain", + Host::Ipv4(_) => "ipv4", + Host::Ipv6(_) => "ipv6", + }); + let query_pairs = parsed + .query_pairs() + .map(|(key, value)| QueryPair { + key: key.into_owned(), + value: value.into_owned(), + }) + .collect(); + let value = ParsedUrl { + href: parsed.as_str(), + scheme: parsed.scheme(), + has_authority: parsed.has_authority(), + cannot_be_a_base: parsed.cannot_be_a_base(), + username: parsed.username(), + password: parsed.password(), + host: parsed.host_str(), + host_type, + domain: parsed.domain(), + port: parsed.port().map(i64::from), + port_or_known_default: parsed.port_or_known_default().map(i64::from), + path: parsed.path(), + path_segments: parsed.path_segments().map(Iterator::collect), + query: parsed.query(), + query_pairs, + fragment: parsed.fragment(), + origin: origin.ascii_serialization(), + origin_is_tuple: origin.is_tuple(), + }; + + ::cel::to_value(value) + .map_err(|error| ExecutionError::function_error("url.parse", error)) +} + +fn is_valid_url(value: Arc) -> bool { + Url::parse(&value).is_ok() +} + +pub struct Program { + inner: CelProgram, + bindings: Vec, +} + +impl Program { + pub fn compile(source: &str) -> Result { + let preprocessed = preprocess(source) + .wrap_err("failed to preprocess cel expression")?; + let bindings = preprocessed + .bindings + .into_iter() + .map(|binding| { + let inner = CelProgram::compile(&binding.expression) + .map_err(|error| eyre!(error)) + .wrap_err_with(|| { + format!( + "failed to compile `#bind {}` on line {}", + binding.name, binding.line + ) + })?; + + Ok(CompiledBinding { + name: binding.name, + inner, + }) + }) + .collect::>>()?; + let inner = CelProgram::compile(&preprocessed.expression) + .map_err(|error| eyre!(error))?; + + Ok(Self { inner, bindings }) + } + + pub fn execute<'a>( + &self, + context: &Context<'a>, + ) -> std::result::Result { + let mut context = context.inner.new_inner_scope(); + + for binding in &self.bindings { + let value = binding.inner.execute(&context).map_err(|error| { + ExecutionError::function_error( + &format!("#bind {}", binding.name), + error, + ) + })?; + context.add_variable_from_value(&binding.name, value); + } + + self.inner.execute(&context) + } +} + +struct CompiledBinding { + name: String, + inner: CelProgram, +} + +#[derive(Debug)] +struct Preprocessed { + expression: String, + bindings: Vec, +} + +#[derive(Debug)] +struct Definition { + name: String, + replacement: String, +} + +#[derive(Debug)] +struct Binding { + name: String, + expression: String, + line: usize, +} + +#[derive(Debug)] +enum Directive { + Define(Definition), + Bind { name: String, expression: String }, +} + +#[derive(Debug, Error)] +enum PreprocessorError { + #[error( + "invalid preprocessor directive on line {line}, column {column}: {message}" + )] + InvalidDirective { + line: usize, + column: usize, + message: String, + }, + #[error("macro `{name}` is defined more than once")] + DuplicateDefinition { name: String }, + #[error("binding `{name}` is declared more than once")] + DuplicateBinding { name: String }, + #[error("`{name}` cannot be both a macro and a binding")] + ConflictingDirective { name: String }, + #[error("recursive macro expansion: {path}")] + RecursiveExpansion { path: String }, + #[error("preprocessed expression exceeds {MAX_PREPROCESSED_SIZE} bytes")] + ExpressionTooLarge, +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +enum LexState { + #[default] + Normal, + String { + quote: u8, + triple: bool, + raw: bool, + }, + BlockComment, +} + +fn directive_parser() -> impl Parser> { + let horizontal_whitespace = one_of(" \t").repeated(); + let identifier = filter(|character: &char| { + character.is_ascii_alphabetic() || *character == '_' + }) + .then( + filter(|character: &char| { + character.is_ascii_alphanumeric() || *character == '_' + }) + .repeated(), + ) + .map(|(first, rest)| { + std::iter::once(first).chain(rest).collect::() + }); + let expression = any().repeated().at_least(1).collect::(); + let kind = just("define").to(true).or(just("bind").to(false)); + + horizontal_whitespace + .clone() + .ignore_then(just('#')) + .then_ignore(horizontal_whitespace.clone()) + .ignore_then(kind) + .then_ignore(horizontal_whitespace.clone().at_least(1)) + .then(identifier) + .then_ignore(horizontal_whitespace.at_least(1)) + .then(expression) + .map(|((define, name), expression)| { + if define { + Directive::Define(Definition { + name, + replacement: expression.trim().to_string(), + }) + } else { + Directive::Bind { + name, + expression: expression.trim().to_string(), + } + } + }) + .then_ignore(end()) +} + +fn preprocess(source: &str) -> Result { + let mut definitions = BTreeMap::new(); + let mut bindings = Vec::new(); + let mut expression = String::with_capacity(source.len()); + let mut state = LexState::Normal; + + for (line_index, line) in source.split_inclusive('\n').enumerate() { + let directive = state == LexState::Normal + && line.trim_start_matches([' ', '\t']).starts_with('#'); + + if directive { + let directive = line.trim_end_matches(['\r', '\n']); + let directive = + directive_parser().parse(directive).map_err(|errors| { + let error = + errors.into_iter().next().unwrap_or_else(|| { + Simple::custom(0..0, "invalid directive") + }); + PreprocessorError::InvalidDirective { + line: line_index + 1, + column: error.span().start + 1, + message: error.to_string(), + } + })?; + + match directive { + Directive::Define(definition) => { + if bindings.iter().any(|binding: &Binding| { + binding.name == definition.name + }) { + return Err(PreprocessorError::ConflictingDirective { + name: definition.name, + }); + } + if definitions + .insert(definition.name.clone(), definition.replacement) + .is_some() + { + return Err(PreprocessorError::DuplicateDefinition { + name: definition.name, + }); + } + } + Directive::Bind { name, expression } => { + if definitions.contains_key(&name) { + return Err(PreprocessorError::ConflictingDirective { + name, + }); + } + if bindings + .iter() + .any(|binding: &Binding| binding.name == name) + { + return Err(PreprocessorError::DuplicateBinding { + name, + }); + } + bindings.push(Binding { + name, + expression, + line: line_index + 1, + }); + } + } + + if line.ends_with('\n') { + expression.push('\n'); + } + } else { + expression.push_str(line); + advance_lex_state(line, &mut state); + } + } + + let mut expanded = String::with_capacity(expression.len()); + expand(&expression, &definitions, &mut Vec::new(), &mut expanded)?; + for binding in &mut bindings { + let mut expanded = String::with_capacity(binding.expression.len()); + expand( + &binding.expression, + &definitions, + &mut Vec::new(), + &mut expanded, + )?; + binding.expression = expanded; + } + + Ok(Preprocessed { + expression: expanded, + bindings, + }) +} + +fn expand( + source: &str, + definitions: &BTreeMap, + stack: &mut Vec, + output: &mut String, +) -> Result<(), PreprocessorError> { + let bytes = source.as_bytes(); + let mut index = 0; + let mut state = LexState::Normal; + + while index < bytes.len() { + match state { + LexState::Normal => { + if bytes[index..].starts_with(b"//") { + push(output, "//")?; + index += 2; + while index < bytes.len() { + let character = source[index..] + .chars() + .next() + .expect("the index is within the string"); + let mut encoded = [0; 4]; + push(output, character.encode_utf8(&mut encoded))?; + index += character.len_utf8(); + if character == '\n' { + break; + } + } + continue; + } + + if bytes[index..].starts_with(b"/*") { + push(output, "/*")?; + index += 2; + state = LexState::BlockComment; + continue; + } + + if let Some((length, string_state)) = string_start(bytes, index) + { + push(output, &source[index..index + length])?; + index += length; + state = string_state; + continue; + } + + if bytes[index].is_ascii_alphabetic() || bytes[index] == b'_' { + let start = index; + index += 1; + while index < bytes.len() + && (bytes[index].is_ascii_alphanumeric() + || bytes[index] == b'_') + { + index += 1; + } + + let identifier = &source[start..index]; + if let Some(replacement) = definitions.get(identifier) { + if let Some(cycle_start) = + stack.iter().position(|name| name == identifier) + { + let mut path = stack[cycle_start..].to_vec(); + path.push(identifier.to_string()); + return Err( + PreprocessorError::RecursiveExpansion { + path: path.join(" -> "), + }, + ); + } + + stack.push(identifier.to_string()); + expand(replacement, definitions, stack, output)?; + stack.pop(); + } else if identifier == "or_null" + && source[..start] + .bytes() + .rev() + .find(|character| !character.is_ascii_whitespace()) + != Some(b'.') + { + let mut open_parenthesis = index; + while bytes + .get(open_parenthesis) + .is_some_and(u8::is_ascii_whitespace) + { + open_parenthesis += 1; + } + + if bytes.get(open_parenthesis) == Some(&b'(') + && let Some(close_parenthesis) = + find_closing_parenthesis( + source, + open_parenthesis, + ) + { + let argument = &source + [open_parenthesis + 1..close_parenthesis]; + let mut expanded_argument = + String::with_capacity(argument.len()); + expand( + argument, + definitions, + stack, + &mut expanded_argument, + )?; + + push(output, "(has(")?; + push(output, &expanded_argument)?; + push(output, ") ? ")?; + push(output, &expanded_argument)?; + push(output, " : null)")?; + index = close_parenthesis + 1; + } else { + push(output, identifier)?; + } + } else { + push(output, identifier)?; + } + continue; + } + } + LexState::String { quote, triple, raw } => { + if !raw && bytes[index] == b'\\' { + let end = (index + 2).min(bytes.len()); + push(output, &source[index..end])?; + index = end; + continue; + } + + let closing_length = if triple { 3 } else { 1 }; + if bytes[index] == quote + && bytes[index..].len() >= closing_length + && bytes[index..index + closing_length] + .iter() + .all(|character| *character == quote) + { + push(output, &source[index..index + closing_length])?; + index += closing_length; + state = LexState::Normal; + continue; + } + } + LexState::BlockComment => { + if bytes[index..].starts_with(b"*/") { + push(output, "*/")?; + index += 2; + state = LexState::Normal; + continue; + } + } + } + + let character = source[index..] + .chars() + .next() + .expect("the index is within the string"); + let mut encoded = [0; 4]; + push(output, character.encode_utf8(&mut encoded))?; + index += character.len_utf8(); + } + + Ok(()) +} + +fn find_closing_parenthesis(source: &str, open: usize) -> Option { + let bytes = source.as_bytes(); + let mut index = open + 1; + let mut depth = 1_usize; + let mut state = LexState::Normal; + + while index < bytes.len() { + match state { + LexState::Normal => { + if bytes[index..].starts_with(b"//") { + index += 2; + while index < bytes.len() && bytes[index] != b'\n' { + index += source[index..].chars().next()?.len_utf8(); + } + continue; + } + if bytes[index..].starts_with(b"/*") { + index += 2; + state = LexState::BlockComment; + continue; + } + if let Some((length, string_state)) = string_start(bytes, index) + { + index += length; + state = string_state; + continue; + } + + match bytes[index] { + b'(' => depth += 1, + b')' => { + depth -= 1; + if depth == 0 { + return Some(index); + } + } + _ => {} + } + } + LexState::String { quote, triple, raw } => { + if !raw && bytes[index] == b'\\' { + index = (index + 2).min(bytes.len()); + continue; + } + + let closing_length = if triple { 3 } else { 1 }; + if bytes[index] == quote + && bytes[index..].len() >= closing_length + && bytes[index..index + closing_length] + .iter() + .all(|character| *character == quote) + { + index += closing_length; + state = LexState::Normal; + continue; + } + } + LexState::BlockComment => { + if bytes[index..].starts_with(b"*/") { + index += 2; + state = LexState::Normal; + continue; + } + } + } + + index += source[index..].chars().next()?.len_utf8(); + } + + None +} + +fn push(output: &mut String, value: &str) -> Result<(), PreprocessorError> { + if output.len().saturating_add(value.len()) > MAX_PREPROCESSED_SIZE { + return Err(PreprocessorError::ExpressionTooLarge); + } + output.push_str(value); + Ok(()) +} + +fn advance_lex_state(source: &str, state: &mut LexState) { + let bytes = source.as_bytes(); + let mut index = 0; + + while index < bytes.len() { + match *state { + LexState::Normal => { + if bytes[index..].starts_with(b"//") { + return; + } + if bytes[index..].starts_with(b"/*") { + *state = LexState::BlockComment; + index += 2; + continue; + } + if let Some((length, string_state)) = string_start(bytes, index) + { + *state = string_state; + index += length; + continue; + } + } + LexState::String { quote, triple, raw } => { + if !raw && bytes[index] == b'\\' { + index = (index + 2).min(bytes.len()); + continue; + } + + let closing_length = if triple { 3 } else { 1 }; + if bytes[index] == quote + && bytes[index..].len() >= closing_length + && bytes[index..index + closing_length] + .iter() + .all(|character| *character == quote) + { + *state = LexState::Normal; + index += closing_length; + continue; + } + } + LexState::BlockComment => { + if bytes[index..].starts_with(b"*/") { + *state = LexState::Normal; + index += 2; + continue; + } + } + } + + index += source[index..] + .chars() + .next() + .expect("the index is within the string") + .len_utf8(); + } + + if matches!(state, LexState::String { triple: false, .. }) { + *state = LexState::Normal; + } +} + +fn string_start(bytes: &[u8], index: usize) -> Option<(usize, LexState)> { + let remaining = &bytes[index..]; + let (prefix_length, raw) = if remaining.len() >= 3 + && matches!(remaining[0].to_ascii_lowercase(), b'b' | b'r') + && matches!(remaining[1].to_ascii_lowercase(), b'b' | b'r') + && !remaining[0].eq_ignore_ascii_case(&remaining[1]) + { + (2, true) + } else if remaining.len() >= 2 + && matches!(remaining[0].to_ascii_lowercase(), b'b' | b'r') + { + (1, remaining[0].eq_ignore_ascii_case(&b'r')) + } else { + (0, false) + }; + + let quote = *remaining.get(prefix_length)?; + if !matches!(quote, b'\'' | b'"') { + return None; + } + + let triple = + remaining + .get(prefix_length..prefix_length + 3) + .is_some_and(|quotes| { + quotes.iter().all(|character| *character == quote) + }); + let quote_length = if triple { 3 } else { 1 }; + + Some(( + prefix_length + quote_length, + LexState::String { quote, triple, raw }, + )) +} + +#[cfg(test)] +mod tests { + use super::{Context, Program, Value, preprocess}; + + #[test] + fn expands_object_macros() { + let source = + "#define ISSUE OBFUSCATED_NAMES\ntrace.issue_type == ISSUE"; + assert_eq!( + preprocess(source).unwrap().expression, + "\ntrace.issue_type == OBFUSCATED_NAMES" + ); + } + + #[test] + fn recursively_expands_macros() { + let source = + "#define RESULT SEVERITY\n#define SEVERITY \"low\"\nRESULT"; + assert_eq!(preprocess(source).unwrap().expression, "\n\n\"low\""); + } + + #[test] + fn does_not_expand_strings_or_comments() { + let source = + "#define VALUE expanded\nVALUE == \"VALUE\" // VALUE\n/* VALUE */"; + assert_eq!( + preprocess(source).unwrap().expression, + "\nexpanded == \"VALUE\" // VALUE\n/* VALUE */" + ); + } + + #[test] + fn reports_recursive_macros() { + let source = "#define FIRST SECOND\n#define SECOND FIRST\nFIRST"; + let error = preprocess(source).unwrap_err(); + assert_eq!( + error.to_string(), + "recursive macro expansion: FIRST -> SECOND -> FIRST" + ); + } + + #[test] + fn expands_or_null_to_a_lazy_presence_check() { + assert_eq!( + preprocess("or_null(trace.data.url)").unwrap().expression, + "(has(trace.data.url) ? trace.data.url : null)" + ); + } + + #[test] + fn does_not_expand_or_null_in_strings_comments_or_member_calls() { + let source = r#""or_null(trace.data.url)" +// or_null(trace.data.url) +value.or_null()"#; + assert_eq!(preprocess(source).unwrap().expression, source); + } + + #[test] + fn or_null_returns_null_for_a_missing_field() { + let program = + Program::compile("or_null(trace.data.url) == null").unwrap(); + let mut context = Context::default(); + context + .add_variable("trace", serde_json::json!({ "data": {} })) + .unwrap(); + + assert_eq!(program.execute(&context).unwrap(), Value::Bool(true)); + } + + #[test] + fn or_null_preserves_an_existing_value() { + let program = Program::compile( + r#"or_null(trace.data.url) == "https://modrinth.com/""#, + ) + .unwrap(); + let mut context = Context::default(); + context + .add_variable( + "trace", + serde_json::json!({ + "data": { "url": "https://modrinth.com/" } + }), + ) + .unwrap(); + + assert_eq!(program.execute(&context).unwrap(), Value::Bool(true)); + } + + #[test] + fn evaluates_runtime_bindings_in_order() { + let source = "#bind DOUBLE input * 2\n#bind RESULT DOUBLE + 1\nRESULT"; + let program = Program::compile(source).unwrap(); + let mut context = Context::default(); + context.add_variable("input", 4).unwrap(); + + assert_eq!(program.execute(&context).unwrap(), Value::Int(9)); + } + + #[test] + fn expands_definitions_in_runtime_bindings() { + let source = + "#define MULTIPLIER 3\n#bind RESULT input * MULTIPLIER\nRESULT"; + let program = Program::compile(source).unwrap(); + let mut context = Context::default(); + context.add_variable("input", 4).unwrap(); + + assert_eq!(program.execute(&context).unwrap(), Value::Int(12)); + } + + #[test] + fn bindings_shadow_context_variables_after_initialization() { + let source = "#bind VALUE VALUE + 1\nVALUE"; + let program = Program::compile(source).unwrap(); + let mut context = Context::default(); + context.add_variable("VALUE", 4).unwrap(); + + assert_eq!(program.execute(&context).unwrap(), Value::Int(5)); + } + + #[test] + fn reports_runtime_binding_errors_with_the_binding_name() { + let program = + Program::compile("#bind RESULT missing + 1\nRESULT").unwrap(); + let error = program.execute(&Context::default()).unwrap_err(); + + assert!(error.to_string().contains("#bind RESULT")); + } + + #[test] + fn parses_urls_with_namespaced_functions() { + let source = r#" +#bind URL url.parse("https://user:password@example.com:8443/a/b?first=one&first=two#fragment") +URL.scheme == "https" && +URL.host == "example.com" && +URL.host_type == "domain" && +URL.port == 8443 && +URL.path == "/a/b" && +URL.path_segments == ["a", "b"] && +URL.query_pairs[1].key == "first" && +URL.query_pairs[1].value == "two" && +URL.fragment == "fragment" +"#; + let program = Program::compile(source).unwrap(); + + assert_eq!( + program.execute(&Context::default()).unwrap(), + Value::Bool(true) + ); + } + + #[test] + fn checks_url_validity() { + let program = Program::compile( + r#"url.is_valid("https://modrinth.com/") && !url.is_valid("not a URL")"#, + ) + .unwrap(); + + assert_eq!( + program.execute(&Context::default()).unwrap(), + Value::Bool(true) + ); + } + + #[test] + fn reports_url_parse_errors() { + let program = Program::compile(r#"url.parse("not a URL")"#).unwrap(); + let error = program.execute(&Context::default()).unwrap_err(); + + assert!(error.to_string().contains("url.parse")); + } +} diff --git a/apps/labrinth/src/util/mod.rs b/apps/labrinth/src/util/mod.rs index aea31aa987..ee1e98419c 100644 --- a/apps/labrinth/src/util/mod.rs +++ b/apps/labrinth/src/util/mod.rs @@ -4,6 +4,7 @@ pub mod archon; pub mod avalara1099; pub mod bitflag; pub mod captcha; +pub mod cel; pub mod cors; pub mod date; pub mod error; diff --git a/packages/api-client/src/modules/labrinth/tech-review/internal.ts b/packages/api-client/src/modules/labrinth/tech-review/internal.ts index 0012474334..8ab98aa915 100644 --- a/packages/api-client/src/modules/labrinth/tech-review/internal.ts +++ b/packages/api-client/src/modules/labrinth/tech-review/internal.ts @@ -6,6 +6,127 @@ export class LabrinthTechReviewInternalModule extends AbstractModule { return 'labrinth_tech_review_internal' } + public async getRules(): Promise { + return this.client.request( + '/moderation/tech-review/rules', + { + api: 'labrinth', + version: 'internal', + method: 'GET', + }, + ) + } + + public async getRuleSchema(): Promise { + return this.client.request( + '/moderation/tech-review/rules/schema', + { + api: 'labrinth', + version: 'internal', + method: 'GET', + }, + ) + } + + public async getIssueTypeSchema(): Promise { + return this.client.request( + '/delphi/issue_type/schema', + { + api: 'labrinth', + version: 'internal', + method: 'GET', + }, + ) + } + + public async getDetailRuleInput( + detailId: string, + ): Promise { + return this.client.request( + `/moderation/tech-review/rules/details/${detailId}/input`, + { + api: 'labrinth', + version: 'internal', + method: 'GET', + }, + ) + } + + public async getRuleAffectedDetails( + id: number, + params: Labrinth.TechReview.Internal.GetRuleAffectedDetailsRequest = {}, + ): Promise { + return this.client.request( + `/moderation/tech-review/rules/${id}/effects`, + { + api: 'labrinth', + version: 'internal', + method: 'GET', + params, + }, + ) + } + + public async testRule( + request: Labrinth.TechReview.Internal.TestDelphiRuleRequest, + ): Promise { + return this.client.request( + '/moderation/tech-review/rules/test', + { + api: 'labrinth', + version: 'internal', + method: 'POST', + body: request, + }, + ) + } + + public async createRule( + rule: Labrinth.TechReview.Internal.WriteDelphiRule, + ): Promise { + return this.client.request( + '/moderation/tech-review/rules', + { + api: 'labrinth', + version: 'internal', + method: 'POST', + body: rule, + }, + ) + } + + public async updateRule( + id: number, + rule: Labrinth.TechReview.Internal.WriteDelphiRule, + ): Promise { + return this.client.request( + `/moderation/tech-review/rules/${id}`, + { + api: 'labrinth', + version: 'internal', + method: 'PUT', + body: rule, + }, + ) + } + + public async deleteRule(id: number): Promise { + return this.client.request(`/moderation/tech-review/rules/${id}`, { + api: 'labrinth', + version: 'internal', + method: 'DELETE', + }) + } + + public async scanRules(signal?: AbortSignal): Promise> { + return this.client.stream('/moderation/tech-review/rules/scan', { + api: 'labrinth', + version: 'internal', + method: 'POST', + signal, + }) + } + /** * Search for projects awaiting technical review. * @@ -70,6 +191,7 @@ export class LabrinthTechReviewInternalModule extends AbstractModule { * Get detailed information about a specific issue. * * @param issueId - The issue ID + * @param params - Options controlling which issue details are returned * @returns Issue with all its details * * @example @@ -78,13 +200,17 @@ export class LabrinthTechReviewInternalModule extends AbstractModule { * console.log(issue.issue_type, issue.status) * ``` */ - public async getIssue(issueId: string): Promise { + public async getIssue( + issueId: string, + params: Labrinth.TechReview.Internal.GetIssueRequest = {}, + ): Promise { return this.client.request( `/moderation/tech-review/issue/${issueId}`, { api: 'labrinth', version: 'internal', method: 'GET', + params, }, ) } @@ -119,7 +245,7 @@ export class LabrinthTechReviewInternalModule extends AbstractModule { public async updateGlobalIssueDetails( data: Labrinth.TechReview.Internal.UpdateGlobalIssueRequest[], ): Promise { - return this.client.request('/moderation/tech-review/global-issue-detail', { + return this.client.request('/moderation/tech-review/global-traces', { api: 'labrinth', version: 'internal', method: 'POST', @@ -131,7 +257,7 @@ export class LabrinthTechReviewInternalModule extends AbstractModule { params: Labrinth.TechReview.Internal.SearchGlobalIssueDetailsRequest, ): Promise { return this.client.request( - '/moderation/tech-review/global-issue-detail/search', + '/moderation/tech-review/global-traces/search', { api: 'labrinth', version: 'internal', @@ -145,7 +271,7 @@ export class LabrinthTechReviewInternalModule extends AbstractModule { params: Labrinth.TechReview.Internal.GetGlobalIssueDetailRequest, ): Promise { return this.client.request( - '/moderation/tech-review/global-issue-detail/local-traces', + '/moderation/tech-review/global-traces/local-traces', { api: 'labrinth', version: 'internal', diff --git a/packages/api-client/src/modules/labrinth/types.ts b/packages/api-client/src/modules/labrinth/types.ts index 354cf4af33..8168d47f0d 100644 --- a/packages/api-client/src/modules/labrinth/types.ts +++ b/packages/api-client/src/modules/labrinth/types.ts @@ -2414,6 +2414,126 @@ export namespace Labrinth { export namespace TechReview { export namespace Internal { + export type DelphiRule = { + id: number + name: string + rule: string + priority: number + on_issue_types: string[] + revision: number + current_revision?: number + created_at: string + updated_at: string + created_by: number | null + updated_by: number | null + affected_details_count: number + affected_details: DelphiRuleAffectedDetail[] + } + + export type DelphiRuleAffectedDetail = { + detail_id: string + issue_id: string + project_id: string | null + project_name: string | null + project_icon_url: string | null + version_id: string | null + version_name: string | null + version_number: string | null + issue_type: string + key: string + jar: string | null + file_path: string + original_severity: DelphiSeverity + severity: DelphiSeverity + } + + export type GetRuleAffectedDetailsRequest = { + limit?: number + page?: number + } + + export type GetRuleAffectedDetailsResponse = { + total: number + details: DelphiRuleAffectedDetail[] + } + + export type WriteDelphiRule = { + name: string + rule: string + priority: number + on_issue_types: string[] + } + + export type DelphiIssueTypeSchemaResponse = Record + + export type TestDelphiRuleRequest = { + rule: string + inputs: RuleInput[] + } + + export type DelphiRuleEffect = { + severity: DelphiSeverity + } + + export type DelphiRuleSchema = Record + + export type DelphiRuleSchemaResponse = { + input: DelphiRuleSchema + output: DelphiRuleSchema + components: Record + } + + export type RuleInput = { + schema_version: number + trace: RuleTrace + file_traces: RuleTrace[] + scan: { + delphi_version: number + } + artifact: { + size: number | null + hashes: Record + } + project: { + id: string | null + types: string[] + } + version: { + id: string | null + loaders: string[] + } + file: { + id: string | null + } + } + + export type RuleTrace = { + key: string + issue_type: string + severity: DelphiSeverity + jar: string | null + file_path: string + data: Record + } + + export type TestDelphiRuleResponse = { + effects: Array + } + + export type DelphiRuleScanPhase = 'scanning' | 'publishing' | 'complete' + + export type DelphiRuleScanEvent = { + phase: DelphiRuleScanPhase + revision: number + scanned: number + total: number + effects: number + } + + export type DelphiRuleScanErrorEvent = { + message: string + } + export type SearchProjectsRequest = { limit?: number page?: number @@ -2539,6 +2659,10 @@ export namespace Labrinth { issues: FileIssue[] } + export type GetIssueRequest = { + include_hidden?: boolean + } + export type FileIssue = { id: string report_id: string @@ -2660,7 +2784,7 @@ export namespace Labrinth { export type FlagReason = 'delphi' - export type DelphiSeverity = 'low' | 'medium' | 'high' | 'severe' + export type DelphiSeverity = 'hidden' | 'low' | 'medium' | 'high' | 'severe' | 'malware' export type DelphiReportIssueStatus = 'pending' | 'safe' | 'unsafe' diff --git a/packages/api-client/src/types/errors.ts b/packages/api-client/src/types/errors.ts index 6e33b6d9a2..a1fd55c837 100644 --- a/packages/api-client/src/types/errors.ts +++ b/packages/api-client/src/types/errors.ts @@ -38,6 +38,11 @@ export type ModrinthErrorResponse = { */ description: string + /** + * Structured details about the error + */ + details?: unknown + /** * Optional context about where the error occurred */ diff --git a/packages/blog/compiled/streamlined_version_creation.content.ts b/packages/blog/compiled/streamlined_version_creation.content.ts index 09abfe58d4..36b7e4c77c 100644 --- a/packages/blog/compiled/streamlined_version_creation.content.ts +++ b/packages/blog/compiled/streamlined_version_creation.content.ts @@ -1,2 +1,2 @@ // AUTO-GENERATED FILE - DO NOT EDIT -export const html = `

Hey everyone! As part of our ongoing work to improve the creator side of the platform, we’re shipping a new project version creation and editing today. This part of the product was showing its age, so we’ve overhauled it to set us up for the new project types we plan to ship in the new year!

TL;DR

  • Multi-file uploads with primary file detection and new supplementary file types
  • Automatic detection of version number, subtitle, loaders, game versions, and environment bundled into a version summary
  • A new loader selector that groups loaders by project type
  • A new game version selector with search and selecting version ranges
  • Project environments moved to be on a per-version basis
  • A new dependency selector with search and smart suggestions
  • Project gallery, versions, and publishing checklist moved into project settings

File uploading

For starters, we’ve been centralizing all project editing into Project Settings to make the experience clearer and more approachable for new creators. Editing project versions now happens directly within Project Settings and projects look slightly different if you’re the creator.

Project page header showing the primary action as "Edit project" for the creator

You can create a new version by drag and dropping files into the versions table or creating a new version and uploading them. Multiple files can be uploaded at once.

The primary file is explicitly listed at the top and separate from any supplementary files. From there, you can add additional supplementary files and assign their types. Newly supported types include sources jar, dev jar, javadoc jar, and signature file.

Version summary

Once you’ve uploaded your files, you’re taken to a summary page where we automatically detect the version number, subtitle, loaders, game versions, and environments based on the primary file and previous project versions.

Any field can be individually edited by clicking the edit button in the top right. For cases where we’re unable to detect something, that field simply won’t appear in the summary and will instead show up as an additional step in the modal flow.

Add details stage of the upload modal, where the user selects version type, number, subtitle, and can edit loaders, game versions, and environment metadata.

Loader selector

We’ve added a refreshed loader selection screen that groups loaders by project type. You can click any loader tag to add it.

Game version selector

Game versions now have their own dedicated step. This was a major pain point for projects that support a wide range of game versions. You can search for versions or toggle between releases and snapshots. Select individual versions with a click, or use shift-click to select a range.

Environment selector

Project environments were released earlier this year, and we heard feedback that some projects need them configured at the version level. We’ve moved environments out of project settings and into versions. For the vast majority of projects environments rarely change, so we automatically carry them over from a previous version that uses the same loader. You can always edit this if needed.

Edit environment screen, showing a bunch of options to select such as client-side only, server-side only, and more.

Dependency selector

Dependencies were another pain point, so we’ve added the ability to search projects and versions directly, no more copying IDs. We also suggest dependencies from the other versions you’ve uploaded with the same loader, making them easy to add with a single click.

Misc

  • Gallery management has now also been moved into Project Settings
  • The project publishing checklist now lives in Project Settings

Thank you all for your continued support. We hope you have a great holiday and get some well-earned time with your families! 🎅

`; +export const html = `

Hey everyone! As part of our ongoing work to improve the creator side of the platform, we’re shipping a new project version creation and editing today. This part of the product was showing its age, so we’ve overhauled it to set us up for the new project types we plan to ship in the new year!

TL;DR

  • Multi-file uploads with primary file detection and new supplementary file types
  • Automatic detection of version number, subtitle, loaders, game versions, and environment bundled into a version summary
  • A new loader selector that groups loaders by project type
  • A new game version selector with search and selecting version ranges
  • Project environments moved to be on a per-version basis
  • A new dependency selector with search and smart suggestions
  • Project gallery, versions, and publishing checklist moved into project settings

File uploading

For starters, we’ve been centralizing all project editing into Project Settings to make the experience clearer and more approachable for new creators. Editing project versions now happens directly within Project Settings and projects look slightly different if you’re the creator.

Project page header showing the primary action as "Edit project" for the creator

You can create a new version by drag and dropping files into the versions table or creating a new version and uploading them. Multiple files can be uploaded at once.

The primary file is explicitly listed at the top and separate from any supplementary files. From there, you can add additional supplementary files and assign their types. Newly supported types include sources jar, dev jar, javadoc jar, and signature file.

Version summary

Once you’ve uploaded your files, you’re taken to a summary page where we automatically detect the version number, subtitle, loaders, game versions, and environments based on the primary file and previous project versions.

Any field can be individually edited by clicking the edit button in the top right. For cases where we’re unable to detect something, that field simply won’t appear in the summary and will instead show up as an additional step in the modal flow.

Add details stage of the upload modal, where the user selects version type, number, subtitle, and can edit loaders, game versions, and environment metadata.

Loader selector

We’ve added a refreshed loader selection screen that groups loaders by project type. You can click any loader tag to add it.

Game version selector

Game versions now have their own dedicated step. This was a major pain point for projects that support a wide range of game versions. You can search for versions or toggle between releases and snapshots. Select individual versions with a click, or use shift-click to select a range.

Environment selector

Project environments were released earlier this year, and we heard feedback that some projects need them configured at the version level. We’ve moved environments out of project settings and into versions. For the vast majority of projects environments rarely change, so we automatically carry them over from a previous version that uses the same loader. You can always edit this if needed.

Edit environment screen, showing a bunch of options to select such as client-side only, server-side only, and more.

Dependency selector

Dependencies were another pain point, so we’ve added the ability to search projects and versions directly, no more copying IDs. We also suggest dependencies from the other versions you’ve uploaded with the same loader, making them easy to add with a single click.

Misc

  • Gallery management has now also been moved into Project Settings
  • The project publishing checklist now lives in Project Settings

Thank you all for your continued support. We hope you have a great holiday and get some well-earned time with your families! 🎅

`; diff --git a/packages/ui/src/components/base/MultiSelect.vue b/packages/ui/src/components/base/MultiSelect.vue index 23c0e8ddc6..fee49996ee 100644 --- a/packages/ui/src/components/base/MultiSelect.vue +++ b/packages/ui/src/components/base/MultiSelect.vue @@ -1238,6 +1238,7 @@ function handleDropdownKeydown(event: KeyboardEvent) { switch (event.key) { case 'Escape': event.preventDefault() + event.stopPropagation() closeDropdown() break case 'ArrowDown': diff --git a/scripts/reset-db.sh b/scripts/reset-db.sh new file mode 100755 index 0000000000..0e473f1d3c --- /dev/null +++ b/scripts/reset-db.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .env + +psql "$DATABASE_URL" -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = current_database() AND pid <> pg_backend_pid();" +cargo sqlx database reset +../../scripts/seed-db.sh