diff --git a/apps/labrinth/.sqlx/query-0f3d943e4fc48a94363b77c8a7d36eb1dd626e77331d8278c406df952691be4c.json b/apps/labrinth/.sqlx/query-0f3d943e4fc48a94363b77c8a7d36eb1dd626e77331d8278c406df952691be4c.json
deleted file mode 100644
index 4bc87e73a5..0000000000
--- a/apps/labrinth/.sqlx/query-0f3d943e4fc48a94363b77c8a7d36eb1dd626e77331d8278c406df952691be4c.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "SELECT COUNT(*) FROM payouts_values_notifications WHERE notified = FALSE AND user_id = $1",
- "describe": {
- "columns": [
- {
- "ordinal": 0,
- "name": "count",
- "type_info": "Int8"
- }
- ],
- "parameters": {
- "Left": [
- "Int8"
- ]
- },
- "nullable": [
- null
- ]
- },
- "hash": "0f3d943e4fc48a94363b77c8a7d36eb1dd626e77331d8278c406df952691be4c"
-}
diff --git a/apps/labrinth/.sqlx/query-0ffebf149d50149d0c128478e6267a2296efc16763cdbd5d8c295a1a7ca08021.json b/apps/labrinth/.sqlx/query-0ffebf149d50149d0c128478e6267a2296efc16763cdbd5d8c295a1a7ca08021.json
deleted file mode 100644
index c8a82d60ef..0000000000
--- a/apps/labrinth/.sqlx/query-0ffebf149d50149d0c128478e6267a2296efc16763cdbd5d8c295a1a7ca08021.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "\n INSERT INTO users (id, username, email, role)\n VALUES\n (1000, 'userland', 'userland@modrinth.com', 'developer'),\n (1001, 'useful', 'useful@modrinth.com', 'developer'),\n (1002, 'Useless', 'useless@modrinth.com', 'developer')\n ",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": []
- },
- "nullable": []
- },
- "hash": "0ffebf149d50149d0c128478e6267a2296efc16763cdbd5d8c295a1a7ca08021"
-}
diff --git a/apps/labrinth/.sqlx/query-1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286.json b/apps/labrinth/.sqlx/query-1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286.json
deleted file mode 100644
index 921f7f92d9..0000000000
--- a/apps/labrinth/.sqlx/query-1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "\n SELECT\n id,\n status AS \"status: PayoutStatus\"\n FROM payouts\n ORDER BY id\n ",
- "describe": {
- "columns": [
- {
- "ordinal": 0,
- "name": "id",
- "type_info": "Int8"
- },
- {
- "ordinal": 1,
- "name": "status: PayoutStatus",
- "type_info": "Varchar"
- }
- ],
- "parameters": {
- "Left": []
- },
- "nullable": [
- false,
- false
- ]
- },
- "hash": "1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286"
-}
diff --git a/apps/labrinth/.sqlx/query-20cff8fdf7971e91c9d473b9a4663ce02ca16781e32232ae0fa7a0af1973d3a4.json b/apps/labrinth/.sqlx/query-20cff8fdf7971e91c9d473b9a4663ce02ca16781e32232ae0fa7a0af1973d3a4.json
deleted file mode 100644
index 3c99ff3fed..0000000000
--- a/apps/labrinth/.sqlx/query-20cff8fdf7971e91c9d473b9a4663ce02ca16781e32232ae0fa7a0af1973d3a4.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "SELECT COUNT(*) FROM payouts_values_notifications WHERE notified = FALSE",
- "describe": {
- "columns": [
- {
- "ordinal": 0,
- "name": "count",
- "type_info": "Int8"
- }
- ],
- "parameters": {
- "Left": []
- },
- "nullable": [
- null
- ]
- },
- "hash": "20cff8fdf7971e91c9d473b9a4663ce02ca16781e32232ae0fa7a0af1973d3a4"
-}
diff --git a/apps/labrinth/.sqlx/query-329c009635ad2a4c214da2f4651b8c14fe329deac983bc2f43ba74e066f9cf8d.json b/apps/labrinth/.sqlx/query-329c009635ad2a4c214da2f4651b8c14fe329deac983bc2f43ba74e066f9cf8d.json
deleted file mode 100644
index 2731d395ff..0000000000
--- a/apps/labrinth/.sqlx/query-329c009635ad2a4c214da2f4651b8c14fe329deac983bc2f43ba74e066f9cf8d.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "UPDATE mods SET status = 'processing' WHERE id = $1",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": [
- "Int8"
- ]
- },
- "nullable": []
- },
- "hash": "329c009635ad2a4c214da2f4651b8c14fe329deac983bc2f43ba74e066f9cf8d"
-}
diff --git a/apps/labrinth/.sqlx/query-38ace0abad724d1111b79a72c7c8518f1272690ce7766dbdc41ec1e98f7f1fec.json b/apps/labrinth/.sqlx/query-38ace0abad724d1111b79a72c7c8518f1272690ce7766dbdc41ec1e98f7f1fec.json
deleted file mode 100644
index 815e7795ec..0000000000
--- a/apps/labrinth/.sqlx/query-38ace0abad724d1111b79a72c7c8518f1272690ce7766dbdc41ec1e98f7f1fec.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "\n INSERT INTO users (id, username, email, role)\n VALUES (2100, 'prefix_under_score', 'prefix_under_score@modrinth.com', 'developer')\n ",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": []
- },
- "nullable": []
- },
- "hash": "38ace0abad724d1111b79a72c7c8518f1272690ce7766dbdc41ec1e98f7f1fec"
-}
diff --git a/apps/labrinth/.sqlx/query-5cb65218a2a4a7343130d838f8a78df08d25844f1019d539712b0cb686e4ae81.json b/apps/labrinth/.sqlx/query-5cb65218a2a4a7343130d838f8a78df08d25844f1019d539712b0cb686e4ae81.json
deleted file mode 100644
index 55ba902620..0000000000
--- a/apps/labrinth/.sqlx/query-5cb65218a2a4a7343130d838f8a78df08d25844f1019d539712b0cb686e4ae81.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "\n INSERT INTO users (id, username, email, role)\n VALUES ($1, $2, $3, 'developer')\n ",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": [
- "Int8",
- "Varchar",
- "Varchar"
- ]
- },
- "nullable": []
- },
- "hash": "5cb65218a2a4a7343130d838f8a78df08d25844f1019d539712b0cb686e4ae81"
-}
diff --git a/apps/labrinth/.sqlx/query-64d02e0fa69fe1cad91d851e4d32dea4a6c830ec1b74ea1e2b71d5f18751ac6c.json b/apps/labrinth/.sqlx/query-64d02e0fa69fe1cad91d851e4d32dea4a6c830ec1b74ea1e2b71d5f18751ac6c.json
new file mode 100644
index 0000000000..38c58dbb1c
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-64d02e0fa69fe1cad91d851e4d32dea4a6c830ec1b74ea1e2b71d5f18751ac6c.json
@@ -0,0 +1,62 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n\t\tSELECT\n\t\t\tid,\n\t\t\tname,\n\t\t\trule,\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\tFROM delphi_rules\n\t\tWHERE NOT delete_on_next_revision\n\t\tORDER BY name, id\n\t\t",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 1,
+ "name": "name",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 2,
+ "name": "rule",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 3,
+ "name": "revision",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 4,
+ "name": "created_at",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 5,
+ "name": "updated_at",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 6,
+ "name": "created_by",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 7,
+ "name": "updated_by",
+ "type_info": "Int8"
+ }
+ ],
+ "parameters": {
+ "Left": []
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ true
+ ]
+ },
+ "hash": "64d02e0fa69fe1cad91d851e4d32dea4a6c830ec1b74ea1e2b71d5f18751ac6c"
+}
diff --git a/apps/labrinth/.sqlx/query-6678cd4c51771cfaae2be8021ba66908ea41a06ba858dc5b523aef6aae27b850.json b/apps/labrinth/.sqlx/query-6678cd4c51771cfaae2be8021ba66908ea41a06ba858dc5b523aef6aae27b850.json
deleted file mode 100644
index b4c2e5a56e..0000000000
--- a/apps/labrinth/.sqlx/query-6678cd4c51771cfaae2be8021ba66908ea41a06ba858dc5b523aef6aae27b850.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "INSERT INTO payouts_values_notifications (date_available, user_id, notified)\n VALUES ($1, $2, FALSE)\n ON CONFLICT (date_available, user_id) DO NOTHING",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": [
- "Timestamptz",
- "Int8"
- ]
- },
- "nullable": []
- },
- "hash": "6678cd4c51771cfaae2be8021ba66908ea41a06ba858dc5b523aef6aae27b850"
-}
diff --git a/apps/labrinth/.sqlx/query-66868d0b35229abffa3f668eaef19dc0cdef502935dd5939a2f3ab89b0766ad2.json b/apps/labrinth/.sqlx/query-66868d0b35229abffa3f668eaef19dc0cdef502935dd5939a2f3ab89b0766ad2.json
deleted file mode 100644
index 5d509fdf07..0000000000
--- a/apps/labrinth/.sqlx/query-66868d0b35229abffa3f668eaef19dc0cdef502935dd5939a2f3ab89b0766ad2.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "CREATE SCHEMA public;",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": []
- },
- "nullable": []
- },
- "hash": "66868d0b35229abffa3f668eaef19dc0cdef502935dd5939a2f3ab89b0766ad2"
-}
diff --git a/apps/labrinth/.sqlx/query-69a1cb4b7f1115a990d1fc4805d58541fc78e910111c09ba3d50a12d9ca4a9f8.json b/apps/labrinth/.sqlx/query-69a1cb4b7f1115a990d1fc4805d58541fc78e910111c09ba3d50a12d9ca4a9f8.json
deleted file mode 100644
index fc7d2ac98d..0000000000
--- a/apps/labrinth/.sqlx/query-69a1cb4b7f1115a990d1fc4805d58541fc78e910111c09ba3d50a12d9ca4a9f8.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "INSERT INTO payouts_values (user_id, mod_id, amount, created, date_available)\n VALUES ($1, NULL, $2, NOW(), $3)",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": [
- "Int8",
- "Numeric",
- "Timestamptz"
- ]
- },
- "nullable": []
- },
- "hash": "69a1cb4b7f1115a990d1fc4805d58541fc78e910111c09ba3d50a12d9ca4a9f8"
-}
diff --git a/apps/labrinth/.sqlx/query-7b4ee9c2fb7ef8da9bce4d86d4271aa078119ed2c60828683f872321fb61bb79.json b/apps/labrinth/.sqlx/query-7b4ee9c2fb7ef8da9bce4d86d4271aa078119ed2c60828683f872321fb61bb79.json
new file mode 100644
index 0000000000..9262ec39d7
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-7b4ee9c2fb7ef8da9bce4d86d4271aa078119ed2c60828683f872321fb61bb79.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\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": "7b4ee9c2fb7ef8da9bce4d86d4271aa078119ed2c60828683f872321fb61bb79"
+}
diff --git a/apps/labrinth/.sqlx/query-8ca7f46618bd72cfd5fe1f246d41a31da1885cc22bb61be6853950ba52be9aa7.json b/apps/labrinth/.sqlx/query-8ca7f46618bd72cfd5fe1f246d41a31da1885cc22bb61be6853950ba52be9aa7.json
deleted file mode 100644
index 444f57bf3b..0000000000
--- a/apps/labrinth/.sqlx/query-8ca7f46618bd72cfd5fe1f246d41a31da1885cc22bb61be6853950ba52be9aa7.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "UPDATE moderation_locks SET locked_at = NOW() - ($1::bigint * INTERVAL '1 minute') - INTERVAL '1 second' WHERE project_id = $2",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": [
- "Int8",
- "Int8"
- ]
- },
- "nullable": []
- },
- "hash": "8ca7f46618bd72cfd5fe1f246d41a31da1885cc22bb61be6853950ba52be9aa7"
-}
diff --git a/apps/labrinth/.sqlx/query-b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3.json b/apps/labrinth/.sqlx/query-b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3.json
deleted file mode 100644
index 89bd8147dc..0000000000
--- a/apps/labrinth/.sqlx/query-b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "SELECT status AS \"status: PayoutStatus\" FROM payouts WHERE id = 1",
- "describe": {
- "columns": [
- {
- "ordinal": 0,
- "name": "status: PayoutStatus",
- "type_info": "Varchar"
- }
- ],
- "parameters": {
- "Left": []
- },
- "nullable": [
- false
- ]
- },
- "hash": "b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3"
-}
diff --git a/apps/labrinth/.sqlx/query-c65295f61b18f7be4fdf30b3e2c2c36b9db3770508be4acbb6b441ed8006d9c7.json b/apps/labrinth/.sqlx/query-c65295f61b18f7be4fdf30b3e2c2c36b9db3770508be4acbb6b441ed8006d9c7.json
deleted file mode 100644
index f245ea1c8a..0000000000
--- a/apps/labrinth/.sqlx/query-c65295f61b18f7be4fdf30b3e2c2c36b9db3770508be4acbb6b441ed8006d9c7.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "DROP SCHEMA public CASCADE;",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": []
- },
- "nullable": []
- },
- "hash": "c65295f61b18f7be4fdf30b3e2c2c36b9db3770508be4acbb6b441ed8006d9c7"
-}
diff --git a/apps/labrinth/.sqlx/query-cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02.json b/apps/labrinth/.sqlx/query-cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02.json
deleted file mode 100644
index 469c30168a..0000000000
--- a/apps/labrinth/.sqlx/query-cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "\n INSERT INTO payouts (id, method, platform_id, status, user_id, amount, created)\n VALUES ($1, $2, $3, $4, $5, 10.0, NOW())\n ",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": [
- "Int8",
- "Text",
- "Text",
- "Varchar",
- "Int8"
- ]
- },
- "nullable": []
- },
- "hash": "cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02"
-}
diff --git a/apps/labrinth/.sqlx/query-cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606.json b/apps/labrinth/.sqlx/query-cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606.json
deleted file mode 100644
index 52e020ebf2..0000000000
--- a/apps/labrinth/.sqlx/query-cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "\n INSERT INTO payouts (id, method, platform_id, status, user_id, amount, created)\n VALUES ($1, $2, NULL, $3, $4, 10.00, NOW())\n ",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": [
- "Int8",
- "Text",
- "Varchar",
- "Int8"
- ]
- },
- "nullable": []
- },
- "hash": "cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606"
-}
diff --git a/apps/labrinth/.sqlx/query-e522b3da5f03cafd265929a8489252f7ef678bd0d6b9cee0be7df36bdc92b80a.json b/apps/labrinth/.sqlx/query-e522b3da5f03cafd265929a8489252f7ef678bd0d6b9cee0be7df36bdc92b80a.json
new file mode 100644
index 0000000000..ea89bf1e61
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-e522b3da5f03cafd265929a8489252f7ef678bd0d6b9cee0be7df36bdc92b80a.json
@@ -0,0 +1,66 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n\t\tINSERT INTO delphi_rules (\n\t\t\tname,\n\t\t\trule,\n\t\t\tcreated_by,\n\t\t\tupdated_by\n\t\t)\n\t\tVALUES ($1, $2, $3, $3)\n\t\tRETURNING\n\t\t\tid,\n\t\t\tname,\n\t\t\trule,\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",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 1,
+ "name": "name",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 2,
+ "name": "rule",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 3,
+ "name": "revision",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 4,
+ "name": "created_at",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 5,
+ "name": "updated_at",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 6,
+ "name": "created_by",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 7,
+ "name": "updated_by",
+ "type_info": "Int8"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Varchar",
+ "Varchar",
+ "Int8"
+ ]
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ true
+ ]
+ },
+ "hash": "e522b3da5f03cafd265929a8489252f7ef678bd0d6b9cee0be7df36bdc92b80a"
+}
diff --git a/apps/labrinth/.sqlx/query-f5b43baf6838a16554aab7282a396540f5b6d265c87af267be0a5c0044d470a9.json b/apps/labrinth/.sqlx/query-f5b43baf6838a16554aab7282a396540f5b6d265c87af267be0a5c0044d470a9.json
new file mode 100644
index 0000000000..75717f39b0
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-f5b43baf6838a16554aab7282a396540f5b6d265c87af267be0a5c0044d470a9.json
@@ -0,0 +1,67 @@
+{
+ "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\tupdated_at = CURRENT_TIMESTAMP,\n\t\t\tupdated_by = $4\n\t\tWHERE id = $1 AND NOT delete_on_next_revision\n\t\tRETURNING\n\t\t\tid,\n\t\t\tname,\n\t\t\trule,\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",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 1,
+ "name": "name",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 2,
+ "name": "rule",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 3,
+ "name": "revision",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 4,
+ "name": "created_at",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 5,
+ "name": "updated_at",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 6,
+ "name": "created_by",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 7,
+ "name": "updated_by",
+ "type_info": "Int8"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8",
+ "Varchar",
+ "Varchar",
+ "Int8"
+ ]
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ true
+ ]
+ },
+ "hash": "f5b43baf6838a16554aab7282a396540f5b6d265c87af267be0a5c0044d470a9"
+}
diff --git a/apps/labrinth/.sqlx/query-fabba52b34e14196b6305671c060d934b9afc6dccc5fc77454fbe45365bdf429.json b/apps/labrinth/.sqlx/query-fabba52b34e14196b6305671c060d934b9afc6dccc5fc77454fbe45365bdf429.json
deleted file mode 100644
index 006563f450..0000000000
--- a/apps/labrinth/.sqlx/query-fabba52b34e14196b6305671c060d934b9afc6dccc5fc77454fbe45365bdf429.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "SELECT pg_advisory_unlock(1)",
- "describe": {
- "columns": [
- {
- "ordinal": 0,
- "name": "pg_advisory_unlock",
- "type_info": "Bool"
- }
- ],
- "parameters": {
- "Left": []
- },
- "nullable": [
- null
- ]
- },
- "hash": "fabba52b34e14196b6305671c060d934b9afc6dccc5fc77454fbe45365bdf429"
-}
diff --git a/apps/labrinth/.sqlx/query-fd5c773a61d35bcd71503ec4d5f86e8917cfab9679d5064074681663ba467e41.json b/apps/labrinth/.sqlx/query-fd5c773a61d35bcd71503ec4d5f86e8917cfab9679d5064074681663ba467e41.json
deleted file mode 100644
index d3e3520bcc..0000000000
--- a/apps/labrinth/.sqlx/query-fd5c773a61d35bcd71503ec4d5f86e8917cfab9679d5064074681663ba467e41.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "SELECT COUNT(*) FROM notifications WHERE user_id = $1 AND body->>'type' = 'payout_available'",
- "describe": {
- "columns": [
- {
- "ordinal": 0,
- "name": "count",
- "type_info": "Int8"
- }
- ],
- "parameters": {
- "Left": [
- "Int8"
- ]
- },
- "nullable": [
- null
- ]
- },
- "hash": "fd5c773a61d35bcd71503ec4d5f86e8917cfab9679d5064074681663ba467e41"
-}
diff --git a/apps/labrinth/Cargo.toml b/apps/labrinth/Cargo.toml
index 3da97e6fa8..50ac869f4a 100644
--- a/apps/labrinth/Cargo.toml
+++ b/apps/labrinth/Cargo.toml
@@ -34,6 +34,7 @@ base64 = { workspace = true }
bitflags = { workspace = true }
bytes = { workspace = true }
censor = { workspace = true }
+cel = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
clap = { workspace = true, features = ["derive"] }
clickhouse = { workspace = true, features = ["time", "uuid"] }
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..7cbe599623
--- /dev/null
+++ b/apps/labrinth/migrations/20260720105358_delphi_trace_rules.sql
@@ -0,0 +1,34 @@
+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,
+ 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,
+ hidden BOOLEAN NOT NULL DEFAULT FALSE,
+ PRIMARY KEY (revision, detail_id)
+);
+
+CREATE INDEX delphi_rule_effects_rule_id ON delphi_rule_effects(rule_id);
diff --git a/apps/labrinth/migrations/20260723120000_delphi_effective_rule_details.sql b/apps/labrinth/migrations/20260723120000_delphi_effective_rule_details.sql
new file mode 100644
index 0000000000..65dc4e2536
--- /dev/null
+++ b/apps/labrinth/migrations/20260723120000_delphi_effective_rule_details.sql
@@ -0,0 +1,37 @@
+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,
+ COALESCE(dre.hidden, FALSE) AS hidden,
+ 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;
diff --git a/apps/labrinth/src/routes/internal/delphi/mod.rs b/apps/labrinth/src/routes/internal/delphi/mod.rs
index cf92fc312b..ed275dfb6c 100644
--- a/apps/labrinth/src/routes/internal/delphi/mod.rs
+++ b/apps/labrinth/src/routes/internal/delphi/mod.rs
@@ -210,6 +210,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
@@ -229,7 +230,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,
@@ -245,9 +246,17 @@ async fn ingest_report_deserialized(
.insert(&mut transaction)
.await
.wrap_internal_err("failed to insert Delphi issue detail")?;
+ inserted_detail_ids.push(detail_id);
}
}
+ 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_sync::sync_project_tech_review_state(
&[DBProjectId::from(report.project_id)],
tech_review_sync::TechReviewExitReason::Resolved,
diff --git a/apps/labrinth/src/routes/internal/delphi/rescan.rs b/apps/labrinth/src/routes/internal/delphi/rescan.rs
index c7d7fa297b..cbaefd58f7 100644
--- a/apps/labrinth/src/routes/internal/delphi/rescan.rs
+++ b/apps/labrinth/src/routes/internal/delphi/rescan.rs
@@ -105,6 +105,7 @@ async fn fetch_unreviewed_tech_review_project_ids(
WHERE
didws.project_id = m.id
AND didws.status = 'pending'
+ AND NOT didws.hidden
-- see delphi.rs todo comment
AND dri.issue_type != '__dummy'
)
@@ -115,6 +116,7 @@ async fn fetch_unreviewed_tech_review_project_ids(
WHERE
didws.project_id = m.id
AND didws.status IN ('safe', 'unsafe')
+ AND NOT didws.hidden
-- see delphi.rs todo comment
AND dri.issue_type != '__dummy'
)
diff --git a/apps/labrinth/src/routes/internal/delphi/tech_review_sync.rs b/apps/labrinth/src/routes/internal/delphi/tech_review_sync.rs
index 23a45d218a..974db515d0 100644
--- a/apps/labrinth/src/routes/internal/delphi/tech_review_sync.rs
+++ b/apps/labrinth/src/routes/internal/delphi/tech_review_sync.rs
@@ -60,9 +60,11 @@ const DUMMY_ISSUE_TYPE: &str = "__dummy";
pub enum TechReviewExitReason {
Resolved,
FileDeleted,
+ RulesChanged,
}
struct ProjectTechReviewState {
+ project_id: DBProjectId,
has_pending_detail: bool,
has_unsafe_detail: bool,
has_dummy: bool,
@@ -88,36 +90,35 @@ pub async fn sync_project_tech_review_state(
r#"
WITH project_ids AS (
SELECT unnest($1::bigint[]) AS project_id
- )
- SELECT
- p.project_id AS "project_id!: DBProjectId",
- 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 = p.project_id
- AND didws.status = 'pending'
+ ),
+ project_detail_state AS (
+ SELECT
+ p.project_id,
+ COALESCE(BOOL_OR(
+ didws.status = 'pending'
+ AND NOT didws.hidden
AND dri.issue_type != $3
- ) AS "has_pending_detail!",
- 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 = p.project_id
- AND didws.status = 'unsafe'
+ ), FALSE) AS has_pending_detail,
+ COALESCE(BOOL_OR(
+ didws.status = 'unsafe'
+ AND NOT didws.hidden
AND dri.issue_type != $3
- ) AS "has_unsafe_detail!",
- 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 = p.project_id
- AND didws.status = 'pending'
+ ), FALSE) AS has_unsafe_detail,
+ COALESCE(BOOL_OR(
+ didws.status = 'pending'
AND dri.issue_type = $3
- ) AS "has_dummy!",
+ ), 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
@@ -144,7 +145,7 @@ pub async fn sync_project_tech_review_state(
ORDER BY dr.created DESC, dr.id DESC
LIMIT 1
) AS "report_id: DelphiReportId"
- FROM project_ids p
+ FROM project_detail_state p
"#,
&project_ids_raw,
&tech_review_message_types,
@@ -156,6 +157,7 @@ pub async fn sync_project_tech_review_state(
for row in rows {
let state = ProjectTechReviewState {
+ project_id: row.project_id,
has_pending_detail: row.has_pending_detail,
has_unsafe_detail: row.has_unsafe_detail,
has_dummy: row.has_dummy,
@@ -250,8 +252,23 @@ async fn sync_one_project_tech_review_state(
exit_reason: TechReviewExitReason,
txn: &mut PgTransaction<'_>,
) -> Result<(), ApiError> {
- let needs_tech_review =
- state.has_pending_detail || state.has_unsafe_detail || state.has_dummy;
+ let has_review_detail = state.has_pending_detail || state.has_unsafe_detail;
+
+ if matches!(exit_reason, TechReviewExitReason::RulesChanged)
+ && !has_review_detail
+ {
+ remove_dummy_issue_details(state.project_id, txn).await?;
+
+ 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?;
+ }
+
+ return Ok(());
+ }
+
+ let needs_tech_review = has_review_detail || state.has_dummy;
if needs_tech_review {
if (state.has_pending_detail || state.has_unsafe_detail)
@@ -325,7 +342,9 @@ async fn insert_exit_message(
txn: &mut PgTransaction<'_>,
) -> Result<(), ApiError> {
let body = match exit_reason {
- TechReviewExitReason::Resolved => MessageBody::TechReviewExited,
+ TechReviewExitReason::Resolved | TechReviewExitReason::RulesChanged => {
+ MessageBody::TechReviewExited
+ }
TechReviewExitReason::FileDeleted => {
MessageBody::TechReviewExitFileDeleted
}
@@ -344,6 +363,36 @@ async fn insert_exit_message(
Ok(())
}
+async fn remove_dummy_issue_details(
+ project_id: DBProjectId,
+ txn: &mut PgTransaction<'_>,
+) -> Result<(), ApiError> {
+ sqlx::query!(
+ r#"
+ DELETE FROM delphi_report_issue_details detail
+ USING
+ delphi_report_issues issue,
+ delphi_reports report,
+ files file,
+ versions version
+ WHERE
+ detail.issue_id = issue.id
+ AND issue.issue_type = $2
+ AND issue.report_id = report.id
+ AND report.file_id = file.id
+ AND file.version_id = version.id
+ AND version.mod_id = $1
+ "#,
+ project_id as DBProjectId,
+ DUMMY_ISSUE_TYPE,
+ )
+ .execute(&mut *txn)
+ .await
+ .wrap_internal_err("failed to remove dummy Delphi issue details")?;
+
+ Ok(())
+}
+
async fn ensure_dummy_issue_detail(
report_id: DelphiReportId,
txn: &mut PgTransaction<'_>,
diff --git a/apps/labrinth/src/routes/internal/mod.rs b/apps/labrinth/src/routes/internal/mod.rs
index 8ab03e1d12..c01423a488 100644
--- a/apps/labrinth/src/routes/internal/mod.rs
+++ b/apps/labrinth/src/routes/internal/mod.rs
@@ -115,6 +115,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 7b3dac3ca1..e745939f89 100644
--- a/apps/labrinth/src/routes/internal/moderation/mod.rs
+++ b/apps/labrinth/src/routes/internal/moderation/mod.rs
@@ -295,6 +295,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 NOT didws.hidden
)
)
),
@@ -542,6 +543,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 NOT didws.hidden
)
)
),
@@ -770,6 +772,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 NOT didws.hidden
)
)
),
@@ -898,6 +901,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 NOT didws.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 2bd5097c58..853d0b05fb 100644
--- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs
+++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs
@@ -44,10 +44,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)
@@ -250,7 +254,9 @@ 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 NOT didws.hidden
)
) AS "data!: sqlx::types::Json
"
FROM delphi_report_issues dri
@@ -305,6 +311,16 @@ 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 severity_issue.issue_type != '__dummy'
+ AND NOT didws.hidden
+ ), 'low'::delphi_severity),
-- TODO: replace with `json_array` in Postgres 16
'issues', (
SELECT coalesce(json_agg(
@@ -327,15 +343,24 @@ 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 NOT didws.hidden
)
)
), '[]'::json)
FROM delphi_report_issues dri
WHERE
- dri.report_id = dr.id
+ dri.report_id = dr.id
-- see delphi.rs todo comment
AND dri.issue_type != '__dummy'
+ AND EXISTS (
+ SELECT 1
+ FROM delphi_issue_details_with_statuses visible_detail
+ WHERE
+ visible_detail.issue_id = dri.id
+ AND NOT visible_detail.hidden
+ )
)
) AS "data!: sqlx::types::Json"
FROM delphi_reports dr
@@ -530,7 +555,9 @@ 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 NOT didws.hidden
ORDER BY didws.issue_id, didws.id
"#,
&issue_ids.iter().map(|i| i.0).collect::>()
@@ -636,6 +663,10 @@ async fn fetch_project_reports(
.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,
@@ -644,12 +675,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(),
@@ -780,6 +822,7 @@ pub async fn search_projects(
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 NOT didws.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')))
@@ -789,8 +832,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 MAX(didws.severity) ELSE 'low'::delphi_severity END ASC,
+ CASE WHEN $3 = 'severity_desc' THEN MAX(didws.severity) ELSE 'low'::delphi_severity END DESC,
-- tie-breaker: oldest reports
MIN(dr.created) ASC
LIMIT $1 OFFSET $2
@@ -1035,6 +1078,7 @@ pub async fn submit_report(
WHERE
m.id = $1
AND didws.status = 'pending'
+ AND NOT didws.hidden
-- see delphi.rs todo comment
AND dri.issue_type != '__dummy'
"#,
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 82aa8055b1..09f47d3ea3 100644
--- a/apps/labrinth/src/routes/internal/moderation/tech_review/global.rs
+++ b/apps/labrinth/src/routes/internal/moderation/tech_review/global.rs
@@ -188,6 +188,7 @@ 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 NOT didws.hidden
LEFT JOIN delphi_report_issues dri
ON dri.id = didws.issue_id
AND dri.issue_type != '__dummy'
@@ -248,6 +249,7 @@ pub async fn search_global_issue_details(
AND didv.detail_key = didws.key
WHERE
didws.key = ANY($1::text[])
+ AND NOT didws.hidden
AND dri.issue_type != '__dummy'
)
SELECT
@@ -367,6 +369,7 @@ 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 NOT didws.hidden
LEFT JOIN delphi_report_issues dri
ON dri.id = didws.issue_id
AND dri.issue_type != '__dummy'
@@ -412,6 +415,7 @@ pub async fn get_global_issue_detail(
WHERE
didws.key = $1
AND ($2::bigint IS NULL OR didws.id > $2)
+ AND NOT didws.hidden
AND dri.issue_type != '__dummy'
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..00c2f4a91a
--- /dev/null
+++ b/apps/labrinth/src/routes/internal/moderation/tech_review/rules.rs
@@ -0,0 +1,684 @@
+use std::collections::{BTreeMap, HashMap};
+
+use actix_web::{HttpRequest, delete, get, post, put, web};
+use chrono::{DateTime, Utc};
+use eyre::eyre;
+use serde::{Deserialize, Serialize};
+
+use super::rules_scan::{
+ RuleArtifact, RuleInput, RuleScan, RuleScope, RuleTrace,
+};
+use crate::{
+ auth::check_is_moderator_from_headers,
+ database::{
+ PgPool, ReadOnlyPgPool,
+ models::{
+ DBProjectId, DBVersionId, DelphiReportIssueDetailsId,
+ DelphiReportIssueId, delphi_report_item::DelphiSeverity,
+ },
+ redis::RedisPool,
+ },
+ models::{
+ ids::{ProjectId, VersionId},
+ pats::Scopes,
+ },
+ queue::session::AuthQueue,
+ routes::ApiError,
+ util::error::Context,
+};
+
+const MAX_RULE_NAME_LENGTH: usize = 256;
+const MAX_RULE_EXPRESSION_LENGTH: usize = 65_536;
+const MAX_RULE_TEST_TRACES: usize = 10;
+
+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: i64,
+ pub name: String,
+ pub rule: String,
+ pub 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: Option,
+ pub hidden: bool,
+}
+
+#[derive(Debug, Deserialize, utoipa::ToSchema)]
+pub struct WriteDelphiRule {
+ pub name: String,
+ pub rule: String,
+}
+
+#[derive(Debug, Deserialize, utoipa::ToSchema)]
+pub struct TestDelphiRule {
+ pub rule: String,
+ pub traces: Vec,
+}
+
+#[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)]
+pub struct TestDelphiRuleTrace {
+ pub key: String,
+ pub issue_type: String,
+ pub severity: DelphiSeverity,
+ pub jar: Option,
+ pub file_path: String,
+ pub data: HashMap,
+}
+
+#[derive(Debug, Serialize, utoipa::ToSchema)]
+pub struct TestDelphiRuleResponse {
+ pub effects: Vec