diff --git a/.gitignore b/.gitignore index e129ca0d..0fcb7d0a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ - # Created by https://www.gitignore.io/api/rust,clion # Edit at https://www.gitignore.io/?templates=rust,clion @@ -107,3 +106,4 @@ fabric.properties # End of https://www.gitignore.io/api/rust,clion +.DS_Store \ No newline at end of file diff --git a/migrations/20230324202117_messaging.sql b/migrations/20230324202117_messaging.sql new file mode 100644 index 00000000..250667ea --- /dev/null +++ b/migrations/20230324202117_messaging.sql @@ -0,0 +1,32 @@ +-- Add migration script here + +-- Add route for users to see their own reports + +CREATE TABLE threads ( + id bigint PRIMARY KEY, + -- can be either "report", "project", or "direct_message". direct message is unused for now + thread_type VARCHAR(64) NOT NULL +); + +CREATE TABLE threads_messages ( + id bigint PRIMARY KEY, + thread_id bigint REFERENCES threads ON UPDATE CASCADE NOT NULL, + -- If this is null, it's a system message + author_id bigint REFERENCES users ON UPDATE CASCADE NULL, + body jsonb NOT NULL, + created timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL, + show_in_mod_inbox BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE TABLE threads_members ( + thread_id bigint REFERENCES threads ON UPDATE CASCADE NOT NULL, + user_id bigint REFERENCES users ON UPDATE CASCADE NOT NULL, + PRIMARY KEY (thread_id, user_id) +); + +ALTER TABLE reports + ADD COLUMN closed boolean NOT NULL DEFAULT FALSE; +ALTER TABLE reports + ADD COLUMN thread_id bigint references threads ON UPDATE CASCADE; +ALTER TABLE mods + ADD COLUMN thread_id bigint references threads ON UPDATE CASCADE; diff --git a/sqlx-data.json b/sqlx-data.json index adb3f8a0..686f6996 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -249,92 +249,6 @@ }, "query": "\n UPDATE versions\n SET name = $1\n WHERE (id = $2)\n " }, - "0b77fb8853f15ba9814c80feab68515aa81b82da1414210635239ae6adcd0dd1": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "mod_id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "author_id", - "ordinal": 2, - "type_info": "Int8" - }, - { - "name": "name", - "ordinal": 3, - "type_info": "Varchar" - }, - { - "name": "version_number", - "ordinal": 4, - "type_info": "Varchar" - }, - { - "name": "changelog", - "ordinal": 5, - "type_info": "Varchar" - }, - { - "name": "date_published", - "ordinal": 6, - "type_info": "Timestamptz" - }, - { - "name": "downloads", - "ordinal": 7, - "type_info": "Int4" - }, - { - "name": "version_type", - "ordinal": 8, - "type_info": "Varchar" - }, - { - "name": "featured", - "ordinal": 9, - "type_info": "Bool" - }, - { - "name": "status", - "ordinal": 10, - "type_info": "Varchar" - }, - { - "name": "requested_status", - "ordinal": 11, - "type_info": "Varchar" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Int8Array" - ] - } - }, - "query": "\n SELECT v.id, v.mod_id, v.author_id, v.name, v.version_number,\n v.changelog, v.date_published, v.downloads,\n v.version_type, v.featured, v.status, v.requested_status\n FROM versions v\n WHERE v.id = ANY($1)\n ORDER BY v.date_published ASC\n " - }, "0ba5a9f4d1381ed37a67b7dc90edf7e3ec86cae6c2860e5db1e53144d4654e58": { "describe": { "columns": [ @@ -373,25 +287,55 @@ }, "query": "\n INSERT INTO files (id, version_id, url, filename, is_primary, size, file_type)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n " }, - "0dbd0fa9a25416716a047184944d243ed5cb55808c6f300d7335c887f02a7f6e": { + "0eee1b0969f3ee800ccf5105d878c8c417f09bdced6bbbefcdc339534d1baf2a": { "describe": { "columns": [ { "name": "id", "ordinal": 0, - "type_info": "Int4" + "type_info": "Int8" + }, + { + "name": "author_id", + "ordinal": 1, + "type_info": "Int8" + }, + { + "name": "thread_id", + "ordinal": 2, + "type_info": "Int8" + }, + { + "name": "body", + "ordinal": 3, + "type_info": "Jsonb" + }, + { + "name": "created", + "ordinal": 4, + "type_info": "Timestamptz" + }, + { + "name": "show_in_mod_inbox", + "ordinal": 5, + "type_info": "Bool" } ], "nullable": [ + false, + true, + false, + false, + false, false ], "parameters": { "Left": [ - "Varchar" + "Int8Array" ] } }, - "query": "\n INSERT INTO report_types (name)\n VALUES ($1)\n ON CONFLICT (name) DO NOTHING\n RETURNING id\n " + "query": "\n SELECT tm.id, tm.author_id, tm.thread_id, tm.body, tm.created, tm.show_in_mod_inbox\n FROM threads_messages tm\n WHERE tm.id = ANY($1)\n " }, "0f0244e77f60e69b3ab1320265749656e25da0b021b3df9013a2da470dbc8d46": { "describe": { @@ -528,7 +472,26 @@ }, "query": "\n UPDATE mods\n SET webhook_sent = TRUE\n WHERE id = $1\n " }, - "154f8fdf00fc9905d8e2cd339687c63a523de7e19f6072457cfe723600fb1690": { + "1411c9ae3af067679aa21d7f45937cd94d457e4eb17a108566776a9bd1ee77e2": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Int4", + "Int8", + "Int8", + "Int8", + "Varchar", + "Int8", + "Int8" + ] + } + }, + "query": "\n INSERT INTO reports (\n id, report_type_id, mod_id, version_id, user_id,\n body, reporter, thread_id\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8\n )\n " + }, + "141370c3df1e8746397d71fd8cda5776c82b98af6bbc97f2072b9877d52479b8": { "describe": { "columns": [ { @@ -537,194 +500,26 @@ "type_info": "Int8" }, { - "name": "project_type", + "name": "thread_type", "ordinal": 1, - "type_info": "Int4" + "type_info": "Varchar" }, { - "name": "title", + "name": "members", "ordinal": 2, - "type_info": "Varchar" + "type_info": "Int8Array" }, { - "name": "description", + "name": "messages", "ordinal": 3, - "type_info": "Varchar" - }, - { - "name": "downloads", - "ordinal": 4, - "type_info": "Int4" - }, - { - "name": "follows", - "ordinal": 5, - "type_info": "Int4" - }, - { - "name": "icon_url", - "ordinal": 6, - "type_info": "Varchar" - }, - { - "name": "body", - "ordinal": 7, - "type_info": "Varchar" - }, - { - "name": "published", - "ordinal": 8, - "type_info": "Timestamptz" - }, - { - "name": "updated", - "ordinal": 9, - "type_info": "Timestamptz" - }, - { - "name": "approved", - "ordinal": 10, - "type_info": "Timestamptz" - }, - { - "name": "queued", - "ordinal": 11, - "type_info": "Timestamptz" - }, - { - "name": "status", - "ordinal": 12, - "type_info": "Varchar" - }, - { - "name": "requested_status", - "ordinal": 13, - "type_info": "Varchar" - }, - { - "name": "issues_url", - "ordinal": 14, - "type_info": "Varchar" - }, - { - "name": "source_url", - "ordinal": 15, - "type_info": "Varchar" - }, - { - "name": "wiki_url", - "ordinal": 16, - "type_info": "Varchar" - }, - { - "name": "discord_url", - "ordinal": 17, - "type_info": "Varchar" - }, - { - "name": "license_url", - "ordinal": 18, - "type_info": "Varchar" - }, - { - "name": "team_id", - "ordinal": 19, - "type_info": "Int8" - }, - { - "name": "client_side", - "ordinal": 20, - "type_info": "Int4" - }, - { - "name": "server_side", - "ordinal": 21, - "type_info": "Int4" - }, - { - "name": "license", - "ordinal": 22, - "type_info": "Varchar" - }, - { - "name": "slug", - "ordinal": 23, - "type_info": "Varchar" - }, - { - "name": "moderation_message", - "ordinal": 24, - "type_info": "Varchar" - }, - { - "name": "moderation_message_body", - "ordinal": 25, - "type_info": "Varchar" - }, - { - "name": "flame_anvil_project", - "ordinal": 26, - "type_info": "Int4" - }, - { - "name": "flame_anvil_user", - "ordinal": 27, - "type_info": "Int8" - }, - { - "name": "webhook_sent", - "ordinal": 28, - "type_info": "Bool" - }, - { - "name": "color", - "ordinal": 29, - "type_info": "Int4" - }, - { - "name": "loaders", - "ordinal": 30, - "type_info": "VarcharArray" - }, - { - "name": "game_versions", - "ordinal": 31, - "type_info": "VarcharArray" + "type_info": "Jsonb" } ], "nullable": [ false, false, - false, - false, - false, - false, - true, - false, - false, - false, - true, - true, - false, - true, - true, - true, - true, - true, - true, - false, - false, - false, - false, - true, - true, - true, - true, - true, - false, - true, - false, - false + null, + null ], "parameters": { "Left": [ @@ -732,7 +527,7 @@ ] } }, - "query": "\n SELECT id, project_type, title, description, downloads, follows,\n icon_url, body, published,\n updated, approved, queued, status, requested_status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug,\n moderation_message, moderation_message_body, flame_anvil_project,\n flame_anvil_user, webhook_sent, color, loaders, game_versions\n FROM mods\n WHERE id = ANY($1)\n " + "query": "\n SELECT t.id, t.thread_type,\n ARRAY_AGG(DISTINCT tm.user_id) filter (where tm.user_id is not null) members,\n JSONB_AGG(DISTINCT jsonb_build_object('id', tmsg.id, 'author_id', tmsg.author_id, 'thread_id', tmsg.thread_id, 'body', tmsg.body, 'created', tmsg.created)) filter (where tmsg.id is not null) messages\n FROM threads t\n LEFT OUTER JOIN threads_messages tmsg ON tmsg.thread_id = t.id\n LEFT OUTER JOIN threads_members tm ON tm.thread_id = t.id\n WHERE t.id = ANY($1)\n GROUP BY t.id\n " }, "15b8ea323c2f6d03c2e385d9c46d7f13460764f2f106fd638226c42ae0217f75": { "describe": { @@ -1006,28 +801,25 @@ }, "query": "\n SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type,\n JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'title', na.title, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.user_id = $1\n GROUP BY n.id, n.user_id;\n " }, - "19422e88b1b13318d75e8eb2ba142a562c550358b3136eef9ef73b5a216bbcdb": { + "1931ff3846345c0af4e15c3a84dcbfc7c9cbb92c98d2e73634f611a1e5358c7a": { "describe": { "columns": [ { - "name": "id", + "name": "exists", "ordinal": 0, - "type_info": "Int4" + "type_info": "Bool" } ], "nullable": [ - false + null ], "parameters": { "Left": [ - "Varchar", - "Int4", - "Varchar", - "Varchar" + "Int8" ] } }, - "query": "\n INSERT INTO categories (category, project_type, icon, header)\n VALUES ($1, $2, $3, $4)\n RETURNING id\n " + "query": "SELECT EXISTS(SELECT 1 FROM threads WHERE id=$1)" }, "196c8ac2228e199f23eaf980f7ea15b37f76e66bb81da1115a754aad0be756e4": { "describe": { @@ -1349,18 +1141,6 @@ }, "query": "\n SELECT DISTINCT ON(v.date_published, v.id) version_id, v.date_published FROM versions v\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id\n INNER JOIN game_versions gv on gvv.game_version_id = gv.id AND (cardinality($2::varchar[]) = 0 OR gv.version = ANY($2::varchar[]))\n INNER JOIN loaders_versions lv ON lv.version_id = v.id\n INNER JOIN loaders l on lv.loader_id = l.id AND (cardinality($3::varchar[]) = 0 OR l.loader = ANY($3::varchar[]))\n WHERE v.mod_id = $1 AND ($4::varchar IS NULL OR v.version_type = $4)\n ORDER BY v.date_published DESC, v.id\n LIMIT $5 OFFSET $6\n " }, - "1ce90594000fa30876bf277d9ebe2901acf9afaf256dd4488166d55fdd950347": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n DELETE FROM donation_platforms\n WHERE short = $1\n " - }, "1cefe4924d3c1f491739858ce844a22903d2dbe26f255219299f1833a10ce3d7": { "describe": { "columns": [ @@ -1395,26 +1175,6 @@ }, "query": "\n UPDATE users\n SET flame_anvil_key = $1\n WHERE (id = $2)\n " }, - "1d3b582e6765e1ae578039e44b5dc9be6f3f845c96ffd43b7ba83f9eab816f93": { - "describe": { - "columns": [ - { - "name": "name", - "ordinal": 0, - "type_info": "Varchar" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Int4" - ] - } - }, - "query": "\n SELECT name FROM report_types\n WHERE id = $1\n " - }, "1d6f3e926fc4a27c5af172f672b7f825f9f5fe2d538b06337ef182ab1a553398": { "describe": { "columns": [ @@ -1665,6 +1425,26 @@ }, "query": "\n DELETE FROM notifications_actions\n WHERE notification_id = ANY($1)\n " }, + "28b9d32b6d200f34e86f890ce477be0b8717f7ad92dc9cffa56eda4b12ee0df2": { + "describe": { + "columns": [ + { + "name": "thread_id", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + true + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT thread_id FROM reports\n WHERE id = $1\n " + }, "28d5825964b0fddc43bd7d6851daf91845b79c9e88c82d5c7d97ae02502d0b4f": { "describe": { "columns": [], @@ -1677,6 +1457,18 @@ }, "query": "INSERT INTO banned_users (github_id) VALUES ($1);" }, + "294f264382ad55475b51776cd5d306c4867e8e6966ab79921bba69dc023f8337": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM threads_members\n WHERE thread_id = $1\n " + }, "299b8ea6e7a0048fa389cc4432715dc2a09e227d2f08e91167a43372a7ac6e35": { "describe": { "columns": [], @@ -1689,6 +1481,26 @@ }, "query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1 AND is_additional = FALSE\n " }, + "29e171bd746ac5dc1fabae4c9f81c3d1df4e69c860b7d0f6a907377664199217": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT id FROM reports\n WHERE closed = FALSE\n ORDER BY created ASC\n LIMIT $1;\n " + }, "29e657d26f0fb24a766f5b5eb6a94d01d1616884d8ca10e91536e974d5b585a6": { "describe": { "columns": [], @@ -1943,17 +1755,92 @@ }, "query": "\n UPDATE mods\n SET follows = follows - 1\n WHERE id = $1\n " }, - "398ac436f5fe2f6a66544204b9ff01ae1ea1204edf03ffc16de657a861cfe0ba": { + "39b32e9fad8113bd3ddb97fa555524a61e0d8d8d50d2b6cf1f829ebac290be56": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "mod_id", + "ordinal": 2, + "type_info": "Int8" + }, + { + "name": "version_id", + "ordinal": 3, + "type_info": "Int8" + }, + { + "name": "user_id", + "ordinal": 4, + "type_info": "Int8" + }, + { + "name": "body", + "ordinal": 5, + "type_info": "Varchar" + }, + { + "name": "reporter", + "ordinal": 6, + "type_info": "Int8" + }, + { + "name": "created", + "ordinal": 7, + "type_info": "Timestamptz" + }, + { + "name": "thread_id", + "ordinal": 8, + "type_info": "Int8" + }, + { + "name": "closed", + "ordinal": 9, + "type_info": "Bool" + } + ], + "nullable": [ + false, + false, + true, + true, + true, + false, + false, + false, + true, + false + ], + "parameters": { + "Left": [ + "Int8Array" + ] + } + }, + "query": "\n SELECT r.id, rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created, r.thread_id, r.closed\n FROM reports r\n INNER JOIN report_types rt ON rt.id = r.report_type_id\n WHERE r.id = ANY($1)\n ORDER BY r.created DESC\n " + }, + "3ae7c4a29dab8bce0e84a9c47a4a4f50a3be4bcb86e5b13d7dd60975d62e9ea3": { "describe": { "columns": [], "nullable": [], "parameters": { "Left": [ - "Text" + "Int8", + "Varchar" ] } }, - "query": "\n DELETE FROM categories\n WHERE category = $1\n " + "query": "\n INSERT INTO threads (\n id, thread_type\n )\n VALUES (\n $1, $2\n )\n " }, "3af747b5543a5a9b10dcce0a1eb9c2a1926dd5a507fe0d8b7f52d8ccc7fcd0af": { "describe": { @@ -2168,6 +2055,19 @@ }, "query": "\n UPDATE mods\n SET title = $1\n WHERE (id = $2)\n " }, + "3dd2c3bfd40e2a3c0bf0fd94023d35bd821186670780a6883691ab1a097e2956": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + } + }, + "query": "\n UPDATE threads_messages\n SET body = '{\"type\": \"deleted\"}', author_id = $2\n WHERE author_id = $1\n " + }, "3f2f05653552ce8c1be95ce0a922ab41f52f40f8ff6c91c6621481102c8f35e3": { "describe": { "columns": [], @@ -2262,19 +2162,6 @@ }, "query": "\n SELECT gv.id id, gv.version version_, gv.type type_, gv.created created, gv.major major FROM game_versions gv\n WHERE major = $1 AND type = $2\n ORDER BY created DESC\n " }, - "44bb1034872a80bbea122e04399470fd5f029b819c70cb6e0cb2db6d3193b97e": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int4", - "Int4" - ] - } - }, - "query": "\n INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id)\n VALUES ($1, $2)\n " - }, "4567790f0dc98ff20b596a33161d1f6ac8af73da67fe8c54192724626c6bf670": { "describe": { "columns": [], @@ -2416,26 +2303,6 @@ }, "query": "\n SELECT DISTINCT ON(v.date_published, v.id) version_id, v.mod_id, v.date_published FROM versions v\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id\n INNER JOIN game_versions gv on gvv.game_version_id = gv.id AND (cardinality($2::varchar[]) = 0 OR gv.version = ANY($2::varchar[]))\n INNER JOIN loaders_versions lv ON lv.version_id = v.id\n INNER JOIN loaders l on lv.loader_id = l.id AND (cardinality($3::varchar[]) = 0 OR l.loader = ANY($3::varchar[]))\n WHERE v.mod_id = ANY($1) AND ($4::varchar IS NULL OR v.version_type = $4)\n ORDER BY v.date_published, v.id ASC\n LIMIT $5 OFFSET $6\n " }, - "4b14b5c69f6a0ee4e06e41d7cea425c7c34d6db45895275a2ce8adfa28dc8f72": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int4" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Varchar" - ] - } - }, - "query": "\n INSERT INTO project_types (name)\n VALUES ($1)\n ON CONFLICT (name) DO NOTHING\n RETURNING id\n " - }, "4c9e2190e2a68ffc093a69aaa1fc9384957138f57ac9cd85cbc6179613c13a08": { "describe": { "columns": [ @@ -2476,6 +2343,18 @@ }, "query": "\n SELECT id FROM mods\n WHERE id = $1\n " }, + "4d093a0f6c87e07f1db3889c05d961c0ead1391fe3e7e9d770de271edee53eb5": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM threads_messages\n WHERE id = $1\n " + }, "4d54032b02c860f4facec39eacb4548a0701d4505e7a80b4834650696df69c2b": { "describe": { "columns": [], @@ -2526,18 +2405,6 @@ }, "query": "\n INSERT INTO states (id, url)\n VALUES ($1, $2)\n " }, - "4fa53dab6de86711825c032077fbe4985d5edf8aeec9003be900d162f46b3631": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int4" - ] - } - }, - "query": "\n DELETE FROM loaders_project_types\n WHERE joining_loader_id = $1\n " - }, "4fb5bd341369b4beb6b4a88de296b608ea5441a96db9f7360fbdccceb4628202": { "describe": { "columns": [], @@ -2598,6 +2465,26 @@ }, "query": "\n DELETE FROM game_versions_versions WHERE joining_version_id = $1\n " }, + "515a3629aeef7d0789fe5e57a28d77aaa35a27cb7b35df70c959f95ccbbc25f3": { + "describe": { + "columns": [ + { + "name": "thread_id", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + true + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT thread_id FROM mods\n WHERE id = $1\n " + }, "5295fba2053675c8414c0b37a59943535b9a438a642ea1c68045e987f05ade13": { "describe": { "columns": [ @@ -2660,51 +2547,53 @@ }, "query": "\n UPDATE versions\n SET version_number = $1\n WHERE (id = $2)\n " }, - "57bb3db92e6a8fb8606005be955e2379f13a04f101f91358322a591a860a7f9e": { + "59859b611ac75b266671daacee0ae3eb0eff1808a046dd0c7b70154f590c28eb": { "describe": { "columns": [ { "name": "id", "ordinal": 0, "type_info": "Int8" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT id FROM reports\n ORDER BY created ASC\n LIMIT $1;\n " - }, - "5917ab5017e27be2c4c5231426b19c3b37fd171ff47f97a0cb4e2094a0234298": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int4" }, { - "name": "name", + "name": "thread_id", "ordinal": 1, - "type_info": "Varchar" + "type_info": "Int8" + }, + { + "name": "author_id", + "ordinal": 2, + "type_info": "Int8" + }, + { + "name": "body", + "ordinal": 3, + "type_info": "Jsonb" + }, + { + "name": "created", + "ordinal": 4, + "type_info": "Timestamptz" + }, + { + "name": "project_id", + "ordinal": 5, + "type_info": "Int8" } ], "nullable": [ + false, + false, + true, + false, false, false ], "parameters": { - "Left": [ - "TextArray" - ] + "Left": [] } }, - "query": "\n SELECT id, name FROM project_types\n WHERE name = ANY($1)\n " + "query": "\n SELECT tm.id, tm.thread_id, tm.author_id, tm.body, tm.created, m.id project_id FROM threads_messages tm\n INNER JOIN mods m ON m.thread_id = tm.thread_id\n WHERE tm.show_in_mod_inbox = TRUE\n " }, "599df07263a2705e57fc70a7c4f5dc606e1730c281e3b573d2f2a2030bed04e0": { "describe": { @@ -2947,6 +2836,52 @@ }, "query": "\n SELECT version.id id FROM (\n SELECT DISTINCT ON(v.id) v.id, v.date_published FROM versions v\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id AND gvv.game_version_id IN (SELECT game_version_id FROM game_versions_versions WHERE joining_version_id = $2)\n INNER JOIN loaders_versions lv ON lv.version_id = v.id AND lv.loader_id IN (SELECT loader_id FROM loaders_versions WHERE version_id = $2)\n WHERE v.mod_id = $1\n ) AS version\n ORDER BY version.date_published DESC\n LIMIT 1\n " }, + "6361874e1d2ee5286086024c1cc4541d8c1d571db2fb27dfbaab518700b4a672": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Varchar", + "Varchar", + "Varchar", + "Timestamptz", + "Int4", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Int4", + "Int4", + "Varchar", + "Varchar", + "Text", + "Int4", + "Int4", + "Int8" + ] + } + }, + "query": "\n INSERT INTO mods (\n id, team_id, title, description, body,\n published, downloads, icon_url, issues_url,\n source_url, wiki_url, status, requested_status, discord_url,\n client_side, server_side, license_url, license,\n slug, project_type, color, thread_id\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8, $9,\n $10, $11, $12, $13, $14,\n $15, $16, $17, $18,\n LOWER($19), $20, $21, $22\n )\n " + }, + "665e294e9737fd0299fc4639127d56811485dc8a5a4e08a4e7292044d8a2fb7a": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + } + }, + "query": "\n UPDATE reports\n SET body = $1\n WHERE (id = $2)\n " + }, "66b06ddcd0a4cf01e716331befa393a12631fe6752a7d078bda06b24d50daae2": { "describe": { "columns": [], @@ -3033,6 +2968,27 @@ }, "query": "\n DELETE FROM mods\n WHERE id = $1\n " }, + "6c4a42c263ae2787744aa6903e3cd85e90beaa5bea7ba78b45dbf55ce007753d": { + "describe": { + "columns": [ + { + "name": "exists", + "ordinal": 0, + "type_info": "Bool" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + } + }, + "query": "SELECT EXISTS(SELECT 1 FROM mods m INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.user_id = $2 WHERE thread_id = $1)" + }, "6c7aeb0db4a4fb3387c37b8d7aca6fdafaa637fd883a44416b56270aeebb7a01": { "describe": { "columns": [], @@ -3149,6 +3105,19 @@ }, "query": "\n INSERT INTO team_members (\n id, team_id, user_id, role, permissions, accepted\n )\n VALUES (\n $1, $2, $3, $4, $5, $6\n )\n " }, + "70b510956a40583eef8c57dcced71c67f525eee455ae8b09e9b2403668068751": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + } + }, + "query": "\n INSERT INTO threads_members (\n thread_id, user_id\n )\n VALUES (\n $1, $2\n )\n " + }, "712a846d6b56609599ee7a6603ad921acd2d5da2b3ce0c5b3f3642ed83927542": { "describe": { "columns": [], @@ -3225,6 +3194,41 @@ }, "query": "\n INSERT INTO game_versions (version, type, created)\n VALUES ($1, COALESCE($2, 'other'), COALESCE($3, timezone('utc', now())))\n ON CONFLICT (version) DO UPDATE\n SET type = COALESCE($2, game_versions.type),\n created = COALESCE($3, game_versions.created)\n RETURNING id\n " }, + "72ce04d19ba84b29ecfa52bdb9b9f5fdc84d1d2371610a7bdeb222ff625cfbed": { + "describe": { + "columns": [ + { + "name": "hash", + "ordinal": 0, + "type_info": "Bytea" + }, + { + "name": "algorithm", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "project_id", + "ordinal": 2, + "type_info": "Int8" + } + ], + "nullable": [ + false, + false, + false + ], + "parameters": { + "Left": [ + "TextArray", + "ByteaArray", + "Text", + "TextArray" + ] + } + }, + "query": "\n SELECT h.hash hash, h.algorithm algorithm, m.id project_id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n INNER JOIN versions v ON v.id = f.version_id AND v.status != ANY($1)\n INNER JOIN mods m on v.mod_id = m.id\n WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ANY($4)\n " + }, "72d6b5f2f11d88981db82c7247c9e7e5ebfd8d34985a1a8209d6628e66490f37": { "describe": { "columns": [ @@ -3467,6 +3471,18 @@ }, "query": "\n SELECT v.mod_id project_id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n INNER JOIN versions v ON v.id = f.version_id AND v.status != ANY($1)\n INNER JOIN mods m on v.mod_id = m.id\n WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ANY($4)\n ORDER BY v.date_published ASC\n " }, + "7a4b588622729603f4c8d0c6e471baa67d19e9b4c333b2a79c28d2dd84fd869a": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n UPDATE threads_messages\n SET show_in_mod_inbox = FALSE\n WHERE id = $1\n " + }, "7ab21e7613dd88e97cf602e76bff62170c13ceef8104a4ce4cb2d101f8ce4f48": { "describe": { "columns": [], @@ -3520,6 +3536,218 @@ }, "query": "\n SELECT id FROM mods_gallery\n WHERE image_url = $1\n " }, + "7c6a65fbb26a0c2b6daa92af6ae1764f1960971b24e73feaed6f6507aca7c8f0": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "project_type", + "ordinal": 1, + "type_info": "Int4" + }, + { + "name": "title", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "description", + "ordinal": 3, + "type_info": "Varchar" + }, + { + "name": "downloads", + "ordinal": 4, + "type_info": "Int4" + }, + { + "name": "follows", + "ordinal": 5, + "type_info": "Int4" + }, + { + "name": "icon_url", + "ordinal": 6, + "type_info": "Varchar" + }, + { + "name": "body", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "published", + "ordinal": 8, + "type_info": "Timestamptz" + }, + { + "name": "updated", + "ordinal": 9, + "type_info": "Timestamptz" + }, + { + "name": "approved", + "ordinal": 10, + "type_info": "Timestamptz" + }, + { + "name": "queued", + "ordinal": 11, + "type_info": "Timestamptz" + }, + { + "name": "status", + "ordinal": 12, + "type_info": "Varchar" + }, + { + "name": "requested_status", + "ordinal": 13, + "type_info": "Varchar" + }, + { + "name": "issues_url", + "ordinal": 14, + "type_info": "Varchar" + }, + { + "name": "source_url", + "ordinal": 15, + "type_info": "Varchar" + }, + { + "name": "wiki_url", + "ordinal": 16, + "type_info": "Varchar" + }, + { + "name": "discord_url", + "ordinal": 17, + "type_info": "Varchar" + }, + { + "name": "license_url", + "ordinal": 18, + "type_info": "Varchar" + }, + { + "name": "team_id", + "ordinal": 19, + "type_info": "Int8" + }, + { + "name": "client_side", + "ordinal": 20, + "type_info": "Int4" + }, + { + "name": "server_side", + "ordinal": 21, + "type_info": "Int4" + }, + { + "name": "license", + "ordinal": 22, + "type_info": "Varchar" + }, + { + "name": "slug", + "ordinal": 23, + "type_info": "Varchar" + }, + { + "name": "moderation_message", + "ordinal": 24, + "type_info": "Varchar" + }, + { + "name": "moderation_message_body", + "ordinal": 25, + "type_info": "Varchar" + }, + { + "name": "flame_anvil_project", + "ordinal": 26, + "type_info": "Int4" + }, + { + "name": "flame_anvil_user", + "ordinal": 27, + "type_info": "Int8" + }, + { + "name": "webhook_sent", + "ordinal": 28, + "type_info": "Bool" + }, + { + "name": "color", + "ordinal": 29, + "type_info": "Int4" + }, + { + "name": "loaders", + "ordinal": 30, + "type_info": "VarcharArray" + }, + { + "name": "game_versions", + "ordinal": 31, + "type_info": "VarcharArray" + }, + { + "name": "thread_id", + "ordinal": 32, + "type_info": "Int8" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + true, + true, + false, + true, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true, + true, + true, + true, + true, + false, + true, + false, + false, + true + ], + "parameters": { + "Left": [ + "Int8Array" + ] + } + }, + "query": "\n SELECT id, project_type, title, description, downloads, follows,\n icon_url, body, published,\n updated, approved, queued, status, requested_status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug,\n moderation_message, moderation_message_body, flame_anvil_project,\n flame_anvil_user, webhook_sent, color, loaders, game_versions, thread_id\n FROM mods\n WHERE id = ANY($1)\n " + }, "7cb691738c28e0d1f28c84ba2dbcfa21a6dbd859bcf0f565f90cd7ce2ea5aa1c": { "describe": { "columns": [], @@ -3578,6 +3806,24 @@ }, "query": "\n INSERT INTO mods_gallery (\n mod_id, image_url, featured, title, description, ordering\n )\n VALUES (\n $1, $2, $3, $4, $5, $6\n )\n " }, + "85c6de008681d9fc9dc51b17330bed09204010813111e66a7ca84bc0e603f537": { + "describe": { + "columns": [ + { + "name": "name", + "ordinal": 0, + "type_info": "Varchar" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [] + } + }, + "query": "\n SELECT name FROM side_types\n " + }, "868c29019bd7e9ad71fb3515ca3489304ade3f6ebe3f77c018a8a521a96fb41f": { "describe": { "columns": [ @@ -3667,18 +3913,6 @@ }, "query": "\n UPDATE mods\n SET loaders = (\n SELECT COALESCE(ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null), array[]::varchar[])\n FROM versions v\n INNER JOIN loaders_versions lv ON lv.version_id = v.id\n INNER JOIN loaders l on lv.loader_id = l.id\n WHERE v.mod_id = mods.id AND v.status != ANY($2)\n )\n WHERE id = $1\n " }, - "87fd169e19ba231c6cf131ad2841d5c3b95adde53e5ed4000f8e7d54c0e87320": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n DELETE FROM project_types\n WHERE name = $1\n " - }, "8a7b2bc070e5e8308e2853ff125bc98f40b22c1d0deeb013dd90ce5768bd0ce8": { "describe": { "columns": [], @@ -3733,18 +3967,6 @@ }, "query": "\n UPDATE users\n SET payout_wallet = $1, payout_wallet_type = $2, payout_address = $3\n WHERE (id = $4)\n " }, - "8f706d78ac4235ea04c59e2c220a4791e1d08fdf287b783b4aaef36fd2445467": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n DELETE FROM loaders\n WHERE loader = $1\n " - }, "9284d7f22617e0a7daf91540ff31791d0921ec5d4eb4809846dc67567bec1a81": { "describe": { "columns": [ @@ -3826,6 +4048,26 @@ }, "query": "\n SELECT id FROM mods\n WHERE slug = LOWER($1)\n " }, + "965a8a34ae559d402f0112ce5f04f1ad18de1eebc92299f4d2ce7250e3fa12d5": { + "describe": { + "columns": [ + { + "name": "status", + "ordinal": 0, + "type_info": "Varchar" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "SELECT m.status FROM mods m WHERE thread_id = $1" + }, "96b2f4e0e619e7ed312d191dc90d64113235d72254fbda8f528ce866d1795cb5": { "describe": { "columns": [ @@ -3982,6 +4224,22 @@ }, "query": "\n INSERT INTO team_members (id, team_id, user_id, role, permissions, accepted, payouts_split, ordering)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n " }, + "9d975d279fa869aad310922653de2fcbe0d2bfb896588067721e5f5560a57593": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Jsonb", + "Int8", + "Bool" + ] + } + }, + "query": "\n INSERT INTO threads_messages (\n id, author_id, body, thread_id, show_in_mod_inbox\n )\n VALUES (\n $1, $2, $3, $4, $5\n )\n " + }, "9dc32a9ef59f57fbad862520b6d3a4795a95d7d0db17e05eb8aedc3a2fe600dc": { "describe": { "columns": [ @@ -4202,6 +4460,18 @@ }, "query": "\n DELETE FROM states\n WHERE expires < CURRENT_DATE\n " }, + "aa59f79136ef87dd4121d5f367f5dbdbca80e936c1b986ec99c09c3e95daa756": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM threads_members\n WHERE user_id = $1\n " + }, "aaec611bae08eac41c163367dc508208178170de91165095405f1b41e47f5e7f": { "describe": { "columns": [ @@ -4315,7 +4585,80 @@ }, "query": "\n SELECT loader_id id FROM loaders_versions\n WHERE version_id = $1\n " }, - "ad9c63f994f1e075fea75cff02b14b5a17f3a09ed0132ed8a38d1d22ff7ae5ac": { + "adbe17a5ad3cea333b30b5d6111aff713a8f7dc79ded21f5ba942c4f1108aa8f": { + "describe": { + "columns": [ + { + "name": "title", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "project_type", + "ordinal": 1, + "type_info": "Varchar" + } + ], + "nullable": [ + false, + false + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT m.title title, pt.name project_type\n FROM mods m\n INNER JOIN project_types pt ON pt.id = m.project_type\n WHERE m.id = $1\n " + }, + "ae1686b8b566dd7ecc57c653c9313a4b324a2ec3a63aa6a44ed1d8ea7999b115": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [] + } + }, + "query": "\n DELETE FROM dependencies WHERE mod_dependency_id = NULL AND dependency_id = NULL AND dependency_file_name = NULL\n " + }, + "b0e3d1c70b87bb54819e3fac04b684a9b857aeedb4dcb7cb400c2af0dbb12922": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM teams\n WHERE id = $1\n " + }, + "b1e77dbaf4b190ab361f4fa203c442e5905cef6c1a135011a59ebd6e2dc0a92a": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Numeric", + "Int8" + ] + } + }, + "query": "\n UPDATE users\n SET balance = balance - $1\n WHERE id = $2\n " + }, + "b69a6f42965b3e7103fcbf46e39528466926789ff31e9ed2591bb175527ec169": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM users\n WHERE id = $1\n " + }, + "b759716e6df7eb13cd25fb782ff9daf62f88105f001d92584cd9dcb4511145c6": { "describe": { "columns": [ { @@ -4494,29 +4837,34 @@ "type_info": "VarcharArray" }, { - "name": "categories", + "name": "thread_id", "ordinal": 35, - "type_info": "VarcharArray" + "type_info": "Int8" }, { - "name": "additional_categories", + "name": "categories", "ordinal": 36, "type_info": "VarcharArray" }, { - "name": "versions", + "name": "additional_categories", "ordinal": 37, - "type_info": "Jsonb" + "type_info": "VarcharArray" }, { - "name": "gallery", + "name": "versions", "ordinal": 38, "type_info": "Jsonb" }, { - "name": "donations", + "name": "gallery", "ordinal": 39, "type_info": "Jsonb" + }, + { + "name": "donations", + "ordinal": 40, + "type_info": "Jsonb" } ], "nullable": [ @@ -4555,6 +4903,7 @@ true, false, false, + true, null, null, null, @@ -4568,80 +4917,7 @@ ] } }, - "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.published published,\n m.updated updated, m.approved approved, m.queued, m.status status, m.requested_status requested_status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user, m.webhook_sent, m.color,\n m.loaders loaders, m.game_versions game_versions,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n JSONB_AGG(DISTINCT jsonb_build_object('id', v.id, 'date_published', v.date_published)) filter (where v.id is not null) versions,\n JSONB_AGG(DISTINCT jsonb_build_object('image_url', mg.image_url, 'featured', mg.featured, 'title', mg.title, 'description', mg.description, 'created', mg.created, 'ordering', mg.ordering)) filter (where mg.image_url is not null) gallery,\n JSONB_AGG(DISTINCT jsonb_build_object('platform_id', md.joining_platform_id, 'platform_short', dp.short, 'platform_name', dp.name,'url', md.url)) filter (where md.joining_platform_id is not null) donations\n FROM mods m\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n LEFT JOIN mods_donations md ON md.joining_mod_id = m.id\n LEFT JOIN donation_platforms dp ON md.joining_platform_id = dp.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n LEFT JOIN versions v ON v.mod_id = m.id AND v.status = ANY($2)\n LEFT JOIN mods_gallery mg ON mg.mod_id = m.id\n WHERE m.id = ANY($1)\n GROUP BY pt.id, cs.id, ss.id, m.id;\n " - }, - "adbe17a5ad3cea333b30b5d6111aff713a8f7dc79ded21f5ba942c4f1108aa8f": { - "describe": { - "columns": [ - { - "name": "title", - "ordinal": 0, - "type_info": "Varchar" - }, - { - "name": "project_type", - "ordinal": 1, - "type_info": "Varchar" - } - ], - "nullable": [ - false, - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT m.title title, pt.name project_type\n FROM mods m\n INNER JOIN project_types pt ON pt.id = m.project_type\n WHERE m.id = $1\n " - }, - "ae1686b8b566dd7ecc57c653c9313a4b324a2ec3a63aa6a44ed1d8ea7999b115": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [] - } - }, - "query": "\n DELETE FROM dependencies WHERE mod_dependency_id = NULL AND dependency_id = NULL AND dependency_file_name = NULL\n " - }, - "b0e3d1c70b87bb54819e3fac04b684a9b857aeedb4dcb7cb400c2af0dbb12922": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n DELETE FROM teams\n WHERE id = $1\n " - }, - "b1e77dbaf4b190ab361f4fa203c442e5905cef6c1a135011a59ebd6e2dc0a92a": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Numeric", - "Int8" - ] - } - }, - "query": "\n UPDATE users\n SET balance = balance - $1\n WHERE id = $2\n " - }, - "b69a6f42965b3e7103fcbf46e39528466926789ff31e9ed2591bb175527ec169": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n DELETE FROM users\n WHERE id = $1\n " + "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.published published,\n m.updated updated, m.approved approved, m.queued, m.status status, m.requested_status requested_status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user, m.webhook_sent, m.color,\n m.loaders loaders, m.game_versions game_versions, m.thread_id thread_id,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n JSONB_AGG(DISTINCT jsonb_build_object('id', v.id, 'date_published', v.date_published)) filter (where v.id is not null) versions,\n JSONB_AGG(DISTINCT jsonb_build_object('image_url', mg.image_url, 'featured', mg.featured, 'title', mg.title, 'description', mg.description, 'created', mg.created, 'ordering', mg.ordering)) filter (where mg.image_url is not null) gallery,\n JSONB_AGG(DISTINCT jsonb_build_object('platform_id', md.joining_platform_id, 'platform_short', dp.short, 'platform_name', dp.name,'url', md.url)) filter (where md.joining_platform_id is not null) donations\n FROM mods m\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n LEFT JOIN mods_donations md ON md.joining_mod_id = m.id\n LEFT JOIN donation_platforms dp ON md.joining_platform_id = dp.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n LEFT JOIN versions v ON v.mod_id = m.id AND v.status = ANY($2)\n LEFT JOIN mods_gallery mg ON mg.mod_id = m.id\n WHERE m.id = ANY($1)\n GROUP BY pt.id, cs.id, ss.id, m.id;\n " }, "b7b2b5b99340c7601de53cc33dc56af054b50b2fe4d1d212901c958115a42baa": { "describe": { @@ -4742,95 +5018,6 @@ }, "query": "\n UPDATE mods\n SET discord_url = $1\n WHERE (id = $2)\n " }, - "bbfb47ae2c972734785df6b7c3e62077dc544ef4ccf8bb89e9c22c2f50a933c1": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n DELETE FROM report_types\n WHERE name = $1\n " - }, - "bc43bbc55ed517c62f2caefea173ea87547061159c8e37282f1ae340dd1d84c5": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "team_id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "role", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "permissions", - "ordinal": 3, - "type_info": "Int8" - }, - { - "name": "accepted", - "ordinal": 4, - "type_info": "Bool" - }, - { - "name": "payouts_split", - "ordinal": 5, - "type_info": "Numeric" - }, - { - "name": "ordering", - "ordinal": 6, - "type_info": "Int8" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT id, team_id, role, permissions, accepted, payouts_split, ordering\n FROM team_members\n WHERE user_id = $1\n ORDER BY ordering\n " - }, - "bc605f80a615c7d0ca9c8207f8b0c5dc1b8f2ad0f9b3346a00078d59e5e3e253": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int4" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Varchar", - "Varchar" - ] - } - }, - "query": "\n INSERT INTO loaders (loader, icon)\n VALUES ($1, $2)\n ON CONFLICT (loader) DO NOTHING\n RETURNING id\n " - }, "bc91841f9672608a28bd45a862919f2bd34fac0b3479e3b4b67a9f6bea2a562a": { "describe": { "columns": [], @@ -4856,6 +5043,27 @@ }, "query": "\n DELETE FROM dependencies WHERE dependent_id = $1\n " }, + "bea2ca01c4939a84b633927d81e5fd97d6bccf18f12a9cedc65719c33ef2c6b4": { + "describe": { + "columns": [ + { + "name": "exists", + "ordinal": 0, + "type_info": "Bool" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + } + }, + "query": "SELECT EXISTS(SELECT 1 FROM reports WHERE thread_id = $1 AND reporter = $2)" + }, "bec1612d4929d143bc5d6860a57cc036c5ab23e69d750ca5791c620297953c50": { "describe": { "columns": [ @@ -4888,38 +5096,6 @@ }, "query": "\n DELETE FROM game_versions_versions gvv\n WHERE gvv.joining_version_id = $1\n " }, - "bf3cc4f6b22db45df42fa8b1c5b26505ebdb312d14cf657891b9ec4dff8bc55c": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Varchar", - "Varchar", - "Varchar", - "Timestamptz", - "Int4", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Int4", - "Int4", - "Varchar", - "Varchar", - "Text", - "Int4", - "Int4" - ] - } - }, - "query": "\n INSERT INTO mods (\n id, team_id, title, description, body,\n published, downloads, icon_url, issues_url,\n source_url, wiki_url, status, requested_status, discord_url,\n client_side, server_side, license_url, license,\n slug, project_type, color\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8, $9,\n $10, $11, $12, $13, $14,\n $15, $16, $17, $18,\n LOWER($19), $20, $21\n )\n " - }, "bf4afeda41a54e09a80a4cc505d1fbb72124c442ebaca731a291f022524daf1a": { "describe": { "columns": [ @@ -5207,80 +5383,6 @@ }, "query": "\n UPDATE versions\n SET version_type = $1\n WHERE (id = $2)\n " }, - "c276ad563ad7c8abcd4e42b4cb51be76e849fee352311a1b1fdb9fd6f2ccaed0": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "team_id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "role", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "permissions", - "ordinal": 3, - "type_info": "Int8" - }, - { - "name": "accepted", - "ordinal": 4, - "type_info": "Bool" - }, - { - "name": "payouts_split", - "ordinal": 5, - "type_info": "Numeric" - }, - { - "name": "ordering", - "ordinal": 6, - "type_info": "Int8" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT id, team_id, role, permissions, accepted, payouts_split, ordering\n FROM team_members\n WHERE (user_id = $1 AND accepted = TRUE)\n ORDER BY ordering\n " - }, - "c3f594d8d0ffcf5df1b36759cf3088bfaec496c5dfdbf496d3b05f0b122a5d0c": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8", - "Int4", - "Int8", - "Int8", - "Int8", - "Varchar", - "Int8" - ] - } - }, - "query": "\n INSERT INTO reports (\n id, report_type_id, mod_id, version_id, user_id,\n body, reporter\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7\n )\n " - }, "c44e260a1f7712b14ac521fd301fea1b3f92238da62aeaf819997aecc365be43": { "describe": { "columns": [ @@ -5355,26 +5457,6 @@ }, "query": "\n UPDATE mods\n SET client_side = $1\n WHERE (id = $2)\n " }, - "c64c487b56a25b252ff070fe03a7416e84260df8a6f938a018cc768598e9435b": { - "describe": { - "columns": [ - { - "name": "category", - "ordinal": 0, - "type_info": "Varchar" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Int4" - ] - } - }, - "query": "\n SELECT category FROM categories\n WHERE id = $1\n " - }, "c79e4f7d3ffbda57daaf58f61cc0397a423b7bc877d2abc975c262d668f41f70": { "describe": { "columns": [ @@ -5720,25 +5802,17 @@ }, "query": "\n SELECT f.id id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n WHERE h.algorithm = $2 AND h.hash = $1\n " }, - "d1866ecc161c3fe3fbe094289510e99b17de563957e1f824c347c1e6ac40c40c": { + "d203b99bd23d16224348e4fae44296aa0e1ea6d6a3fac26908303069b36a8dd0": { "describe": { - "columns": [ - { - "name": "loader", - "ordinal": 0, - "type_info": "Varchar" - } - ], - "nullable": [ - false - ], + "columns": [], + "nullable": [], "parameters": { "Left": [ - "Int4" + "Int8" ] } }, - "query": "\n SELECT loader FROM loaders\n WHERE id = $1\n " + "query": "\n DELETE FROM threads_messages\n WHERE thread_id = $1\n " }, "d331ca8f22da418cf654985c822ce4466824beaa00dea64cde90dc651a03024b": { "describe": { @@ -5779,27 +5853,6 @@ }, "query": "\n INSERT INTO mods_categories (joining_mod_id, joining_category_id, is_additional)\n VALUES ($1, $2, TRUE)\n " }, - "d5b00d6237b04018822db529995f0b001cd1cabf5ca93b4aff37f12c4feb83f6": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int4" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Varchar", - "Varchar" - ] - } - }, - "query": "\n INSERT INTO donation_platforms (short, name)\n VALUES ($1, $2)\n ON CONFLICT (short) DO NOTHING\n RETURNING id\n " - }, "d6453e50041b5521fa9e919a9162e533bb9426f8c584d98474c6ad414db715c8": { "describe": { "columns": [ @@ -5820,67 +5873,18 @@ }, "query": "SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)" }, - "d7127fd7f257cc7779841108c75f6fd8b20f9619bef1cacd0fbaf011cf0b25b3": { + "d75b73151ba84715c06bbada22b66c819de8eac87c088b0a501212ad3fe4d618": { "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "name", - "ordinal": 1, - "type_info": "Varchar" - }, - { - "name": "mod_id", - "ordinal": 2, - "type_info": "Int8" - }, - { - "name": "version_id", - "ordinal": 3, - "type_info": "Int8" - }, - { - "name": "user_id", - "ordinal": 4, - "type_info": "Int8" - }, - { - "name": "body", - "ordinal": 5, - "type_info": "Varchar" - }, - { - "name": "reporter", - "ordinal": 6, - "type_info": "Int8" - }, - { - "name": "created", - "ordinal": 7, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false, - true, - true, - true, - false, - false, - false - ], + "columns": [], + "nullable": [], "parameters": { "Left": [ - "Int8Array" + "Bool", + "Int8" ] } }, - "query": "\n SELECT r.id, rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created\n FROM reports r\n INNER JOIN report_types rt ON rt.id = r.report_type_id\n WHERE r.id = ANY($1)\n ORDER BY r.created DESC\n " + "query": "\n UPDATE reports\n SET closed = $1\n WHERE (id = $2)\n " }, "d7c65c30898110d801a5bdf092564e5726e35c1033c69dba69008989a087357c": { "describe": { @@ -6043,6 +6047,18 @@ }, "query": "\n SELECT id, user_id, role, permissions, accepted, payouts_split, ordering\n FROM team_members\n WHERE (team_id = $1 AND user_id = $2)\n " }, + "dcc32d760692674180471e7b19a9a1f73e77bb170e92cc7d60da37596ef840b0": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM threads\n WHERE id = $1\n " + }, "dd57a6dd89fefedbde796ef02b308ce7dba17ca0c65ffd5f9e35e296a72d4c1c": { "describe": { "columns": [ @@ -6273,18 +6289,6 @@ }, "query": "\n SELECT f.url url, h.hash hash, h.algorithm algorithm, f.version_id version_id, v.mod_id project_id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n INNER JOIN versions v ON v.id = f.version_id AND v.status != ANY($1)\n INNER JOIN mods m on v.mod_id = m.id\n WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ANY($4)\n " }, - "e673006d1355fa91ba5739d7cf569eec5e1ec501f7b1dc2b431f0b1c25ac07d5": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n DELETE FROM game_versions\n WHERE version = $1\n " - }, "e7d0a64a08df6783c942f2fcadd94dd45f8d96ad3d3736e52ce90f68d396cdab": { "describe": { "columns": [ @@ -6305,62 +6309,6 @@ }, "query": "SELECT EXISTS(SELECT 1 FROM team_members WHERE id=$1)" }, - "e845ace468e484025d7b052008f703dee66ce7fcc33862df7e9ccfe7553eef96": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "user_id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "role", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "permissions", - "ordinal": 3, - "type_info": "Int8" - }, - { - "name": "accepted", - "ordinal": 4, - "type_info": "Bool" - }, - { - "name": "payouts_split", - "ordinal": 5, - "type_info": "Numeric" - }, - { - "name": "ordering", - "ordinal": 6, - "type_info": "Int8" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT id, user_id, role, permissions, accepted, payouts_split, ordering\n FROM team_members\n WHERE team_id = $1\n ORDER BY ordering\n " - }, "e876f64db82d618dce53b108509d67a1108aa747d16892499481fe9f8b95200b": { "describe": { "columns": [], @@ -6438,26 +6386,6 @@ }, "query": "SELECT EXISTS(SELECT 1 FROM teams WHERE id=$1)" }, - "e8dc09a76d69e689d4b97527755aebfc049bbb4d470627a688eb9d56f01f8bd5": { - "describe": { - "columns": [ - { - "name": "name", - "ordinal": 0, - "type_info": "Varchar" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Int4" - ] - } - }, - "query": "\n SELECT name FROM project_types\n WHERE id = $1\n " - }, "e925b15ec46f0263c7775ba1ba00ed11cfd6749fa792d4eabed73b619f230585": { "describe": { "columns": [], @@ -6568,13 +6496,13 @@ }, "query": "\n SELECT name FROM project_types pt\n INNER JOIN mods ON mods.project_type = pt.id\n WHERE mods.id = $1\n " }, - "f0db9d8606ccc2196a9cfafe0e7090dab42bf790f25e0469b8947fac1cf043d5": { + "f17a109913015a7a5ab847bb2e73794d6261a08d450de24b450222755e520881": { "describe": { "columns": [ { - "name": "version", + "name": "id", "ordinal": 0, - "type_info": "Varchar" + "type_info": "Int8" } ], "nullable": [ @@ -6582,11 +6510,12 @@ ], "parameters": { "Left": [ - "Int4" + "Int8", + "Int8" ] } }, - "query": "\n SELECT version FROM game_versions\n WHERE id = $1\n " + "query": "\n SELECT id FROM reports\n WHERE closed = FALSE AND reporter = $1\n ORDER BY created ASC\n LIMIT $2;\n " }, "f191675ebb8f77548ba7e4385288a2f1899c7c611395249db85bea9a3ce9a54a": { "describe": { @@ -6767,6 +6696,26 @@ }, "query": "\n UPDATE users\n SET midas_expires = $1, is_overdue = FALSE\n WHERE (stripe_customer_id = $2)\n " }, + "f85fc13148aafc03a4df68eaa389945e9dc6472a759525a48cfb23d31181535c": { + "describe": { + "columns": [ + { + "name": "exists", + "ordinal": 0, + "type_info": "Bool" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "SELECT EXISTS(SELECT 1 FROM threads_messages WHERE id=$1)" + }, "f8be3053274b00ee9743e798886696062009c5f681baaf29dfc24cfbbda93742": { "describe": { "columns": [ @@ -6787,32 +6736,6 @@ }, "query": "\n SELECT EXISTS(SELECT 1 FROM mods WHERE slug = LOWER($1))\n " }, - "f8c00875a7450c74423f9913cc3500898e9fcb6aa7eb8fc2f6fd16dc560773de": { - "describe": { - "columns": [ - { - "name": "short", - "ordinal": 0, - "type_info": "Varchar" - }, - { - "name": "name", - "ordinal": 1, - "type_info": "Varchar" - } - ], - "nullable": [ - false, - false - ], - "parameters": { - "Left": [ - "Int4" - ] - } - }, - "query": "\n SELECT short, name FROM donation_platforms\n WHERE id = $1\n " - }, "fa1b92b15cc108fa046998f789c8b259e0226e7dac16c635927ca74abc78cea9": { "describe": { "columns": [ diff --git a/src/database/models/categories.rs b/src/database/models/categories.rs index 898595cb..bbea1520 100644 --- a/src/database/models/categories.rs +++ b/src/database/models/categories.rs @@ -10,6 +10,11 @@ pub struct ProjectType { pub name: String, } +pub struct SideType { + pub id: SideTypeId, + pub name: String, +} + pub struct Loader { pub id: LoaderId, pub loader: String, @@ -46,23 +51,7 @@ pub struct DonationPlatform { pub name: String, } -pub struct CategoryBuilder<'a> { - pub name: Option<&'a str>, - pub project_type: Option<&'a ProjectTypeId>, - pub icon: Option<&'a str>, - pub header: Option<&'a str>, -} - impl Category { - pub fn builder() -> CategoryBuilder<'static> { - CategoryBuilder { - name: None, - project_type: None, - icon: None, - header: None, - } - } - pub async fn get_id<'a, E>( name: &str, exec: E, @@ -105,26 +94,6 @@ impl Category { Ok(result.map(|r| CategoryId(r.id))) } - pub async fn get_name<'a, E>( - id: CategoryId, - exec: E, - ) -> Result - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - let result = sqlx::query!( - " - SELECT category FROM categories - WHERE id = $1 - ", - id as CategoryId - ) - .fetch_one(exec) - .await?; - - Ok(result.category) - } - pub async fn list<'a, E>(exec: E) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, @@ -152,118 +121,9 @@ impl Category { Ok(result) } - - pub async fn remove<'a, E>( - name: &str, - exec: E, - ) -> Result, DatabaseError> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - let result = sqlx::query!( - " - DELETE FROM categories - WHERE category = $1 - ", - name - ) - .execute(exec) - .await?; - - if result.rows_affected() == 0 { - // Nothing was deleted - Ok(None) - } else { - Ok(Some(())) - } - } -} - -impl<'a> CategoryBuilder<'a> { - /// The name of the category. Must be ASCII alphanumeric or `-`/`_` - pub fn name( - self, - name: &'a str, - ) -> Result, DatabaseError> { - Ok(Self { - name: Some(name), - ..self - }) - } - - pub fn header( - self, - header: &'a str, - ) -> Result, DatabaseError> { - Ok(Self { - header: Some(header), - ..self - }) - } - - pub fn project_type( - self, - project_type: &'a ProjectTypeId, - ) -> Result, DatabaseError> { - Ok(Self { - project_type: Some(project_type), - ..self - }) - } - - pub fn icon( - self, - icon: &'a str, - ) -> Result, DatabaseError> { - Ok(Self { - icon: Some(icon), - ..self - }) - } - - pub async fn insert<'b, E>( - self, - exec: E, - ) -> Result - where - E: sqlx::Executor<'b, Database = sqlx::Postgres>, - { - let id = *self.project_type.ok_or_else(|| { - DatabaseError::Other("No project type specified.".to_string()) - })?; - let result = sqlx::query!( - " - INSERT INTO categories (category, project_type, icon, header) - VALUES ($1, $2, $3, $4) - RETURNING id - ", - self.name, - id as ProjectTypeId, - self.icon, - self.header - ) - .fetch_one(exec) - .await?; - - Ok(CategoryId(result.id)) - } -} - -pub struct LoaderBuilder<'a> { - pub name: Option<&'a str>, - pub icon: Option<&'a str>, - pub supported_project_types: Option<&'a [ProjectTypeId]>, } impl Loader { - pub fn builder() -> LoaderBuilder<'static> { - LoaderBuilder { - name: None, - icon: None, - supported_project_types: None, - } - } - pub async fn get_id<'a, E>( name: &str, exec: E, @@ -284,26 +144,6 @@ impl Loader { Ok(result.map(|r| LoaderId(r.id))) } - pub async fn get_name<'a, E>( - id: LoaderId, - exec: E, - ) -> Result - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - let result = sqlx::query!( - " - SELECT loader FROM loaders - WHERE id = $1 - ", - id as LoaderId - ) - .fetch_one(exec) - .await?; - - Ok(result.loader) - } - pub async fn list<'a, E>(exec: E) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, @@ -337,110 +177,6 @@ impl Loader { Ok(result) } - - // TODO: remove loaders with projects using them - pub async fn remove<'a, E>( - name: &str, - exec: E, - ) -> Result, DatabaseError> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - let result = sqlx::query!( - " - DELETE FROM loaders - WHERE loader = $1 - ", - name - ) - .execute(exec) - .await?; - - if result.rows_affected() == 0 { - // Nothing was deleted - Ok(None) - } else { - Ok(Some(())) - } - } -} - -impl<'a> LoaderBuilder<'a> { - /// The name of the loader. Must be ASCII alphanumeric or `-`/`_` - pub fn name( - self, - name: &'a str, - ) -> Result, DatabaseError> { - Ok(Self { - name: Some(name), - ..self - }) - } - - pub fn icon( - self, - icon: &'a str, - ) -> Result, DatabaseError> { - Ok(Self { - icon: Some(icon), - ..self - }) - } - - pub fn supported_project_types( - self, - supported_project_types: &'a [ProjectTypeId], - ) -> Result, DatabaseError> { - Ok(Self { - supported_project_types: Some(supported_project_types), - ..self - }) - } - - pub async fn insert( - self, - transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, - ) -> Result { - let result = sqlx::query!( - " - INSERT INTO loaders (loader, icon) - VALUES ($1, $2) - ON CONFLICT (loader) DO NOTHING - RETURNING id - ", - self.name, - self.icon - ) - .fetch_one(&mut *transaction) - .await?; - - if let Some(project_types) = self.supported_project_types { - sqlx::query!( - " - DELETE FROM loaders_project_types - WHERE joining_loader_id = $1 - ", - result.id - ) - .execute(&mut *transaction) - .await?; - - for project_type in project_types { - sqlx::query!( - " - INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id) - VALUES ($1, $2) - ", - result.id, - project_type.0, - ) - .execute(&mut *transaction) - .await?; - } - } - - Ok(LoaderId(result.id)) - } } #[derive(Default)] @@ -475,26 +211,6 @@ impl GameVersion { Ok(result.map(|r| GameVersionId(r.id))) } - pub async fn get_name<'a, E>( - id: GameVersionId, - exec: E, - ) -> Result - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - let result = sqlx::query!( - " - SELECT version FROM game_versions - WHERE id = $1 - ", - id as GameVersionId - ) - .fetch_one(exec) - .await?; - - Ok(result.version) - } - pub async fn list<'a, E>(exec: E) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, @@ -595,31 +311,6 @@ impl GameVersion { Ok(result) } - - pub async fn remove<'a, E>( - name: &str, - exec: E, - ) -> Result, DatabaseError> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - let result = sqlx::query!( - " - DELETE FROM game_versions - WHERE version = $1 - ", - name - ) - .execute(exec) - .await?; - - if result.rows_affected() == 0 { - // Nothing was deleted - Ok(None) - } else { - Ok(Some(())) - } - } } impl<'a> GameVersionBuilder<'a> { @@ -681,17 +372,7 @@ impl<'a> GameVersionBuilder<'a> { } } -#[derive(Default)] -pub struct DonationPlatformBuilder<'a> { - pub short: Option<&'a str>, - pub name: Option<&'a str>, -} - impl DonationPlatform { - pub fn builder() -> DonationPlatformBuilder<'static> { - DonationPlatformBuilder::default() - } - pub async fn get_id<'a, E>( id: &str, exec: E, @@ -712,30 +393,6 @@ impl DonationPlatform { Ok(result.map(|r| DonationPlatformId(r.id))) } - pub async fn get<'a, E>( - id: DonationPlatformId, - exec: E, - ) -> Result - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - let result = sqlx::query!( - " - SELECT short, name FROM donation_platforms - WHERE id = $1 - ", - id as DonationPlatformId - ) - .fetch_one(exec) - .await?; - - Ok(DonationPlatform { - id, - short: result.short, - name: result.name, - }) - } - pub async fn list<'a, E>( exec: E, ) -> Result, DatabaseError> @@ -760,89 +417,9 @@ impl DonationPlatform { Ok(result) } - - pub async fn remove<'a, E>( - short: &str, - exec: E, - ) -> Result, DatabaseError> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - let result = sqlx::query!( - " - DELETE FROM donation_platforms - WHERE short = $1 - ", - short - ) - .execute(exec) - .await?; - - if result.rows_affected() == 0 { - // Nothing was deleted - Ok(None) - } else { - Ok(Some(())) - } - } -} - -impl<'a> DonationPlatformBuilder<'a> { - /// The donation platform short name. Spaces must be replaced with '_' for it to be valid - pub fn short( - self, - short: &'a str, - ) -> Result, DatabaseError> { - Ok(Self { - short: Some(short), - ..self - }) - } - - /// The donation platform long name - pub fn name( - self, - name: &'a str, - ) -> Result, DatabaseError> { - Ok(Self { - name: Some(name), - ..self - }) - } - - pub async fn insert<'b, E>( - self, - exec: E, - ) -> Result - where - E: sqlx::Executor<'b, Database = sqlx::Postgres>, - { - let result = sqlx::query!( - " - INSERT INTO donation_platforms (short, name) - VALUES ($1, $2) - ON CONFLICT (short) DO NOTHING - RETURNING id - ", - self.short, - self.name, - ) - .fetch_one(exec) - .await?; - - Ok(DonationPlatformId(result.id)) - } -} - -pub struct ReportTypeBuilder<'a> { - pub name: Option<&'a str>, } impl ReportType { - pub fn builder() -> ReportTypeBuilder<'static> { - ReportTypeBuilder { name: None } - } - pub async fn get_id<'a, E>( name: &str, exec: E, @@ -863,26 +440,6 @@ impl ReportType { Ok(result.map(|r| ReportTypeId(r.id))) } - pub async fn get_name<'a, E>( - id: ReportTypeId, - exec: E, - ) -> Result - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - let result = sqlx::query!( - " - SELECT name FROM report_types - WHERE id = $1 - ", - id as ReportTypeId - ) - .fetch_one(exec) - .await?; - - Ok(result.name) - } - pub async fn list<'a, E>(exec: E) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, @@ -899,74 +456,9 @@ impl ReportType { Ok(result) } - - pub async fn remove<'a, E>( - name: &str, - exec: E, - ) -> Result, DatabaseError> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - let result = sqlx::query!( - " - DELETE FROM report_types - WHERE name = $1 - ", - name - ) - .execute(exec) - .await?; - - if result.rows_affected() == 0 { - // Nothing was deleted - Ok(None) - } else { - Ok(Some(())) - } - } -} - -impl<'a> ReportTypeBuilder<'a> { - /// The name of the report type. Must be ASCII alphanumeric or `-`/`_` - pub fn name( - self, - name: &'a str, - ) -> Result, DatabaseError> { - Ok(Self { name: Some(name) }) - } - - pub async fn insert<'b, E>( - self, - exec: E, - ) -> Result - where - E: sqlx::Executor<'b, Database = sqlx::Postgres>, - { - let result = sqlx::query!( - " - INSERT INTO report_types (name) - VALUES ($1) - ON CONFLICT (name) DO NOTHING - RETURNING id - ", - self.name - ) - .fetch_one(exec) - .await?; - - Ok(ReportTypeId(result.id)) - } -} - -pub struct ProjectTypeBuilder<'a> { - pub name: Option<&'a str>, } impl ProjectType { - pub fn builder() -> ProjectTypeBuilder<'static> { - ProjectTypeBuilder { name: None } - } - pub async fn get_id<'a, E>( name: &str, exec: E, @@ -987,53 +479,6 @@ impl ProjectType { Ok(result.map(|r| ProjectTypeId(r.id))) } - pub async fn get_many_id<'a, E>( - names: &[String], - exec: E, - ) -> Result, sqlx::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - let project_types = sqlx::query!( - " - SELECT id, name FROM project_types - WHERE name = ANY($1) - ", - names - ) - .fetch_many(exec) - .try_filter_map(|e| async { - Ok(e.right().map(|x| ProjectType { - id: ProjectTypeId(x.id), - name: x.name, - })) - }) - .try_collect::>() - .await?; - - Ok(project_types) - } - - pub async fn get_name<'a, E>( - id: ProjectTypeId, - exec: E, - ) -> Result - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - let result = sqlx::query!( - " - SELECT name FROM project_types - WHERE id = $1 - ", - id as ProjectTypeId - ) - .fetch_one(exec) - .await?; - - Ok(result.name) - } - pub async fn list<'a, E>(exec: E) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, @@ -1050,62 +495,43 @@ impl ProjectType { Ok(result) } +} - // TODO: remove loaders with mods using them - pub async fn remove<'a, E>( +impl SideType { + pub async fn get_id<'a, E>( name: &str, exec: E, - ) -> Result, DatabaseError> + ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let result = sqlx::query!( " - DELETE FROM project_types + SELECT id FROM side_types WHERE name = $1 ", name ) - .execute(exec) + .fetch_optional(exec) .await?; - if result.rows_affected() == 0 { - // Nothing was deleted - Ok(None) - } else { - Ok(Some(())) - } - } -} - -impl<'a> ProjectTypeBuilder<'a> { - /// The name of the project type. Must be ASCII alphanumeric or `-`/`_` - pub fn name( - self, - name: &'a str, - ) -> Result, DatabaseError> { - Ok(Self { name: Some(name) }) + Ok(result.map(|r| SideTypeId(r.id))) } - pub async fn insert<'b, E>( - self, - exec: E, - ) -> Result + pub async fn list<'a, E>(exec: E) -> Result, DatabaseError> where - E: sqlx::Executor<'b, Database = sqlx::Postgres>, + E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let result = sqlx::query!( " - INSERT INTO project_types (name) - VALUES ($1) - ON CONFLICT (name) DO NOTHING - RETURNING id - ", - self.name + SELECT name FROM side_types + " ) - .fetch_one(exec) + .fetch_many(exec) + .try_filter_map(|e| async { Ok(e.right().map(|c| c.name)) }) + .try_collect::>() .await?; - Ok(ProjectTypeId(result.id)) + Ok(result) } } diff --git a/src/database/models/ids.rs b/src/database/models/ids.rs index 1e150a94..dbc52e5d 100644 --- a/src/database/models/ids.rs +++ b/src/database/models/ids.rs @@ -106,7 +106,22 @@ generate_ids!( NotificationId ); -#[derive(Copy, Clone, Debug, PartialEq, Eq, Type)] +generate_ids!( + pub generate_thread_id, + ThreadId, + 8, + "SELECT EXISTS(SELECT 1 FROM threads WHERE id=$1)", + ThreadId +); +generate_ids!( + pub generate_thread_message_id, + ThreadMessageId, + 8, + "SELECT EXISTS(SELECT 1 FROM threads_messages WHERE id=$1)", + ThreadMessageId +); + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Type, Deserialize)] #[sqlx(transparent)] pub struct UserId(pub i64); @@ -169,6 +184,13 @@ pub struct NotificationId(pub i64); #[sqlx(transparent)] pub struct NotificationActionId(pub i32); +#[derive(Copy, Clone, Debug, Type, Deserialize)] +#[sqlx(transparent)] +pub struct ThreadId(pub i64); +#[derive(Copy, Clone, Debug, Type, Deserialize)] +#[sqlx(transparent)] +pub struct ThreadMessageId(pub i64); + use crate::models::ids; impl From for ProjectId { @@ -231,3 +253,23 @@ impl From for ids::NotificationId { ids::NotificationId(id.0 as u64) } } +impl From for ThreadId { + fn from(id: ids::ThreadId) -> Self { + ThreadId(id.0 as i64) + } +} +impl From for ids::ThreadId { + fn from(id: ThreadId) -> Self { + ids::ThreadId(id.0 as u64) + } +} +impl From for ThreadMessageId { + fn from(id: ids::ThreadMessageId) -> Self { + ThreadMessageId(id.0 as i64) + } +} +impl From for ids::ThreadMessageId { + fn from(id: ThreadMessageId) -> Self { + ids::ThreadMessageId(id.0 as u64) + } +} diff --git a/src/database/models/mod.rs b/src/database/models/mod.rs index d1df5538..a952d22d 100644 --- a/src/database/models/mod.rs +++ b/src/database/models/mod.rs @@ -1,7 +1,3 @@ -#![allow(dead_code)] -// TODO: remove attr once routes are created - -use chrono::{DateTime, Utc}; use thiserror::Error; pub mod categories; @@ -10,6 +6,7 @@ pub mod notification_item; pub mod project_item; pub mod report_item; pub mod team_item; +pub mod thread_item; pub mod user_item; pub mod version_item; @@ -17,6 +14,7 @@ pub use ids::*; pub use project_item::Project; pub use team_item::Team; pub use team_item::TeamMember; +pub use thread_item::{Thread, ThreadMessage}; pub use user_item::User; pub use version_item::Version; @@ -28,82 +26,6 @@ pub enum DatabaseError { RandomId, #[error("A database request failed")] Other(String), -} - -impl ids::SideTypeId { - pub async fn get_id<'a, E>( - side: &crate::models::projects::SideType, - exec: E, - ) -> Result, DatabaseError> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - let result = sqlx::query!( - " - SELECT id FROM side_types - WHERE name = $1 - ", - side.as_str() - ) - .fetch_optional(exec) - .await?; - - Ok(result.map(|r| ids::SideTypeId(r.id))) - } -} - -impl ids::DonationPlatformId { - pub async fn get_id<'a, E>( - id: &str, - exec: E, - ) -> Result, DatabaseError> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - let result = sqlx::query!( - " - SELECT id FROM donation_platforms - WHERE short = $1 - ", - id - ) - .fetch_optional(exec) - .await?; - - Ok(result.map(|r| ids::DonationPlatformId(r.id))) - } -} - -impl ids::ProjectTypeId { - pub async fn get_id<'a, E>( - project_type: String, - exec: E, - ) -> Result, DatabaseError> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - let result = sqlx::query!( - " - SELECT id FROM project_types - WHERE name = $1 - ", - project_type - ) - .fetch_optional(exec) - .await?; - - Ok(result.map(|r| ProjectTypeId(r.id))) - } -} - -pub fn convert_postgres_date(input: &str) -> DateTime { - let mut result = DateTime::parse_from_str(input, "%Y-%m-%d %T.%f%#z"); - - if result.is_err() { - result = DateTime::parse_from_str(input, "%Y-%m-%d %T%#z") - } - - result - .map(|x| x.with_timezone(&Utc)) - .unwrap_or_else(|_| Utc::now()) + #[error("Error while parsing JSON: {0}")] + Json(#[from] serde_json::Error), } diff --git a/src/database/models/project_item.rs b/src/database/models/project_item.rs index 4ae28702..5c1d83d4 100644 --- a/src/database/models/project_item.rs +++ b/src/database/models/project_item.rs @@ -101,6 +101,7 @@ pub struct ProjectBuilder { pub donation_urls: Vec, pub gallery_items: Vec, pub color: Option, + pub thread_id: ThreadId, } impl ProjectBuilder { @@ -146,6 +147,7 @@ impl ProjectBuilder { color: self.color, loaders: vec![], game_versions: vec![], + thread_id: Some(self.thread_id), }; project_struct.insert(&mut *transaction).await?; @@ -230,6 +232,7 @@ pub struct Project { pub color: Option, pub loaders: Vec, pub game_versions: Vec, + pub thread_id: Option, } impl Project { @@ -244,14 +247,14 @@ impl Project { published, downloads, icon_url, issues_url, source_url, wiki_url, status, requested_status, discord_url, client_side, server_side, license_url, license, - slug, project_type, color + slug, project_type, color, thread_id ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, - LOWER($19), $20, $21 + LOWER($19), $20, $21, $22 ) ", self.id as ProjectId, @@ -274,7 +277,8 @@ impl Project { &self.license, self.slug.as_ref(), self.project_type as ProjectTypeId, - self.color.map(|x| x as i32) + self.color.map(|x| x as i32), + self.thread_id.map(|x| x.0), ) .execute(&mut *transaction) .await?; @@ -313,7 +317,7 @@ impl Project { issues_url, source_url, wiki_url, discord_url, license_url, team_id, client_side, server_side, license, slug, moderation_message, moderation_message_body, flame_anvil_project, - flame_anvil_user, webhook_sent, color, loaders, game_versions + flame_anvil_user, webhook_sent, color, loaders, game_versions, thread_id FROM mods WHERE id = ANY($1) ", @@ -359,6 +363,7 @@ impl Project { loaders: m.loaders, game_versions: m.game_versions, queued: m.queued, + thread_id: m.thread_id.map(ThreadId), })) }) .try_collect::>() @@ -386,6 +391,26 @@ impl Project { return Ok(None); }; + let thread_id = sqlx::query!( + " + SELECT thread_id FROM mods + WHERE id = $1 + ", + id as ProjectId + ) + .fetch_optional(&mut *transaction) + .await?; + + if let Some(thread_id) = thread_id { + if let Some(id) = thread_id.thread_id { + crate::database::models::Thread::remove_full( + ThreadId(id), + transaction, + ) + .await?; + } + } + sqlx::query!( " DELETE FROM mod_follows @@ -654,7 +679,7 @@ impl Project { m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url, m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body, cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user, m.webhook_sent, m.color, - m.loaders loaders, m.game_versions game_versions, + m.loaders loaders, m.game_versions game_versions, m.thread_id thread_id, ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories, ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories, JSONB_AGG(DISTINCT jsonb_build_object('id', v.id, 'date_published', v.date_published)) filter (where v.id is not null) versions, @@ -720,6 +745,7 @@ impl Project { loaders: m.loaders, game_versions: m.game_versions, queued: m.queued, + thread_id: m.thread_id.map(ThreadId), }, project_type: m.project_type_name, categories: m.categories.unwrap_or_default(), diff --git a/src/database/models/report_item.rs b/src/database/models/report_item.rs index 6e2faa24..a975ed1a 100644 --- a/src/database/models/report_item.rs +++ b/src/database/models/report_item.rs @@ -10,6 +10,8 @@ pub struct Report { pub body: String, pub reporter: UserId, pub created: DateTime, + pub closed: bool, + pub thread_id: ThreadId, } pub struct QueryReport { @@ -21,6 +23,8 @@ pub struct QueryReport { pub body: String, pub reporter: UserId, pub created: DateTime, + pub closed: bool, + pub thread_id: Option, } impl Report { @@ -32,11 +36,11 @@ impl Report { " INSERT INTO reports ( id, report_type_id, mod_id, version_id, user_id, - body, reporter + body, reporter, thread_id ) VALUES ( $1, $2, $3, $4, $5, - $6, $7 + $6, $7, $8 ) ", self.id as ReportId, @@ -45,7 +49,8 @@ impl Report { self.version_id.map(|x| x.0 as i64), self.user_id.map(|x| x.0 as i64), self.body, - self.reporter as UserId + self.reporter as UserId, + self.thread_id as ThreadId, ) .execute(&mut *transaction) .await?; @@ -78,7 +83,7 @@ impl Report { report_ids.iter().map(|x| x.0).collect(); let reports = sqlx::query!( " - SELECT r.id, rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created + SELECT r.id, rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created, r.thread_id, r.closed FROM reports r INNER JOIN report_types rt ON rt.id = r.report_type_id WHERE r.id = ANY($1) @@ -97,6 +102,8 @@ impl Report { body: x.body, reporter: UserId(x.reporter), created: x.created, + closed: x.closed, + thread_id: x.thread_id.map(ThreadId), })) }) .try_collect::>() @@ -105,33 +112,50 @@ impl Report { Ok(reports) } - pub async fn remove_full<'a, E>( + pub async fn remove_full( id: ReportId, - exec: E, - ) -> Result, sqlx::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, - { + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result, sqlx::error::Error> { let result = sqlx::query!( " SELECT EXISTS(SELECT 1 FROM reports WHERE id = $1) ", id as ReportId ) - .fetch_one(exec) + .fetch_one(&mut *transaction) .await?; if !result.exists.unwrap_or(false) { return Ok(None); } + let thread_id = sqlx::query!( + " + SELECT thread_id FROM reports + WHERE id = $1 + ", + id as ReportId + ) + .fetch_optional(&mut *transaction) + .await?; + + if let Some(thread_id) = thread_id { + if let Some(id) = thread_id.thread_id { + crate::database::models::Thread::remove_full( + ThreadId(id), + transaction, + ) + .await?; + } + } + sqlx::query!( " DELETE FROM reports WHERE id = $1 ", id as ReportId, ) - .execute(exec) + .execute(&mut *transaction) .await?; Ok(Some(())) diff --git a/src/database/models/team_item.rs b/src/database/models/team_item.rs index f09f3e57..c0414169 100644 --- a/src/database/models/team_item.rs +++ b/src/database/models/team_item.rs @@ -104,53 +104,6 @@ pub struct QueryTeamMember { } impl TeamMember { - /// Lists the members of a team - pub async fn get_from_team<'a, 'b, E>( - id: TeamId, - executor: E, - ) -> Result, super::DatabaseError> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - use futures::stream::TryStreamExt; - - let team_members = sqlx::query!( - " - SELECT id, user_id, role, permissions, accepted, payouts_split, ordering - FROM team_members - WHERE team_id = $1 - ORDER BY ordering - ", - id as TeamId, - ) - .fetch_many(executor) - .try_filter_map(|e| async { - if let Some(m) = e.right() { - Ok(Some(Ok(TeamMember { - id: TeamMemberId(m.id), - team_id: id, - user_id: UserId(m.user_id), - role: m.role, - permissions: Permissions::from_bits(m.permissions as u64) - .unwrap_or_default(), - accepted: m.accepted, - payouts_split: m.payouts_split, - ordering: m.ordering, - }))) - } else { - Ok(None) - } - }) - .try_collect::>>() - .await?; - - let team_members = team_members - .into_iter() - .collect::, super::DatabaseError>>()?; - - Ok(team_members) - } - // Lists the full members of a team pub async fn get_from_team_full<'a, 'b, E>( id: TeamId, @@ -232,100 +185,6 @@ impl TeamMember { Ok(team_members) } - /// Lists the team members for a user. Does not list pending requests. - pub async fn get_from_user_public<'a, 'b, E>( - id: UserId, - executor: E, - ) -> Result, super::DatabaseError> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - use futures::stream::TryStreamExt; - - let team_members = sqlx::query!( - " - SELECT id, team_id, role, permissions, accepted, payouts_split, ordering - FROM team_members - WHERE (user_id = $1 AND accepted = TRUE) - ORDER BY ordering - ", - id as UserId, - ) - .fetch_many(executor) - .try_filter_map(|e| async { - if let Some(m) = e.right() { - Ok(Some(Ok(TeamMember { - id: TeamMemberId(m.id), - team_id: TeamId(m.team_id), - user_id: id, - role: m.role, - permissions: Permissions::from_bits(m.permissions as u64) - .unwrap_or_default(), - accepted: m.accepted, - payouts_split: m.payouts_split, - ordering: m.ordering, - }))) - } else { - Ok(None) - } - }) - .try_collect::>>() - .await?; - - let team_members = team_members - .into_iter() - .collect::, super::DatabaseError>>()?; - - Ok(team_members) - } - - /// Lists the team members for a user. Includes pending requests. - pub async fn get_from_user_private<'a, 'b, E>( - id: UserId, - executor: E, - ) -> Result, super::DatabaseError> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - use futures::stream::TryStreamExt; - - let team_members = sqlx::query!( - " - SELECT id, team_id, role, permissions, accepted, payouts_split, ordering - FROM team_members - WHERE user_id = $1 - ORDER BY ordering - ", - id as UserId, - ) - .fetch_many(executor) - .try_filter_map(|e| async { - if let Some(m) = e.right() { - Ok(Some(Ok(TeamMember { - id: TeamMemberId(m.id), - team_id: TeamId(m.team_id), - user_id: id, - role: m.role, - permissions: Permissions::from_bits(m.permissions as u64) - .unwrap_or_default(), - accepted: m.accepted, - payouts_split: m.payouts_split, - ordering: m.ordering, - }))) - } else { - Ok(None) - } - }) - .try_collect::>>() - .await?; - - let team_members = team_members - .into_iter() - .collect::, super::DatabaseError>>()?; - - Ok(team_members) - } - /// Gets a team member from a user id and team id. Does not return pending members. pub async fn get_from_user_id<'a, 'b, E>( id: TeamId, diff --git a/src/database/models/thread_item.rs b/src/database/models/thread_item.rs new file mode 100644 index 00000000..651d28fd --- /dev/null +++ b/src/database/models/thread_item.rs @@ -0,0 +1,267 @@ +use super::ids::*; +use crate::database::models::DatabaseError; +use crate::models::threads::{MessageBody, ThreadType}; +use chrono::{DateTime, Utc}; +use serde::Deserialize; + +pub struct ThreadBuilder { + pub type_: ThreadType, + pub members: Vec, +} + +pub struct Thread { + pub id: ThreadId, + pub type_: ThreadType, + pub messages: Vec, + pub members: Vec, +} + +pub struct ThreadMessageBuilder { + pub author_id: Option, + pub body: MessageBody, + pub thread_id: ThreadId, + pub show_in_mod_inbox: Option, +} + +#[derive(Deserialize)] +pub struct ThreadMessage { + pub id: ThreadMessageId, + pub thread_id: ThreadId, + pub author_id: Option, + pub body: MessageBody, + pub created: DateTime, + pub show_in_mod_inbox: bool, +} + +impl ThreadMessageBuilder { + pub async fn insert( + &self, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + let thread_message_id = + generate_thread_message_id(&mut *transaction).await?; + + sqlx::query!( + " + INSERT INTO threads_messages ( + id, author_id, body, thread_id, show_in_mod_inbox + ) + VALUES ( + $1, $2, $3, $4, $5 + ) + ", + thread_message_id as ThreadMessageId, + self.author_id.map(|x| x.0), + serde_json::value::to_value(self.body.clone())?, + self.thread_id as ThreadId, + self.show_in_mod_inbox, + ) + .execute(&mut *transaction) + .await?; + + Ok(thread_message_id) + } +} + +impl ThreadBuilder { + pub async fn insert( + &self, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + let thread_id = generate_thread_id(&mut *transaction).await?; + + sqlx::query!( + " + INSERT INTO threads ( + id, thread_type + ) + VALUES ( + $1, $2 + ) + ", + thread_id as ThreadId, + self.type_.as_str(), + ) + .execute(&mut *transaction) + .await?; + + for member in &self.members { + sqlx::query!( + " + INSERT INTO threads_members ( + thread_id, user_id + ) + VALUES ( + $1, $2 + ) + ", + thread_id as ThreadId, + *member as UserId, + ) + .execute(&mut *transaction) + .await?; + } + + Ok(thread_id) + } +} + +impl Thread { + pub async fn get<'a, E>( + id: ThreadId, + exec: E, + ) -> Result, sqlx::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + Self::get_many(&[id], exec) + .await + .map(|x| x.into_iter().next()) + } + + pub async fn get_many<'a, E>( + thread_ids: &[ThreadId], + exec: E, + ) -> Result, sqlx::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + use futures::stream::TryStreamExt; + + let thread_ids_parsed: Vec = + thread_ids.iter().map(|x| x.0).collect(); + let threads = sqlx::query!( + " + SELECT t.id, t.thread_type, + ARRAY_AGG(DISTINCT tm.user_id) filter (where tm.user_id is not null) members, + JSONB_AGG(DISTINCT jsonb_build_object('id', tmsg.id, 'author_id', tmsg.author_id, 'thread_id', tmsg.thread_id, 'body', tmsg.body, 'created', tmsg.created)) filter (where tmsg.id is not null) messages + FROM threads t + LEFT OUTER JOIN threads_messages tmsg ON tmsg.thread_id = t.id + LEFT OUTER JOIN threads_members tm ON tm.thread_id = t.id + WHERE t.id = ANY($1) + GROUP BY t.id + ", + &thread_ids_parsed + ) + .fetch_many(exec) + .try_filter_map(|e| async { + Ok(e.right().map(|x| Thread { + id: ThreadId(x.id), + type_: ThreadType::from_str(&x.thread_type), + messages: serde_json::from_value( + x.messages.unwrap_or_default(), + ) + .ok() + .unwrap_or_default(), + members: x.members.unwrap_or_default().into_iter().map(UserId).collect(), + })) + }) + .try_collect::>() + .await?; + + Ok(threads) + } + + pub async fn remove_full( + id: ThreadId, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result, sqlx::error::Error> { + sqlx::query!( + " + DELETE FROM threads_messages + WHERE thread_id = $1 + ", + id as ThreadId, + ) + .execute(&mut *transaction) + .await?; + sqlx::query!( + " + DELETE FROM threads_members + WHERE thread_id = $1 + ", + id as ThreadId + ) + .execute(&mut *transaction) + .await?; + sqlx::query!( + " + DELETE FROM threads + WHERE id = $1 + ", + id as ThreadId, + ) + .execute(&mut *transaction) + .await?; + + Ok(Some(())) + } +} + +impl ThreadMessage { + pub async fn get<'a, E>( + id: ThreadMessageId, + exec: E, + ) -> Result, sqlx::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + Self::get_many(&[id], exec) + .await + .map(|x| x.into_iter().next()) + } + + pub async fn get_many<'a, E>( + message_ids: &[ThreadMessageId], + exec: E, + ) -> Result, sqlx::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + use futures::stream::TryStreamExt; + + let message_ids_parsed: Vec = + message_ids.iter().map(|x| x.0).collect(); + let messages = sqlx::query!( + " + SELECT tm.id, tm.author_id, tm.thread_id, tm.body, tm.created, tm.show_in_mod_inbox + FROM threads_messages tm + WHERE tm.id = ANY($1) + ", + &message_ids_parsed + ) + .fetch_many(exec) + .try_filter_map(|e| async { + Ok(e.right().map(|x| ThreadMessage { + id: ThreadMessageId(x.id), + thread_id: ThreadId(x.thread_id), + author_id: x.author_id.map(UserId), + body: serde_json::from_value(x.body) + .unwrap_or(MessageBody::Deleted), + created: x.created, + show_in_mod_inbox: x.show_in_mod_inbox, + })) + }) + .try_collect::>() + .await?; + + Ok(messages) + } + + pub async fn remove_full( + id: ThreadMessageId, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result, sqlx::error::Error> { + sqlx::query!( + " + DELETE FROM threads_messages + WHERE id = $1 + ", + id as ThreadMessageId, + ) + .execute(&mut *transaction) + .await?; + + Ok(Some(())) + } +} diff --git a/src/database/models/user_item.rs b/src/database/models/user_item.rs index 14837a50..abdc505f 100644 --- a/src/database/models/user_item.rs +++ b/src/database/models/user_item.rs @@ -451,6 +451,28 @@ impl User { .execute(&mut *transaction) .await?; + sqlx::query!( + r#" + UPDATE threads_messages + SET body = '{"type": "deleted"}', author_id = $2 + WHERE author_id = $1 + "#, + id as UserId, + deleted_user as UserId, + ) + .execute(&mut *transaction) + .await?; + + sqlx::query!( + " + DELETE FROM threads_members + WHERE user_id = $1 + ", + id as UserId, + ) + .execute(&mut *transaction) + .await?; + sqlx::query!( " DELETE FROM users diff --git a/src/database/models/version_item.rs b/src/database/models/version_item.rs index 82497643..577ba4fc 100644 --- a/src/database/models/version_item.rs +++ b/src/database/models/version_item.rs @@ -568,66 +568,6 @@ impl Version { Ok(map) } - pub async fn get<'a, 'b, E>( - id: VersionId, - executor: E, - ) -> Result, sqlx::error::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, - { - Self::get_many(&[id], executor) - .await - .map(|x| x.into_iter().next()) - } - - pub async fn get_many<'a, E>( - version_ids: &[VersionId], - exec: E, - ) -> Result, sqlx::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, - { - use futures::stream::TryStreamExt; - - let version_ids_parsed: Vec = - version_ids.iter().map(|x| x.0).collect(); - let versions = sqlx::query!( - " - SELECT v.id, v.mod_id, v.author_id, v.name, v.version_number, - v.changelog, v.date_published, v.downloads, - v.version_type, v.featured, v.status, v.requested_status - FROM versions v - WHERE v.id = ANY($1) - ORDER BY v.date_published ASC - ", - &version_ids_parsed - ) - .fetch_many(exec) - .try_filter_map(|e| async { - Ok(e.right().map(|v| Version { - id: VersionId(v.id), - project_id: ProjectId(v.mod_id), - author_id: UserId(v.author_id), - name: v.name, - version_number: v.version_number, - changelog: v.changelog, - changelog_url: None, - date_published: v.date_published, - downloads: v.downloads, - featured: v.featured, - version_type: v.version_type, - status: VersionStatus::from_str(&v.status), - requested_status: v - .requested_status - .map(|x| VersionStatus::from_str(&x)), - })) - }) - .try_collect::>() - .await?; - - Ok(versions) - } - pub async fn get_full<'a, 'b, E>( id: VersionId, executor: E, diff --git a/src/models/ids.rs b/src/models/ids.rs index 02202078..69e57c77 100644 --- a/src/models/ids.rs +++ b/src/models/ids.rs @@ -4,6 +4,8 @@ pub use super::notifications::NotificationId; pub use super::projects::{ProjectId, VersionId}; pub use super::reports::ReportId; pub use super::teams::TeamId; +pub use super::threads::ThreadId; +pub use super::threads::ThreadMessageId; pub use super::users::UserId; /// Generates a random 64 bit integer that is exactly `n` characters @@ -109,6 +111,8 @@ base62_id_impl!(VersionId, VersionId); base62_id_impl!(TeamId, TeamId); base62_id_impl!(ReportId, ReportId); base62_id_impl!(NotificationId, NotificationId); +base62_id_impl!(ThreadId, ThreadId); +base62_id_impl!(ThreadMessageId, ThreadMessageId); pub mod base62_impl { use serde::de::{self, Deserializer, Visitor}; diff --git a/src/models/mod.rs b/src/models/mod.rs index a4608452..54df8b61 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -5,4 +5,5 @@ pub mod pack; pub mod projects; pub mod reports; pub mod teams; +pub mod threads; pub mod users; diff --git a/src/models/projects.rs b/src/models/projects.rs index 3b7fa682..2855ab2d 100644 --- a/src/models/projects.rs +++ b/src/models/projects.rs @@ -3,6 +3,7 @@ use super::teams::TeamId; use super::users::UserId; use crate::database::models::project_item::QueryProject; use crate::database::models::version_item::QueryVersion; +use crate::models::threads::ThreadId; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use validator::Validate; @@ -56,6 +57,7 @@ pub struct Project { /// The requested status of this projct pub requested_status: Option, + /// DEPRECATED: moved to threads system /// The rejection data of the project pub moderator_message: Option, @@ -107,6 +109,9 @@ pub struct Project { /// The color of the project (picked from icon) pub color: Option, + + /// The thread of the moderation messages of the project + pub thread_id: Option, } impl From for Project { @@ -195,6 +200,7 @@ impl From for Project { flame_anvil_project: m.flame_anvil_project, flame_anvil_user: m.flame_anvil_user.map(|x| x.into()), color: m.color, + thread_id: m.thread_id.map(|x| x.into()), } } } diff --git a/src/models/reports.rs b/src/models/reports.rs index e970f343..5f39e9aa 100644 --- a/src/models/reports.rs +++ b/src/models/reports.rs @@ -1,5 +1,5 @@ use super::ids::Base62Id; -use crate::models::ids::UserId; +use crate::models::ids::{ThreadId, UserId}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -17,6 +17,8 @@ pub struct Report { pub reporter: UserId, pub body: String, pub created: DateTime, + pub closed: bool, + pub thread_id: Option, } #[derive(Serialize, Deserialize, Clone)] diff --git a/src/models/threads.rs b/src/models/threads.rs new file mode 100644 index 00000000..20943732 --- /dev/null +++ b/src/models/threads.rs @@ -0,0 +1,80 @@ +use super::ids::Base62Id; +use crate::models::projects::ProjectStatus; +use crate::models::users::{User, UserId}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(from = "Base62Id")] +#[serde(into = "Base62Id")] +pub struct ThreadId(pub u64); + +#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(from = "Base62Id")] +#[serde(into = "Base62Id")] +pub struct ThreadMessageId(pub u64); + +#[derive(Serialize, Deserialize)] +pub struct Thread { + pub id: ThreadId, + #[serde(rename = "type")] + pub type_: ThreadType, + pub messages: Vec, + pub members: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct ThreadMessage { + pub id: ThreadMessageId, + pub author_id: Option, + pub body: MessageBody, + pub created: DateTime, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum MessageBody { + Text { + body: String, + }, + StatusChange { + new_status: ProjectStatus, + old_status: ProjectStatus, + }, + ThreadClosure, + Deleted, +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Copy, Clone)] +#[serde(rename_all = "snake_case")] +pub enum ThreadType { + Report, + Project, + DirectMessage, +} + +impl std::fmt::Display for ThreadType { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(fmt, "{}", self.as_str()) + } +} + +impl ThreadType { + // These are constant, so this can remove unneccessary allocations (`to_string`) + pub fn as_str(&self) -> &'static str { + match self { + ThreadType::Report => "report", + ThreadType::Project => "project", + ThreadType::DirectMessage => "direct_message", + } + } + + pub fn from_str(string: &str) -> ThreadType { + match string { + "report" => ThreadType::Report, + "project" => ThreadType::Project, + "direct_message" => ThreadType::DirectMessage, + _ => ThreadType::DirectMessage, + } + } +} diff --git a/src/routes/v2/mod.rs b/src/routes/v2/mod.rs index 573b588b..8386321c 100644 --- a/src/routes/v2/mod.rs +++ b/src/routes/v2/mod.rs @@ -9,6 +9,7 @@ mod reports; mod statistics; mod tags; mod teams; +mod threads; mod users; mod version_creation; mod version_file; @@ -30,6 +31,7 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) { .configure(statistics::config) .configure(tags::config) .configure(teams::config) + .configure(threads::config) .configure(users::config) .configure(version_file::config) .configure(versions::config), diff --git a/src/routes/v2/project_creation.rs b/src/routes/v2/project_creation.rs index eeeb276c..1da9c13a 100644 --- a/src/routes/v2/project_creation.rs +++ b/src/routes/v2/project_creation.rs @@ -1,11 +1,13 @@ use super::version_creation::InitialVersionData; use crate::database::models; +use crate::database::models::thread_item::ThreadBuilder; use crate::file_hosting::{FileHost, FileHostingError}; use crate::models::error::ApiError; use crate::models::projects::{ DonationLink, License, ProjectId, ProjectStatus, SideType, VersionId, VersionStatus, }; +use crate::models::threads::ThreadType; use crate::models::users::UserId; use crate::search::indexing::IndexingError; use crate::util::auth::{get_user_from_headers, AuthenticationError}; @@ -464,8 +466,8 @@ async fn project_create_inner( project_create_data = create_data; } - let project_type_id = models::ProjectTypeId::get_id( - project_create_data.project_type.clone(), + let project_type_id = models::categories::ProjectType::get_id( + project_create_data.project_type.as_str(), &mut *transaction, ) .await? @@ -698,8 +700,8 @@ async fn project_create_inner( ))); } - let client_side_id = models::SideTypeId::get_id( - &project_create_data.client_side, + let client_side_id = models::categories::SideType::get_id( + project_create_data.client_side.as_str(), &mut *transaction, ) .await? @@ -709,8 +711,8 @@ async fn project_create_inner( ) })?; - let server_side_id = models::SideTypeId::get_id( - &project_create_data.server_side, + let server_side_id = models::categories::SideType::get_id( + project_create_data.server_side.as_str(), &mut *transaction, ) .await? @@ -733,7 +735,7 @@ async fn project_create_inner( if let Some(urls) = &project_create_data.donation_urls { for url in urls { - let platform_id = models::DonationPlatformId::get_id( + let platform_id = models::categories::DonationPlatform::get_id( &url.id, &mut *transaction, ) @@ -754,6 +756,13 @@ async fn project_create_inner( } } + let thread_id = ThreadBuilder { + type_: ThreadType::Project, + members: vec![], + } + .insert(&mut *transaction) + .await?; + let project_builder = models::project_item::ProjectBuilder { project_id: project_id.into(), project_type_id, @@ -790,6 +799,7 @@ async fn project_create_inner( }) .collect(), color: icon_data.and_then(|x| x.1), + thread_id, }; let now = Utc::now(); @@ -838,6 +848,7 @@ async fn project_create_inner( flame_anvil_project: None, flame_anvil_user: None, color: project_builder.color, + thread_id: Some(project_builder.thread_id.into()), }; let _project_id = project_builder.insert(&mut *transaction).await?; diff --git a/src/routes/v2/projects.rs b/src/routes/v2/projects.rs index 740a4202..d37c1a73 100644 --- a/src/routes/v2/projects.rs +++ b/src/routes/v2/projects.rs @@ -1,5 +1,6 @@ use crate::database; use crate::database::models::notification_item::NotificationBuilder; +use crate::database::models::thread_item::ThreadMessageBuilder; use crate::file_hosting::FileHost; use crate::models; use crate::models::ids::base62_impl::parse_base62; @@ -7,6 +8,7 @@ use crate::models::projects::{ DonationLink, Project, ProjectId, ProjectStatus, SearchRequest, SideType, }; use crate::models::teams::Permissions; +use crate::models::threads::MessageBody; use crate::routes::ApiError; use crate::search::{search_for_project, SearchConfig, SearchError}; use crate::util::auth::{ @@ -45,10 +47,12 @@ pub fn config(cfg: &mut web::ServiceConfig) { .service(project_unfollow) .service(project_schedule) .service(super::teams::team_members_get_project) - .service(web::scope("{project_id}") - .service(super::versions::version_list) - .service(super::versions::version_project_get) - .service(dependency_list)), + .service( + web::scope("{project_id}") + .service(super::versions::version_list) + .service(super::versions::version_project_get) + .service(dependency_list), + ), ); } @@ -160,7 +164,7 @@ pub async fn project_get_check( ) -> Result { let slug = info.into_inner().0; - let id_option = models::ids::base62_impl::parse_base62(&slug).ok(); + let id_option = parse_base62(&slug).ok(); let id = if let Some(id) = id_option { let id = sqlx::query!( @@ -315,8 +319,7 @@ pub async fn dependency_list( } } -/// A project returned from the API -#[derive(Serialize, Deserialize, Validate)] +#[derive(Deserialize, Validate)] pub struct EditProject { #[validate( length(min = 3, max = 64), @@ -634,6 +637,20 @@ pub async fn project_edit( .await?; } + if let Some(thread) = project_item.inner.thread_id { + ThreadMessageBuilder { + author_id: Some(user.id.into()), + body: MessageBody::StatusChange { + new_status: *status, + old_status: project_item.inner.status, + }, + thread_id: thread, + show_in_mod_inbox: None, + } + .insert(&mut transaction) + .await?; + } + sqlx::query!( " UPDATE mods @@ -916,7 +933,7 @@ pub async fn project_edit( // We are able to unwrap here because the slug is always set if !slug.eq(&project_item.inner.slug.unwrap_or_default()) { let results = sqlx::query!( - " + " SELECT EXISTS(SELECT 1 FROM mods WHERE slug = LOWER($1)) ", slug @@ -953,12 +970,13 @@ pub async fn project_edit( )); } - let side_type_id = database::models::SideTypeId::get_id( - new_side, - &mut *transaction, - ) - .await? - .expect("No database entry found for side type"); + let side_type_id = + database::models::categories::SideType::get_id( + new_side.as_str(), + &mut *transaction, + ) + .await? + .expect("No database entry found for side type"); sqlx::query!( " @@ -981,12 +999,13 @@ pub async fn project_edit( )); } - let side_type_id = database::models::SideTypeId::get_id( - new_side, - &mut *transaction, - ) - .await? - .expect("No database entry found for side type"); + let side_type_id = + database::models::categories::SideType::get_id( + new_side.as_str(), + &mut *transaction, + ) + .await? + .expect("No database entry found for side type"); sqlx::query!( " @@ -1054,7 +1073,7 @@ pub async fn project_edit( for donation in donations { let platform_id = - database::models::DonationPlatformId::get_id( + database::models::categories::DonationPlatform::get_id( &donation.id, &mut *transaction, ) diff --git a/src/routes/v2/reports.rs b/src/routes/v2/reports.rs index ec05860a..fb0c1173 100644 --- a/src/routes/v2/reports.rs +++ b/src/routes/v2/reports.rs @@ -1,21 +1,28 @@ +use crate::database::models::thread_item::{ + ThreadBuilder, ThreadMessageBuilder, +}; use crate::models::ids::{ base62_impl::parse_base62, ProjectId, UserId, VersionId, }; use crate::models::reports::{ItemType, Report}; +use crate::models::threads::{MessageBody, ThreadType}; use crate::routes::ApiError; use crate::util::auth::{ check_is_moderator_from_headers, get_user_from_headers, }; -use actix_web::{delete, get, post, web, HttpRequest, HttpResponse}; +use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; use chrono::Utc; use futures::StreamExt; use serde::Deserialize; use sqlx::PgPool; +use validator::Validate; pub fn config(cfg: &mut web::ServiceConfig) { cfg.service(reports); cfg.service(report_create); - cfg.service(delete_report); + cfg.service(report_edit); + cfg.service(report_delete); + cfg.service(report_get); } #[derive(Deserialize)] @@ -60,6 +67,14 @@ pub async fn report_create( new_report.report_type )) })?; + + let thread_id = ThreadBuilder { + type_: ThreadType::Report, + members: vec![], + } + .insert(&mut transaction) + .await?; + let mut report = crate::database::models::report_item::Report { id, report_type_id: report_type, @@ -69,6 +84,8 @@ pub async fn report_create( body: new_report.body.clone(), reporter: current_user.id.into(), created: Utc::now(), + closed: false, + thread_id, }; match new_report.item_type { @@ -150,44 +167,72 @@ pub async fn report_create( reporter: current_user.id, body: new_report.body.clone(), created: Utc::now(), + closed: false, + thread_id: Some(report.thread_id.into()), })) } #[derive(Deserialize)] -pub struct ResultCount { +pub struct ReportsRequestOptions { #[serde(default = "default_count")] count: i16, + #[serde(default = "default_all")] + all: bool, } fn default_count() -> i16 { 100 } +fn default_all() -> bool { + true +} #[get("report")] pub async fn reports( req: HttpRequest, pool: web::Data, - count: web::Query, + count: web::Query, ) -> Result { - check_is_moderator_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool).await?; use futures::stream::TryStreamExt; - let report_ids = sqlx::query!( - " - SELECT id FROM reports - ORDER BY created ASC - LIMIT $1; - ", - count.count as i64 - ) - .fetch_many(&**pool) - .try_filter_map(|e| async { - Ok(e.right() - .map(|m| crate::database::models::ids::ReportId(m.id))) - }) - .try_collect::>() - .await?; + let report_ids = if user.role.is_mod() && count.all { + sqlx::query!( + " + SELECT id FROM reports + WHERE closed = FALSE + ORDER BY created ASC + LIMIT $1; + ", + count.count as i64 + ) + .fetch_many(&**pool) + .try_filter_map(|e| async { + Ok(e.right() + .map(|m| crate::database::models::ids::ReportId(m.id))) + }) + .try_collect::>() + .await? + } else { + sqlx::query!( + " + SELECT id FROM reports + WHERE closed = FALSE AND reporter = $1 + ORDER BY created ASC + LIMIT $2; + ", + user.id.0 as i64, + count.count as i64 + ) + .fetch_many(&**pool) + .try_filter_map(|e| async { + Ok(e.right() + .map(|m| crate::database::models::ids::ReportId(m.id))) + }) + .try_collect::>() + .await? + }; let query_reports = crate::database::models::report_item::Report::get_many( &report_ids, @@ -198,47 +243,130 @@ pub async fn reports( let mut reports = Vec::new(); for x in query_reports { - let mut item_id = "".to_string(); - let mut item_type = ItemType::Unknown; - - if let Some(project_id) = x.project_id { - item_id = serde_json::to_string::(&project_id.into())?; - item_type = ItemType::Project; - } else if let Some(version_id) = x.version_id { - item_id = serde_json::to_string::(&version_id.into())?; - item_type = ItemType::Version; - } else if let Some(user_id) = x.user_id { - item_id = serde_json::to_string::(&user_id.into())?; - item_type = ItemType::User; - } - - reports.push(Report { - id: x.id.into(), - report_type: x.report_type, - item_id, - item_type, - reporter: x.reporter.into(), - body: x.body, - created: x.created, - }) + reports.push(to_report(x)?); } Ok(HttpResponse::Ok().json(reports)) } +#[get("report/{id}")] +pub async fn report_get( + req: HttpRequest, + pool: web::Data, + info: web::Path<(crate::models::reports::ReportId,)>, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + let id = info.into_inner().0.into(); + + let report = + crate::database::models::report_item::Report::get(id, &**pool).await?; + + if let Some(report) = report { + if !user.role.is_mod() && report.user_id != Some(user.id.into()) { + return Ok(HttpResponse::NotFound().body("")); + } + + Ok(HttpResponse::Ok().json(to_report(report)?)) + } else { + Ok(HttpResponse::NotFound().body("")) + } +} + +#[derive(Deserialize, Validate)] +pub struct EditReport { + #[validate(length(max = 65536))] + pub body: Option, + pub closed: Option, +} + +#[patch("report/{id}")] +pub async fn report_edit( + req: HttpRequest, + pool: web::Data, + info: web::Path<(crate::models::reports::ReportId,)>, + edit_report: web::Json, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + let id = info.into_inner().0.into(); + + let report = + crate::database::models::report_item::Report::get(id, &**pool).await?; + + if let Some(report) = report { + if !user.role.is_mod() && report.user_id != Some(user.id.into()) { + return Ok(HttpResponse::NotFound().body("")); + } + + let mut transaction = pool.begin().await?; + + if let Some(edit_body) = &edit_report.body { + sqlx::query!( + " + UPDATE reports + SET body = $1 + WHERE (id = $2) + ", + edit_body, + id as crate::database::models::ids::ReportId, + ) + .execute(&mut *transaction) + .await?; + } + + if let Some(edit_closed) = edit_report.closed { + if report.closed && !edit_closed && !user.role.is_mod() { + return Err(ApiError::InvalidInput( + "You cannot reopen a report!".to_string(), + )); + } + + if let Some(thread) = report.thread_id { + ThreadMessageBuilder { + author_id: Some(user.id.into()), + body: MessageBody::ThreadClosure, + thread_id: thread, + show_in_mod_inbox: None, + } + .insert(&mut transaction) + .await?; + } + + sqlx::query!( + " + UPDATE reports + SET closed = $1 + WHERE (id = $2) + ", + edit_closed, + id as crate::database::models::ids::ReportId, + ) + .execute(&mut *transaction) + .await?; + } + + transaction.commit().await?; + + Ok(HttpResponse::NoContent().body("")) + } else { + Ok(HttpResponse::NotFound().body("")) + } +} + #[delete("report/{id}")] -pub async fn delete_report( +pub async fn report_delete( req: HttpRequest, pool: web::Data, info: web::Path<(crate::models::reports::ReportId,)>, ) -> Result { check_is_moderator_from_headers(req.headers(), &**pool).await?; + let mut transaction = pool.begin().await?; let result = crate::database::models::report_item::Report::remove_full( info.into_inner().0.into(), - &**pool, + &mut transaction, ) .await?; + transaction.commit().await?; if result.is_some() { Ok(HttpResponse::NoContent().body("")) @@ -246,3 +374,33 @@ pub async fn delete_report( Ok(HttpResponse::NotFound().body("")) } } + +fn to_report( + x: crate::database::models::report_item::QueryReport, +) -> Result { + let mut item_id = "".to_string(); + let mut item_type = ItemType::Unknown; + + if let Some(project_id) = x.project_id { + item_id = serde_json::to_string::(&project_id.into())?; + item_type = ItemType::Project; + } else if let Some(version_id) = x.version_id { + item_id = serde_json::to_string::(&version_id.into())?; + item_type = ItemType::Version; + } else if let Some(user_id) = x.user_id { + item_id = serde_json::to_string::(&user_id.into())?; + item_type = ItemType::User; + } + + Ok(Report { + id: x.id.into(), + report_type: x.report_type, + item_id, + item_type, + reporter: x.reporter.into(), + body: x.body, + created: x.created, + closed: x.closed, + thread_id: x.thread_id.map(|x| x.into()), + }) +} diff --git a/src/routes/v2/tags.rs b/src/routes/v2/tags.rs index 2e878342..bee3aefa 100644 --- a/src/routes/v2/tags.rs +++ b/src/routes/v2/tags.rs @@ -1,10 +1,9 @@ use super::ApiError; use crate::database::models; use crate::database::models::categories::{ - DonationPlatform, ProjectType, ReportType, + DonationPlatform, ProjectType, ReportType, SideType, }; -use crate::util::auth::check_is_admin_from_headers; -use actix_web::{delete, get, put, web, HttpRequest, HttpResponse}; +use actix_web::{get, web, HttpResponse}; use chrono::{DateTime, Utc}; use models::categories::{Category, GameVersion, Loader}; use sqlx::PgPool; @@ -13,22 +12,14 @@ pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("tag") .service(category_list) - .service(category_create) - .service(category_delete) .service(loader_list) - .service(loader_create) - .service(loader_delete) .service(game_version_list) - .service(game_version_create) - .service(game_version_delete) .service(license_list) .service(license_text) - .service(donation_platform_create) .service(donation_platform_list) - .service(donation_platform_delete) - .service(report_type_create) - .service(report_type_delete) - .service(report_type_list), + .service(report_type_list) + .service(project_type_list) + .service(side_type_list), ); } @@ -60,62 +51,6 @@ pub async fn category_list( Ok(HttpResponse::Ok().json(results)) } -#[put("category")] -pub async fn category_create( - req: HttpRequest, - pool: web::Data, - new_category: web::Json, -) -> Result { - check_is_admin_from_headers(req.headers(), &**pool).await?; - - let project_type = crate::database::models::ProjectTypeId::get_id( - new_category.project_type.clone(), - &**pool, - ) - .await? - .ok_or_else(|| { - ApiError::InvalidInput( - "Specified project type does not exist!".to_string(), - ) - })?; - - let _id = Category::builder() - .name(&new_category.name)? - .project_type(&project_type)? - .icon(&new_category.icon)? - .header(&new_category.header)? - .insert(&**pool) - .await?; - - Ok(HttpResponse::NoContent().body("")) -} - -#[delete("category/{name}")] -pub async fn category_delete( - req: HttpRequest, - pool: web::Data, - category: web::Path<(String,)>, -) -> Result { - check_is_admin_from_headers(req.headers(), &**pool).await?; - - let name = category.into_inner().0; - let mut transaction = - pool.begin().await.map_err(models::DatabaseError::from)?; - - let result = Category::remove(&name, &mut transaction).await?; - - transaction - .commit() - .await - .map_err(models::DatabaseError::from)?; - - if result.is_some() { - Ok(HttpResponse::NoContent().body("")) - } else { - Ok(HttpResponse::NotFound().body("")) - } -} - #[derive(serde::Serialize, serde::Deserialize)] pub struct LoaderData { icon: String, @@ -142,62 +77,6 @@ pub async fn loader_list( Ok(HttpResponse::Ok().json(results)) } -#[put("loader")] -pub async fn loader_create( - req: HttpRequest, - pool: web::Data, - new_loader: web::Json, -) -> Result { - check_is_admin_from_headers(req.headers(), &**pool).await?; - - let mut transaction = pool.begin().await?; - - let project_types = ProjectType::get_many_id( - &new_loader.supported_project_types, - &mut *transaction, - ) - .await?; - - let _id = Loader::builder() - .name(&new_loader.name)? - .icon(&new_loader.icon)? - .supported_project_types( - &project_types.into_iter().map(|x| x.id).collect::>(), - )? - .insert(&mut transaction) - .await?; - - transaction.commit().await?; - - Ok(HttpResponse::NoContent().body("")) -} - -#[delete("loader/{name}")] -pub async fn loader_delete( - req: HttpRequest, - pool: web::Data, - loader: web::Path<(String,)>, -) -> Result { - check_is_admin_from_headers(req.headers(), &**pool).await?; - - let name = loader.into_inner().0; - let mut transaction = - pool.begin().await.map_err(models::DatabaseError::from)?; - - let result = Loader::remove(&name, &mut transaction).await?; - - transaction - .commit() - .await - .map_err(models::DatabaseError::from)?; - - if result.is_some() { - Ok(HttpResponse::NoContent().body("")) - } else { - Ok(HttpResponse::NotFound().body("")) - } -} - #[derive(serde::Serialize)] pub struct GameVersionQueryData { pub version: String, @@ -238,66 +117,6 @@ pub async fn game_version_list( Ok(HttpResponse::Ok().json(results)) } -#[derive(serde::Deserialize)] -pub struct GameVersionData { - #[serde(rename = "type")] - type_: String, - date: Option>, -} - -#[put("game_version/{name}")] -pub async fn game_version_create( - req: HttpRequest, - pool: web::Data, - game_version: web::Path<(String,)>, - version_data: web::Json, -) -> Result { - check_is_admin_from_headers(req.headers(), &**pool).await?; - - let name = game_version.into_inner().0; - - // The version type currently isn't limited, but it should be one of: - // "release", "snapshot", "alpha", "beta", "other" - - let mut builder = GameVersion::builder() - .version(&name)? - .version_type(&version_data.type_)?; - - if let Some(date) = &version_data.date { - builder = builder.created(date); - } - - let _id = builder.insert(&**pool).await?; - - Ok(HttpResponse::NoContent().body("")) -} - -#[delete("game_version/{name}")] -pub async fn game_version_delete( - req: HttpRequest, - pool: web::Data, - game_version: web::Path<(String,)>, -) -> Result { - check_is_admin_from_headers(req.headers(), &**pool).await?; - - let name = game_version.into_inner().0; - let mut transaction = - pool.begin().await.map_err(models::DatabaseError::from)?; - - let result = GameVersion::remove(&name, &mut transaction).await?; - - transaction - .commit() - .await - .map_err(models::DatabaseError::from)?; - - if result.is_some() { - Ok(HttpResponse::NoContent().body("")) - } else { - Ok(HttpResponse::NotFound().body("")) - } -} - #[derive(serde::Serialize)] pub struct License { short: String, @@ -372,57 +191,6 @@ pub async fn donation_platform_list( Ok(HttpResponse::Ok().json(results)) } -#[derive(serde::Deserialize)] -pub struct DonationPlatformData { - name: String, -} - -#[put("donation_platform/{name}")] -pub async fn donation_platform_create( - req: HttpRequest, - pool: web::Data, - license: web::Path<(String,)>, - license_data: web::Json, -) -> Result { - check_is_admin_from_headers(req.headers(), &**pool).await?; - - let short = license.into_inner().0; - - let _id = DonationPlatform::builder() - .short(&short)? - .name(&license_data.name)? - .insert(&**pool) - .await?; - - Ok(HttpResponse::NoContent().body("")) -} - -#[delete("donation_platform/{name}")] -pub async fn donation_platform_delete( - req: HttpRequest, - pool: web::Data, - loader: web::Path<(String,)>, -) -> Result { - check_is_admin_from_headers(req.headers(), &**pool).await?; - - let name = loader.into_inner().0; - let mut transaction = - pool.begin().await.map_err(models::DatabaseError::from)?; - - let result = DonationPlatform::remove(&name, &mut transaction).await?; - - transaction - .commit() - .await - .map_err(models::DatabaseError::from)?; - - if result.is_some() { - Ok(HttpResponse::NoContent().body("")) - } else { - Ok(HttpResponse::NotFound().body("")) - } -} - #[get("report_type")] pub async fn report_type_list( pool: web::Data, @@ -431,43 +199,18 @@ pub async fn report_type_list( Ok(HttpResponse::Ok().json(results)) } -#[put("report_type/{name}")] -pub async fn report_type_create( - req: HttpRequest, +#[get("project_type")] +pub async fn project_type_list( pool: web::Data, - loader: web::Path<(String,)>, ) -> Result { - check_is_admin_from_headers(req.headers(), &**pool).await?; - - let name = loader.into_inner().0; - - let _id = ReportType::builder().name(&name)?.insert(&**pool).await?; - - Ok(HttpResponse::NoContent().body("")) + let results = ProjectType::list(&**pool).await?; + Ok(HttpResponse::Ok().json(results)) } -#[delete("report_type/{name}")] -pub async fn report_type_delete( - req: HttpRequest, +#[get("side_type")] +pub async fn side_type_list( pool: web::Data, - report_type: web::Path<(String,)>, ) -> Result { - check_is_admin_from_headers(req.headers(), &**pool).await?; - - let name = report_type.into_inner().0; - let mut transaction = - pool.begin().await.map_err(models::DatabaseError::from)?; - - let result = ReportType::remove(&name, &mut transaction).await?; - - transaction - .commit() - .await - .map_err(models::DatabaseError::from)?; - - if result.is_some() { - Ok(HttpResponse::NoContent().body("")) - } else { - Ok(HttpResponse::NotFound().body("")) - } + let results = SideType::list(&**pool).await?; + Ok(HttpResponse::Ok().json(results)) } diff --git a/src/routes/v2/threads.rs b/src/routes/v2/threads.rs new file mode 100644 index 00000000..83fc7d63 --- /dev/null +++ b/src/routes/v2/threads.rs @@ -0,0 +1,278 @@ +use crate::database; +use crate::database::models::thread_item::ThreadMessageBuilder; +use crate::models::ids::ThreadMessageId; +use crate::models::projects::ProjectStatus; +use crate::models::threads::{ + MessageBody, Thread, ThreadId, ThreadMessage, ThreadType, +}; +use crate::models::users::User; +use crate::routes::ApiError; +use crate::util::auth::{ + check_is_moderator_from_headers, get_user_from_headers, +}; +use actix_web::{delete, get, post, web, HttpRequest, HttpResponse}; +use serde::Deserialize; +use sqlx::PgPool; + +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("thread") + .service(moderation_inbox) + .service(thread_get) + .service(thread_send_message), + ); + cfg.service(web::scope("message").service(message_delete)); +} + +pub async fn is_authorized_thread( + thread: &database::models::Thread, + user: &User, + pool: &PgPool, +) -> Result { + if user.role.is_mod() { + return Ok(true); + } + + let user_id: database::models::UserId = user.id.into(); + Ok(match thread.type_ { + ThreadType::Report => { + let report_exists = sqlx::query!( + "SELECT EXISTS(SELECT 1 FROM reports WHERE thread_id = $1 AND reporter = $2)", + thread.id as database::models::ids::ThreadId, + user_id as database::models::ids::UserId, + ) + .fetch_one(pool) + .await? + .exists; + + report_exists.unwrap_or(false) + } + ThreadType::Project => { + let project_exists = sqlx::query!( + "SELECT EXISTS(SELECT 1 FROM mods m INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.user_id = $2 WHERE thread_id = $1)", + thread.id as database::models::ids::ThreadId, + user_id as database::models::ids::UserId, + ) + .fetch_one(pool) + .await? + .exists; + + project_exists.unwrap_or(false) + } + ThreadType::DirectMessage => thread.members.contains(&user_id), + }) +} + +#[get("{id}")] +pub async fn thread_get( + req: HttpRequest, + info: web::Path<(ThreadId,)>, + pool: web::Data, +) -> Result { + let string = info.into_inner().0.into(); + + let thread_data = database::models::Thread::get(string, &**pool).await?; + + let user = get_user_from_headers(req.headers(), &**pool).await?; + + if let Some(data) = thread_data { + if is_authorized_thread(&data, &user, &pool).await? { + let users: Vec = database::models::User::get_many( + &data + .messages + .iter() + .filter_map(|x| x.author_id) + .collect::>(), + &**pool, + ) + .await? + .into_iter() + .map(From::from) + .collect(); + + let thread_type = data.type_; + + return Ok(HttpResponse::Ok().json(Thread { + id: data.id.into(), + type_: thread_type, + messages: data + .messages + .into_iter() + .map(|x| ThreadMessage { + id: x.id.into(), + author_id: if thread_type == ThreadType::Report + && users + .iter() + .find(|y| x.author_id == Some(y.id.into())) + .map(|x| x.role.is_mod()) + .unwrap_or(false) + { + None + } else { + x.author_id.map(|x| x.into()) + }, + body: x.body, + created: x.created, + }) + .collect(), + members: users, + })); + } + } + Ok(HttpResponse::NotFound().body("")) +} + +#[derive(Deserialize)] +pub struct NewThreadMessage { + pub body: MessageBody, +} + +#[post("{id}")] +pub async fn thread_send_message( + req: HttpRequest, + info: web::Path<(ThreadId,)>, + pool: web::Data, + new_message: web::Json, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + + if let MessageBody::Text { body } = &new_message.body { + if body.len() > 65536 { + return Err(ApiError::InvalidInput( + "Input body is too long!".to_string(), + )); + } + } + + let string: database::models::ThreadId = info.into_inner().0.into(); + let result = database::models::Thread::get(string, &**pool).await?; + + if let Some(thread) = result { + if !is_authorized_thread(&thread, &user, &pool).await? { + return Ok(HttpResponse::NotFound().body("")); + } + + let mod_notif = if thread.type_ == ThreadType::Project { + let status = sqlx::query!( + "SELECT m.status FROM mods m WHERE thread_id = $1", + thread.id as database::models::ids::ThreadId, + ) + .fetch_one(&**pool) + .await?; + + let status = ProjectStatus::from_str(&status.status); + + status == ProjectStatus::Processing && !user.role.is_mod() + } else { + false + }; + + let mut transaction = pool.begin().await?; + ThreadMessageBuilder { + author_id: Some(user.id.into()), + body: new_message.body.clone(), + thread_id: thread.id, + show_in_mod_inbox: Some(mod_notif), + } + .insert(&mut transaction) + .await?; + transaction.commit().await?; + + Ok(HttpResponse::NoContent().body("")) + } else { + Ok(HttpResponse::NotFound().body("")) + } +} + +#[get("inbox")] +pub async fn moderation_inbox( + req: HttpRequest, + pool: web::Data, +) -> Result { + check_is_moderator_from_headers(req.headers(), &**pool).await?; + + let messages = sqlx::query!( + " + SELECT tm.id, tm.thread_id, tm.author_id, tm.body, tm.created, m.id project_id FROM threads_messages tm + INNER JOIN mods m ON m.thread_id = tm.thread_id + WHERE tm.show_in_mod_inbox = TRUE + " + ) + .fetch_all(&**pool) + .await? + .into_iter() + .map(|x| serde_json::json! ({ + "message": ThreadMessage { + id: ThreadMessageId(x.id as u64), + author_id: x.author_id.map(|x| crate::models::users::UserId(x as u64)), + body: serde_json::from_value(x.body).unwrap_or(MessageBody::Deleted), + created: x.created + }, + "project_id": crate::models::projects::ProjectId(x.project_id as u64), + })) + .collect::>(); + + Ok(HttpResponse::Ok().json(messages)) +} + +#[post("{id}/read")] +pub async fn read_message( + req: HttpRequest, + info: web::Path<(ThreadMessageId,)>, + pool: web::Data, +) -> Result { + check_is_moderator_from_headers(req.headers(), &**pool).await?; + + let id = info.into_inner().0; + let mut transaction = pool.begin().await?; + + sqlx::query!( + " + UPDATE threads_messages + SET show_in_mod_inbox = FALSE + WHERE id = $1 + ", + id.0 as i64, + ) + .execute(&mut *transaction) + .await?; + + transaction.commit().await?; + + Ok(HttpResponse::NoContent().body("")) +} + +#[delete("{id}")] +pub async fn message_delete( + req: HttpRequest, + info: web::Path<(ThreadMessageId,)>, + pool: web::Data, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + + let result = database::models::ThreadMessage::get( + info.into_inner().0.into(), + &**pool, + ) + .await?; + + if let Some(thread) = result { + if !user.role.is_mod() && thread.author_id != Some(user.id.into()) { + return Err(ApiError::CustomAuthentication( + "You cannot delete this message!".to_string(), + )); + } + + let mut transaction = pool.begin().await?; + database::models::ThreadMessage::remove_full( + thread.id, + &mut transaction, + ) + .await?; + transaction.commit().await?; + + Ok(HttpResponse::NoContent().body("")) + } else { + Ok(HttpResponse::NotFound().body("")) + } +} diff --git a/src/routes/v2/users.rs b/src/routes/v2/users.rs index c91a30ff..0e7bdf43 100644 --- a/src/routes/v2/users.rs +++ b/src/routes/v2/users.rs @@ -203,11 +203,8 @@ pub async fn user_edit( ApiError::Validation(validation_errors_to_string(err, None)) })?; - let id_option = crate::database::models::User::get_id_from_username_or_id( - &info.into_inner().0, - &**pool, - ) - .await?; + let id_option = + User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?; if let Some(id) = id_option { let user_id: UserId = id.into(); @@ -217,10 +214,7 @@ pub async fn user_edit( if let Some(username) = &new_user.username { let existing_user_id_option = - crate::database::models::User::get_id_from_username_or_id( - username, &**pool, - ) - .await?; + User::get_id_from_username_or_id(username, &**pool).await?; if existing_user_id_option .map(UserId::from) @@ -754,6 +748,8 @@ pub async fn user_payouts_request( data: web::Json, payouts_queue: web::Data>>, ) -> Result { + let mut payouts_queue = payouts_queue.lock().await; + let user = get_user_from_headers(req.headers(), &**pool).await?; let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?; @@ -775,8 +771,6 @@ pub async fn user_payouts_request( return if data.amount < payouts_data.balance { let mut transaction = pool.begin().await?; - let mut payouts_queue = payouts_queue.lock().await; - let leftover = payouts_queue .send_payout(PayoutItem { amount: PayoutAmount { diff --git a/src/routes/v2/version_file.rs b/src/routes/v2/version_file.rs index 254dfb72..cf8a2c1f 100644 --- a/src/routes/v2/version_file.rs +++ b/src/routes/v2/version_file.rs @@ -1,7 +1,7 @@ use super::ApiError; use crate::database::models::{version_item::QueryVersion, DatabaseError}; use crate::models::ids::VersionId; -use crate::models::projects::{GameVersion, Loader, Version}; +use crate::models::projects::{GameVersion, Loader, Project, Version}; use crate::models::teams::Permissions; use crate::util::auth::get_user_from_headers; use crate::util::routes::ok_or_not_found; @@ -404,6 +404,65 @@ pub async fn get_versions_from_hashes( Ok(HttpResponse::Ok().json(response?)) } +#[post("project")] +pub async fn get_projects_from_hashes( + pool: web::Data, + file_data: web::Json, +) -> Result { + let hashes_parsed: Vec> = file_data + .hashes + .iter() + .map(|x| x.to_lowercase().as_bytes().to_vec()) + .collect(); + + let result = sqlx::query!( + " + SELECT h.hash hash, h.algorithm algorithm, m.id project_id FROM hashes h + INNER JOIN files f ON h.file_id = f.id + INNER JOIN versions v ON v.id = f.version_id AND v.status != ANY($1) + INNER JOIN mods m on v.mod_id = m.id + WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ANY($4) + ", + &*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::>(), + hashes_parsed.as_slice(), + file_data.algorithm, + &*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::>(), + ) + .fetch_all(&**pool) + .await?; + + let project_ids = result + .iter() + .map(|x| database::models::ProjectId(x.project_id)) + .collect::>(); + let versions_data = + database::models::Project::get_many_full(&project_ids, &**pool).await?; + + let response: Result, ApiError> = result + .into_iter() + .filter_map(|row| { + versions_data + .clone() + .into_iter() + .find(|x| x.inner.id.0 == row.project_id) + .map(|v| { + if let Ok(parsed_hash) = String::from_utf8(row.hash) { + Ok(( + parsed_hash, + crate::models::projects::Project::from(v), + )) + } else { + Err(ApiError::Database(DatabaseError::Other(format!( + "Could not parse hash for version {}", + row.project_id + )))) + } + }) + }) + .collect(); + Ok(HttpResponse::Ok().json(response?)) +} + #[post("download")] pub async fn download_files( pool: web::Data, diff --git a/src/util/auth.rs b/src/util/auth.rs index f4b0875b..9932bd8f 100644 --- a/src/util/auth.rs +++ b/src/util/auth.rs @@ -117,21 +117,6 @@ where } } -pub async fn check_is_admin_from_headers<'a, 'b, E>( - headers: &HeaderMap, - executor: E, -) -> Result -where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, -{ - let user = get_user_from_headers(headers, executor).await?; - - match user.role { - Role::Admin => Ok(user), - _ => Err(AuthenticationError::InvalidCredentials), - } -} - pub async fn is_authorized( project_data: &Project, user_option: &Option,