diff --git a/Cargo.lock b/Cargo.lock index 2a06cad97..423012a72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -950,6 +950,41 @@ dependencies = [ "subtle", ] +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "dashmap" version = "3.11.10" @@ -1509,6 +1544,12 @@ dependencies = [ "tokio-tls", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.0" @@ -1627,6 +1668,7 @@ dependencies = [ "rust-s3", "serde", "serde_json", + "serde_with", "sha1", "sqlx", "sqlx-macros", @@ -2397,6 +2439,28 @@ dependencies = [ "url", ] +[[package]] +name = "serde_with" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bac272128fb3b1e98872dca27a05c18d8b78b9bd089d3edb7b5871501b50bce" +dependencies = [ + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c747a9ab2e833b807f74f6b6141530655010bfa9c9c06d5508bce75c8f8072f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha-1" version = "0.9.1" @@ -2635,6 +2699,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + [[package]] name = "subtle" version = "2.3.0" diff --git a/Cargo.toml b/Cargo.toml index d1d2c3aee..3018d79cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ reqwest = { version = "0.10.8", features = ["json"] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } +serde_with = "1.5.1" chrono = { version = "0.4", features = ["serde"] } rand = "0.7.3" base64 = "0.13.0" diff --git a/migrations/20201112052516_moderation.sql b/migrations/20201112052516_moderation.sql new file mode 100644 index 000000000..ab34d34a2 --- /dev/null +++ b/migrations/20201112052516_moderation.sql @@ -0,0 +1,7 @@ +-- Add migration script here +DELETE FROM release_channels WHERE channel = 'release-hidden'; +DELETE FROM release_channels WHERE channel = 'beta-hidden'; +DELETE FROM release_channels WHERE channel = 'alpha-hidden'; + +ALTER TABLE versions +ADD COLUMN accepted BOOLEAN NOT NULL default FALSE; \ No newline at end of file diff --git a/sqlx-data.json b/sqlx-data.json index bcd22127b..c81116374 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -19,6 +19,32 @@ "nullable": [] } }, + "037b285b751880c643b0dc711ba23bc598959f0ff4a1eb4f7201da68a3b5d377": { + "query": "\n UPDATE versions\n SET accepted = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Bool", + "Int8" + ] + }, + "nullable": [] + } + }, + "04345d9c23430267f755b1420520df91bd403524fd60ba1a94e3a239ea70cae7": { + "query": "\n UPDATE mods\n SET source_url = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + } + }, "0739834cfbef869855ed4e1aea7e1f7601f6519867ee48c573ee901c4498e04c": { "query": "\n UPDATE team_members\n SET permissions = $1\n WHERE (team_id = $2 AND user_id = $3 AND NOT role = $4)\n ", "describe": { @@ -60,6 +86,19 @@ ] } }, + "0a1a470c12b84c7e171f0f51e8e541e9abe8bbee17fc441a5054e1dfd5607c05": { + "query": "\n UPDATE versions\n SET name = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + } + }, "0ca11a32b2860e4f5c3d20892a5be3cb419e084f42ba0f98e09b9995027fcc4e": { "query": "\n SELECT id FROM statuses\n WHERE status = $1\n ", "describe": { @@ -107,68 +146,6 @@ ] } }, - "1016a0bf55e9474357ac5ef725605ac337e82e1a2b93726ae795ec48f0d696dd": { - "query": "\n SELECT v.mod_id, v.author_id, v.name, v.version_number,\n v.changelog_url, v.date_published, v.downloads,\n release_channels.channel\n FROM versions v\n INNER JOIN release_channels ON v.release_channel = release_channels.id\n WHERE v.id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "mod_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "author_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "name", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "version_number", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "changelog_url", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "date_published", - "type_info": "Timestamptz" - }, - { - "ordinal": 6, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 7, - "name": "channel", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - true, - false, - false, - false - ] - } - }, "1220d15a56dbf823eaa452fbafa17442ab0568bc81a31fa38e16e3df3278e5f9": { "query": "SELECT EXISTS(SELECT 1 FROM users WHERE id = $1)", "describe": { @@ -215,6 +192,108 @@ ] } }, + "15978ec367b2768eea87dcdf1ee2497aa03b8a926139fecffbca22031e3ae7f9": { + "query": "SELECT EXISTS(SELECT 1 FROM team_members WHERE id = $1 AND user_id = $2)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + null + ] + } + }, + "16871e66d8762452be3ca0c80f4733f2db49980205fbf7cb6f9829cdd99cdb65": { + "query": "\n INSERT INTO dependencies (dependent_id, dependency_id)\n VALUES ($1, $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [] + } + }, + "16a648b46b0035e2bd4fff411ef1129b37e1c022c8c659071b456bada0db97c8": { + "query": "\n SELECT v.mod_id, v.author_id, v.name, v.version_number,\n v.changelog_url, v.date_published, v.downloads,\n release_channels.channel, v.accepted\n FROM versions v\n INNER JOIN release_channels ON v.release_channel = release_channels.id\n WHERE v.id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "mod_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "author_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "version_number", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "changelog_url", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "date_published", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 7, + "name": "channel", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "accepted", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + false, + false + ] + } + }, "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": { @@ -303,72 +382,6 @@ ] } }, - "1d144ed2bfb98d93f0515bb663708f2f9eff26548a50383f4013ba80305f7ac5": { - "query": "\n SELECT m.id, m.title, m.description, m.downloads, m.icon_url, m.body_url, m.published, m.updated, m.team_id FROM mods m\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "description", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 4, - "name": "icon_url", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "body_url", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "published", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "updated", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "team_id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false, - false, - true, - false, - false, - false, - false - ] - } - }, "1ffce9b2d5c9fa6c8b9abce4bad9f9419c44ad6367b7463b979c91b9b5b4fea1": { "query": "SELECT EXISTS(SELECT 1 FROM versions WHERE id=$1)", "describe": { @@ -616,6 +629,93 @@ "nullable": [] } }, + "3d700aaeb0d5129ac8c297ee0542757435a50a35ec94582d9d6ce67aa5302291": { + "query": "\n UPDATE mods\n SET title = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + } + }, + "3dd9ebabafe083e7d79ac76ee9da1fec45a5901eb19de82c196391c6c2fda231": { + "query": "\n SELECT v.id, v.mod_id, v.author_id, v.name, v.version_number,\n v.changelog_url, v.date_published, v.downloads,\n v.release_channel, accepted\n FROM versions v\n WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))\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": "name", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "version_number", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "changelog_url", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "date_published", + "type_info": "Timestamptz" + }, + { + "ordinal": 7, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 8, + "name": "release_channel", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "accepted", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + false, + false, + false, + false + ] + } + }, "42e072309779598d0c213280dd8052d1b4889cb24ef5204ca13b74f693b94328": { "query": "\n SELECT user_id FROM team_members tm\n INNER JOIN mods ON mods.team_id = tm.team_id\n WHERE mods.id = $1\n ", "describe": { @@ -636,6 +736,19 @@ ] } }, + "43b793e2df30a6ace9e037e38bb4ea456656cfbe276c151e3a9e0a408d2c249f": { + "query": "\n UPDATE versions\n SET release_channel = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int8" + ] + }, + "nullable": [] + } + }, "4411f2aefd43881450da34db81e826110ac86c3a6cef9fd6a3e9e341508d1f09": { "query": "\n SELECT id FROM versions\n WHERE mod_id = $1\n ", "describe": { @@ -671,6 +784,93 @@ "nullable": [] } }, + "45f8a06abdd17fc437f5355ad109efcb5d7e247ef397b1a0cd98d7fb6bd9ce17": { + "query": "\n INSERT INTO mods_categories (joining_mod_id, joining_category_id)\n VALUES ($1, $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int4" + ] + }, + "nullable": [] + } + }, + "486c13abb9648b16a4c354f25754408d649b13980fe7486cdcfdcacfa2725c2b": { + "query": "\n SELECT * FROM versions\n WHERE accepted = FALSE\n ORDER BY date_published ASC\n LIMIT $1;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "mod_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "version_number", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "changelog_url", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "date_published", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 7, + "name": "release_channel", + "type_info": "Int4" + }, + { + "ordinal": 8, + "name": "author_id", + "type_info": "Int8" + }, + { + "ordinal": 9, + "name": "accepted", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + false, + false, + false + ] + } + }, "49e36828e3a0214b48234435e34311735ae32e08d8be1270f8f0db4b27e708ba": { "query": "\n INSERT INTO loaders (loader)\n VALUES ($1)\n ON CONFLICT (loader) DO NOTHING\n RETURNING id\n ", "describe": { @@ -731,6 +931,19 @@ ] } }, + "4e9f9eafbfd705dfc94571018cb747245a98ea61bad3fae4b3ce284229d99955": { + "query": "\n UPDATE mods\n SET description = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + } + }, "4f307a8851b0cab7870798ba017955c8ebaba7444791dd65ffebcbac32d3585d": { "query": "\n INSERT INTO states (id, url)\n VALUES ($1, $2)\n ", "describe": { @@ -744,6 +957,33 @@ "nullable": [] } }, + "55df56cd9938bca0edbf8d91649c8ce6d946125ad64e570ab127f6d9c6061787": { + "query": "\n SELECT version_id, filename FROM files\n INNER JOIN hashes ON hash = $1 AND algorithm = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "version_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "filename", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Bytea", + "Text" + ] + }, + "nullable": [ + false, + false + ] + } + }, "560c3ba57c965c3ebdbe393b062da8a30a8a7116a9bace2aa7de2e8431fe0bc7": { "query": "\n INSERT INTO mods_categories (joining_mod_id, joining_category_id)\n VALUES ($1, $2)\n ", "describe": { @@ -801,58 +1041,38 @@ "nullable": [] } }, - "621c3c5e5b3ac00c291b5f9cae2134420ef3e23f1f236267c4132222299c87a2": { - "query": "\n SELECT gv.version FROM versions\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id=versions.id\n INNER JOIN game_versions gv ON gvv.game_version_id=gv.id\n WHERE versions.mod_id = $1\n ORDER BY gv.created ASC\n ", + "637fd5f9564a79b625e00a705b3c9fe70ba3cba9050c0993557ca46f50d89623": { + "query": "\n SELECT * FROM mods\n WHERE status = (\n SELECT id FROM statuses WHERE status = $1\n )\n ORDER BY updated ASC\n LIMIT $2;\n ", "describe": { "columns": [ { "ordinal": 0, - "name": "version", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false - ] - } - }, - "6562c876826ad3091a14eb50fa1f961a971c1d1bb158fc3dcb55d469a73facc6": { - "query": "\n SELECT v.mod_id, v.author_id, v.name, v.version_number,\n v.changelog_url, v.date_published, v.downloads,\n v.release_channel\n FROM versions v\n WHERE v.id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "mod_id", + "name": "id", "type_info": "Int8" }, { "ordinal": 1, - "name": "author_id", + "name": "team_id", "type_info": "Int8" }, { "ordinal": 2, - "name": "name", + "name": "title", "type_info": "Varchar" }, { "ordinal": 3, - "name": "version_number", + "name": "description", "type_info": "Varchar" }, { "ordinal": 4, - "name": "changelog_url", + "name": "body_url", "type_info": "Varchar" }, { "ordinal": 5, - "name": "date_published", + "name": "published", "type_info": "Timestamptz" }, { @@ -862,12 +1082,38 @@ }, { "ordinal": 7, - "name": "release_channel", + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "issues_url", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "source_url", + "type_info": "Varchar" + }, + { + "ordinal": 10, + "name": "wiki_url", + "type_info": "Varchar" + }, + { + "ordinal": 11, + "name": "status", "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "updated", + "type_info": "Timestamptz" } ], "parameters": { "Left": [ + "Text", "Int8" ] }, @@ -876,9 +1122,14 @@ false, false, false, - true, false, false, + false, + true, + true, + true, + true, + false, false ] } @@ -910,6 +1161,19 @@ "nullable": [] } }, + "6c7aeb0db4a4fb3387c37b8d7aca6fdafaa637fd883a44416b56270aeebb7a01": { + "query": "\n INSERT INTO loaders_versions (loader_id, version_id)\n VALUES ($1, $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int8" + ] + }, + "nullable": [] + } + }, "6d8f1863579977d367784a5b457e21c841886834afe69a964de039d6f92795d4": { "query": "\n SELECT id, user_id, member_name, role, permissions, accepted\n FROM team_members\n WHERE (team_id = $1 AND user_id = $2)\n ", "describe": { @@ -961,6 +1225,86 @@ ] } }, + "6f1fb4c3269b2a8190f328df025be76241eae757d9c4f3e5eb1cc01b191837df": { + "query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, + "6f5b7cd37343d424fe3303427c6d8d08024dd646f8b467dd98b34d2988f0eda2": { + "query": "\n SELECT v.mod_id, v.author_id, v.name, v.version_number,\n v.changelog_url, v.date_published, v.downloads,\n v.release_channel, v.accepted\n FROM versions v\n WHERE v.id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "mod_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "author_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "version_number", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "changelog_url", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "date_published", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 7, + "name": "release_channel", + "type_info": "Int4" + }, + { + "ordinal": 8, + "name": "accepted", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + false, + false + ] + } + }, "71db1bc306ff6da3a92544e1585aa11c5627b50d95b15e794b2fa5dc838ea1a3": { "query": "\n SELECT mod_id, version_number, author_id\n FROM versions\n WHERE id = $1\n ", "describe": { @@ -1117,24 +1461,25 @@ "nullable": [] } }, - "73d9b1e00609919f3adbe5f4ca9e41304bffb1cd4397a85a9911f2260e9a98f5": { - "query": "\n INSERT INTO versions (\n id, mod_id, author_id, name, version_number,\n changelog_url, date_published,\n downloads, release_channel\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7,\n $8, $9\n )\n ", + "75a1099a12e73484cf0e7dd4b346ea154ea1ff915fe9ee15f936e1e8faed4118": { + "query": "SELECT EXISTS(SELECT 1 FROM team_members tm INNER JOIN mods m ON m.team_id = tm.id AND m.id = $1 WHERE tm.user_id = $2)", "describe": { - "columns": [], + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], "parameters": { "Left": [ "Int8", - "Int8", - "Int8", - "Varchar", - "Varchar", - "Varchar", - "Timestamptz", - "Int4", - "Int4" + "Int8" ] }, - "nullable": [] + "nullable": [ + null + ] } }, "78c8b561e37e3aed48d3a4108ce7fd81866c6835ea91517ffc90c30e1284246e": { @@ -1157,6 +1502,18 @@ ] } }, + "79b896b1a8ddab285294638302976b75d0d915f36036383cc21bd2fc48d4502c": { + "query": "\n DELETE FROM loaders_versions WHERE version_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, "7bbbeecf3246a8e07ad073a07f7d057e0990a810d69ae18cec41de60b704b174": { "query": "\n SELECT id, user_id, member_name, role, permissions, accepted\n FROM team_members\n WHERE (team_id = $1 AND user_id = $2 AND accepted = TRUE)\n ", "describe": { @@ -1234,6 +1591,40 @@ "nullable": [] } }, + "96585aa2586e69eeae18f5a0c97a93d2c221c8a97470e5f59839b1a52b2e353a": { + "query": "\n SELECT version_id FROM files\n INNER JOIN hashes ON hash = $1 AND algorithm = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "version_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Bytea", + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "99a1eac69d7f5a5139703df431e6a5c3012a90143a8c635f93632f04d0bc41d4": { + "query": "\n UPDATE mods\n SET wiki_url = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + } + }, "9a41d6c1d5c250df6114157edf5621a88bc336c5c628ba89182ba999e0af3ba8": { "query": "\n SELECT id, title, description, downloads,\n icon_url, body_url, published,\n updated, status,\n issues_url, source_url, wiki_url,\n team_id\n FROM mods\n WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))\n ", "describe": { @@ -1326,6 +1717,27 @@ ] } }, + "9d0fe7452cf3fb23d743a5e479b5580e0fd73fdabf7c5c7e42f367a1d955cca4": { + "query": "\n INSERT INTO versions (\n id, mod_id, author_id, name, version_number,\n changelog_url, date_published,\n downloads, release_channel, accepted\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7,\n $8, $9,\n $10\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Int8", + "Varchar", + "Varchar", + "Varchar", + "Timestamptz", + "Int4", + "Int4", + "Bool" + ] + }, + "nullable": [] + } + }, "9d95d136d0e6eedee57e6aa524232c02609b89e4e26032e07403aabb69bea0d8": { "query": "\n SELECT u.id, u.username FROM users u\n INNER JOIN team_members tm ON tm.user_id = u.id\n WHERE tm.team_id = $2 AND tm.role = $1\n ", "describe": { @@ -1371,6 +1783,18 @@ "nullable": [] } }, + "9ef9174fa003186a07fbf465e9ac083f6d452cf3c702c6ca05b1f3bbc0e30b5a": { + "query": "\n DELETE FROM files\n WHERE files.version_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, "a2a99a640468a9fb8f0718e5aea6740cf5b33dafd5e038c154d6a13674fa999b": { "query": "\n INSERT INTO mods (\n id, team_id, title, description, body_url,\n published, downloads, icon_url, issues_url,\n source_url, wiki_url, status\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8, $9,\n $10, $11, $12\n )\n ", "describe": { @@ -1406,6 +1830,18 @@ "nullable": [] } }, + "a40e4075ba1bff5b6fde104ed1557ad8d4a75d7d90d481decd222f31685c4981": { + "query": "\n DELETE FROM dependencies WHERE dependent_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, "a5d47fb171b0a1ba322125e7cedebf5af9c5831c319bbc4f8f087cb63322bee3": { "query": "\n SELECT files.id, files.url, files.filename FROM files\n WHERE files.version_id = $1\n ", "describe": { @@ -1450,24 +1886,6 @@ "nullable": [] } }, - "a82f1a23eaa2d6a0d1b331ce68c3fa307a20bbcde9b4846cea06e5502cb77a78": { - "query": "\n SELECT loader FROM loaders\n ORDER BY loader ASC\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "loader", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false - ] - } - }, "a94eb4862ba30ca21f15198d9b7b9fd80ce01d45457e0b4d68270b5e3f9be8c6": { "query": "\n SELECT u.github_id, u.name, u.email,\n u.avatar_url, u.username, u.bio,\n u.created, u.role\n FROM users u\n WHERE u.id = $1\n ", "describe": { @@ -1645,6 +2063,103 @@ ] } }, + "bbefd1a6eb97ff17b185cfd704dda537334e23b254a1ce9e565216a967f204a3": { + "query": "\n SELECT m.id, m.title, m.description, m.downloads, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.status FROM mods m\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "body_url", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "published", + "type_info": "Timestamptz" + }, + { + "ordinal": 7, + "name": "updated", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 9, + "name": "status", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + false, + false, + false + ] + } + }, + "bc91841f9672608a28bd45a862919f2bd34fac0b3479e3b4b67a9f6bea2a562a": { + "query": "\n UPDATE mods\n SET issues_url = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + } + }, + "bd0d1da185dc7d21ccbbfde86fc093ce9eda7dd7e07f7a53882d427010fd58ca": { + "query": "\n DELETE FROM dependencies WHERE dependent_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, "bec1612d4929d143bc5d6860a57cc036c5ab23e69d750ca5791c620297953c50": { "query": "\n SELECT team_id FROM mods WHERE id = $1\n ", "describe": { @@ -1717,26 +2232,6 @@ ] } }, - "c59de96d66ebf26c0497674308550da125e3ce2314a8ae5b2f95d892f4205f90": { - "query": "\n SELECT gv.version FROM versions\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id=versions.id\n INNER JOIN game_versions gv ON gvv.game_version_id=gv.id\n WHERE versions.mod_id = $1\n ORDER BY gv.created ASC\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "version", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false - ] - } - }, "c64c487b56a25b252ff070fe03a7416e84260df8a6f938a018cc768598e9435b": { "query": "\n SELECT category FROM categories\n WHERE id = $1\n ", "describe": { @@ -1822,6 +2317,19 @@ ] } }, + "cc9fa2f65f62cea689ab15d4848bbb8efa77ac4a6f23440fa945847844183dd7": { + "query": "\n DELETE FROM hashes\n WHERE hash = $1 AND algorithm = $2\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Bytea", + "Text" + ] + }, + "nullable": [] + } + }, "ccd913bb2f3006ffe881ce2fc4ef1e721d18fe2eed6ac62627046c955129610c": { "query": "SELECT EXISTS(SELECT 1 FROM files WHERE id=$1)", "describe": { @@ -1926,74 +2434,6 @@ "nullable": [] } }, - "d98f0713c6bd3463f851644c88a2cf91601a499494ef1e2ad92a49d6798f5b2f": { - "query": "\n SELECT v.id, v.mod_id, v.author_id, v.name, v.version_number,\n v.changelog_url, v.date_published, v.downloads,\n v.release_channel\n FROM versions v\n WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))\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": "name", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "version_number", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "changelog_url", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "date_published", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 8, - "name": "release_channel", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Int8Array" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - true, - false, - false, - false - ] - } - }, "d99c8f5a2d8f73f6c91ac7e72e352e03e608522142aab1b569ef5eced5ec18f8": { "query": "\n INSERT INTO team_members (\n id, user_id, member_name, role, permissions, accepted\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6\n )\n ", "describe": { @@ -2168,6 +2608,19 @@ ] } }, + "e925b15ec46f0263c7775ba1ba00ed11cfd6749fa792d4eabed73b619f230585": { + "query": "\n UPDATE mods\n SET status = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int8" + ] + }, + "nullable": [] + } + }, "ea877d50ba461eae97ba3a35c3da71e7cdb7a92de1bb877d6b5dd766aca4e4ef": { "query": "\n SELECT u.id, u.name, u.email,\n u.avatar_url, u.username, u.bio,\n u.created, u.role\n FROM users u\n WHERE u.github_id = $1\n ", "describe": { @@ -2250,24 +2703,6 @@ ] } }, - "ed2e4c5bf2df01ef670f5b0b4c3bd4b9aae9b7019938542a1b39ebd23d616617": { - "query": "\n SELECT category FROM categories\n ORDER BY category ASC\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "category", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false - ] - } - }, "ed4c0b620d01cdcdd0c2b3b5727ae3485d51114ca76e17331cec0d244d7f972d": { "query": "\n SELECT version FROM game_versions\n ORDER BY created DESC\n ", "describe": { diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 1765ef08c..557eb1e21 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -95,9 +95,10 @@ where { let user = get_user_from_headers(headers, executor).await?; - match user.role { - Role::Moderator | Role::Admin => Ok(user), - _ => Err(AuthenticationError::InvalidCredentialsError), + if user.role.is_mod() { + Ok(user) + } else { + Err(AuthenticationError::InvalidCredentialsError) } } diff --git a/src/database/models/version_item.rs b/src/database/models/version_item.rs index 15b906f5a..cadd86e54 100644 --- a/src/database/models/version_item.rs +++ b/src/database/models/version_item.rs @@ -80,6 +80,7 @@ impl VersionBuilder { date_published: chrono::Utc::now(), downloads: 0, release_channel: self.release_channel, + accepted: false, }; version.insert(&mut *transaction).await?; @@ -152,6 +153,7 @@ pub struct Version { pub date_published: chrono::DateTime, pub downloads: i32, pub release_channel: ChannelId, + pub accepted: bool, } impl Version { @@ -164,12 +166,13 @@ impl Version { INSERT INTO versions ( id, mod_id, author_id, name, version_number, changelog_url, date_published, - downloads, release_channel + downloads, release_channel, accepted ) VALUES ( $1, $2, $3, $4, $5, $6, $7, - $8, $9 + $8, $9, + $10 ) ", self.id as VersionId, @@ -181,6 +184,7 @@ impl Version { self.date_published, self.downloads, self.release_channel as ChannelId, + self.accepted ) .execute(&mut *transaction) .await?; @@ -291,6 +295,15 @@ impl Version { .execute(exec) .await?; + sqlx::query!( + " + DELETE FROM dependencies WHERE dependent_id = $1 + ", + id as VersionId, + ) + .execute(exec) + .await?; + Ok(Some(())) } @@ -354,7 +367,7 @@ impl Version { " SELECT v.mod_id, v.author_id, v.name, v.version_number, v.changelog_url, v.date_published, v.downloads, - v.release_channel + v.release_channel, v.accepted FROM versions v WHERE v.id = $1 ", @@ -374,6 +387,7 @@ impl Version { date_published: row.date_published, downloads: row.downloads, release_channel: ChannelId(row.release_channel), + accepted: row.accepted, })) } else { Ok(None) @@ -394,7 +408,7 @@ impl Version { " SELECT v.id, v.mod_id, v.author_id, v.name, v.version_number, v.changelog_url, v.date_published, v.downloads, - v.release_channel + v.release_channel, accepted FROM versions v WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[])) ", @@ -412,6 +426,7 @@ impl Version { date_published: v.date_published, downloads: v.downloads, release_channel: ChannelId(v.release_channel), + accepted: v.accepted, })) }) .try_collect::>() @@ -431,7 +446,7 @@ impl Version { " SELECT v.mod_id, v.author_id, v.name, v.version_number, v.changelog_url, v.date_published, v.downloads, - release_channels.channel + release_channels.channel, v.accepted FROM versions v INNER JOIN release_channels ON v.release_channel = release_channels.id WHERE v.id = $1 @@ -519,6 +534,7 @@ impl Version { files, loaders, game_versions, + accepted: row.accepted, })) } else { Ok(None) @@ -570,6 +586,7 @@ pub struct QueryVersion { pub files: Vec, pub game_versions: Vec, pub loaders: Vec, + pub accepted: bool, } pub struct QueryFile { diff --git a/src/models/mods.rs b/src/models/mods.rs index 88274972d..5ddacfb30 100644 --- a/src/models/mods.rs +++ b/src/models/mods.rs @@ -58,7 +58,7 @@ pub struct Mod { /// Draft - Mod is not displayed on search, and not accessible by URL /// Unlisted - Mod is not displayed on search, but accessible by URL /// Processing - Mod is not displayed on search, and not accessible by URL (Temporary state, mod under review) -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] #[serde(rename_all = "lowercase")] pub enum ModStatus { Approved, @@ -103,6 +103,24 @@ impl ModStatus { ModStatus::Unknown => "unknown", } } + + pub fn is_hidden(&self) -> bool { + match self { + ModStatus::Approved => false, + ModStatus::Rejected => true, + ModStatus::Draft => true, + ModStatus::Unlisted => false, + ModStatus::Processing => true, + ModStatus::Unknown => true, + } + } + + pub fn is_searchable(&self) -> bool { + match self { + ModStatus::Approved => true, + _ => false, + } + } } /// A specific version of a mod diff --git a/src/models/users.rs b/src/models/users.rs index 8f2d87e00..67f5850f3 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -45,4 +45,11 @@ impl Role { _ => Role::Developer, } } + + pub fn is_mod(&self) -> bool { + match self { + Role::Developer => false, + Role::Moderator | Role::Admin => true, + } + } } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index f3e7f7472..bf961d1b2 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -3,6 +3,7 @@ use actix_web::web; mod auth; mod index; mod mod_creation; +mod moderation; mod mods; mod not_found; mod tags; @@ -16,6 +17,7 @@ pub use tags::config as tags_config; pub use self::index::index_get; pub use self::not_found::not_found; +use crate::file_hosting::FileHostingError; pub fn mods_config(cfg: &mut web::ServiceConfig) { cfg.service(mods::mod_search); @@ -26,6 +28,7 @@ pub fn mods_config(cfg: &mut web::ServiceConfig) { web::scope("mod") .service(mods::mod_get) .service(mods::mod_delete) + .service(mods::mod_edit) .service(web::scope("{mod_id}").service(versions::version_list)), ); } @@ -37,7 +40,13 @@ pub fn versions_config(cfg: &mut web::ServiceConfig) { web::scope("version") .service(versions::version_get) .service(versions::version_delete) - .service(version_creation::upload_file_to_version), + .service(version_creation::upload_file_to_version) + .service(versions::version_edit), + ); + cfg.service( + web::scope("version_file") + .service(versions::delete_file) + .service(versions::get_version_from_hash), ); } @@ -67,6 +76,8 @@ pub fn teams_config(cfg: &mut web::ServiceConfig) { #[derive(thiserror::Error, Debug)] pub enum ApiError { + #[error("Error while uploading file")] + FileHostingError(#[from] FileHostingError), #[error("Internal server error")] DatabaseError(#[from] crate::database::models::DatabaseError), #[error("Deserialization error: {0}")] @@ -89,6 +100,7 @@ impl actix_web::ResponseError for ApiError { ApiError::CustomAuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED, ApiError::JsonError(..) => actix_web::http::StatusCode::BAD_REQUEST, ApiError::SearchError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, + ApiError::FileHostingError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, ApiError::InvalidInputError(..) => actix_web::http::StatusCode::BAD_REQUEST, } } @@ -102,6 +114,7 @@ impl actix_web::ResponseError for ApiError { ApiError::CustomAuthenticationError(..) => "unauthorized", ApiError::JsonError(..) => "json_error", ApiError::SearchError(..) => "search_error", + ApiError::FileHostingError(..) => "file_hosting_error", ApiError::InvalidInputError(..) => "invalid_input", }, description: &self.to_string(), diff --git a/src/routes/mod_creation.rs b/src/routes/mod_creation.rs index d015d3560..7e9261767 100644 --- a/src/routes/mod_creation.rs +++ b/src/routes/mod_creation.rs @@ -113,6 +113,8 @@ struct ModCreateData { pub source_url: Option, /// An optional link to the mod's wiki page or other relevant information. pub wiki_url: Option, + /// An optional boolean. If true, the mod will be created as a draft. + pub is_draft: Option, } pub struct UploadedFile { @@ -277,6 +279,12 @@ async fn mod_create_inner( check_length(3..=2048, "mod description", &create_data.mod_description)?; check_length(..65536, "mod body", &create_data.mod_body)?; + if create_data.categories.len() > 3 { + return Err(CreateError::InvalidInput( + "The maximum number of categories for a mod is four.".to_string(), + )); + } + create_data .categories .iter() @@ -444,7 +452,13 @@ async fn mod_create_inner( let team_id = team.insert(&mut *transaction).await?; - let status = ModStatus::Processing; + let status; + if mod_create_data.is_draft.unwrap_or(false) { + status = ModStatus::Draft; + } else { + status = ModStatus::Processing; + } + let status_id = models::StatusId::get_id(&status, &mut *transaction) .await? .expect("No database entry found for status"); diff --git a/src/routes/moderation.rs b/src/routes/moderation.rs new file mode 100644 index 000000000..932722bea --- /dev/null +++ b/src/routes/moderation.rs @@ -0,0 +1,112 @@ +use super::ApiError; +use crate::auth::check_is_moderator_from_headers; +use crate::database; +use crate::models; +use crate::models::mods::{ModStatus, VersionType}; +use actix_web::{get, web, HttpRequest, HttpResponse}; +use serde::Deserialize; +use sqlx::PgPool; + +#[derive(Deserialize)] +pub struct ResultCount { + #[serde(default = "default_count")] + count: i16, +} + +fn default_count() -> i16 { + 100 +} + +#[get("mods")] +pub async fn mods( + req: HttpRequest, + pool: web::Data, + count: web::Query, +) -> Result { + check_is_moderator_from_headers(req.headers(), &**pool).await?; + + use futures::stream::TryStreamExt; + + let mods = sqlx::query!( + " + SELECT * FROM mods + WHERE status = ( + SELECT id FROM statuses WHERE status = $1 + ) + ORDER BY updated ASC + LIMIT $2; + ", + ModStatus::Processing.as_str(), + count.count as i64 + ) + .fetch_many(&**pool) + .try_filter_map(|e| async { + Ok(e.right().map(|m| models::mods::Mod { + id: database::models::ids::ModId(m.id).into(), + team: database::models::ids::TeamId(m.team_id).into(), + title: m.title, + description: m.description, + body_url: m.body_url, + published: m.published, + categories: vec![], + versions: vec![], + icon_url: m.icon_url, + issues_url: m.issues_url, + source_url: m.source_url, + status: ModStatus::Processing, + updated: m.updated, + downloads: m.downloads as u32, + wiki_url: m.wiki_url, + })) + }) + .try_collect::>() + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + Ok(HttpResponse::Ok().json(mods)) +} + +/// Returns a list of versions that need to be approved +#[get("versions")] +pub async fn versions( + req: HttpRequest, + pool: web::Data, + count: web::Query, +) -> Result { + check_is_moderator_from_headers(req.headers(), &**pool).await?; + + use futures::stream::TryStreamExt; + + let versions = sqlx::query!( + " + SELECT * FROM versions + WHERE accepted = FALSE + ORDER BY date_published ASC + LIMIT $1; + ", + count.count as i64 + ) + .fetch_many(&**pool) + .try_filter_map(|e| async { + Ok(e.right().map(|m| models::mods::Version { + id: database::models::ids::VersionId(m.id).into(), + mod_id: database::models::ids::ModId(m.mod_id).into(), + author_id: database::models::ids::UserId(m.author_id).into(), + name: m.name, + version_number: m.version_number, + changelog_url: m.changelog_url, + date_published: m.date_published, + downloads: m.downloads as u32, + version_type: VersionType::Release, + files: vec![], + dependencies: vec![], + game_versions: vec![], + loaders: vec![], + })) + }) + .try_collect::>() + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + Ok(HttpResponse::Ok().json(versions)) +} diff --git a/src/routes/mods.rs b/src/routes/mods.rs index f81a09681..8b91db6f9 100644 --- a/src/routes/mods.rs +++ b/src/routes/mods.rs @@ -1,14 +1,15 @@ use super::ApiError; use crate::auth::get_user_from_headers; use crate::database; +use crate::file_hosting::FileHost; use crate::models; -use crate::models::mods::SearchRequest; +use crate::models::mods::{ModStatus, SearchRequest}; use crate::models::teams::Permissions; -use crate::models::users::Role; use crate::search::{search_for_mod, SearchConfig, SearchError}; -use actix_web::{delete, get, web, HttpRequest, HttpResponse}; +use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; +use std::sync::Arc; #[get("mod")] pub async fn mod_search( @@ -27,6 +28,7 @@ pub struct ModIds { // TODO: Make this return the full mod struct #[get("mods")] pub async fn mods_get( + req: HttpRequest, web::Query(ids): web::Query, pool: web::Data, ) -> Result { @@ -39,17 +41,48 @@ pub async fn mods_get( .await .map_err(|e| ApiError::DatabaseError(e.into()))?; - let mods = mods_data - .into_iter() - .filter_map(|m| m) - .map(convert_mod) - .collect::>(); + let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); + + let mut mods = Vec::new(); + + for mod_data_option in mods_data { + if let Some(mod_data) = mod_data_option { + let mut authorized = !mod_data.status.is_hidden(); + + if let Some(user) = &user_option { + if !authorized { + if user.role.is_mod() { + authorized = true; + } else { + let user_id: database::models::ids::UserId = user.id.into(); + + let mod_exists = sqlx::query!( + "SELECT EXISTS(SELECT 1 FROM team_members WHERE id = $1 AND user_id = $2)", + mod_data.inner.team_id as database::models::ids::TeamId, + user_id as database::models::ids::UserId, + ) + .fetch_one(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))? + .exists; + + authorized = mod_exists.unwrap_or(false); + } + } + } + + if authorized { + mods.push(convert_mod(mod_data)); + } + } + } Ok(HttpResponse::Ok().json(mods)) } #[get("{id}")] pub async fn mod_get( + req: HttpRequest, info: web::Path<(models::ids::ModId,)>, pool: web::Data, ) -> Result { @@ -57,9 +90,38 @@ pub async fn mod_get( let mod_data = database::models::Mod::get_full(id.into(), &**pool) .await .map_err(|e| ApiError::DatabaseError(e.into()))?; + let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); if let Some(data) = mod_data { - Ok(HttpResponse::Ok().json(convert_mod(data))) + let mut authorized = !data.status.is_hidden(); + + if let Some(user) = user_option { + if !authorized { + if user.role.is_mod() { + authorized = true; + } else { + let user_id: database::models::ids::UserId = user.id.into(); + + let mod_exists = sqlx::query!( + "SELECT EXISTS(SELECT 1 FROM team_members WHERE id = $1 AND user_id = $2)", + data.inner.team_id as database::models::ids::TeamId, + user_id as database::models::ids::UserId, + ) + .fetch_one(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))? + .exists; + + authorized = mod_exists.unwrap_or(false); + } + } + } + + if authorized { + return Ok(HttpResponse::Ok().json(convert_mod(data))); + } + + Ok(HttpResponse::NotFound().body("")) } else { Ok(HttpResponse::NotFound().body("")) } @@ -87,6 +149,309 @@ fn convert_mod(data: database::models::mod_item::QueryMod) -> models::mods::Mod } } +/// A mod returned from the API +#[derive(Serialize, Deserialize)] +pub struct EditMod { + pub title: Option, + pub description: Option, + pub body: Option, + pub status: Option, + pub categories: Option>, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "::serde_with::rust::double_option" + )] + pub issues_url: Option>, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "::serde_with::rust::double_option" + )] + pub source_url: Option>, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "::serde_with::rust::double_option" + )] + pub wiki_url: Option>, +} + +#[patch("{id}")] +pub async fn mod_edit( + req: HttpRequest, + info: web::Path<(models::ids::ModId,)>, + pool: web::Data, + config: web::Data, + file_host: web::Data>, + new_mod: web::Json, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + + let mod_id = info.into_inner().0; + let id = mod_id.into(); + + let result = database::models::Mod::get_full(id, &**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + if let Some(mod_item) = result { + let team_member = database::models::TeamMember::get_from_user_id( + mod_item.inner.team_id, + user.id.into(), + &**pool, + ) + .await?; + let permissions; + + if let Some(member) = team_member { + permissions = Some(member.permissions) + } else if user.role.is_mod() { + permissions = Some(Permissions::ALL) + } else { + permissions = None + } + + if let Some(perms) = permissions { + let mut transaction = pool + .begin() + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + if let Some(title) = &new_mod.title { + if !perms.contains(Permissions::EDIT_DETAILS) { + return Err(ApiError::CustomAuthenticationError( + "You do not have the permissions to edit the title of this mod!" + .to_string(), + )); + } + + sqlx::query!( + " + UPDATE mods + SET title = $1 + WHERE (id = $2) + ", + title, + id as database::models::ids::ModId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + + if let Some(description) = &new_mod.description { + if !perms.contains(Permissions::EDIT_DETAILS) { + return Err(ApiError::CustomAuthenticationError( + "You do not have the permissions to edit the description of this mod!" + .to_string(), + )); + } + + sqlx::query!( + " + UPDATE mods + SET description = $1 + WHERE (id = $2) + ", + description, + id as database::models::ids::ModId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + + if let Some(status) = &new_mod.status { + if !perms.contains(Permissions::EDIT_DETAILS) { + return Err(ApiError::CustomAuthenticationError( + "You do not have the permissions to edit the status of this mod!" + .to_string(), + )); + } + + if status == &ModStatus::Rejected || status == &ModStatus::Approved { + if !user.role.is_mod() { + return Err(ApiError::CustomAuthenticationError( + "You don't have permission to set this status".to_string(), + )); + } + } + + let status_id = database::models::StatusId::get_id(&status, &mut *transaction) + .await? + .ok_or_else(|| { + ApiError::InvalidInputError( + "No database entry for status provided.".to_string(), + ) + })?; + sqlx::query!( + " + UPDATE mods + SET status = $1 + WHERE (id = $2) + ", + status_id as database::models::ids::StatusId, + id as database::models::ids::ModId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + if mod_item.status.is_searchable() && status.is_searchable() { + delete_from_index(id.into(), config).await?; + } + } + + if let Some(categories) = &new_mod.categories { + if !perms.contains(Permissions::EDIT_DETAILS) { + return Err(ApiError::CustomAuthenticationError( + "You do not have the permissions to edit the categories of this mod!" + .to_string(), + )); + } + + if categories.len() > 3 { + return Err(ApiError::InvalidInputError( + "The maximum number of categories for a mod is four.".to_string(), + )); + } + + sqlx::query!( + " + DELETE FROM mods_categories + WHERE joining_mod_id = $1 + ", + id as database::models::ids::ModId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + for category in categories { + let category_id = database::models::categories::Category::get_id( + &category, + &mut *transaction, + ) + .await? + .ok_or_else(|| { + ApiError::InvalidInputError(format!( + "Category {} does not exist.", + category.clone() + )) + })?; + + sqlx::query!( + " + INSERT INTO mods_categories (joining_mod_id, joining_category_id) + VALUES ($1, $2) + ", + id as database::models::ids::ModId, + category_id as database::models::ids::CategoryId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + } + + if let Some(issues_url) = &new_mod.issues_url { + if !perms.contains(Permissions::EDIT_DETAILS) { + return Err(ApiError::CustomAuthenticationError( + "You do not have the permissions to edit the issues URL of this mod!" + .to_string(), + )); + } + + sqlx::query!( + " + UPDATE mods + SET issues_url = $1 + WHERE (id = $2) + ", + issues_url.as_deref(), + id as database::models::ids::ModId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + + if let Some(source_url) = &new_mod.source_url { + if !perms.contains(Permissions::EDIT_DETAILS) { + return Err(ApiError::CustomAuthenticationError( + "You do not have the permissions to edit the source URL of this mod!" + .to_string(), + )); + } + + sqlx::query!( + " + UPDATE mods + SET source_url = $1 + WHERE (id = $2) + ", + source_url.as_deref(), + id as database::models::ids::ModId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + + if let Some(wiki_url) = &new_mod.wiki_url { + if !perms.contains(Permissions::EDIT_DETAILS) { + return Err(ApiError::CustomAuthenticationError( + "You do not have the permissions to edit the wiki URL of this mod!" + .to_string(), + )); + } + + sqlx::query!( + " + UPDATE mods + SET wiki_url = $1 + WHERE (id = $2) + ", + wiki_url.as_deref(), + id as database::models::ids::ModId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + + if let Some(body) = &new_mod.body { + if !perms.contains(Permissions::EDIT_BODY) { + return Err(ApiError::CustomAuthenticationError( + "You do not have the permissions to edit the body of this mod!".to_string(), + )); + } + + let body_path = format!("data/{}/description.md", mod_id); + + file_host.delete_file_version("", &*body_path).await?; + + file_host + .upload_file("text/plain", &body_path, body.clone().into_bytes()) + .await?; + } + + transaction + .commit() + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + Ok(HttpResponse::Ok().body("")) + } else { + Err(ApiError::CustomAuthenticationError( + "You do not have permission to edit this mod!".to_string(), + )) + } + } else { + Ok(HttpResponse::NotFound().body("")) + } +} + #[delete("{id}")] pub async fn mod_delete( req: HttpRequest, @@ -97,7 +462,7 @@ pub async fn mod_delete( let user = get_user_from_headers(req.headers(), &**pool).await?; let id = info.into_inner().0; - if user.role != Role::Moderator || user.role != Role::Admin { + if !user.role.is_mod() { let mod_item = database::models::Mod::get(id.into(), &**pool) .await .map_err(|e| ApiError::DatabaseError(e.into()))? @@ -122,12 +487,7 @@ pub async fn mod_delete( .await .map_err(|e| ApiError::DatabaseError(e.into()))?; - let client = meilisearch_sdk::client::Client::new(&*config.address, &*config.key); - - let indexes: Vec = client.get_indexes().await?; - for index in indexes { - index.delete_document(format!("local-{}", id)).await?; - } + delete_from_index(id, config).await?; if result.is_some() { Ok(HttpResponse::Ok().body("")) @@ -135,3 +495,17 @@ pub async fn mod_delete( Ok(HttpResponse::NotFound().body("")) } } + +pub async fn delete_from_index( + id: crate::models::mods::ModId, + config: web::Data, +) -> Result<(), meilisearch_sdk::errors::Error> { + let client = meilisearch_sdk::client::Client::new(&*config.address, &*config.key); + + let indexes: Vec = client.get_indexes().await?; + for index in indexes { + index.delete_document(format!("local-{}", id)).await?; + } + + Ok(()) +} diff --git a/src/routes/versions.rs b/src/routes/versions.rs index c591da1ca..dd36af9dc 100644 --- a/src/routes/versions.rs +++ b/src/routes/versions.rs @@ -1,12 +1,13 @@ use super::ApiError; -use crate::auth::get_user_from_headers; +use crate::auth::{check_is_moderator_from_headers, get_user_from_headers}; use crate::database; +use crate::file_hosting::FileHost; use crate::models; use crate::models::teams::Permissions; -use crate::models::users::Role; -use actix_web::{delete, get, web, HttpRequest, HttpResponse}; +use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; +use std::sync::Arc; // TODO: this needs filtering, and a better response type // Currently it only gives a list of ids, which have to be @@ -52,6 +53,7 @@ pub struct VersionIds { #[get("versions")] pub async fn versions_get( + req: HttpRequest, web::Query(ids): web::Query, pool: web::Data, ) -> Result { @@ -63,17 +65,48 @@ pub async fn versions_get( .await .map_err(|e| ApiError::DatabaseError(e.into()))?; - let versions: Vec = versions_data - .into_iter() - .filter_map(|v| v) - .map(convert_version) - .collect(); + let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); + + let mut versions = Vec::new(); + + for version_data in versions_data { + if let Some(version) = version_data { + let mut authorized = version.accepted; + + if let Some(user) = &user_option { + if !authorized { + if user.role.is_mod() { + authorized = true; + } else { + let user_id: database::models::ids::UserId = user.id.into(); + + let member_exists = sqlx::query!( + "SELECT EXISTS(SELECT 1 FROM team_members tm INNER JOIN mods m ON m.team_id = tm.id AND m.id = $1 WHERE tm.user_id = $2)", + version.mod_id as database::models::ModId, + user_id as database::models::ids::UserId, + ) + .fetch_one(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))? + .exists; + + authorized = member_exists.unwrap_or(false); + } + } + } + + if authorized { + versions.push(convert_version(version)); + } + } + } Ok(HttpResponse::Ok().json(versions)) } #[get("{version_id}")] pub async fn version_get( + req: HttpRequest, info: web::Path<(models::ids::VersionId,)>, pool: web::Data, ) -> Result { @@ -81,8 +114,29 @@ pub async fn version_get( let version_data = database::models::Version::get_full(id.into(), &**pool) .await .map_err(|e| ApiError::DatabaseError(e.into()))?; + let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); if let Some(data) = version_data { + if let Some(user) = user_option { + if !data.accepted && !user.role.is_mod() { + let user_id: database::models::ids::UserId = user.id.into(); + + let member_exists = sqlx::query!( + "SELECT EXISTS(SELECT 1 FROM team_members tm INNER JOIN mods m ON m.team_id = tm.id AND m.id = $1 WHERE tm.user_id = $2)", + data.mod_id as database::models::ModId, + user_id as database::models::ids::UserId, + ) + .fetch_one(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))? + .exists; + + if !member_exists.unwrap_or(false) { + return Ok(HttpResponse::NotFound().body("")); + } + } + } + Ok(HttpResponse::Ok().json(convert_version(data))) } else { Ok(HttpResponse::NotFound().body("")) @@ -106,7 +160,7 @@ fn convert_version(data: database::models::version_item::QueryVersion) -> models "release" => VersionType::Release, "beta" => VersionType::Beta, "alpha" => VersionType::Alpha, - _ => VersionType::Alpha, + _ => VersionType::Release, }, files: data @@ -141,6 +195,228 @@ fn convert_version(data: database::models::version_item::QueryVersion) -> models } } +#[derive(Serialize, Deserialize)] +pub struct EditVersion { + pub name: Option, + pub changelog: Option, + pub version_type: Option, + pub dependencies: Option>, + pub game_versions: Option>, + pub loaders: Option>, + pub accepted: Option, +} + +#[patch("{id}")] +pub async fn version_edit( + req: HttpRequest, + info: web::Path<(models::ids::VersionId,)>, + pool: web::Data, + file_host: web::Data>, + new_version: web::Json, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + + let version_id = info.into_inner().0; + let id = version_id.into(); + + let result = database::models::Version::get_full(id, &**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + if let Some(version_item) = result { + let mod_item = database::models::Mod::get(version_item.mod_id, &**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))? + .ok_or_else(|| { + ApiError::InvalidInputError( + "Attempted to edit version not attached to mod. How did this happen?" + .to_string(), + ) + })?; + + let team_member = database::models::TeamMember::get_from_user_id( + mod_item.team_id, + user.id.into(), + &**pool, + ) + .await?; + let permissions; + + if let Some(member) = team_member { + permissions = Some(member.permissions) + } else if user.role.is_mod() { + permissions = Some(Permissions::ALL) + } else { + permissions = None + } + + if let Some(perms) = permissions { + if !perms.contains(Permissions::UPLOAD_VERSION) { + return Err(ApiError::CustomAuthenticationError( + "You do not have the permissions to edit this version!".to_string(), + )); + } + + let mut transaction = pool + .begin() + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + if let Some(accepted) = &new_version.accepted { + if !user.role.is_mod() { + return Err(ApiError::CustomAuthenticationError( + "You do not have the permissions to edit the approval of this version!" + .to_string(), + )); + } + + sqlx::query!( + " + UPDATE versions + SET accepted = $1 + WHERE (id = $2) + ", + accepted, + id as database::models::ids::VersionId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + + if let Some(name) = &new_version.name { + sqlx::query!( + " + UPDATE versions + SET name = $1 + WHERE (id = $2) + ", + name, + id as database::models::ids::VersionId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + + if let Some(version_type) = &new_version.version_type { + let channel = database::models::ids::ChannelId::get_id( + version_type.as_str(), + &mut *transaction, + ) + .await? + .ok_or_else(|| { + ApiError::InvalidInputError( + "No database entry for version type provided.".to_string(), + ) + })?; + + sqlx::query!( + " + UPDATE versions + SET release_channel = $1 + WHERE (id = $2) + ", + channel as database::models::ids::ChannelId, + id as database::models::ids::VersionId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + + if let Some(dependencies) = &new_version.dependencies { + sqlx::query!( + " + DELETE FROM dependencies WHERE dependent_id = $1 + ", + id as database::models::ids::VersionId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + for dependency in dependencies { + let dependency_id: database::models::ids::VersionId = dependency.clone().into(); + + sqlx::query!( + " + INSERT INTO dependencies (dependent_id, dependency_id) + VALUES ($1, $2) + ", + id as database::models::ids::VersionId, + dependency_id as database::models::ids::VersionId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + } + + if let Some(loaders) = &new_version.loaders { + sqlx::query!( + " + DELETE FROM loaders_versions WHERE version_id = $1 + ", + id as database::models::ids::VersionId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + for loader in loaders { + let loader_id = + database::models::categories::Loader::get_id(&loader.0, &mut *transaction) + .await? + .ok_or_else(|| { + ApiError::InvalidInputError( + "No database entry for loader provided.".to_string(), + ) + })?; + + sqlx::query!( + " + INSERT INTO loaders_versions (loader_id, version_id) + VALUES ($1, $2) + ", + loader_id as database::models::ids::LoaderId, + id as database::models::ids::VersionId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + } + + if let Some(body) = &new_version.changelog { + let mod_id: models::mods::ModId = version_item.mod_id.into(); + let body_path = format!( + "data/{}/versions/{}/changelog.md", + mod_id, version_item.version_number + ); + + file_host.delete_file_version("", &*body_path).await?; + + file_host + .upload_file("text/plain", &body_path, body.clone().into_bytes()) + .await?; + } + + transaction + .commit() + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + Ok(HttpResponse::Ok().body("")) + } else { + Err(ApiError::CustomAuthenticationError( + "You do not have permission to edit this version!".to_string(), + )) + } + } else { + Ok(HttpResponse::NotFound().body("")) + } +} + #[delete("{version_id}")] pub async fn version_delete( req: HttpRequest, @@ -150,7 +426,7 @@ pub async fn version_delete( let user = get_user_from_headers(req.headers(), &**pool).await?; let id = info.into_inner().0; - if user.role != Role::Moderator || user.role != Role::Admin { + if user.role.is_mod() { let version = database::models::Version::get(id.into(), &**pool) .await .map_err(|e| ApiError::DatabaseError(e.into()))? @@ -192,3 +468,139 @@ pub async fn version_delete( Ok(HttpResponse::NotFound().body("")) } } + +#[derive(Deserialize)] +pub struct Algorithm { + #[serde(default = "default_algorithm")] + algorithm: String, +} + +fn default_algorithm() -> String { + "sha1".into() +} + +// under /api/v1/version_file/{hash} +#[get("{version_id}")] +pub async fn get_version_from_hash( + info: web::Path<(String,)>, + pool: web::Data, + algorithm: web::Query, +) -> Result { + let hash = info.into_inner().0; + + let result = sqlx::query!( + " + SELECT version_id FROM files + INNER JOIN hashes ON hash = $1 AND algorithm = $2 + ", + hash.as_bytes(), + algorithm.algorithm + ) + .fetch_optional(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + if let Some(id) = result { + let version_data = database::models::Version::get_full( + database::models::VersionId(id.version_id), + &**pool, + ) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + if let Some(data) = version_data { + Ok(HttpResponse::Ok().json(convert_version(data))) + } else { + Ok(HttpResponse::NotFound().body("")) + } + } else { + Ok(HttpResponse::NotFound().body("")) + } +} + +// under /api/v1/version_file/{hash} +#[delete("{version_id}")] +pub async fn delete_file( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + file_host: web::Data>, + algorithm: web::Query, +) -> Result { + check_is_moderator_from_headers(req.headers(), &**pool).await?; + + let hash = info.into_inner().0; + + let result = sqlx::query!( + " + SELECT version_id, filename FROM files + INNER JOIN hashes ON hash = $1 AND algorithm = $2 + ", + hash.as_bytes(), + algorithm.algorithm + ) + .fetch_optional(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + if let Some(row) = result { + let version_data = database::models::Version::get_full( + database::models::VersionId(row.version_id), + &**pool, + ) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + if let Some(data) = version_data { + let mut transaction = pool + .begin() + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + sqlx::query!( + " + DELETE FROM hashes + WHERE hash = $1 AND algorithm = $2 + ", + hash.as_bytes(), + algorithm.algorithm + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + sqlx::query!( + " + DELETE FROM files + WHERE files.version_id = $1 + ", + data.id as database::models::ids::VersionId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + let mod_id: models::mods::ModId = data.mod_id.into(); + file_host + .delete_file_version( + "", + &format!( + "data/{}/versions/{}/{}", + mod_id, data.version_number, row.filename + ), + ) + .await?; + + transaction + .commit() + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + Ok(HttpResponse::Ok().body("")) + } else { + Ok(HttpResponse::NotFound().body("")) + } + } else { + Ok(HttpResponse::NotFound().body("")) + } +} diff --git a/src/search/indexing/local_import.rs b/src/search/indexing/local_import.rs index 87635e058..7ca907cc7 100644 --- a/src/search/indexing/local_import.rs +++ b/src/search/indexing/local_import.rs @@ -14,12 +14,29 @@ pub async fn index_local(pool: PgPool) -> Result, IndexingE let mut mods = sqlx::query!( " - SELECT m.id, m.title, m.description, m.downloads, m.icon_url, m.body_url, m.published, m.updated, m.team_id FROM mods m + SELECT m.id, m.title, m.description, m.downloads, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.status FROM mods m " ).fetch(&pool); while let Some(result) = mods.next().await { if let Ok(mod_data) = result { + let status = crate::models::mods::ModStatus::from_str( + &sqlx::query!( + " + SELECT status FROM statuses + WHERE id = $1 + ", + mod_data.status, + ) + .fetch_one(&pool) + .await? + .status, + ); + + if !status.is_searchable() { + continue; + } + let versions = sqlx::query!( " SELECT DISTINCT gv.version, gv.created FROM versions