diff --git a/.env b/.env index e04a54a2b..9848a1bf1 100644 --- a/.env +++ b/.env @@ -1,9 +1,15 @@ DEBUG=true RUST_LOG=info,sqlx::query=warn +SITE_URL=https://modrinth.com CDN_URL=https://cdn.modrinth.com +MODERATION_DISCORD_WEBHOOK= +CLOUDFLARE_INTEGRATION=false + DATABASE_URL=postgresql://labrinth:labrinth@localhost/labrinth +DATABASE_MIN_CONNECTIONS=0 +DATABASE_MAX_CONNECTIONS=16 MEILISEARCH_ADDR=http://localhost:7700 MEILISEARCH_KEY=modrinth diff --git a/migrations/20210611024943_archived-status-notifications-icon-rejection-reasons.sql b/migrations/20210611024943_archived-status-notifications-icon-rejection-reasons.sql new file mode 100644 index 000000000..f64f13278 --- /dev/null +++ b/migrations/20210611024943_archived-status-notifications-icon-rejection-reasons.sql @@ -0,0 +1,18 @@ +INSERT INTO statuses (status) VALUES ('archived'); + +ALTER TABLE notifications + ADD COLUMN type varchar(256); + +ALTER TABLE mods + ADD COLUMN rejection_reason varchar(2000), + ADD COLUMN rejection_body varchar(65536); + +DROP TABLE dependencies; + +CREATE TABLE dependencies ( + id serial PRIMARY KEY, + dependent_id bigint REFERENCES versions ON UPDATE CASCADE NOT NULL, + dependency_type varchar(255) NOT NULL, + dependency_id bigint REFERENCES versions ON UPDATE CASCADE, + mod_dependency_id bigint REFERENCES mods ON UPDATE CASCADE +); \ No newline at end of file diff --git a/sqlx-data.json b/sqlx-data.json index 003f8eb4e..533b2f4d4 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -315,66 +315,21 @@ ] } }, - "0fd612828feb009aab2f78fbbb271dcdd9ba2770b7d6f323415b958100cd3fb8": { - "query": "\n SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read,\n STRING_AGG(DISTINCT na.id || ', ' || na.title || ', ' || na.action_route || ', ' || na.action_route_method, ' ,') actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.id IN (SELECT * FROM UNNEST($1::bigint[]))\n GROUP BY n.id, n.user_id\n ORDER BY n.created DESC;\n ", + "114df19aa81498b77022bd7347dd4449c7cc48efdab19003bde62c2f2f837d3c": { + "query": "\n INSERT INTO notifications (\n id, user_id, title, text, link, type\n )\n VALUES (\n $1, $2, $3, $4, $5, $6\n )\n ", "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "user_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "text", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "link", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "created", - "type_info": "Timestamptz" - }, - { - "ordinal": 6, - "name": "read", - "type_info": "Bool" - }, - { - "ordinal": 7, - "name": "actions", - "type_info": "Text" - } - ], + "columns": [], "parameters": { "Left": [ - "Int8Array" + "Int8", + "Int8", + "Varchar", + "Varchar", + "Varchar", + "Varchar" ] }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - null - ] + "nullable": [] } }, "153100dc632392c4d446cc768235d071bac26a0818a4a72d203d8e549f969eea": { @@ -468,6 +423,26 @@ "nullable": [] } }, + "164e5168aabe47d64f99ea851392c9d8479022cff360a610f185c342a24e88d8": { + "query": "\n SELECT mod_id FROM versions WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "mod_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + } + }, "16b3ac53ef5e94f51ab39484add21e2f76d49015917dc877560607a31f5537e9": { "query": "\n UPDATE users\n SET email = $1\n WHERE (id = $2)\n ", "describe": { @@ -481,68 +456,6 @@ "nullable": [] } }, - "16c4d33d4cffa54333e0c56a1cf35ceab2596773ac5c77d42daaacc04a75f404": { - "query": "\n SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read,\n STRING_AGG(DISTINCT na.id || ', ' || na.title || ', ' || na.action_route || ', ' || na.action_route_method, ' ,') 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 ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "user_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "text", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "link", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "created", - "type_info": "Timestamptz" - }, - { - "ordinal": 6, - "name": "read", - "type_info": "Bool" - }, - { - "ordinal": 7, - "name": "actions", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - null - ] - } - }, "17e6d30c3693e9bd9f772f3dc4e2eafe75fdeecfdcf2746eac641f77ced6b8a8": { "query": "\n SELECT u.id, u.github_id, u.name, u.email,\n u.avatar_url, u.username, u.bio,\n u.created, u.role FROM users u\n WHERE u.id IN (SELECT * FROM UNNEST($1::bigint[]))\n ", "describe": { @@ -637,8 +550,100 @@ "nullable": [] } }, - "1b6c38ac65be4d4f10decabf0e80fd45c8e4d15e4f916ac1bdc9348b188fc469": { - "query": "\n SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,\n v.changelog changelog, v.changelog_url changelog_url, v.date_published date_published, v.downloads downloads,\n rc.channel release_channel, v.featured featured,\n STRING_AGG(DISTINCT gv.version, ',') game_versions, STRING_AGG(DISTINCT l.loader, ',') loaders,\n STRING_AGG(DISTINCT f.id || ', ' || f.filename || ', ' || f.is_primary || ', ' || f.url, ' ,') files,\n STRING_AGG(DISTINCT h.algorithm || ', ' || encode(h.hash, 'escape') || ', ' || h.file_id, ' ,') hashes,\n STRING_AGG(DISTINCT d.dependency_id || ', ' || d.dependency_type, ' ,') dependencies\n FROM versions v\n INNER JOIN release_channels rc on v.release_channel = rc.id\n LEFT OUTER JOIN game_versions_versions gvv on v.id = gvv.joining_version_id\n LEFT OUTER JOIN game_versions gv on gvv.game_version_id = gv.id\n LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id\n LEFT OUTER JOIN loaders l on lv.loader_id = l.id\n LEFT OUTER JOIN files f on v.id = f.version_id\n LEFT OUTER JOIN hashes h on f.id = h.file_id\n LEFT OUTER JOIN dependencies d on v.id = d.dependent_id\n WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))\n GROUP BY v.id, rc.id\n ORDER BY v.date_published ASC;\n ", + "1c7b0eb4341af5a7942e52f632cf582561f10b4b6a41a082fb8a60f04ac17c6e": { + "query": "SELECT EXISTS(SELECT 1 FROM states WHERE id=$1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + null + ] + } + }, + "1ce90594000fa30876bf277d9ebe2901acf9afaf256dd4488166d55fdd950347": { + "query": "\n DELETE FROM donation_platforms\n WHERE short = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + } + }, + "1d3b582e6765e1ae578039e44b5dc9be6f3f845c96ffd43b7ba83f9eab816f93": { + "query": "\n SELECT name FROM report_types\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false + ] + } + }, + "1d6f3e926fc4a27c5af172f672b7f825f9f5fe2d538b06337ef182ab1a553398": { + "query": "\n SELECT name FROM project_types pt\n INNER JOIN mods ON mods.project_type = pt.id\n WHERE mods.id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + } + }, + "1db6be78a74ff04c52ee105e0df30acf5bbf18f1de328980bb7f3da7f5f6569e": { + "query": "\n SELECT id FROM side_types\n WHERE name = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "1ec4696a332b1ec4b905095bef60190bdbe0f170a1a50c8a3a16d963de33cb2c": { + "query": "\n SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,\n v.changelog changelog, v.changelog_url changelog_url, v.date_published date_published, v.downloads downloads,\n rc.channel release_channel, v.featured featured,\n STRING_AGG(DISTINCT gv.version, ',') game_versions, STRING_AGG(DISTINCT l.loader, ',') loaders,\n STRING_AGG(DISTINCT f.id || ', ' || f.filename || ', ' || f.is_primary || ', ' || f.url, ' ,') files,\n STRING_AGG(DISTINCT h.algorithm || ', ' || encode(h.hash, 'escape') || ', ' || h.file_id, ' ,') hashes,\n STRING_AGG(DISTINCT COALESCE(d.dependency_id, 0) || ', ' || COALESCE(d.mod_dependency_id, 0) || ', ' || d.dependency_type, ' ,') dependencies\n FROM versions v\n INNER JOIN release_channels rc on v.release_channel = rc.id\n LEFT OUTER JOIN game_versions_versions gvv on v.id = gvv.joining_version_id\n LEFT OUTER JOIN game_versions gv on gvv.game_version_id = gv.id\n LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id\n LEFT OUTER JOIN loaders l on lv.loader_id = l.id\n LEFT OUTER JOIN files f on v.id = f.version_id\n LEFT OUTER JOIN hashes h on f.id = h.file_id\n LEFT OUTER JOIN dependencies d on v.id = d.dependent_id\n WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))\n GROUP BY v.id, rc.id\n ORDER BY v.date_published ASC;\n ", "describe": { "columns": [ { @@ -747,98 +752,6 @@ ] } }, - "1c7b0eb4341af5a7942e52f632cf582561f10b4b6a41a082fb8a60f04ac17c6e": { - "query": "SELECT EXISTS(SELECT 1 FROM states WHERE id=$1)", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "exists", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - null - ] - } - }, - "1ce90594000fa30876bf277d9ebe2901acf9afaf256dd4488166d55fdd950347": { - "query": "\n DELETE FROM donation_platforms\n WHERE short = $1\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [] - } - }, - "1d3b582e6765e1ae578039e44b5dc9be6f3f845c96ffd43b7ba83f9eab816f93": { - "query": "\n SELECT name FROM report_types\n WHERE id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "name", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [ - false - ] - } - }, - "1d6f3e926fc4a27c5af172f672b7f825f9f5fe2d538b06337ef182ab1a553398": { - "query": "\n SELECT name FROM project_types pt\n INNER JOIN mods ON mods.project_type = pt.id\n WHERE mods.id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "name", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false - ] - } - }, - "1db6be78a74ff04c52ee105e0df30acf5bbf18f1de328980bb7f3da7f5f6569e": { - "query": "\n SELECT id FROM side_types\n WHERE name = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false - ] - } - }, "1f24988f92819272c10a45fecd7eb96cc901c2f7f4ec191bc1c1cf4982bf1b38": { "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 IN (SELECT * FROM UNNEST($1::bigint[]))\n ", "describe": { @@ -921,6 +834,107 @@ ] } }, + "20dae681a20388311026819ffc389f0be77506fcba5ccb25cad8d363666dc080": { + "query": "\n DELETE FROM notifications_actions\n WHERE notification_id IN (SELECT * FROM UNNEST($1::bigint[]))\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [] + } + }, + "22185b4e3826d5ff4907b66b53ad3d0b64fb0904967c7e4d8d6aa5105b1486f5": { + "query": "\n SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type,\n STRING_AGG(DISTINCT na.id || ', ' || na.title || ', ' || na.action_route || ', ' || na.action_route_method, ' ,') 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 ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "text", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "link", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "created", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "read", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "notification_type", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "actions", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true, + null + ] + } + }, + "22f3f089050594199c3a3265da8ca68264a7457ae6ec4aef3644035a2022a830": { + "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 ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + false + ] + } + }, "24e5daad907eec54505274f93952d5c20f4bbdd3f771eb0a2fdfa6324768df39": { "query": "\n SELECT short, name FROM licenses\n WHERE id = $1\n ", "describe": { @@ -1034,200 +1048,6 @@ "nullable": [] } }, - "365d2cdfdbac0b906580fc28cff69c9c27209c7e120da3d301737ef97ca4d535": { - "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.body_url body_url, m.published published,\n m.updated updated, m.status 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,\n s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name,\n STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN statuses s ON s.id = m.status\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 INNER JOIN licenses l ON m.license = l.id\n WHERE m.id = $1\n GROUP BY m.id, s.id, cs.id, ss.id, l.id, pt.id;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "project_type", - "type_info": "Int4" - }, - { - "ordinal": 2, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "description", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 5, - "name": "follows", - "type_info": "Int4" - }, - { - "ordinal": 6, - "name": "icon_url", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "body", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "body_url", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "published", - "type_info": "Timestamptz" - }, - { - "ordinal": 10, - "name": "updated", - "type_info": "Timestamptz" - }, - { - "ordinal": 11, - "name": "status", - "type_info": "Int4" - }, - { - "ordinal": 12, - "name": "issues_url", - "type_info": "Varchar" - }, - { - "ordinal": 13, - "name": "source_url", - "type_info": "Varchar" - }, - { - "ordinal": 14, - "name": "wiki_url", - "type_info": "Varchar" - }, - { - "ordinal": 15, - "name": "discord_url", - "type_info": "Varchar" - }, - { - "ordinal": 16, - "name": "license_url", - "type_info": "Varchar" - }, - { - "ordinal": 17, - "name": "team_id", - "type_info": "Int8" - }, - { - "ordinal": 18, - "name": "client_side", - "type_info": "Int4" - }, - { - "ordinal": 19, - "name": "server_side", - "type_info": "Int4" - }, - { - "ordinal": 20, - "name": "license", - "type_info": "Int4" - }, - { - "ordinal": 21, - "name": "slug", - "type_info": "Varchar" - }, - { - "ordinal": 22, - "name": "status_name", - "type_info": "Varchar" - }, - { - "ordinal": 23, - "name": "client_side_type", - "type_info": "Varchar" - }, - { - "ordinal": 24, - "name": "server_side_type", - "type_info": "Varchar" - }, - { - "ordinal": 25, - "name": "short", - "type_info": "Varchar" - }, - { - "ordinal": 26, - "name": "license_name", - "type_info": "Varchar" - }, - { - "ordinal": 27, - "name": "project_type_name", - "type_info": "Varchar" - }, - { - "ordinal": 28, - "name": "categories", - "type_info": "Text" - }, - { - "ordinal": 29, - "name": "versions", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - false, - true, - false, - false, - false, - true, - true, - true, - true, - true, - false, - false, - false, - false, - true, - false, - false, - false, - false, - false, - false, - null, - null - ] - } - }, "371048e45dd74c855b84cdb8a6a565ccbef5ad166ec9511ab20621c336446da6": { "query": "\n UPDATE mods\n SET follows = follows - 1\n WHERE id = $1\n ", "describe": { @@ -1240,20 +1060,6 @@ "nullable": [] } }, - "381974f80a890a59f89c46b0c709e4511c0216eb8059ee47bb1e1456caf68fd7": { - "query": "\n INSERT INTO dependencies (dependent_id, dependency_id, dependency_type)\n VALUES ($1, $2, $3)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Varchar" - ] - }, - "nullable": [] - } - }, "3831c1b321e47690f1f54597506a0d43362eda9540c56acb19c06532bba50b01": { "query": "\n SELECT id, user_id, role, permissions, accepted\n FROM team_members\n WHERE team_id = $1\n ", "describe": { @@ -1570,146 +1376,6 @@ ] } }, - "4b74531cfe100a37f1ebfe0cee42926055f2c55fe8c8d5e6fff642c722dbb92f": { - "query": "\n SELECT project_type, title, description, downloads, follows,\n icon_url, body, body_url, published,\n updated, status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug\n FROM mods\n WHERE id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "project_type", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "description", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 4, - "name": "follows", - "type_info": "Int4" - }, - { - "ordinal": 5, - "name": "icon_url", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "body", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "body_url", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "published", - "type_info": "Timestamptz" - }, - { - "ordinal": 9, - "name": "updated", - "type_info": "Timestamptz" - }, - { - "ordinal": 10, - "name": "status", - "type_info": "Int4" - }, - { - "ordinal": 11, - "name": "issues_url", - "type_info": "Varchar" - }, - { - "ordinal": 12, - "name": "source_url", - "type_info": "Varchar" - }, - { - "ordinal": 13, - "name": "wiki_url", - "type_info": "Varchar" - }, - { - "ordinal": 14, - "name": "discord_url", - "type_info": "Varchar" - }, - { - "ordinal": 15, - "name": "license_url", - "type_info": "Varchar" - }, - { - "ordinal": 16, - "name": "team_id", - "type_info": "Int8" - }, - { - "ordinal": 17, - "name": "client_side", - "type_info": "Int4" - }, - { - "ordinal": 18, - "name": "server_side", - "type_info": "Int4" - }, - { - "ordinal": 19, - "name": "license", - "type_info": "Int4" - }, - { - "ordinal": 20, - "name": "slug", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - true, - false, - true, - false, - false, - false, - true, - true, - true, - true, - true, - false, - false, - false, - false, - true - ] - } - }, "4ccf5373d9593fd19622dba270ae2b194f8029f2fb05ad00ff6b3f2ac4d589b0": { "query": "\n SELECT m.id FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.accepted = TRUE\n WHERE tm.user_id = $1 AND m.status = (SELECT s.id FROM statuses s WHERE s.status = $2)\n ", "describe": { @@ -1782,22 +1448,6 @@ "nullable": [] } }, - "4ff46a178f181a11c6c57c628770ef3b80e7344cff5d8bf268876a863cbfda36": { - "query": "\n INSERT INTO notifications (\n id, user_id, title, text, link\n )\n VALUES (\n $1, $2, $3, $4, $5\n )\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Varchar", - "Varchar", - "Varchar" - ] - }, - "nullable": [] - } - }, "507314fdcacaa3c7751738c9d0baee2b90aec719b6b203f922824eced5ea8369": { "query": "\n DELETE FROM game_versions_versions WHERE joining_version_id = $1\n ", "describe": { @@ -1823,6 +1473,18 @@ "nullable": [] } }, + "547230de9a5ea9b6ee326e6a35cd1016d67afdda3d7ee382c2f2b7832be875e9": { + "query": "\n UPDATE mods\n SET rejection_body = NULL\n WHERE (id = $1)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, "5564434408e4b88ff1bdd14e0d32a35136e5ee0c837655fbde7d3ca9182dc25b": { "query": "\n SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.permissions, tm.accepted FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = TRUE\n WHERE m.id = $1\n ", "describe": { @@ -2048,6 +1710,38 @@ ] } }, + "5a03c653f1ff3339a01422ee4267a66157e6da9a51cc7d9beb0f87d59c3a444c": { + "query": "\n SELECT d.dependent_id, d.dependency_id, d.mod_dependency_id\n FROM versions v\n INNER JOIN dependencies d ON d.dependent_id = v.id\n WHERE v.mod_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "dependent_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "dependency_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "mod_dependency_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + true, + true + ] + } + }, "5a13a79ebb1ab975f88b58e6deaba9685fe16e242c0fa4a5eea54f12f9448e6b": { "query": "\n DELETE FROM reports\n WHERE version_id = $1\n ", "describe": { @@ -2238,6 +1932,164 @@ ] } }, + "612847a6ba8c88c3f63e5e15f21df7360a0e038baee5b84e6dabaa4a9a1461f7": { + "query": "\n SELECT id, project_type, title, description, downloads, follows,\n icon_url, body, body_url, published,\n updated, status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug,\n rejection_reason, rejection_body\n FROM mods\n WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "project_type", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "follows", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "body", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "body_url", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "published", + "type_info": "Timestamptz" + }, + { + "ordinal": 10, + "name": "updated", + "type_info": "Timestamptz" + }, + { + "ordinal": 11, + "name": "status", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "issues_url", + "type_info": "Varchar" + }, + { + "ordinal": 13, + "name": "source_url", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "wiki_url", + "type_info": "Varchar" + }, + { + "ordinal": 15, + "name": "discord_url", + "type_info": "Varchar" + }, + { + "ordinal": 16, + "name": "license_url", + "type_info": "Varchar" + }, + { + "ordinal": 17, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 18, + "name": "client_side", + "type_info": "Int4" + }, + { + "ordinal": 19, + "name": "server_side", + "type_info": "Int4" + }, + { + "ordinal": 20, + "name": "license", + "type_info": "Int4" + }, + { + "ordinal": 21, + "name": "slug", + "type_info": "Varchar" + }, + { + "ordinal": 22, + "name": "rejection_reason", + "type_info": "Varchar" + }, + { + "ordinal": 23, + "name": "rejection_body", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + true, + false, + false, + false, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true, + true, + true + ] + } + }, "6131d32a65f5e04775308386812f25c6d8464582678536a392a4a3737667f363": { "query": "\n SELECT id, short, name FROM licenses\n ", "describe": { @@ -2268,6 +2120,18 @@ ] } }, + "65aa86d8ce11be1ff3a52a53e5a63a0b352cfb6c8c19812e4491a4afc869c15d": { + "query": "\n DELETE FROM notifications\n WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [] + } + }, "67d021f0776276081d3c50ca97afa6b78b98860bf929009e845e9c00a192e3b5": { "query": "\n SELECT id FROM report_types\n WHERE name = $1\n ", "describe": { @@ -2552,6 +2416,116 @@ "nullable": [] } }, + "717f66f49adea4869633b601e76aa9a0990ff5cf3ceb0cdb6cdf9e025935fd4a": { + "query": "\n SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,\n v.changelog changelog, v.changelog_url changelog_url, v.date_published date_published, v.downloads downloads,\n rc.channel release_channel, v.featured featured,\n STRING_AGG(DISTINCT gv.version, ',') game_versions, STRING_AGG(DISTINCT l.loader, ',') loaders,\n STRING_AGG(DISTINCT f.id || ', ' || f.filename || ', ' || f.is_primary || ', ' || f.url, ' ,') files,\n STRING_AGG(DISTINCT h.algorithm || ', ' || encode(h.hash, 'escape') || ', ' || h.file_id, ' ,') hashes,\n STRING_AGG(DISTINCT COALESCE(d.dependency_id, 0) || ', ' || COALESCE(d.mod_dependency_id, 0) || ', ' || d.dependency_type, ' ,') dependencies\n FROM versions v\n INNER JOIN release_channels rc on v.release_channel = rc.id\n LEFT OUTER JOIN game_versions_versions gvv on v.id = gvv.joining_version_id\n LEFT OUTER JOIN game_versions gv on gvv.game_version_id = gv.id\n LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id\n LEFT OUTER JOIN loaders l on lv.loader_id = l.id\n LEFT OUTER JOIN files f on v.id = f.version_id\n LEFT OUTER JOIN hashes h on f.id = h.file_id\n LEFT OUTER JOIN dependencies d on v.id = d.dependent_id\n WHERE v.id = $1\n GROUP BY v.id, rc.id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "mod_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "author_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "version_name", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "version_number", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "changelog", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "changelog_url", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "date_published", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "release_channel", + "type_info": "Varchar" + }, + { + "ordinal": 10, + "name": "featured", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "game_versions", + "type_info": "Text" + }, + { + "ordinal": 12, + "name": "loaders", + "type_info": "Text" + }, + { + "ordinal": 13, + "name": "files", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "hashes", + "type_info": "Text" + }, + { + "ordinal": 15, + "name": "dependencies", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + null, + null, + null, + null, + null + ] + } + }, "72ad6f4be40d7620a0ec557e3806da41ce95335aeaa910fe35aca2ec7c3f09b6": { "query": "\n SELECT id FROM users\n WHERE id = $1\n ", "describe": { @@ -2676,6 +2650,26 @@ ] } }, + "78bf8232ddae2db486b9ff791ea525af1330e6904740b2a943c4ae3466bf02d0": { + "query": "\n SELECT game_version_id id FROM game_versions_versions\n WHERE joining_version_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + } + }, "796f057ea8eb5b01d3eedeee9840fb37464ea567f32871953fb07e14ed86af1c": { "query": "SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)", "describe": { @@ -2697,152 +2691,6 @@ ] } }, - "79aa918a1dfb8e7cc958c4b7a75172ddb38fc28a95c881e766ecbcef5fd2475a": { - "query": "\n SELECT id, project_type, title, description, downloads, follows,\n icon_url, body, body_url, published,\n updated, status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug\n FROM mods\n WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "project_type", - "type_info": "Int4" - }, - { - "ordinal": 2, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "description", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 5, - "name": "follows", - "type_info": "Int4" - }, - { - "ordinal": 6, - "name": "icon_url", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "body", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "body_url", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "published", - "type_info": "Timestamptz" - }, - { - "ordinal": 10, - "name": "updated", - "type_info": "Timestamptz" - }, - { - "ordinal": 11, - "name": "status", - "type_info": "Int4" - }, - { - "ordinal": 12, - "name": "issues_url", - "type_info": "Varchar" - }, - { - "ordinal": 13, - "name": "source_url", - "type_info": "Varchar" - }, - { - "ordinal": 14, - "name": "wiki_url", - "type_info": "Varchar" - }, - { - "ordinal": 15, - "name": "discord_url", - "type_info": "Varchar" - }, - { - "ordinal": 16, - "name": "license_url", - "type_info": "Varchar" - }, - { - "ordinal": 17, - "name": "team_id", - "type_info": "Int8" - }, - { - "ordinal": 18, - "name": "client_side", - "type_info": "Int4" - }, - { - "ordinal": 19, - "name": "server_side", - "type_info": "Int4" - }, - { - "ordinal": 20, - "name": "license", - "type_info": "Int4" - }, - { - "ordinal": 21, - "name": "slug", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8Array" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - false, - true, - false, - false, - false, - true, - true, - true, - true, - true, - false, - false, - false, - false, - true - ] - } - }, "79b896b1a8ddab285294638302976b75d0d915f36036383cc21bd2fc48d4502c": { "query": "\n DELETE FROM loaders_versions WHERE version_id = $1\n ", "describe": { @@ -2906,6 +2754,28 @@ "nullable": [] } }, + "7c04b3e56e053089b89b9a1319ef61229a339e32716c30da88e8eb44e549701f": { + "query": "\n SELECT d.id id\n FROM versions v\n INNER JOIN dependencies d ON d.dependent_id = v.id\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id AND gvv.game_version_id IN (SELECT * FROM UNNEST($2::integer[]))\n INNER JOIN loaders_versions lv ON lv.version_id = v.id AND lv.loader_id IN (SELECT * FROM UNNEST($3::integer[]))\n WHERE v.mod_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int4Array", + "Int4Array" + ] + }, + "nullable": [ + false + ] + } + }, "7e73d3a17807f57ba6def5ff718e6dcb3a65ef8da653d839560b24635334cf05": { "query": "\n SELECT m.title FROM mods m\n WHERE id = $1\n ", "describe": { @@ -3263,8 +3133,8 @@ ] } }, - "8e80037fa07a2632ea39f0bd38f04fee827c76b043b4c80391bb5c7510a8efb8": { - "query": "\n SELECT n.user_id, n.title, n.text, n.link, n.created, n.read,\n STRING_AGG(DISTINCT na.id || ', ' || na.title || ', ' || na.action_route || ', ' || na.action_route_method, ' ,') actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.id = $1\n GROUP BY n.id, n.user_id;\n ", + "8ee18890537b7263475c4864d18b6010882486e5bed58c2f01f5bc1e2a5e7d19": { + "query": "\n SELECT n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type,\n STRING_AGG(DISTINCT na.id || ', ' || na.title || ', ' || na.action_route || ', ' || na.action_route_method, ' ,') actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.id = $1\n GROUP BY n.id, n.user_id;\n ", "describe": { "columns": [ { @@ -3299,6 +3169,11 @@ }, { "ordinal": 6, + "name": "notification_type", + "type_info": "Varchar" + }, + { + "ordinal": 7, "name": "actions", "type_info": "Text" } @@ -3315,6 +3190,7 @@ false, false, false, + true, null ] } @@ -3344,6 +3220,16 @@ "nullable": [] } }, + "94a823b6e8b2610d72843008706c448432aab21690b4727aea77ad687a98f634": { + "query": "\n DELETE FROM dependencies WHERE mod_dependency_id = NULL AND dependency_id = NULL\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + } + }, "94c49b879a95a9068d93e01e270755c8acedfcfad178b0ed1efbf253b23431aa": { "query": "\n INSERT INTO versions (\n id, mod_id, author_id, name, version_number,\n changelog, changelog_url, date_published,\n downloads, release_channel, featured\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7,\n $8, $9,\n $10, $11\n )\n ", "describe": { @@ -3473,6 +3359,19 @@ "nullable": [] } }, + "9ceca63fb11f35f09f77bb9db175a1ac74dfcc2200c8134866922742fbbedea3": { + "query": "\n UPDATE dependencies\n SET dependency_id = $2\n WHERE dependency_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [] + } + }, "a39ce28b656032f862b205cffa393a76b989f4803654a615477a94fda5f57354": { "query": "\n DELETE FROM states\n WHERE id = $1\n ", "describe": { @@ -3731,6 +3630,232 @@ "nullable": [] } }, + "ad5bb49aacf1699e276fca9900d80b588c3e62b5ae872cd665222e9e9972588b": { + "query": "\n SELECT loader_id id FROM loaders_versions\n WHERE version_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + } + }, + "adca565e97b4cdd8095c9ee56a449a8ecd7858489a5f82628201e7dfd30217d0": { + "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.body_url body_url, m.published published,\n m.updated updated, m.status 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.rejection_reason rejection_reason, m.rejection_body rejection_body,\n s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name,\n STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN statuses s ON s.id = m.status\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 INNER JOIN licenses l ON m.license = l.id\n WHERE m.id IN (SELECT * FROM UNNEST($1::bigint[]))\n GROUP BY m.id, s.id, cs.id, ss.id, l.id, pt.id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "project_type", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "follows", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "body", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "body_url", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "published", + "type_info": "Timestamptz" + }, + { + "ordinal": 10, + "name": "updated", + "type_info": "Timestamptz" + }, + { + "ordinal": 11, + "name": "status", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "issues_url", + "type_info": "Varchar" + }, + { + "ordinal": 13, + "name": "source_url", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "wiki_url", + "type_info": "Varchar" + }, + { + "ordinal": 15, + "name": "discord_url", + "type_info": "Varchar" + }, + { + "ordinal": 16, + "name": "license_url", + "type_info": "Varchar" + }, + { + "ordinal": 17, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 18, + "name": "client_side", + "type_info": "Int4" + }, + { + "ordinal": 19, + "name": "server_side", + "type_info": "Int4" + }, + { + "ordinal": 20, + "name": "license", + "type_info": "Int4" + }, + { + "ordinal": 21, + "name": "slug", + "type_info": "Varchar" + }, + { + "ordinal": 22, + "name": "rejection_reason", + "type_info": "Varchar" + }, + { + "ordinal": 23, + "name": "rejection_body", + "type_info": "Varchar" + }, + { + "ordinal": 24, + "name": "status_name", + "type_info": "Varchar" + }, + { + "ordinal": 25, + "name": "client_side_type", + "type_info": "Varchar" + }, + { + "ordinal": 26, + "name": "server_side_type", + "type_info": "Varchar" + }, + { + "ordinal": 27, + "name": "short", + "type_info": "Varchar" + }, + { + "ordinal": 28, + "name": "license_name", + "type_info": "Varchar" + }, + { + "ordinal": 29, + "name": "project_type_name", + "type_info": "Varchar" + }, + { + "ordinal": 30, + "name": "categories", + "type_info": "Text" + }, + { + "ordinal": 31, + "name": "versions", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + true, + false, + false, + false, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true, + true, + true, + false, + false, + false, + false, + false, + false, + null, + null + ] + } + }, "b0e3d1c70b87bb54819e3fac04b684a9b857aeedb4dcb7cb400c2af0dbb12922": { "query": "\n DELETE FROM teams\n WHERE id = $1\n ", "describe": { @@ -3743,6 +3868,183 @@ "nullable": [] } }, + "b143e2172d3478546537393290a9f4d7da275af673aefaea5499270df6fd11b2": { + "query": "\n UPDATE mods\n SET rejection_reason = NULL\n WHERE (id = $1)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, + "b18c41ba7edb7d1f12316642e26d56b0c6d2737569a0a6bf53e878312b565982": { + "query": "\n SELECT project_type, title, description, downloads, follows,\n icon_url, body, body_url, published,\n updated, status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug,\n rejection_reason, rejection_body\n FROM mods\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "project_type", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "follows", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "body", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "body_url", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "published", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "updated", + "type_info": "Timestamptz" + }, + { + "ordinal": 10, + "name": "status", + "type_info": "Int4" + }, + { + "ordinal": 11, + "name": "issues_url", + "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "source_url", + "type_info": "Varchar" + }, + { + "ordinal": 13, + "name": "wiki_url", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "discord_url", + "type_info": "Varchar" + }, + { + "ordinal": 15, + "name": "license_url", + "type_info": "Varchar" + }, + { + "ordinal": 16, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 17, + "name": "client_side", + "type_info": "Int4" + }, + { + "ordinal": 18, + "name": "server_side", + "type_info": "Int4" + }, + { + "ordinal": 19, + "name": "license", + "type_info": "Int4" + }, + { + "ordinal": 20, + "name": "slug", + "type_info": "Varchar" + }, + { + "ordinal": 21, + "name": "rejection_reason", + "type_info": "Varchar" + }, + { + "ordinal": 22, + "name": "rejection_body", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + false, + true, + false, + false, + false, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true, + true, + true + ] + } + }, + "b2a4fabfca61da6816a68b4508132b463bff7f3748fdd8e75589be9611fa1229": { + "query": "\n UPDATE dependencies\n SET dependency_id = $2\n WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8Array", + "Int8" + ] + }, + "nullable": [] + } + }, "b69a6f42965b3e7103fcbf46e39528466926789ff31e9ed2591bb175527ec169": { "query": "\n DELETE FROM users\n WHERE id = $1\n ", "describe": { @@ -3905,16 +4207,72 @@ "nullable": [] } }, - "bd0d1da185dc7d21ccbbfde86fc093ce9eda7dd7e07f7a53882d427010fd58ca": { - "query": "\n DELETE FROM dependencies WHERE dependent_id = $1\n ", + "bd56d8c762eb5958b88064654f7ea77f1bcbc989535e10c763d99b3c5d42c9d5": { + "query": "\n SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type,\n STRING_AGG(DISTINCT na.id || ', ' || na.title || ', ' || na.action_route || ', ' || na.action_route_method, ' ,') actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.id IN (SELECT * FROM UNNEST($1::bigint[]))\n GROUP BY n.id, n.user_id\n ORDER BY n.created DESC;\n ", "describe": { - "columns": [], + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "text", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "link", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "created", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "read", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "notification_type", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "actions", + "type_info": "Text" + } + ], "parameters": { "Left": [ - "Int8" + "Int8Array" ] }, - "nullable": [] + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true, + null + ] } }, "bdaab7da16d07169c29d96330fcc17ef2fb87fdfbadca23b7289c64420ac3a04": { @@ -4006,198 +4364,17 @@ "nullable": [] } }, - "bf35a3797ff92917ba3568284b5dbae4ae08eedf3516b39b9a02f8bf03164bc3": { - "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.body_url body_url, m.published published,\n m.updated updated, m.status 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,\n s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name,\n STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN statuses s ON s.id = m.status\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 INNER JOIN licenses l ON m.license = l.id\n WHERE m.id IN (SELECT * FROM UNNEST($1::bigint[]))\n GROUP BY m.id, s.id, cs.id, ss.id, l.id, pt.id;\n ", + "bf67dcb3ced403fb998737ca4a30f0bcefc34a3102ff0ba908f95b555e180f8c": { + "query": "\n UPDATE mods\n SET rejection_reason = $1\n WHERE (id = $2)\n ", "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "project_type", - "type_info": "Int4" - }, - { - "ordinal": 2, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "description", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 5, - "name": "follows", - "type_info": "Int4" - }, - { - "ordinal": 6, - "name": "icon_url", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "body", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "body_url", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "published", - "type_info": "Timestamptz" - }, - { - "ordinal": 10, - "name": "updated", - "type_info": "Timestamptz" - }, - { - "ordinal": 11, - "name": "status", - "type_info": "Int4" - }, - { - "ordinal": 12, - "name": "issues_url", - "type_info": "Varchar" - }, - { - "ordinal": 13, - "name": "source_url", - "type_info": "Varchar" - }, - { - "ordinal": 14, - "name": "wiki_url", - "type_info": "Varchar" - }, - { - "ordinal": 15, - "name": "discord_url", - "type_info": "Varchar" - }, - { - "ordinal": 16, - "name": "license_url", - "type_info": "Varchar" - }, - { - "ordinal": 17, - "name": "team_id", - "type_info": "Int8" - }, - { - "ordinal": 18, - "name": "client_side", - "type_info": "Int4" - }, - { - "ordinal": 19, - "name": "server_side", - "type_info": "Int4" - }, - { - "ordinal": 20, - "name": "license", - "type_info": "Int4" - }, - { - "ordinal": 21, - "name": "slug", - "type_info": "Varchar" - }, - { - "ordinal": 22, - "name": "status_name", - "type_info": "Varchar" - }, - { - "ordinal": 23, - "name": "client_side_type", - "type_info": "Varchar" - }, - { - "ordinal": 24, - "name": "server_side_type", - "type_info": "Varchar" - }, - { - "ordinal": 25, - "name": "short", - "type_info": "Varchar" - }, - { - "ordinal": 26, - "name": "license_name", - "type_info": "Varchar" - }, - { - "ordinal": 27, - "name": "project_type_name", - "type_info": "Varchar" - }, - { - "ordinal": 28, - "name": "categories", - "type_info": "Text" - }, - { - "ordinal": 29, - "name": "versions", - "type_info": "Text" - } - ], + "columns": [], "parameters": { "Left": [ - "Int8Array" + "Varchar", + "Int8" ] }, - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - false, - true, - false, - false, - false, - true, - true, - true, - true, - true, - false, - false, - false, - false, - true, - false, - false, - false, - false, - false, - false, - null, - null - ] + "nullable": [] } }, "bf7f721664f5e0ed41adc41b5483037256635f28ff6c4e5d3cbcec4387f9c8ef": { @@ -4233,6 +4410,21 @@ "nullable": [] } }, + "c11f52e25edd7239a7a499c55d7127b4f51786e1b7666e3c61925c49fb41e05e": { + "query": "\n INSERT INTO dependencies (dependent_id, dependency_type, dependency_id, mod_dependency_id)\n VALUES ($1, $2, $3, $4)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Varchar", + "Int8", + "Int8" + ] + }, + "nullable": [] + } + }, "c1a3f6dcef6110d6ea884670fb82bac14b98e922bb5673c048ccce7b7300539b": { "query": "\n SELECT EXISTS(SELECT 1 FROM reports WHERE id = $1)\n ", "describe": { @@ -4285,6 +4477,28 @@ "nullable": [] } }, + "c2c9c73813f41928f333960099b9b19f844cbc830450462b6468f90397da8e83": { + "query": "\n SELECT v.id id\n FROM versions v\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id AND gvv.game_version_id IN (SELECT * FROM UNNEST($2::integer[]))\n INNER JOIN loaders_versions lv ON lv.version_id = v.id AND lv.loader_id IN (SELECT * FROM UNNEST($3::integer[]))\n WHERE v.mod_id = $1\n ORDER BY v.date_published DESC\n LIMIT 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int4Array", + "Int4Array" + ] + }, + "nullable": [ + false + ] + } + }, "c3dcb5a8b798ea6c0922698a007dbc8ab549f5f85bad780da59163f4d6371238": { "query": "\n SELECT id FROM mods\n WHERE status = (\n SELECT id FROM statuses WHERE status = $1\n )\n ORDER BY updated ASC\n LIMIT $2;\n ", "describe": { @@ -4445,8 +4659,8 @@ ] } }, - "ca5e6d56640069ea9b1c32881d59be8eb1c2ee06ececf223ad378d0422a6f198": { - "query": "\n SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,\n v.changelog changelog, v.changelog_url changelog_url, v.date_published date_published, v.downloads downloads,\n rc.channel release_channel, v.featured featured,\n STRING_AGG(DISTINCT gv.version, ',') game_versions, STRING_AGG(DISTINCT l.loader, ',') loaders,\n STRING_AGG(DISTINCT f.id || ', ' || f.filename || ', ' || f.is_primary || ', ' || f.url, ' ,') files,\n STRING_AGG(DISTINCT h.algorithm || ', ' || encode(h.hash, 'escape') || ', ' || h.file_id, ' ,') hashes,\n STRING_AGG(DISTINCT d.dependency_id || ', ' || d.dependency_type, ' ,') dependencies\n FROM versions v\n INNER JOIN release_channels rc on v.release_channel = rc.id\n LEFT OUTER JOIN game_versions_versions gvv on v.id = gvv.joining_version_id\n LEFT OUTER JOIN game_versions gv on gvv.game_version_id = gv.id\n LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id\n LEFT OUTER JOIN loaders l on lv.loader_id = l.id\n LEFT OUTER JOIN files f on v.id = f.version_id\n LEFT OUTER JOIN hashes h on f.id = h.file_id\n LEFT OUTER JOIN dependencies d on v.id = d.dependent_id\n WHERE v.id = $1\n GROUP BY v.id, rc.id;\n ", + "caf24e9714afdea82cbfb8405cb291c8aee7c94bddc51260152ed1cbc629ffa1": { + "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.body_url body_url, m.published published,\n m.updated updated, m.status 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.rejection_reason rejection_reason, m.rejection_body rejection_body,\n s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name,\n STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN statuses s ON s.id = m.status\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 INNER JOIN licenses l ON m.license = l.id\n WHERE m.id = $1\n GROUP BY m.id, s.id, cs.id, ss.id, l.id, pt.id;\n ", "describe": { "columns": [ { @@ -4456,77 +4670,157 @@ }, { "ordinal": 1, - "name": "mod_id", - "type_info": "Int8" + "name": "project_type", + "type_info": "Int4" }, { "ordinal": 2, - "name": "author_id", - "type_info": "Int8" + "name": "title", + "type_info": "Varchar" }, { "ordinal": 3, - "name": "version_name", + "name": "description", "type_info": "Varchar" }, { "ordinal": 4, - "name": "version_number", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "changelog", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "changelog_url", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "date_published", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, "name": "downloads", "type_info": "Int4" }, { - "ordinal": 9, - "name": "release_channel", + "ordinal": 5, + "name": "follows", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "icon_url", "type_info": "Varchar" }, + { + "ordinal": 7, + "name": "body", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "body_url", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "published", + "type_info": "Timestamptz" + }, { "ordinal": 10, - "name": "featured", - "type_info": "Bool" + "name": "updated", + "type_info": "Timestamptz" }, { "ordinal": 11, - "name": "game_versions", - "type_info": "Text" + "name": "status", + "type_info": "Int4" }, { "ordinal": 12, - "name": "loaders", - "type_info": "Text" + "name": "issues_url", + "type_info": "Varchar" }, { "ordinal": 13, - "name": "files", - "type_info": "Text" + "name": "source_url", + "type_info": "Varchar" }, { "ordinal": 14, - "name": "hashes", - "type_info": "Text" + "name": "wiki_url", + "type_info": "Varchar" }, { "ordinal": 15, - "name": "dependencies", + "name": "discord_url", + "type_info": "Varchar" + }, + { + "ordinal": 16, + "name": "license_url", + "type_info": "Varchar" + }, + { + "ordinal": 17, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 18, + "name": "client_side", + "type_info": "Int4" + }, + { + "ordinal": 19, + "name": "server_side", + "type_info": "Int4" + }, + { + "ordinal": 20, + "name": "license", + "type_info": "Int4" + }, + { + "ordinal": 21, + "name": "slug", + "type_info": "Varchar" + }, + { + "ordinal": 22, + "name": "rejection_reason", + "type_info": "Varchar" + }, + { + "ordinal": 23, + "name": "rejection_body", + "type_info": "Varchar" + }, + { + "ordinal": 24, + "name": "status_name", + "type_info": "Varchar" + }, + { + "ordinal": 25, + "name": "client_side_type", + "type_info": "Varchar" + }, + { + "ordinal": 26, + "name": "server_side_type", + "type_info": "Varchar" + }, + { + "ordinal": 27, + "name": "short", + "type_info": "Varchar" + }, + { + "ordinal": 28, + "name": "license_name", + "type_info": "Varchar" + }, + { + "ordinal": 29, + "name": "project_type_name", + "type_info": "Varchar" + }, + { + "ordinal": 30, + "name": "categories", + "type_info": "Text" + }, + { + "ordinal": 31, + "name": "versions", "type_info": "Text" } ], @@ -4544,12 +4838,28 @@ false, true, false, + true, + false, + false, + false, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true, + true, + true, + false, + false, + false, false, false, false, - null, - null, - null, null, null ] @@ -4877,6 +5187,19 @@ ] } }, + "d63386fcc792df45e34001563dd22f78d65f567119f306734c32a01405af703d": { + "query": "\n UPDATE mods\n SET rejection_body = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + } + }, "d6453e50041b5521fa9e919a9162e533bb9426f8c584d98474c6ad414db715c8": { "query": "SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)", "describe": { @@ -5302,6 +5625,18 @@ "nullable": [] } }, + "ed5c72e789353869837e0653914c86d5d1002a4227d022567e02f280684d71a7": { + "query": "\n DELETE FROM dependencies WHERE mod_dependency_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, "ef3d43d3424824eed67370f10cc0672581a95a169bf404022cbe3cac0415d99c": { "query": "\n SELECT f.id id, f.version_id version_id, f.filename filename, v.version_number version_number, 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\n WHERE h.algorithm = $2 AND h.hash = $1\n ", "describe": { @@ -5522,26 +5857,6 @@ "nullable": [] } }, - "fb6178b27856ff583039a974173efe5d6be4e347b6cc1d4904cf750a40d1b77f": { - "query": "\n SELECT dependency_id id FROM dependencies\n WHERE dependent_id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false - ] - } - }, "fb955ca41b95120f66c98c0b528b1db10c4be4a55e9641bb104d772e390c9bb7": { "query": "SELECT EXISTS(SELECT 1 FROM notifications WHERE id=$1)", "describe": { @@ -5562,20 +5877,6 @@ ] } }, - "fbd55f89e9bd8c67605f67944cd585abf8a475b83f0b926d7dbcb26478df4da0": { - "query": "\n INSERT INTO dependencies (dependent_id, dependency_id, dependency_type)\n VALUES ($1, $2, $3)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Varchar" - ] - }, - "nullable": [] - } - }, "fc12e683844642245dae7ffad7aff29f2b65c7441be7f22e319da468e7f3d323": { "query": "\n SELECT v.mod_id, v.author_id, v.name, v.version_number,\n v.changelog, v.changelog_url, v.date_published, v.downloads,\n v.release_channel, v.featured\n FROM versions v\n WHERE v.id = $1\n ", "describe": { diff --git a/src/database/cache/mod.rs b/src/database/cache/mod.rs index 442796b83..c2a72186c 100644 --- a/src/database/cache/mod.rs +++ b/src/database/cache/mod.rs @@ -2,7 +2,7 @@ //pub mod project_query_cache; #[macro_export] macro_rules! generate_cache { - ($name:ident,$id:ty, $val:ty, $cache_name:ident, $mod_name:ident, $getter_name:ident, $setter_name:ident) => { + ($name:ident,$id:ty, $val:ty, $cache_name:ident, $mod_name:ident, $getter_name:ident, $setter_name:ident, $remover_name:ident) => { pub mod $mod_name { use cached::async_mutex::Mutex; use cached::{Cached, SizedCache}; @@ -20,6 +20,10 @@ macro_rules! generate_cache { let mut cache = $cache_name.lock().await; Cached::cache_set(&mut *cache, id, val.clone()); } + pub async fn $remover_name<'a>(id: $id) { + let mut cache = $cache_name.lock().await; + Cached::cache_remove(&mut *cache, &id); + } } }; } @@ -31,7 +35,8 @@ generate_cache!( PROJECT_CACHE, project_cache, get_cache_project, - set_cache_project + set_cache_project, + remove_cache_project ); generate_cache!( query_project, @@ -40,5 +45,6 @@ generate_cache!( QUERY_PROJECT_CACHE, query_project_cache, get_cache_query_project, - set_cache_query_project + set_cache_query_project, + remove_cache_query_project ); diff --git a/src/database/mod.rs b/src/database/mod.rs index 7c7f600d1..d321f146f 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,4 +1,4 @@ -mod cache; +pub mod cache; pub mod models; mod postgres_database; pub use models::Project; diff --git a/src/database/models/ids.rs b/src/database/models/ids.rs index 4adbe825a..8f3f00079 100644 --- a/src/database/models/ids.rs +++ b/src/database/models/ids.rs @@ -113,7 +113,7 @@ pub struct TeamId(pub i64); #[sqlx(transparent)] pub struct TeamMemberId(pub i64); -#[derive(Copy, Clone, Debug, Type)] +#[derive(Copy, Clone, Debug, Type, PartialEq)] #[sqlx(transparent)] pub struct ProjectId(pub i64); #[derive(Copy, Clone, Debug, Type)] @@ -133,7 +133,7 @@ pub struct LicenseId(pub i32); #[sqlx(transparent)] pub struct DonationPlatformId(pub i32); -#[derive(Copy, Clone, Debug, Type)] +#[derive(Copy, Clone, Debug, Type, PartialEq)] #[sqlx(transparent)] pub struct VersionId(pub i64); #[derive(Copy, Clone, Debug, Type)] diff --git a/src/database/models/notification_item.rs b/src/database/models/notification_item.rs index 4ac075d0c..5d7ba471f 100644 --- a/src/database/models/notification_item.rs +++ b/src/database/models/notification_item.rs @@ -2,6 +2,7 @@ use super::ids::*; use crate::database::models::DatabaseError; pub struct NotificationBuilder { + pub notification_type: Option, pub title: String, pub text: String, pub link: String, @@ -16,6 +17,7 @@ pub struct NotificationActionBuilder { pub struct Notification { pub id: NotificationId, pub user_id: UserId, + pub notification_type: Option, pub title: String, pub text: String, pub link: String, @@ -64,6 +66,7 @@ impl NotificationBuilder { Notification { id, user_id: user, + notification_type: self.notification_type.clone(), title: self.title.clone(), text: self.text.clone(), link: self.link.clone(), @@ -87,17 +90,18 @@ impl Notification { sqlx::query!( " INSERT INTO notifications ( - id, user_id, title, text, link + id, user_id, title, text, link, type ) VALUES ( - $1, $2, $3, $4, $5 + $1, $2, $3, $4, $5, $6 ) ", self.id as NotificationId, self.user_id as UserId, &self.title, &self.text, - &self.link + &self.link, + self.notification_type ) .execute(&mut *transaction) .await?; @@ -118,7 +122,7 @@ impl Notification { { let result = sqlx::query!( " - SELECT n.user_id, n.title, n.text, n.link, n.created, n.read, + SELECT n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type, STRING_AGG(DISTINCT na.id || ', ' || na.title || ', ' || na.action_route || ', ' || na.action_route_method, ' ,') actions FROM notifications n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id @@ -150,6 +154,7 @@ impl Notification { Ok(Some(Notification { id, user_id: UserId(row.user_id), + notification_type: row.notification_type, title: row.title, text: row.text, link: row.link, @@ -174,7 +179,7 @@ impl Notification { let notification_ids_parsed: Vec = notification_ids.into_iter().map(|x| x.0).collect(); sqlx::query!( " - SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, + SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type, STRING_AGG(DISTINCT na.id || ', ' || na.title || ', ' || na.action_route || ', ' || na.action_route_method, ' ,') actions FROM notifications n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id @@ -207,6 +212,7 @@ impl Notification { Notification { id, user_id: UserId(row.user_id), + notification_type: row.notification_type, title: row.title, text: row.text, link: row.link, @@ -231,7 +237,7 @@ impl Notification { sqlx::query!( " - SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, + SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type, STRING_AGG(DISTINCT na.id || ', ' || na.title || ', ' || na.action_route || ', ' || na.action_route_method, ' ,') actions FROM notifications n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id @@ -263,6 +269,7 @@ impl Notification { Notification { id, user_id: UserId(row.user_id), + notification_type: row.notification_type, title: row.title, text: row.text, link: row.link, @@ -276,13 +283,10 @@ impl Notification { .await } - pub async fn remove<'a, 'b, E>( + pub async fn remove( id: NotificationId, - exec: E, - ) -> Result, sqlx::error::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, - { + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result, sqlx::error::Error> { sqlx::query!( " DELETE FROM notifications_actions @@ -290,7 +294,7 @@ impl Notification { ", id as NotificationId, ) - .execute(exec) + .execute(&mut *transaction) .await?; sqlx::query!( @@ -300,7 +304,36 @@ impl Notification { ", id as NotificationId, ) - .execute(exec) + .execute(&mut *transaction) + .await?; + + Ok(Some(())) + } + + pub async fn remove_many( + notification_ids: Vec, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result, sqlx::error::Error> { + let notification_ids_parsed: Vec = notification_ids.into_iter().map(|x| x.0).collect(); + + sqlx::query!( + " + DELETE FROM notifications_actions + WHERE notification_id IN (SELECT * FROM UNNEST($1::bigint[])) + ", + ¬ification_ids_parsed + ) + .execute(&mut *transaction) + .await?; + + sqlx::query!( + " + DELETE FROM notifications + WHERE id IN (SELECT * FROM UNNEST($1::bigint[])) + ", + ¬ification_ids_parsed + ) + .execute(&mut *transaction) .await?; Ok(Some(())) diff --git a/src/database/models/project_item.rs b/src/database/models/project_item.rs index 0561bc5a7..c91703fca 100644 --- a/src/database/models/project_item.rs +++ b/src/database/models/project_item.rs @@ -88,6 +88,8 @@ impl ProjectBuilder { server_side: self.server_side, license: self.license, slug: self.slug, + rejection_reason: None, + rejection_body: None, }; project_struct.insert(&mut *transaction).await?; @@ -141,6 +143,8 @@ pub struct Project { pub server_side: SideTypeId, pub license: LicenseId, pub slug: Option, + pub rejection_reason: Option, + pub rejection_body: Option, } impl Project { @@ -204,7 +208,8 @@ impl Project { icon_url, body, body_url, published, updated, status, issues_url, source_url, wiki_url, discord_url, license_url, - team_id, client_side, server_side, license, slug + team_id, client_side, server_side, license, slug, + rejection_reason, rejection_body FROM mods WHERE id = $1 ", @@ -237,6 +242,8 @@ impl Project { slug: row.slug, body: row.body, follows: row.follows, + rejection_reason: row.rejection_reason, + rejection_body: row.rejection_body, })) } else { Ok(None) @@ -259,7 +266,8 @@ impl Project { icon_url, body, body_url, published, updated, status, issues_url, source_url, wiki_url, discord_url, license_url, - team_id, client_side, server_side, license, slug + team_id, client_side, server_side, license, slug, + rejection_reason, rejection_body FROM mods WHERE id IN (SELECT * FROM UNNEST($1::bigint[])) ", @@ -290,6 +298,8 @@ impl Project { slug: m.slug, body: m.body, follows: m.follows, + rejection_reason: m.rejection_reason, + rejection_body: m.rejection_body, })) }) .try_collect::>() @@ -298,20 +308,17 @@ impl Project { Ok(projects) } - pub async fn remove_full<'a, 'b, E>( + pub async fn remove_full( id: ProjectId, - exec: E, - ) -> Result, sqlx::error::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 team_id FROM mods WHERE id = $1 ", id as ProjectId, ) - .fetch_optional(exec) + .fetch_optional(&mut *transaction) .await?; let team_id: TeamId = if let Some(id) = result { @@ -327,7 +334,7 @@ impl Project { ", id as ProjectId ) - .execute(exec) + .execute(&mut *transaction) .await?; sqlx::query!( @@ -337,7 +344,7 @@ impl Project { ", id as ProjectId, ) - .execute(exec) + .execute(&mut *transaction) .await?; sqlx::query!( @@ -347,7 +354,7 @@ impl Project { ", id as ProjectId, ) - .execute(exec) + .execute(&mut *transaction) .await?; sqlx::query!( @@ -357,7 +364,7 @@ impl Project { ", id as ProjectId, ) - .execute(exec) + .execute(&mut *transaction) .await?; sqlx::query!( @@ -367,7 +374,7 @@ impl Project { ", id as ProjectId, ) - .execute(exec) + .execute(&mut *transaction) .await?; use futures::TryStreamExt; @@ -378,15 +385,24 @@ impl Project { ", id as ProjectId, ) - .fetch_many(exec) + .fetch_many(&mut *transaction) .try_filter_map(|e| async { Ok(e.right().map(|c| VersionId(c.id))) }) .try_collect::>() .await?; for version in versions { - super::Version::remove_full(version, exec).await?; + super::Version::remove_full(version, transaction).await?; } + sqlx::query!( + " + DELETE FROM dependencies WHERE mod_dependency_id = $1 + ", + id as ProjectId, + ) + .execute(&mut *transaction) + .await?; + sqlx::query!( " DELETE FROM mods @@ -394,7 +410,7 @@ impl Project { ", id as ProjectId, ) - .execute(exec) + .execute(&mut *transaction) .await?; sqlx::query!( @@ -404,7 +420,7 @@ impl Project { ", team_id as TeamId, ) - .execute(exec) + .execute(&mut *transaction) .await?; sqlx::query!( @@ -414,7 +430,7 @@ impl Project { ", team_id as TeamId, ) - .execute(exec) + .execute(&mut *transaction) .await?; Ok(Some(())) @@ -552,7 +568,7 @@ impl Project { executor: E, ) -> Result, sqlx::error::Error> where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let result = sqlx::query!( " @@ -560,7 +576,7 @@ impl Project { m.icon_url icon_url, m.body body, m.body_url body_url, m.published published, m.updated updated, m.status status, 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.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.rejection_reason rejection_reason, m.rejection_body rejection_body, s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name, STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions FROM mods m @@ -605,6 +621,8 @@ impl Project { slug: m.slug.clone(), body: m.body.clone(), follows: m.follows, + rejection_reason: m.rejection_reason, + rejection_body: m.rejection_body, }, project_type: m.project_type_name, categories: m @@ -647,7 +665,7 @@ impl Project { m.icon_url icon_url, m.body body, m.body_url body_url, m.published published, m.updated updated, m.status status, 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.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.rejection_reason rejection_reason, m.rejection_body rejection_body, s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name, STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions FROM mods m @@ -689,7 +707,9 @@ impl Project { license: LicenseId(m.license), slug: m.slug.clone(), body: m.body.clone(), - follows: m.follows + follows: m.follows, + rejection_reason: m.rejection_reason, + rejection_body: m.rejection_body, }, project_type: m.project_type_name, categories: m.categories.unwrap_or_default().split(',').map(|x| x.to_string()).collect(), diff --git a/src/database/models/user_item.rs b/src/database/models/user_item.rs index e1d715a2c..ac3537122 100644 --- a/src/database/models/user_item.rs +++ b/src/database/models/user_item.rs @@ -238,10 +238,10 @@ impl User { Ok(projects) } - pub async fn remove<'a, 'b, E>(id: UserId, exec: E) -> Result, sqlx::error::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, - { + pub async fn remove( + id: UserId, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result, sqlx::error::Error> { let deleted_user: UserId = crate::models::users::DELETED_USER.into(); sqlx::query!( @@ -254,7 +254,7 @@ impl User { id as UserId, crate::models::teams::OWNER_ROLE ) - .execute(exec) + .execute(&mut *transaction) .await?; sqlx::query!( @@ -266,7 +266,7 @@ impl User { deleted_user as UserId, id as UserId, ) - .execute(exec) + .execute(&mut *transaction) .await?; use futures::TryStreamExt; @@ -277,7 +277,7 @@ impl User { ", id as UserId, ) - .fetch_many(exec) + .fetch_many(&mut *transaction) .try_filter_map(|e| async { Ok(e.right().map(|m| m.id as i64)) }) .try_collect::>() .await?; @@ -289,7 +289,7 @@ impl User { ", id as UserId, ) - .execute(exec) + .execute(&mut *transaction) .await?; sqlx::query!( @@ -299,7 +299,7 @@ impl User { ", id as UserId, ) - .execute(exec) + .execute(&mut *transaction) .await?; sqlx::query!( @@ -309,7 +309,7 @@ impl User { ", id as UserId, ) - .execute(exec) + .execute(&mut *transaction) .await?; sqlx::query!( @@ -319,7 +319,7 @@ impl User { ", ¬ifications ) - .execute(exec) + .execute(&mut *transaction) .await?; sqlx::query!( @@ -329,7 +329,7 @@ impl User { ", id as UserId, ) - .execute(exec) + .execute(&mut *transaction) .await?; sqlx::query!( @@ -339,19 +339,16 @@ impl User { ", id as UserId, ) - .execute(exec) + .execute(&mut *transaction) .await?; Ok(Some(())) } - pub async fn remove_full<'a, 'b, E>( + pub async fn remove_full( id: UserId, - exec: E, - ) -> Result, sqlx::error::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, - { + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result, sqlx::error::Error> { use futures::TryStreamExt; let projects: Vec = sqlx::query!( " @@ -362,13 +359,14 @@ impl User { id as UserId, crate::models::teams::OWNER_ROLE ) - .fetch_many(exec) + .fetch_many(&mut *transaction) .try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.id))) }) .try_collect::>() .await?; for project_id in projects { - let _result = super::project_item::Project::remove_full(project_id, exec).await?; + let _result = + super::project_item::Project::remove_full(project_id, transaction).await?; } let notifications: Vec = sqlx::query!( @@ -378,7 +376,7 @@ impl User { ", id as UserId, ) - .fetch_many(exec) + .fetch_many(&mut *transaction) .try_filter_map(|e| async { Ok(e.right().map(|m| m.id as i64)) }) .try_collect::>() .await?; @@ -390,7 +388,7 @@ impl User { ", id as UserId, ) - .execute(exec) + .execute(&mut *transaction) .await?; sqlx::query!( @@ -400,7 +398,7 @@ impl User { ", ¬ifications ) - .execute(exec) + .execute(&mut *transaction) .await?; let deleted_user: UserId = crate::models::users::DELETED_USER.into(); @@ -414,7 +412,7 @@ impl User { deleted_user as UserId, id as UserId, ) - .execute(exec) + .execute(&mut *transaction) .await?; sqlx::query!( @@ -424,7 +422,7 @@ impl User { ", id as UserId, ) - .execute(exec) + .execute(&mut *transaction) .await?; sqlx::query!( @@ -434,7 +432,7 @@ impl User { ", id as UserId, ) - .execute(exec) + .execute(&mut *transaction) .await?; Ok(Some(())) diff --git a/src/database/models/version_item.rs b/src/database/models/version_item.rs index beac46a12..bbf536459 100644 --- a/src/database/models/version_item.rs +++ b/src/database/models/version_item.rs @@ -10,13 +10,62 @@ pub struct VersionBuilder { pub version_number: String, pub changelog: String, pub files: Vec, - pub dependencies: Vec<(VersionId, String)>, + pub dependencies: Vec, pub game_versions: Vec, pub loaders: Vec, pub release_channel: ChannelId, pub featured: bool, } +pub struct DependencyBuilder { + pub project_id: Option, + pub version_id: Option, + pub dependency_type: String, +} + +impl DependencyBuilder { + pub async fn insert( + self, + version_id: VersionId, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result<(), DatabaseError> { + let version_dependency_id = if let Some(project_id) = self.project_id { + sqlx::query!( + " + SELECT version.id id FROM ( + SELECT DISTINCT ON(v.id) v.id, v.date_published FROM versions v + 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) + 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) + WHERE v.mod_id = $1 + ) AS version + ORDER BY version.date_published DESC + LIMIT 1 + ", + project_id as ProjectId, + version_id as VersionId, + ) + .fetch_optional(&mut *transaction).await?.map(|x| VersionId(x.id)) + } else { + self.version_id + }; + + sqlx::query!( + " + INSERT INTO dependencies (dependent_id, dependency_type, dependency_id, mod_dependency_id) + VALUES ($1, $2, $3, $4) + ", + version_id as VersionId, + self.dependency_type, + version_dependency_id.map(|x| x.0), + self.project_id.map(|x| x.0), + ) + .execute(&mut *transaction) + .await?; + + Ok(()) + } +} + pub struct VersionFileBuilder { pub url: String, pub filename: String, @@ -105,20 +154,10 @@ impl VersionBuilder { } for dependency in self.dependencies { - sqlx::query!( - " - INSERT INTO dependencies (dependent_id, dependency_id, dependency_type) - VALUES ($1, $2, $3) - ", - self.version_id as VersionId, - dependency.0 as VersionId, - dependency.1, - ) - .execute(&mut *transaction) - .await?; + dependency.insert(self.version_id, transaction).await?; } - for loader in self.loaders { + for loader in self.loaders.clone() { sqlx::query!( " INSERT INTO loaders_versions (loader_id, version_id) @@ -131,7 +170,7 @@ impl VersionBuilder { .await?; } - for game_version in self.game_versions { + for game_version in self.game_versions.clone() { sqlx::query!( " INSERT INTO game_versions_versions (game_version_id, joining_version_id) @@ -144,6 +183,42 @@ impl VersionBuilder { .await?; } + // Sync dependencies + + use futures::stream::TryStreamExt; + + let dependencies = sqlx::query!( + " + SELECT d.id id + FROM versions v + INNER JOIN dependencies d ON d.dependent_id = v.id + INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id AND gvv.game_version_id IN (SELECT * FROM UNNEST($2::integer[])) + INNER JOIN loaders_versions lv ON lv.version_id = v.id AND lv.loader_id IN (SELECT * FROM UNNEST($3::integer[])) + WHERE v.mod_id = $1 + ", + self.project_id as ProjectId, + &self.game_versions.iter().map(|x| x.0).collect::>(), + &self.loaders.iter().map(|x| x.0).collect::>(), + ) + .fetch_many(&mut *transaction) + .try_filter_map(|e| async { + Ok(e.right().map(|d| d.id as i64)) + }) + .try_collect::>() + .await?; + + sqlx::query!( + " + UPDATE dependencies + SET dependency_id = $2 + WHERE id IN (SELECT * FROM UNNEST($1::bigint[])) + ", + &dependencies, + self.version_id as VersionId, + ) + .execute(&mut *transaction) + .await?; + Ok(self.version_id) } } @@ -200,17 +275,17 @@ impl Version { } // TODO: someone verify this - pub async fn remove_full<'a, E>(id: VersionId, exec: E) -> Result, sqlx::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, - { + pub async fn remove_full( + id: VersionId, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result, sqlx::Error> { let result = sqlx::query!( " SELECT EXISTS(SELECT 1 FROM versions WHERE id = $1) ", id as VersionId, ) - .fetch_one(exec) + .fetch_one(&mut *transaction) .await?; if !result.exists.unwrap_or(false) { @@ -224,7 +299,33 @@ impl Version { ", id as VersionId, ) - .execute(exec) + .execute(&mut *transaction) + .await?; + + use futures::TryStreamExt; + + let game_versions: Vec = sqlx::query!( + " + SELECT game_version_id id FROM game_versions_versions + WHERE joining_version_id = $1 + ", + id as VersionId, + ) + .fetch_many(&mut *transaction) + .try_filter_map(|e| async { Ok(e.right().map(|c| c.id)) }) + .try_collect::>() + .await?; + + let loaders: Vec = sqlx::query!( + " + SELECT loader_id id FROM loaders_versions + WHERE version_id = $1 + ", + id as VersionId, + ) + .fetch_many(&mut *transaction) + .try_filter_map(|e| async { Ok(e.right().map(|c| c.id)) }) + .try_collect::>() .await?; sqlx::query!( @@ -234,7 +335,7 @@ impl Version { ", id as VersionId, ) - .execute(exec) + .execute(&mut *transaction) .await?; sqlx::query!( @@ -244,7 +345,7 @@ impl Version { ", id as VersionId, ) - .execute(exec) + .execute(&mut *transaction) .await?; sqlx::query!( @@ -254,11 +355,9 @@ impl Version { ", id as VersionId, ) - .execute(exec) + .execute(&mut *transaction) .await?; - use futures::TryStreamExt; - let files = sqlx::query!( " SELECT files.id, files.url, files.filename, files.is_primary FROM files @@ -266,7 +365,7 @@ impl Version { ", id as VersionId, ) - .fetch_many(exec) + .fetch_many(&mut *transaction) .try_filter_map(|e| async { Ok(e.right().map(|c| VersionFile { id: FileId(c.id), @@ -301,7 +400,7 @@ impl Version { ", id as VersionId ) - .execute(exec) + .execute(&mut *transaction) .await?; sqlx::query!( @@ -311,54 +410,71 @@ impl Version { ", id as VersionId, ) - .execute(exec) + .execute(&mut *transaction) .await?; + // Sync dependencies + + let project_id = sqlx::query!( + " + SELECT mod_id FROM versions WHERE id = $1 + ", + id as VersionId, + ) + .fetch_one(&mut *transaction) + .await?; + + let new_version_id = sqlx::query!( + " + SELECT v.id id + FROM versions v + INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id AND gvv.game_version_id IN (SELECT * FROM UNNEST($2::integer[])) + INNER JOIN loaders_versions lv ON lv.version_id = v.id AND lv.loader_id IN (SELECT * FROM UNNEST($3::integer[])) + WHERE v.mod_id = $1 + ORDER BY v.date_published DESC + LIMIT 1 + ", + project_id.mod_id, + &game_versions, + &loaders, + ) + .fetch_optional(&mut *transaction) + .await? + .map(|x| x.id); + + sqlx::query!( + " + UPDATE dependencies + SET dependency_id = $2 + WHERE dependency_id = $1 + ", + id as VersionId, + new_version_id, + ) + .execute(&mut *transaction) + .await?; + + sqlx::query!( + " + DELETE FROM dependencies WHERE mod_dependency_id = NULL AND dependency_id = NULL + ", + ) + .execute(&mut *transaction) + .await?; + + // delete version + sqlx::query!( " DELETE FROM versions WHERE id = $1 ", id as VersionId, ) - .execute(exec) + .execute(&mut *transaction) .await?; - - sqlx::query!( - " - DELETE FROM dependencies WHERE dependent_id = $1 - ", - id as VersionId, - ) - .execute(exec) - .await?; - Ok(Some(())) } - pub async fn get_dependencies<'a, E>( - id: VersionId, - exec: E, - ) -> Result, sqlx::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - use futures::stream::TryStreamExt; - - let vec = sqlx::query!( - " - SELECT dependency_id id FROM dependencies - WHERE dependent_id = $1 - ", - id as VersionId, - ) - .fetch_many(exec) - .try_filter_map(|e| async { Ok(e.right().map(|v| VersionId(v.id))) }) - .try_collect::>() - .await?; - - Ok(vec) - } - pub async fn get_project_versions<'a, E>( project_id: ProjectId, game_versions: Option>, @@ -491,7 +607,7 @@ impl Version { STRING_AGG(DISTINCT gv.version, ',') game_versions, STRING_AGG(DISTINCT l.loader, ',') loaders, STRING_AGG(DISTINCT f.id || ', ' || f.filename || ', ' || f.is_primary || ', ' || f.url, ' ,') files, STRING_AGG(DISTINCT h.algorithm || ', ' || encode(h.hash, 'escape') || ', ' || h.file_id, ' ,') hashes, - STRING_AGG(DISTINCT d.dependency_id || ', ' || d.dependency_type, ' ,') dependencies + STRING_AGG(DISTINCT COALESCE(d.dependency_id, 0) || ', ' || COALESCE(d.mod_dependency_id, 0) || ', ' || d.dependency_type, ' ,') dependencies FROM versions v INNER JOIN release_channels rc on v.release_channel = rc.id LEFT OUTER JOIN game_versions_versions gvv on v.id = gvv.joining_version_id @@ -557,11 +673,24 @@ impl Version { .for_each(|f| { let dependency: Vec<&str> = f.split(", ").collect(); - if dependency.len() >= 2 { - dependencies.push(( - VersionId(dependency[0].parse().unwrap_or(0)), - dependency[1].to_string(), - )) + if dependency.len() >= 3 { + dependencies.push(QueryDependency { + project_id: match &*dependency[2] { + "0" => None, + _ => match dependency[2].parse() { + Ok(x) => Some(ProjectId(x)), + Err(_) => None, + }, + }, + version_id: match &*dependency[0] { + "0" => None, + _ => match dependency[0].parse() { + Ok(x) => Some(VersionId(x)), + Err(_) => None, + }, + }, + dependency_type: dependency[1].to_string(), + }); } }); @@ -615,7 +744,7 @@ impl Version { STRING_AGG(DISTINCT gv.version, ',') game_versions, STRING_AGG(DISTINCT l.loader, ',') loaders, STRING_AGG(DISTINCT f.id || ', ' || f.filename || ', ' || f.is_primary || ', ' || f.url, ' ,') files, STRING_AGG(DISTINCT h.algorithm || ', ' || encode(h.hash, 'escape') || ', ' || h.file_id, ' ,') hashes, - STRING_AGG(DISTINCT d.dependency_id || ', ' || d.dependency_type, ' ,') dependencies + STRING_AGG(DISTINCT COALESCE(d.dependency_id, 0) || ', ' || COALESCE(d.mod_dependency_id, 0) || ', ' || d.dependency_type, ' ,') dependencies FROM versions v INNER JOIN release_channels rc on v.release_channel = rc.id LEFT OUTER JOIN game_versions_versions gvv on v.id = gvv.joining_version_id @@ -678,8 +807,28 @@ impl Version { v.dependencies.unwrap_or_default().split(" ,").for_each(|f| { let dependency: Vec<&str> = f.split(", ").collect(); - if dependency.len() >= 2 { - dependencies.push((VersionId(dependency[0].parse().unwrap_or(0)), dependency[1].to_string())) + if dependency.len() >= 3 { + dependencies.push(QueryDependency { + project_id: match &*dependency[2] { + "0" => None, + _ => { + match dependency[2].parse() { + Ok(x) => Some(ProjectId(x)), + Err(_) => None, + } + }, + }, + version_id: match &*dependency[0] { + "0" => None, + _ => { + match dependency[0].parse() { + Ok(x) => Some(VersionId(x)), + Err(_) => None, + } + }, + }, + dependency_type: dependency[1].to_string() + }); } }); @@ -743,7 +892,14 @@ pub struct QueryVersion { pub game_versions: Vec, pub loaders: Vec, pub featured: bool, - pub dependencies: Vec<(VersionId, String)>, + pub dependencies: Vec, +} + +#[derive(Clone)] +pub struct QueryDependency { + pub project_id: Option, + pub version_id: Option, + pub dependency_type: String, } #[derive(Clone)] diff --git a/src/database/postgres_database.rs b/src/database/postgres_database.rs index eed2ad17c..b04dbb959 100644 --- a/src/database/postgres_database.rs +++ b/src/database/postgres_database.rs @@ -11,7 +11,20 @@ pub async fn connect() -> Result { let database_url = dotenv::var("DATABASE_URL").expect("`DATABASE_URL` not in .env"); let pool = PgPoolOptions::new() - .max_connections(20) + .min_connections( + dotenv::var("DATABASE_MIN_CONNECTIONS") + .ok() + .map(|x| x.parse::().ok()) + .flatten() + .unwrap_or(16), + ) + .max_connections( + dotenv::var("DATABASE_MAX_CONNECTIONS") + .ok() + .map(|x| x.parse::().ok()) + .flatten() + .unwrap_or(16), + ) .connect(&database_url) .await?; diff --git a/src/main.rs b/src/main.rs index ab013f238..0305cea79 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,13 +11,13 @@ use search::indexing::index_projects; use search::indexing::IndexingSettings; use std::sync::Arc; -mod auth; mod database; mod file_hosting; mod models; mod routes; mod scheduler; mod search; +mod util; mod validate; #[derive(Debug, Options)] @@ -265,9 +265,23 @@ async fn main() -> std::io::Result<()> { .with_identifier(|req| { let connection_info = req.connection_info(); let ip = String::from( - connection_info - .remote_addr() - .ok_or(ARError::IdentificationError)?, + if dotenv::var("CLOUDFLARE_INTEGRATION") + .ok() + .map(|i| i.parse().unwrap()) + .unwrap_or(false) + { + if let Some(header) = req.headers().get("CF-Connecting-IP") { + header.to_str().map_err(|_| ARError::IdentificationError)? + } else { + connection_info + .remote_addr() + .ok_or(ARError::IdentificationError)? + } + } else { + connection_info + .remote_addr() + .ok_or(ARError::IdentificationError)? + }, ); let ignore_ips = dotenv::var("RATE_LIMIT_IGNORE_IPS") @@ -277,16 +291,16 @@ async fn main() -> std::io::Result<()> { if ignore_ips.contains(&ip) { // At an even distribution of numbers, this will allow at the most - // 3000 requests per minute from the frontend, which is reasonable - // (50 requests per second) - let random = rand::thread_rng().gen_range(1, 15); + // 18000 requests per minute from the frontend, which is reasonable + // (300 requests per second) + let random = rand::thread_rng().gen_range(1, 30); return Ok(format!("{}-{}", ip, random)); } Ok(ip) }) .with_interval(std::time::Duration::from_secs(60)) - .with_max_requests(200), + .with_max_requests(300), ) .wrap(sentry_actix::Sentry::new()) .data(pool.clone()) @@ -335,6 +349,7 @@ fn check_env_vars() -> bool { failed |= true; } + failed |= check_var::("SITE_URL"); failed |= check_var::("CDN_URL"); failed |= check_var::("DATABASE_URL"); failed |= check_var::("MEILISEARCH_ADDR"); diff --git a/src/models/notifications.rs b/src/models/notifications.rs index a768e8b1f..743ff2438 100644 --- a/src/models/notifications.rs +++ b/src/models/notifications.rs @@ -12,6 +12,8 @@ pub struct NotificationId(pub u64); pub struct Notification { pub id: NotificationId, pub user_id: UserId, + #[serde(rename = "type")] + pub type_: Option, pub title: String, pub text: String, pub link: String, diff --git a/src/models/projects.rs b/src/models/projects.rs index 6174cf8f1..6cd9beacc 100644 --- a/src/models/projects.rs +++ b/src/models/projects.rs @@ -12,13 +12,13 @@ use validator::Validate; pub struct ProjectId(pub u64); /// The ID of a specific version of a project -#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] #[serde(from = "Base62Id")] #[serde(into = "Base62Id")] pub struct VersionId(pub u64); /// A project returned from the API -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct Project { /// The ID of the project, encoded as a base62 string. pub id: ProjectId, @@ -40,8 +40,12 @@ pub struct Project { pub published: DateTime, /// The date at which the project was first published. pub updated: DateTime, + /// The status of the project pub status: ProjectStatus, + /// The rejection data of the project + pub rejection_data: Option, + /// The license of this project pub license: License, @@ -73,6 +77,12 @@ pub struct Project { pub donation_urls: Option>, } +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct RejectionReason { + pub reason: String, + pub body: Option, +} + #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "kebab-case")] pub enum SideType { @@ -134,6 +144,7 @@ pub struct DonationLink { #[serde(rename_all = "lowercase")] pub enum ProjectStatus { Approved, + Archived, Rejected, Draft, Unlisted, @@ -155,6 +166,7 @@ impl ProjectStatus { "approved" => ProjectStatus::Approved, "draft" => ProjectStatus::Draft, "unlisted" => ProjectStatus::Unlisted, + "archived" => ProjectStatus::Archived, _ => ProjectStatus::Unknown, } } @@ -166,6 +178,7 @@ impl ProjectStatus { ProjectStatus::Unlisted => "unlisted", ProjectStatus::Processing => "processing", ProjectStatus::Unknown => "unknown", + ProjectStatus::Archived => "archived", } } @@ -177,6 +190,7 @@ impl ProjectStatus { ProjectStatus::Unlisted => false, ProjectStatus::Processing => true, ProjectStatus::Unknown => true, + ProjectStatus::Archived => false, } } @@ -240,9 +254,11 @@ pub struct VersionFile { /// version's functionality #[derive(Serialize, Deserialize, Clone)] pub struct Dependency { - /// The filename of the file. - pub version_id: VersionId, - /// Whether the file is the primary file of a version + /// The specific version id that the dependency uses + pub version_id: Option, + /// The project ID that the dependency is synced with and auto-updated + pub project_id: Option, + /// The type of the dependency pub dependency_type: DependencyType, } diff --git a/src/routes/auth.rs b/src/routes/auth.rs index c59cf99e2..436c3f9a7 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -1,9 +1,9 @@ -use crate::auth::get_github_user_from_token; use crate::database::models::{generate_state_id, User}; use crate::models::error::ApiError; use crate::models::ids::base62_impl::{parse_base62, to_base62}; use crate::models::ids::DecodingError; use crate::models::users::Role; +use crate::util::auth::get_github_user_from_token; use actix_web::http::StatusCode; use actix_web::web::{scope, Data, Query, ServiceConfig}; use actix_web::{get, HttpResponse}; @@ -32,7 +32,7 @@ pub enum AuthorizationError { #[error("Invalid Authentication credentials")] InvalidCredentialsError, #[error("Authentication Error: {0}")] - AuthenticationError(#[from] crate::auth::AuthenticationError), + AuthenticationError(#[from] crate::util::auth::AuthenticationError), #[error("Error while decoding Base62")] DecodingError(#[from] DecodingError), } @@ -129,78 +129,82 @@ pub async fn auth_callback( let mut transaction = client.begin().await?; let state_id = parse_base62(&*info.state)?; - let result = sqlx::query!( + let result_option = sqlx::query!( " SELECT url,expires FROM states WHERE id = $1 ", state_id as i64 ) - .fetch_one(&mut *transaction) + .fetch_optional(&mut *transaction) .await?; - let now = Utc::now(); - let duration = result.expires.signed_duration_since(now); + if let Some(result) = result_option { + let now = Utc::now(); + let duration = result.expires.signed_duration_since(now); - if duration.num_seconds() < 0 { - return Err(AuthorizationError::InvalidCredentialsError); - } + if duration.num_seconds() < 0 { + return Err(AuthorizationError::InvalidCredentialsError); + } - sqlx::query!( - " + sqlx::query!( + " DELETE FROM states WHERE id = $1 ", - state_id as i64 - ) - .execute(&mut *transaction) - .await?; - - let client_id = dotenv::var("GITHUB_CLIENT_ID")?; - let client_secret = dotenv::var("GITHUB_CLIENT_SECRET")?; - - let url = format!( - "https://github.com/login/oauth/access_token?client_id={}&client_secret={}&code={}", - client_id, client_secret, info.code - ); - - let token: AccessToken = reqwest::Client::new() - .post(&url) - .header(reqwest::header::ACCEPT, "application/json") - .send() - .await? - .json() + state_id as i64 + ) + .execute(&mut *transaction) .await?; - let user = get_github_user_from_token(&*token.access_token).await?; + let client_id = dotenv::var("GITHUB_CLIENT_ID")?; + let client_secret = dotenv::var("GITHUB_CLIENT_SECRET")?; - let user_result = User::get_from_github_id(user.id, &mut *transaction).await?; - match user_result { - Some(x) => info!("{:?}", x.id), - None => { - let user_id = crate::database::models::generate_user_id(&mut transaction).await?; + let url = format!( + "https://github.com/login/oauth/access_token?client_id={}&client_secret={}&code={}", + client_id, client_secret, info.code + ); - User { - id: user_id, - github_id: Some(user.id as i64), - username: user.login, - name: user.name, - email: user.email, - avatar_url: Some(user.avatar_url), - bio: user.bio, - created: Utc::now(), - role: Role::Developer.to_string(), - } - .insert(&mut transaction) + let token: AccessToken = reqwest::Client::new() + .post(&url) + .header(reqwest::header::ACCEPT, "application/json") + .send() + .await? + .json() .await?; + + let user = get_github_user_from_token(&*token.access_token).await?; + + let user_result = User::get_from_github_id(user.id, &mut *transaction).await?; + match user_result { + Some(x) => info!("{:?}", x.id), + None => { + let user_id = crate::database::models::generate_user_id(&mut transaction).await?; + + User { + id: user_id, + github_id: Some(user.id as i64), + username: user.login, + name: user.name, + email: user.email, + avatar_url: Some(user.avatar_url), + bio: user.bio, + created: Utc::now(), + role: Role::Developer.to_string(), + } + .insert(&mut transaction) + .await?; + } } + + transaction.commit().await?; + + let redirect_url = format!("{}?code={}", result.url, token.access_token); + + Ok(HttpResponse::TemporaryRedirect() + .header("Location", &*redirect_url) + .json(AuthorizationInit { url: redirect_url })) + } else { + Err(AuthorizationError::InvalidCredentialsError) } - - transaction.commit().await?; - - let redirect_url = format!("{}?code={}", result.url, token.access_token); - - Ok(HttpResponse::TemporaryRedirect() - .header("Location", &*redirect_url) - .json(AuthorizationInit { url: redirect_url })) } diff --git a/src/routes/maven.rs b/src/routes/maven.rs index b695ca6ca..02df86c56 100644 --- a/src/routes/maven.rs +++ b/src/routes/maven.rs @@ -1,7 +1,7 @@ -use crate::auth::get_user_from_headers; use crate::database; use crate::models::projects::ProjectId; use crate::routes::ApiError; +use crate::util::auth::get_user_from_headers; use actix_web::{get, web, HttpRequest, HttpResponse}; use sqlx::PgPool; use yaserde_derive::YaSerialize; diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 0becc6ed3..8dfb66714 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -55,7 +55,8 @@ pub fn projects_config(cfg: &mut web::ServiceConfig) { .service(projects::project_follow) .service(projects::project_unfollow) .service(teams::team_members_get_project) - .service(web::scope("{project_id}").service(versions::version_list)), + .service(web::scope("{project_id}").service(versions::version_list)) + .service(projects::dependency_list), ); } @@ -119,6 +120,7 @@ pub fn teams_config(cfg: &mut web::ServiceConfig) { pub fn notifications_config(cfg: &mut web::ServiceConfig) { cfg.service(notifications::notifications_get); + cfg.service(notifications::notification_delete); cfg.service( web::scope("notification") @@ -152,13 +154,13 @@ pub enum ApiError { #[error("Deserialization error: {0}")] JsonError(#[from] serde_json::Error), #[error("Authentication Error: {0}")] - AuthenticationError(#[from] crate::auth::AuthenticationError), + AuthenticationError(#[from] crate::util::auth::AuthenticationError), #[error("Authentication Error: {0}")] CustomAuthenticationError(String), #[error("Invalid Input: {0}")] InvalidInputError(String), #[error("Error while validating input: {0}")] - ValidationError(#[from] validator::ValidationErrors), + ValidationError(String), #[error("Search Error: {0}")] SearchError(#[from] meilisearch_sdk::errors::Error), #[error("Indexing Error: {0}")] diff --git a/src/routes/moderation.rs b/src/routes/moderation.rs index f2d548a77..5c665fb31 100644 --- a/src/routes/moderation.rs +++ b/src/routes/moderation.rs @@ -1,7 +1,7 @@ use super::ApiError; -use crate::auth::check_is_moderator_from_headers; use crate::database; use crate::models::projects::{Project, ProjectStatus}; +use crate::util::auth::check_is_moderator_from_headers; use actix_web::{get, web, HttpRequest, HttpResponse}; use serde::Deserialize; use sqlx::PgPool; diff --git a/src/routes/notifications.rs b/src/routes/notifications.rs index 2cb9b4974..c9d536d60 100644 --- a/src/routes/notifications.rs +++ b/src/routes/notifications.rs @@ -1,8 +1,8 @@ -use crate::auth::get_user_from_headers; use crate::database; use crate::models::ids::NotificationId; use crate::models::notifications::{Notification, NotificationAction}; use crate::routes::ApiError; +use crate::util::auth::get_user_from_headers; use actix_web::{delete, get, web, HttpRequest, HttpResponse}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; @@ -70,6 +70,7 @@ pub fn convert_notification( Notification { id: notif.id.into(), user_id: notif.user_id.into(), + type_: notif.notification_type, title: notif.title, text: notif.text, link: notif.link, @@ -101,7 +102,12 @@ pub async fn notification_delete( if let Some(data) = notification_data { if data.user_id == user.id.into() || user.role.is_mod() { - database::models::notification_item::Notification::remove(id.into(), &**pool).await?; + let mut transaction = pool.begin().await?; + + database::models::notification_item::Notification::remove(id.into(), &mut transaction) + .await?; + + transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { @@ -113,3 +119,38 @@ pub async fn notification_delete( Ok(HttpResponse::NotFound().body("")) } } + +#[delete("notifications")] +pub async fn notifications_delete( + req: HttpRequest, + web::Query(ids): web::Query, + pool: web::Data, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + + let notification_ids = serde_json::from_str::>(&*ids.ids)? + .into_iter() + .map(|x| x.into()) + .collect(); + + let mut transaction = pool.begin().await?; + + let notifications_data = + database::models::notification_item::Notification::get_many(notification_ids, &**pool) + .await?; + + let mut notifications: Vec = Vec::new(); + + for notification in notifications_data { + if notification.user_id == user.id.into() || user.role.is_mod() { + notifications.push(notification.id); + } + } + + database::models::notification_item::Notification::remove_many(notifications, &mut transaction) + .await?; + + transaction.commit().await?; + + Ok(HttpResponse::NoContent().body("")) +} diff --git a/src/routes/project_creation.rs b/src/routes/project_creation.rs index ccc527748..24d48b652 100644 --- a/src/routes/project_creation.rs +++ b/src/routes/project_creation.rs @@ -1,4 +1,3 @@ -use crate::auth::{get_user_from_headers, AuthenticationError}; use crate::database::models; use crate::file_hosting::{FileHost, FileHostingError}; use crate::models::error::ApiError; @@ -8,13 +7,13 @@ use crate::models::projects::{ use crate::models::users::UserId; use crate::routes::version_creation::InitialVersionData; use crate::search::indexing::{queue::CreationQueue, IndexingError}; +use crate::util::auth::{get_user_from_headers, AuthenticationError}; +use crate::util::validate::validation_errors_to_string; use actix_multipart::{Field, Multipart}; use actix_web::http::StatusCode; use actix_web::web::Data; use actix_web::{post, HttpRequest, HttpResponse}; use futures::stream::StreamExt; -use lazy_static::lazy_static; -use regex::Regex; use serde::{Deserialize, Serialize}; use sqlx::postgres::PgPool; use std::sync::Arc; @@ -36,7 +35,7 @@ pub enum CreateError { #[error("Error while parsing JSON: {0}")] SerDeError(#[from] serde_json::Error), #[error("Error while validating input: {0}")] - ValidationError(#[from] validator::ValidationErrors), + ValidationError(String), #[error("Error while uploading file")] FileHostingError(#[from] FileHostingError), #[error("Error while validating uploaded file: {0}")] @@ -116,10 +115,6 @@ impl actix_web::ResponseError for CreateError { } } -lazy_static! { - static ref RE_URL_SAFE: Regex = Regex::new(r"^[a-zA-Z0-9_-]*$").unwrap(); -} - fn default_project_type() -> String { "mod".to_string() } @@ -134,7 +129,10 @@ struct ProjectCreateData { #[serde(default = "default_project_type")] /// The project type of this mod pub project_type: String, - #[validate(length(min = 3, max = 64), regex = "RE_URL_SAFE")] + #[validate( + length(min = 3, max = 64), + regex = "crate::util::validate::RE_URL_SAFE" + )] #[serde(alias = "mod_slug")] /// The slug of a project, used for vanity URLs pub slug: String, @@ -153,6 +151,7 @@ struct ProjectCreateData { pub server_side: SideType, #[validate(length(max = 64))] + #[validate] /// A list of initial versions to upload with the created project pub initial_versions: Vec, #[validate(length(max = 3))] @@ -326,7 +325,9 @@ pub async fn project_create_inner( } let create_data: ProjectCreateData = serde_json::from_slice(&data)?; - create_data.validate()?; + create_data + .validate() + .map_err(|err| CreateError::InvalidInput(validation_errors_to_string(err, None)))?; let slug_project_id_option: Option = serde_json::from_str(&*format!("\"{}\"", create_data.slug)).ok(); @@ -498,6 +499,12 @@ pub async fn project_create_inner( status = ProjectStatus::Draft; } else { status = ProjectStatus::Processing; + + if project_create_data.initial_versions.is_empty() { + return Err(CreateError::InvalidInput(String::from( + "Project submitted for review with no initial versions", + ))); + } } let status_id = models::StatusId::get_id(&status, &mut *transaction) @@ -590,6 +597,7 @@ pub async fn project_create_inner( published: now, updated: now, status: status.clone(), + rejection_data: None, license: License { id: project_create_data.license_id.clone(), name: "".to_string(), @@ -622,6 +630,12 @@ pub async fn project_create_inner( ) .await?; indexing_queue.add(index_project); + + if let Ok(webhook_url) = dotenv::var("MODERATION_DISCORD_WEBHOOK") { + crate::util::webhook::send_discord_webhook(response.clone(), webhook_url) + .await + .ok(); + } } Ok(HttpResponse::Ok().json(response)) @@ -643,7 +657,9 @@ async fn create_initial_version( ))); } - version_data.validate()?; + version_data + .validate() + .map_err(|err| CreateError::ValidationError(validation_errors_to_string(err, None)))?; // Randomly generate a new id to be used for the version let version_id: VersionId = models::generate_version_id(transaction).await?.into(); @@ -684,7 +700,11 @@ async fn create_initial_version( let dependencies = version_data .dependencies .iter() - .map(|x| ((x.version_id).into(), x.dependency_type.to_string())) + .map(|d| models::version_item::DependencyBuilder { + version_id: d.version_id.map(|x| x.into()), + project_id: d.project_id.map(|x| x.into()), + dependency_type: d.dependency_type.to_string(), + }) .collect::>(); let version = models::version_item::VersionBuilder { diff --git a/src/routes/projects.rs b/src/routes/projects.rs index 257359e10..c4e52715c 100644 --- a/src/routes/projects.rs +++ b/src/routes/projects.rs @@ -1,21 +1,23 @@ -use crate::auth::get_user_from_headers; use crate::database; +use crate::database::cache::project_cache::remove_cache_project; +use crate::database::cache::query_project_cache::remove_cache_query_project; use crate::file_hosting::FileHost; use crate::models; use crate::models::projects::{ - DonationLink, License, ProjectId, ProjectStatus, SearchRequest, SideType, + DonationLink, License, ProjectId, ProjectStatus, RejectionReason, SearchRequest, SideType, }; use crate::models::teams::Permissions; use crate::routes::ApiError; use crate::search::indexing::queue::CreationQueue; use crate::search::{search_for_project, SearchConfig, SearchError}; +use crate::util::auth::get_user_from_headers; +use crate::util::validate::validation_errors_to_string; use actix_web::web::Data; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; use futures::StreamExt; -use lazy_static::lazy_static; -use regex::Regex; use serde::{Deserialize, Serialize}; use sqlx::PgPool; +use std::collections::HashMap; use std::sync::Arc; use validator::Validate; @@ -91,7 +93,8 @@ pub async fn project_get( let string = info.into_inner().0; let project_data = - database::models::Project::get_full_from_slug_or_project_id(string, &**pool).await?; + database::models::Project::get_full_from_slug_or_project_id(string.clone(), &**pool) + .await?; let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); @@ -129,6 +132,94 @@ pub async fn project_get( } } +struct DependencyInfo { + pub project: Option, + pub version: Option, +} + +#[get("dependencies")] +pub async fn dependency_list( + info: web::Path<(String,)>, + pool: web::Data, +) -> Result { + let string = info.into_inner().0; + + let result = database::models::Project::get_from_slug_or_project_id(string, &**pool).await?; + + if let Some(project) = result { + let id = project.id; + + use futures::stream::TryStreamExt; + + let dependencies = sqlx::query!( + " + SELECT d.dependent_id, d.dependency_id, d.mod_dependency_id + FROM versions v + INNER JOIN dependencies d ON d.dependent_id = v.id + WHERE v.mod_id = $1 + ", + id as database::models::ProjectId + ) + .fetch_many(&**pool) + .try_filter_map(|e| async { + Ok(e.right().map(|x| { + ( + database::models::VersionId(x.dependent_id), + x.dependency_id.map(database::models::VersionId), + x.mod_dependency_id.map(database::models::ProjectId), + ) + })) + }) + .try_collect::, + Option, + )>>() + .await?; + + let projects = database::Project::get_many_full( + dependencies.iter().map(|x| x.2).flatten().collect(), + &**pool, + ) + .await?; + let versions = database::Version::get_many_full( + dependencies.iter().map(|x| x.1).flatten().collect(), + &**pool, + ) + .await?; + + let mut response: HashMap = HashMap::new(); + + for dependency in dependencies { + response.insert( + dependency.0.into(), + DependencyInfo { + project: if let Some(id) = dependency.2 { + projects + .iter() + .find(|x| x.inner.id == id) + .map(|x| convert_project(x.clone())) + } else { + None + }, + version: if let Some(id) = dependency.1 { + versions + .iter() + .find(|x| x.id == id) + .map(|x| super::versions::convert_version(x.clone())) + } else { + None + }, + }, + ); + } + + Ok(HttpResponse::NotFound().body("")) + } else { + Ok(HttpResponse::NotFound().body("")) + } +} + pub fn convert_project( data: database::models::project_item::QueryProject, ) -> models::projects::Project { @@ -146,6 +237,14 @@ pub fn convert_project( published: m.published, updated: m.updated, status: data.status, + rejection_data: if let Some(reason) = m.rejection_reason { + Some(RejectionReason { + reason, + body: m.rejection_body, + }) + } else { + None + }, license: License { id: data.license_id, name: data.license_name, @@ -175,10 +274,6 @@ pub fn convert_project( } } -lazy_static! { - static ref RE_URL_SAFE: Regex = Regex::new(r"^[a-zA-Z0-9_-]*$").unwrap(); -} - /// A project returned from the API #[derive(Serialize, Deserialize, Validate)] pub struct EditProject { @@ -188,7 +283,6 @@ pub struct EditProject { pub description: Option, #[validate(length(max = 65536))] pub body: Option, - pub status: Option, #[validate(length(max = 3))] pub categories: Option>, #[serde( @@ -236,8 +330,26 @@ pub struct EditProject { skip_serializing_if = "Option::is_none", with = "::serde_with::rust::double_option" )] - #[validate(length(min = 3, max = 64), regex = "RE_URL_SAFE")] + #[validate( + length(min = 3, max = 64), + regex = "crate::util::validate::RE_URL_SAFE" + )] pub slug: Option>, + pub status: Option, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "::serde_with::rust::double_option" + )] + #[validate(length(max = 2000))] + pub rejection_reason: Option>, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "::serde_with::rust::double_option" + )] + #[validate(length(max = 65536))] + pub rejection_body: Option>, } #[patch("{id}")] @@ -251,11 +363,14 @@ pub async fn project_edit( ) -> Result { let user = get_user_from_headers(req.headers(), &**pool).await?; - new_project.validate()?; + new_project + .validate() + .map_err(|err| ApiError::ValidationError(validation_errors_to_string(err, None)))?; let string = info.into_inner().0; let result = - database::models::Project::get_full_from_slug_or_project_id(string, &**pool).await?; + database::models::Project::get_full_from_slug_or_project_id(string.clone(), &**pool) + .await?; if let Some(project_item) = result { let id = project_item.inner.id; @@ -337,6 +452,12 @@ pub async fn project_edit( )); } + if status == &ProjectStatus::Processing && project_item.versions.is_empty() { + return Err(ApiError::InvalidInputError(String::from( + "Project submitted for review with no initial versions", + ))); + } + let status_id = database::models::StatusId::get_id(&status, &mut *transaction) .await? .ok_or_else(|| { @@ -357,6 +478,30 @@ pub async fn project_edit( .execute(&mut *transaction) .await?; + if project_item.status == ProjectStatus::Processing { + sqlx::query!( + " + UPDATE mods + SET rejection_reason = NULL + WHERE (id = $1) + ", + id as database::models::ids::ProjectId, + ) + .execute(&mut *transaction) + .await?; + + sqlx::query!( + " + UPDATE mods + SET rejection_body = NULL + WHERE (id = $1) + ", + id as database::models::ids::ProjectId, + ) + .execute(&mut *transaction) + .await?; + } + if project_item.status.is_searchable() && !status.is_searchable() { delete_from_index(id.into(), config).await?; } else if !project_item.status.is_searchable() && status.is_searchable() { @@ -365,6 +510,15 @@ pub async fn project_edit( .await?; indexing_queue.add(index_project); + + if let Ok(webhook_url) = dotenv::var("MODERATION_DISCORD_WEBHOOK") { + crate::util::webhook::send_discord_webhook( + convert_project(project_item.clone()), + webhook_url, + ) + .await + .ok(); + } } } @@ -684,6 +838,48 @@ pub async fn project_edit( } } + if let Some(rejection_reason) = &new_project.rejection_reason { + if !user.role.is_mod() { + return Err(ApiError::CustomAuthenticationError( + "You do not have the permissions to edit the rejection reason of this project!" + .to_string(), + )); + } + + sqlx::query!( + " + UPDATE mods + SET rejection_reason = $1 + WHERE (id = $2) + ", + rejection_reason.as_deref(), + id as database::models::ids::ProjectId, + ) + .execute(&mut *transaction) + .await?; + } + + if let Some(rejection_body) = &new_project.rejection_body { + if !user.role.is_mod() { + return Err(ApiError::CustomAuthenticationError( + "You do not have the permissions to edit the rejection body of this project!" + .to_string(), + )); + } + + sqlx::query!( + " + UPDATE mods + SET rejection_body = $1 + WHERE (id = $2) + ", + rejection_body.as_deref(), + id as database::models::ids::ProjectId, + ) + .execute(&mut *transaction) + .await?; + } + if let Some(body) = &new_project.body { if !perms.contains(Permissions::EDIT_BODY) { return Err(ApiError::CustomAuthenticationError( @@ -705,6 +901,9 @@ pub async fn project_edit( .await?; } + remove_cache_project(string.clone()).await; + remove_cache_query_project(string).await; + transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { @@ -736,11 +935,12 @@ pub async fn project_icon_edit( let user = get_user_from_headers(req.headers(), &**pool).await?; let string = info.into_inner().0; - let project_item = database::models::Project::get_from_slug_or_project_id(string, &**pool) - .await? - .ok_or_else(|| { - ApiError::InvalidInputError("The specified project does not exist!".to_string()) - })?; + let project_item = + database::models::Project::get_from_slug_or_project_id(string.clone(), &**pool) + .await? + .ok_or_else(|| { + ApiError::InvalidInputError("The specified project does not exist!".to_string()) + })?; if !user.role.is_mod() { let team_member = database::models::TeamMember::get_from_user_id( @@ -782,12 +982,14 @@ pub async fn project_icon_edit( ))); } + let hash = sha1::Sha1::from(bytes.clone()).hexdigest(); + let project_id: ProjectId = project_item.id.into(); let upload_data = file_host .upload_file( content_type, - &format!("data/{}/icon.{}", project_id, ext.ext), + &format!("data/{}/{}.{}", project_id, hash, ext.ext), bytes.to_vec(), ) .await?; @@ -804,6 +1006,9 @@ pub async fn project_icon_edit( .execute(&**pool) .await?; + remove_cache_project(string.clone()).await; + remove_cache_query_project(string).await; + Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::InvalidInputError(format!( @@ -823,7 +1028,7 @@ pub async fn project_delete( let user = get_user_from_headers(req.headers(), &**pool).await?; let string = info.into_inner().0; - let project = database::models::Project::get_from_slug_or_project_id(string, &**pool) + let project = database::models::Project::get_from_slug_or_project_id(string.clone(), &**pool) .await? .ok_or_else(|| { ApiError::InvalidInputError("The specified project does not exist!".to_string()) @@ -851,7 +1056,14 @@ pub async fn project_delete( } } - let result = database::models::Project::remove_full(project.id, &**pool).await?; + let mut transaction = pool.begin().await?; + + let result = database::models::Project::remove_full(project.id, &mut transaction).await?; + + remove_cache_project(string.clone()).await; + remove_cache_query_project(string).await; + + transaction.commit().await?; delete_from_index(project.id.into(), config).await?; @@ -893,6 +1105,8 @@ pub async fn project_follow( .unwrap_or(false); if !following { + let mut transaction = pool.begin().await?; + sqlx::query!( " UPDATE mods @@ -901,7 +1115,7 @@ pub async fn project_follow( ", project_id as database::models::ids::ProjectId, ) - .execute(&**pool) + .execute(&mut *transaction) .await?; sqlx::query!( @@ -912,9 +1126,11 @@ pub async fn project_follow( user_id as database::models::ids::UserId, project_id as database::models::ids::ProjectId ) - .execute(&**pool) + .execute(&mut *transaction) .await?; + transaction.commit().await?; + Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::InvalidInputError( @@ -954,6 +1170,8 @@ pub async fn project_unfollow( .unwrap_or(false); if following { + let mut transaction = pool.begin().await?; + sqlx::query!( " UPDATE mods @@ -962,7 +1180,7 @@ pub async fn project_unfollow( ", project_id as database::models::ids::ProjectId, ) - .execute(&**pool) + .execute(&mut *transaction) .await?; sqlx::query!( @@ -973,9 +1191,11 @@ pub async fn project_unfollow( user_id as database::models::ids::UserId, project_id as database::models::ids::ProjectId ) - .execute(&**pool) + .execute(&mut *transaction) .await?; + transaction.commit().await?; + Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::InvalidInputError( diff --git a/src/routes/reports.rs b/src/routes/reports.rs index 2fa047ccf..b8cb8e9bf 100644 --- a/src/routes/reports.rs +++ b/src/routes/reports.rs @@ -1,7 +1,7 @@ -use crate::auth::{check_is_moderator_from_headers, get_user_from_headers}; use crate::models::ids::{ProjectId, UserId, VersionId}; use crate::models::reports::{ItemType, Report}; 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 futures::StreamExt; use serde::Deserialize; diff --git a/src/routes/tags.rs b/src/routes/tags.rs index 8e345f767..cbdffa1e2 100644 --- a/src/routes/tags.rs +++ b/src/routes/tags.rs @@ -1,7 +1,7 @@ use super::ApiError; -use crate::auth::check_is_admin_from_headers; use crate::database::models; use crate::database::models::categories::{DonationPlatform, License, ProjectType, ReportType}; +use crate::util::auth::check_is_admin_from_headers; use actix_web::{delete, get, put, web, HttpRequest, HttpResponse}; use models::categories::{Category, GameVersion, Loader}; use sqlx::PgPool; diff --git a/src/routes/teams.rs b/src/routes/teams.rs index 651411a54..398b1fd65 100644 --- a/src/routes/teams.rs +++ b/src/routes/teams.rs @@ -1,4 +1,3 @@ -use crate::auth::get_user_from_headers; use crate::database::models::notification_item::{NotificationActionBuilder, NotificationBuilder}; use crate::database::models::team_item::QueryTeamMember; use crate::database::models::TeamMember; @@ -6,6 +5,7 @@ use crate::models::ids::ProjectId; use crate::models::teams::{Permissions, TeamId}; use crate::models::users::UserId; use crate::routes::ApiError; +use crate::util::auth::get_user_from_headers; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; @@ -246,6 +246,7 @@ pub async fn add_team_member( let team: TeamId = team_id.into(); NotificationBuilder { + notification_type: Some("team_invite".to_string()), title: "You have been invited to join a team!".to_string(), text: format!( "Team invite from {} to join the team for project {}", diff --git a/src/routes/users.rs b/src/routes/users.rs index 6e8af994e..841876954 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -1,4 +1,3 @@ -use crate::auth::get_user_from_headers; use crate::database::models::User; use crate::file_hosting::FileHost; use crate::models::notifications::Notification; @@ -6,6 +5,8 @@ use crate::models::projects::{Project, ProjectStatus}; use crate::models::users::{Role, UserId}; use crate::routes::notifications::convert_notification; use crate::routes::ApiError; +use crate::util::auth::get_user_from_headers; +use crate::util::validate::validation_errors_to_string; use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse}; use futures::StreamExt; use lazy_static::lazy_static; @@ -166,7 +167,9 @@ pub async fn user_edit( ) -> Result { let user = get_user_from_headers(req.headers(), &**pool).await?; - new_user.validate()?; + new_user + .validate() + .map_err(|err| ApiError::ValidationError(validation_errors_to_string(err, None)))?; let id_option = crate::database::models::User::get_id_from_username_or_id(info.into_inner().0, &**pool) @@ -396,13 +399,17 @@ pub async fn user_delete( )); } + let mut transaction = pool.begin().await?; + let result; if &*removal_type.removal_type == "full" { - result = crate::database::models::User::remove_full(id, &**pool).await?; + result = crate::database::models::User::remove_full(id, &mut transaction).await?; } else { - result = crate::database::models::User::remove(id, &**pool).await?; + result = crate::database::models::User::remove(id, &mut transaction).await?; }; + transaction.commit().await?; + if result.is_some() { Ok(HttpResponse::NoContent().body("")) } else { diff --git a/src/routes/v1/moderation.rs b/src/routes/v1/moderation.rs index 7f6adf3b5..d206bfff8 100644 --- a/src/routes/v1/moderation.rs +++ b/src/routes/v1/moderation.rs @@ -1,8 +1,8 @@ -use crate::auth::check_is_moderator_from_headers; use crate::database; use crate::models::projects::{Project, ProjectStatus}; use crate::routes::moderation::ResultCount; use crate::routes::ApiError; +use crate::util::auth::check_is_moderator_from_headers; use actix_web::web; use actix_web::{get, HttpRequest, HttpResponse}; use sqlx::PgPool; diff --git a/src/routes/v1/mods.rs b/src/routes/v1/mods.rs index e72609562..443062934 100644 --- a/src/routes/v1/mods.rs +++ b/src/routes/v1/mods.rs @@ -1,4 +1,3 @@ -use crate::auth::get_user_from_headers; use crate::file_hosting::FileHost; use crate::models::projects::SearchRequest; use crate::routes::project_creation::{project_create_inner, undo_uploads, CreateError}; @@ -6,6 +5,7 @@ use crate::routes::projects::{convert_project, ProjectIds}; use crate::routes::ApiError; use crate::search::indexing::queue::CreationQueue; use crate::search::{search_for_project, SearchConfig, SearchError}; +use crate::util::auth::get_user_from_headers; use crate::{database, models}; use actix_multipart::Multipart; use actix_web::web; diff --git a/src/routes/v1/reports.rs b/src/routes/v1/reports.rs index 4ad37e07a..f9bcd0a7e 100644 --- a/src/routes/v1/reports.rs +++ b/src/routes/v1/reports.rs @@ -1,8 +1,8 @@ -use crate::auth::{check_is_moderator_from_headers, get_user_from_headers}; use crate::models::ids::ReportId; use crate::models::projects::{ProjectId, VersionId}; use crate::models::users::UserId; use crate::routes::ApiError; +use crate::util::auth::{check_is_moderator_from_headers, get_user_from_headers}; use actix_web::web; use actix_web::{get, post, HttpRequest, HttpResponse}; use chrono::{DateTime, Utc}; diff --git a/src/routes/v1/tags.rs b/src/routes/v1/tags.rs index deb4aebb4..a447c5727 100644 --- a/src/routes/v1/tags.rs +++ b/src/routes/v1/tags.rs @@ -1,6 +1,6 @@ -use crate::auth::check_is_admin_from_headers; use crate::database::models::categories::{Category, GameVersion, Loader, ProjectType}; use crate::routes::ApiError; +use crate::util::auth::check_is_admin_from_headers; use actix_web::{get, put, web}; use actix_web::{HttpRequest, HttpResponse}; use sqlx::PgPool; diff --git a/src/routes/v1/teams.rs b/src/routes/v1/teams.rs index 4ac474465..fe125d7b3 100644 --- a/src/routes/v1/teams.rs +++ b/src/routes/v1/teams.rs @@ -1,7 +1,7 @@ -use crate::auth::get_user_from_headers; use crate::models::teams::{Permissions, TeamId}; use crate::models::users::UserId; use crate::routes::ApiError; +use crate::util::auth::get_user_from_headers; use actix_web::{get, web, HttpRequest, HttpResponse}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; diff --git a/src/routes/v1/users.rs b/src/routes/v1/users.rs index a191067c0..06e8807d1 100644 --- a/src/routes/v1/users.rs +++ b/src/routes/v1/users.rs @@ -1,8 +1,8 @@ -use crate::auth::get_user_from_headers; use crate::database::models::User; use crate::models::ids::UserId; use crate::models::projects::{ProjectId, ProjectStatus}; use crate::routes::ApiError; +use crate::util::auth::get_user_from_headers; use actix_web::web; use actix_web::{get, HttpRequest, HttpResponse}; use sqlx::PgPool; diff --git a/src/routes/v1/versions.rs b/src/routes/v1/versions.rs index e84b90ae8..3e0df3b59 100644 --- a/src/routes/v1/versions.rs +++ b/src/routes/v1/versions.rs @@ -1,10 +1,10 @@ -use crate::auth::get_user_from_headers; use crate::file_hosting::FileHost; use crate::models::ids::{ProjectId, UserId, VersionId}; use crate::models::projects::{Dependency, GameVersion, Loader, Version, VersionFile, VersionType}; use crate::models::teams::Permissions; use crate::routes::versions::{convert_version, VersionIds, VersionListFilters}; use crate::routes::ApiError; +use crate::util::auth::get_user_from_headers; use crate::{database, models, Pepper}; use actix_web::{delete, get, web, HttpRequest, HttpResponse}; use chrono::{DateTime, Utc}; diff --git a/src/routes/version_creation.rs b/src/routes/version_creation.rs index b31b4f40a..8bf2d39da 100644 --- a/src/routes/version_creation.rs +++ b/src/routes/version_creation.rs @@ -1,4 +1,3 @@ -use crate::auth::get_user_from_headers; use crate::database::models; use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::version_item::{VersionBuilder, VersionFileBuilder}; @@ -8,28 +7,27 @@ use crate::models::projects::{ }; use crate::models::teams::Permissions; use crate::routes::project_creation::{CreateError, UploadedFile}; +use crate::util::auth::get_user_from_headers; +use crate::util::validate::validation_errors_to_string; use crate::validate::{validate_file, ValidationResult}; use actix_multipart::{Field, Multipart}; use actix_web::web::Data; use actix_web::{post, HttpRequest, HttpResponse}; use futures::stream::StreamExt; -use lazy_static::lazy_static; -use regex::Regex; use serde::{Deserialize, Serialize}; use sqlx::postgres::PgPool; use validator::Validate; -lazy_static! { - static ref RE_URL_SAFE: Regex = Regex::new(r"^[a-zA-Z0-9_\-.]*$").unwrap(); -} - #[derive(Serialize, Deserialize, Validate, Clone)] pub struct InitialVersionData { #[serde(alias = "mod_id")] pub project_id: Option, #[validate(length(min = 1, max = 256))] pub file_parts: Vec, - #[validate(length(min = 1, max = 64), regex = "RE_URL_SAFE")] + #[validate( + length(min = 1, max = 64), + regex = "crate::util::validate::RE_URL_SAFE" + )] pub version_number: String, #[validate(length(min = 3, max = 256))] pub version_title: String, @@ -127,7 +125,9 @@ async fn version_create_inner( )); } - version_create_data.validate()?; + version_create_data.validate().map_err(|err| { + CreateError::ValidationError(validation_errors_to_string(err, None)) + })?; let project_id: models::ProjectId = version_create_data.project_id.unwrap().into(); @@ -234,7 +234,11 @@ async fn version_create_inner( let dependencies = version_create_data .dependencies .iter() - .map(|x| ((x.version_id).into(), x.dependency_type.to_string())) + .map(|d| models::version_item::DependencyBuilder { + version_id: d.version_id.map(|x| x.into()), + project_id: d.project_id.map(|x| x.into()), + dependency_type: d.dependency_type.to_string(), + }) .collect::>(); version_builder = Some(VersionBuilder { @@ -332,9 +336,10 @@ async fn version_create_inner( let version_id: VersionId = builder.version_id.into(); NotificationBuilder { - title: "A project you followed has been updated!".to_string(), + notification_type: Some("project_update".to_string()), + title: format!("**{}** has been updated!", result.title), text: format!( - "Project {} has been updated to version {}", + "The project, {}, has released a new version: {}", result.title, version_data.version_number.clone() ), diff --git a/src/routes/version_file.rs b/src/routes/version_file.rs index 360ec880f..3bf7f34cb 100644 --- a/src/routes/version_file.rs +++ b/src/routes/version_file.rs @@ -1,9 +1,9 @@ use super::ApiError; -use crate::auth::get_user_from_headers; use crate::file_hosting::FileHost; use crate::models; use crate::models::projects::{GameVersion, Loader}; use crate::models::teams::Permissions; +use crate::util::auth::get_user_from_headers; use crate::{database, Pepper}; use actix_web::{delete, get, post, web, HttpRequest, HttpResponse}; use serde::{Deserialize, Serialize}; @@ -118,7 +118,19 @@ async fn download_version_inner( pepper: &web::Data, ) -> Result<(), ApiError> { let real_ip = req.connection_info(); - let ip_option = real_ip.borrow().remote_addr(); + let ip_option = if dotenv::var("CLOUDFLARE_INTEGRATION") + .ok() + .map(|i| i.parse().unwrap()) + .unwrap_or(false) + { + if let Some(header) = req.headers().get("CF-Connecting-IP") { + header.to_str().ok() + } else { + real_ip.borrow().remote_addr() + } + } else { + real_ip.borrow().remote_addr() + }; if let Some(ip) = ip_option { let hash = sha1::Sha1::from(format!("{}{}", ip, pepper.pepper)).hexdigest(); diff --git a/src/routes/versions.rs b/src/routes/versions.rs index 2f5669a67..63e59c22b 100644 --- a/src/routes/versions.rs +++ b/src/routes/versions.rs @@ -1,12 +1,11 @@ use super::ApiError; -use crate::auth::get_user_from_headers; use crate::database; use crate::models; use crate::models::projects::{Dependency, DependencyType}; use crate::models::teams::Permissions; +use crate::util::auth::get_user_from_headers; +use crate::util::validate::validation_errors_to_string; use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse}; -use lazy_static::lazy_static; -use regex::Regex; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use validator::Validate; @@ -189,8 +188,9 @@ pub fn convert_version( .dependencies .into_iter() .map(|d| Dependency { - version_id: d.0.into(), - dependency_type: DependencyType::from_str(&*d.1), + version_id: d.version_id.map(|x| x.into()), + project_id: d.project_id.map(|x| x.into()), + dependency_type: DependencyType::from_str(&*d.dependency_type), }) .collect(), game_versions: data @@ -206,15 +206,14 @@ pub fn convert_version( } } -lazy_static! { - static ref RE_URL_SAFE: Regex = Regex::new(r"^[a-zA-Z0-9_-]*$").unwrap(); -} - #[derive(Serialize, Deserialize, Validate)] pub struct EditVersion { #[validate(length(min = 3, max = 256))] pub name: Option, - #[validate(length(min = 1, max = 64), regex = "RE_URL_SAFE")] + #[validate( + length(min = 1, max = 64), + regex = "crate::util::validate::RE_URL_SAFE" + )] pub version_number: Option, #[validate(length(max = 65536))] pub changelog: Option, @@ -236,7 +235,9 @@ pub async fn version_edit( ) -> Result { let user = get_user_from_headers(req.headers(), &**pool).await?; - new_version.validate()?; + new_version + .validate() + .map_err(|err| ApiError::ValidationError(validation_errors_to_string(err, None)))?; let version_id = info.into_inner().0; let id = version_id.into(); @@ -332,21 +333,17 @@ pub async fn version_edit( .execute(&mut *transaction) .await?; - for dependency in dependencies { - let dependency_id: database::models::ids::VersionId = - dependency.version_id.clone().into(); + let builders = dependencies + .iter() + .map(|x| database::models::version_item::DependencyBuilder { + project_id: x.project_id.clone().map(|x| x.into()), + version_id: x.version_id.clone().map(|x| x.into()), + dependency_type: x.dependency_type.to_string(), + }) + .collect::>(); - sqlx::query!( - " - INSERT INTO dependencies (dependent_id, dependency_id, dependency_type) - VALUES ($1, $2, $3) - ", - id as database::models::ids::VersionId, - dependency_id as database::models::ids::VersionId, - dependency.dependency_type.as_str() - ) - .execute(&mut *transaction) - .await?; + for dependency in builders { + dependency.insert(version_item.id, &mut transaction).await?; } } @@ -533,7 +530,11 @@ pub async fn version_delete( } } - let result = database::models::Version::remove_full(id.into(), &**pool).await?; + let mut transaction = pool.begin().await?; + + let result = database::models::Version::remove_full(id.into(), &mut transaction).await?; + + transaction.commit().await?; if result.is_some() { Ok(HttpResponse::NoContent().body("")) diff --git a/src/auth/mod.rs b/src/util/auth.rs similarity index 100% rename from src/auth/mod.rs rename to src/util/auth.rs diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 000000000..ded21f6de --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,3 @@ +pub mod auth; +pub mod validate; +pub mod webhook; diff --git a/src/util/validate.rs b/src/util/validate.rs new file mode 100644 index 000000000..6bfd7f247 --- /dev/null +++ b/src/util/validate.rs @@ -0,0 +1,55 @@ +use lazy_static::lazy_static; +use regex::Regex; +use validator::{ValidationErrors, ValidationErrorsKind}; + +lazy_static! { + pub static ref RE_URL_SAFE: Regex = Regex::new(r#"^[a-zA-Z0-9!@$()`.+,_"-]*$"#).unwrap(); +} + +//TODO: In order to ensure readability, only the first error is printed, this may need to be expanded on in the future! +pub fn validation_errors_to_string(errors: ValidationErrors, adder: Option) -> String { + let mut output = String::new(); + + let map = errors.into_errors(); + + let key_option = map.keys().next().copied(); + + if let Some(field) = key_option { + if let Some(error) = map.get(field) { + return match error { + ValidationErrorsKind::Struct(errors) => { + validation_errors_to_string(*errors.clone(), Some(format!("of item {}", field))) + } + ValidationErrorsKind::List(list) => { + if let Some(errors) = list.get(&0) { + output.push_str(&*validation_errors_to_string( + *errors.clone(), + Some(format!("of list {} with index 0", field)), + )); + } + + output + } + ValidationErrorsKind::Field(errors) => { + if let Some(error) = errors.get(0) { + if let Some(adder) = adder { + output.push_str(&*format!( + "Field {} {} failed validation with error {}", + field, adder, error.code + )); + } else { + output.push_str(&*format!( + "Field {} failed validation with error {}", + field, error.code + )); + } + } + + output + } + }; + } + } + + "".to_string() +} diff --git a/src/util/webhook.rs b/src/util/webhook.rs new file mode 100644 index 000000000..d617ea98a --- /dev/null +++ b/src/util/webhook.rs @@ -0,0 +1,107 @@ +use crate::models::projects::Project; +use chrono::{DateTime, Utc}; +use serde::Serialize; + +#[derive(Serialize)] +struct DiscordEmbed { + pub title: String, + pub description: String, + pub url: String, + pub timestamp: DateTime, + pub color: u32, + pub fields: Vec, + pub image: DiscordEmbedImage, +} + +#[derive(Serialize)] +struct DiscordEmbedField { + pub name: String, + pub value: String, + pub inline: bool, +} + +#[derive(Serialize)] +struct DiscordEmbedImage { + pub url: Option, +} + +#[derive(Serialize)] +struct DiscordWebhook { + pub embeds: Vec, +} + +pub async fn send_discord_webhook( + project: Project, + webhook_url: String, +) -> Result<(), reqwest::Error> { + let mut fields = Vec::new(); + + fields.push(DiscordEmbedField { + name: "id".to_string(), + value: project.id.to_string(), + inline: true, + }); + + if let Some(slug) = project.slug.clone() { + fields.push(DiscordEmbedField { + name: "slug".to_string(), + value: slug, + inline: true, + }); + } + + fields.push(DiscordEmbedField { + name: "project_type".to_string(), + value: project.project_type.to_string(), + inline: true, + }); + + fields.push(DiscordEmbedField { + name: "client_side".to_string(), + value: project.client_side.to_string(), + inline: true, + }); + + fields.push(DiscordEmbedField { + name: "server_side".to_string(), + value: project.server_side.to_string(), + inline: true, + }); + + fields.push(DiscordEmbedField { + name: "categories".to_string(), + value: project.categories.join(", "), + inline: true, + }); + + let embed = DiscordEmbed { + url: format!( + "{}/mod/{}", + dotenv::var("SITE_URL").unwrap_or_default(), + project + .clone() + .slug + .unwrap_or_else(|| project.id.to_string()) + ), + title: project.title, + description: project.description, + timestamp: project.published, + color: 6137157, + fields, + image: DiscordEmbedImage { + url: project.icon_url, + }, + }; + + let client = reqwest::Client::new(); + + client + .post(&webhook_url) + .json(&DiscordWebhook { + embeds: vec![embed], + }) + .send() + .await?; + + Ok(()) +}