diff --git a/migrations/20201122043349_more-mod-data.sql b/migrations/20201122043349_more-mod-data.sql new file mode 100644 index 000000000..101dba36c --- /dev/null +++ b/migrations/20201122043349_more-mod-data.sql @@ -0,0 +1,63 @@ +CREATE TABLE donation_platforms ( + id serial PRIMARY KEY, + short varchar(100) UNIQUE NOT NULL, + name varchar(500) UNIQUE NOT NULL +); + +INSERT INTO donation_platforms (short, name) VALUES ('patreon', 'Patreon'); +INSERT INTO donation_platforms (short, name) VALUES ('bmac', 'Buy Me a Coffee'); +INSERT INTO donation_platforms (short, name) VALUES ('paypal', 'PayPal'); +INSERT INTO donation_platforms (short, name) VALUES ('github', 'GitHub Sponsors'); +INSERT INTO donation_platforms (short, name) VALUES ('ko-fi', 'Ko-fi'); +INSERT INTO donation_platforms (short, name) VALUES ('other', 'Other'); + +CREATE TABLE mods_donations ( + joining_mod_id bigint REFERENCES mods ON UPDATE CASCADE NOT NULL, + joining_platform_id int REFERENCES donation_platforms ON UPDATE CASCADE NOT NULL, + url varchar(2048) NOT NULL, + PRIMARY KEY (joining_mod_id, joining_platform_id) +); + +CREATE TABLE side_types ( + id serial PRIMARY KEY, + name varchar(64) UNIQUE NOT NULL +); + +INSERT INTO side_types (name) VALUES ('required'); +INSERT INTO side_types (name) VALUES ('no-functionality'); +INSERT INTO side_types (name) VALUES ('unsupported'); +INSERT INTO side_types (name) VALUES ('unknown'); + +CREATE TABLE licenses ( + id serial PRIMARY KEY, + short varchar(60) UNIQUE NOT NULL, + name varchar(1000) UNIQUE NOT NULL +); + +INSERT INTO licenses (short, name) VALUES ('custom', 'Custom License'); + +ALTER TABLE versions + ADD COLUMN featured BOOLEAN NOT NULL default FALSE; +ALTER TABLE files + ADD COLUMN is_primary BOOLEAN NOT NULL default FALSE; + +ALTER TABLE mods + ADD COLUMN license integer REFERENCES licenses NOT NULL default 1; +ALTER TABLE mods + ADD COLUMN license_url varchar(1000) NULL; +ALTER TABLE mods + ADD COLUMN client_side integer REFERENCES side_types NOT NULL default 4; +ALTER TABLE mods + ADD COLUMN server_side integer REFERENCES side_types NOT NULL default 4; +ALTER TABLE mods + ADD COLUMN discord_url varchar(255) NULL; +ALTER TABLE mods + ADD COLUMN slug varchar(255) NULL UNIQUE; + +CREATE TABLE downloads ( + id serial PRIMARY KEY, + version_id bigint REFERENCES versions ON UPDATE CASCADE NOT NULL, + date timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL, + -- A SHA1 hash of the downloader IP address + identifier varchar(40) NOT NULL +); \ No newline at end of file diff --git a/sqlx-data.json b/sqlx-data.json index c81116374..f35d0e19d 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -1,5 +1,17 @@ { "db": "PostgreSQL", + "0267d1ea5387d4acfc132aeb4776004a1ebb048e7789e686bfaba3357d392f62": { + "query": "\n DELETE FROM mods_donations\n WHERE joining_mod_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, "03209c5bda2d704e688439919a7b3903db6ad7caebf7ddafb3ea52d312d47bfb": { "query": "\n INSERT INTO users (\n id, github_id, username, name, email,\n avatar_url, bio, created\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8\n )\n ", "describe": { @@ -86,6 +98,20 @@ ] } }, + "07ebc9dc82cd012cd4f5880b1eb3d82602c195a3e3ddd557103ee037aa6dad1c": { + "query": "\n INSERT INTO mods_donations (joining_mod_id, joining_platform_id, url)\n VALUES ($1, $2, $3)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int4", + "Varchar" + ] + }, + "nullable": [] + } + }, "0a1a470c12b84c7e171f0f51e8e541e9abe8bbee17fc441a5054e1dfd5607c05": { "query": "\n UPDATE versions\n SET name = $1\n WHERE (id = $2)\n ", "describe": { @@ -226,72 +252,17 @@ "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 ", + "16b3ac53ef5e94f51ab39484add21e2f76d49015917dc877560607a31f5537e9": { + "query": "\n UPDATE users\n SET email = $1\n WHERE (id = $2)\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" - } - ], + "columns": [], "parameters": { "Left": [ + "Varchar", "Int8" ] }, - "nullable": [ - false, - false, - false, - false, - true, - false, - false, - false, - false - ] + "nullable": [] } }, "17e6d30c3693e9bd9f772f3dc4e2eafe75fdeecfdcf2746eac641f77ced6b8a8": { @@ -362,6 +333,19 @@ ] } }, + "19dc22c4d6d14222f8e8bace74c2961761c53b7375460ade15af921754d5d7da": { + "query": "\n UPDATE mods\n SET license = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int8" + ] + }, + "nullable": [] + } + }, "1c7b0eb4341af5a7942e52f632cf582561f10b4b6a41a082fb8a60f04ac17c6e": { "query": "SELECT EXISTS(SELECT 1 FROM states WHERE id=$1)", "describe": { @@ -382,6 +366,38 @@ ] } }, + "1ce90594000fa30876bf277d9ebe2901acf9afaf256dd4488166d55fdd950347": { + "query": "\n DELETE FROM donation_platforms\n WHERE short = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + } + }, + "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 + ] + } + }, "1ffce9b2d5c9fa6c8b9abce4bad9f9419c44ad6367b7463b979c91b9b5b4fea1": { "query": "SELECT EXISTS(SELECT 1 FROM versions WHERE id=$1)", "describe": { @@ -402,6 +418,35 @@ ] } }, + "1fff206f9fce8353668875267a5ac31a3610f0d6a2131c2230e3c98c6e6b963a": { + "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, discord_url,\n client_side, server_side, license_url, license,\n slug\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8, $9,\n $10, $11, $12, $13,\n $14, $15, $16, $17,\n $18\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Varchar", + "Varchar", + "Varchar", + "Timestamptz", + "Int4", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Int4", + "Varchar", + "Int4", + "Int4", + "Varchar", + "Int4", + "Varchar" + ] + }, + "nullable": [] + } + }, "21d268dbad5ffd34d476998aea4475cdb071e8cfbb245c4853ee5f4c44b0c8ae": { "query": "\n SELECT id, user_id, member_name, role, permissions, accepted\n FROM team_members\n WHERE (team_id = $1 AND accepted = TRUE)\n ", "describe": { @@ -520,6 +565,44 @@ ] } }, + "2439ae8db5ae81aad03351e9a2e19259ef033110ed1c2d71bc0a643104ca32d0": { + "query": "\n UPDATE versions\n SET downloads = downloads + 1\n WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, + "24e5daad907eec54505274f93952d5c20f4bbdd3f771eb0a2fdfa6324768df39": { + "query": "\n SELECT short, name FROM licenses\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "short", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false, + false + ] + } + }, "25131559cb73a088000ab6379a769233440ade6c7511542da410065190d203fc": { "query": "\n SELECT id FROM loaders\n WHERE loader = $1\n ", "describe": { @@ -553,6 +636,111 @@ "nullable": [] } }, + "2abecb467a9ad3b792babf20e09601c011fc2622e101e98054baeaacaa16795a": { + "query": "\n DELETE FROM licenses\n WHERE short = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + } + }, + "2b8dafe9c3df9fd25235a13868e8e7607decfbe96a413cc576919a1fb510f269": { + "query": "\n UPDATE mods\n SET discord_url = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + } + }, + "2f5ce51392035ccfd846b16824ad6ee607de87bb55e7152a51f21035f83021c7": { + "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, v.accepted, v.featured\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" + }, + { + "ordinal": 10, + "name": "featured", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false + ] + } + }, "2fa070eef3fe8f708a1495104f78eda2bfa0fe19ada2bf66ac35fb2468631774": { "query": "\n SELECT category FROM categories\n ", "describe": { @@ -571,6 +759,19 @@ ] } }, + "30bb72960840e11c2ef0f7ebebe33010ebdd6f0a7a977542c7a82c2ad0fb1e85": { + "query": "\n INSERT INTO downloads (\n version_id, identifier\n )\n VALUES (\n $1, $2\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Varchar" + ] + }, + "nullable": [] + } + }, "3135db1c5309dac7580a731b2829397ae7bdd6c9a67b21e813f26a4f5aa251a9": { "query": "\n SELECT status FROM statuses\n WHERE id = $1\n ", "describe": { @@ -591,6 +792,27 @@ ] } }, + "31cd009b0bc579abe1d90169cdfbbd201c6696e9eed5c880d2046d030adf6ffd": { + "query": "\n SELECT id FROM files\n INNER JOIN hashes ON hash = $1 AND algorithm = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Bytea", + "Text" + ] + }, + "nullable": [ + false + ] + } + }, "33fc96ac71cfa382991cfb153e89da1e9f43ebf5367c28b30c336b758222307b": { "query": "\n DELETE FROM loaders_versions\n WHERE loaders_versions.version_id = $1\n ", "describe": { @@ -642,80 +864,6 @@ "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": { @@ -797,6 +945,80 @@ "nullable": [] } }, + "4805a7c9f1dbc9b16cff88da43ba506dd5cfa2b96adca596929579ef336fba6d": { + "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, v.featured\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" + }, + { + "ordinal": 9, + "name": "featured", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + false, + false, + false + ] + } + }, "486c13abb9648b16a4c354f25754408d649b13980fe7486cdcfdcacfa2725c2b": { "query": "\n SELECT * FROM versions\n WHERE accepted = FALSE\n ORDER BY date_published ASC\n LIMIT $1;\n ", "describe": { @@ -850,6 +1072,11 @@ "ordinal": 9, "name": "accepted", "type_info": "Bool" + }, + { + "ordinal": 10, + "name": "featured", + "type_info": "Bool" } ], "parameters": { @@ -867,6 +1094,7 @@ false, false, false, + false, false ] } @@ -891,6 +1119,19 @@ ] } }, + "4a54d350b4695c32a802675506e85b0506fc62a63ca0ee5f38890824301d6515": { + "query": "\n UPDATE mods\n SET server_side = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int8" + ] + }, + "nullable": [] + } + }, "4c99c0840159d18e88cd6094a41117258f2337346c145d926b5b610c76b5125f": { "query": "\n SELECT c.category\n FROM mods_categories mc\n INNER JOIN categories c ON mc.joining_category_id=c.id\n WHERE mc.joining_mod_id = $1\n ", "describe": { @@ -957,6 +1198,28 @@ "nullable": [] } }, + "50a15c443b01cefc478a3b5ca03bb9b279782d74bcf42ee4e7c06581457c130d": { + "query": "\n INSERT INTO versions (\n id, mod_id, author_id, name, version_number,\n changelog_url, date_published,\n downloads, release_channel, accepted, featured\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7,\n $8, $9,\n $10, $11\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Int8", + "Varchar", + "Varchar", + "Varchar", + "Timestamptz", + "Int4", + "Int4", + "Bool", + "Bool" + ] + }, + "nullable": [] + } + }, "55df56cd9938bca0edbf8d91649c8ce6d946125ad64e570ab127f6d9c6061787": { "query": "\n SELECT version_id, filename FROM files\n INNER JOIN hashes ON hash = $1 AND algorithm = $2\n ", "describe": { @@ -997,36 +1260,49 @@ "nullable": [] } }, - "59cf9d085593887595ea45246291f2cd64fc6677d551e96bdb60c09ff1eebf99": { - "query": "\n SELECT files.id, files.url, files.filename FROM files\n WHERE files.version_id = $1\n ", + "56fc196cbe33032b699348d7a2f3366100bc54decb1d18bb6aad865a88096c67": { + "query": "\n SELECT id FROM mods\n WHERE slug = $1\n ", "describe": { "columns": [ { "ordinal": 0, "name": "id", "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "url", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "filename", - "type_info": "Varchar" } ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "5c4262689205aafdd97a74bee0003f39eef0a34c97f97a939c14fb8fe349f7eb": { + "query": "\n UPDATE files\n SET is_primary = TRUE\n WHERE (id = $1)\n ", + "describe": { + "columns": [], "parameters": { "Left": [ "Int8" ] }, - "nullable": [ - false, - false, - false - ] + "nullable": [] + } + }, + "5ca43f2fddda27ad857f230a3427087f1e58150949adc6273156718730c10f69": { + "query": "\n UPDATE users\n SET role = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] } }, "5d7425cfa91e332bf7cc14aa5c300b997e941c49757606f6b906cb5e060d3179": { @@ -1041,6 +1317,49 @@ "nullable": [] } }, + "5eb2795d25d6d03e22564048c198d821cd5ff22eb4e39b9dd7f198c9113d4f87": { + "query": "\n UPDATE users\n SET name = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + } + }, + "6131d32a65f5e04775308386812f25c6d8464582678536a392a4a3737667f363": { + "query": "\n SELECT id, short, name FROM licenses\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "short", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "name", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false + ] + } + }, "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": { @@ -1109,6 +1428,36 @@ "ordinal": 12, "name": "updated", "type_info": "Timestamptz" + }, + { + "ordinal": 13, + "name": "license", + "type_info": "Int4" + }, + { + "ordinal": 14, + "name": "license_url", + "type_info": "Varchar" + }, + { + "ordinal": 15, + "name": "client_side", + "type_info": "Int4" + }, + { + "ordinal": 16, + "name": "server_side", + "type_info": "Int4" + }, + { + "ordinal": 17, + "name": "discord_url", + "type_info": "Varchar" + }, + { + "ordinal": 18, + "name": "slug", + "type_info": "Varchar" } ], "parameters": { @@ -1130,6 +1479,51 @@ true, true, false, + false, + false, + true, + false, + false, + true, + true + ] + } + }, + "6a0d8560c0a392f66023b4186834f84d822e797b0c8b5ad360ef93a2c2300319": { + "query": "\n SELECT f.url url, f.id id, f.version_id version_id, v.mod_id mod_id FROM files f\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN hashes ON hash = $1 AND algorithm = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "url", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "version_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "mod_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Bytea", + "Text" + ] + }, + "nullable": [ + false, + false, + false, false ] } @@ -1174,6 +1568,18 @@ "nullable": [] } }, + "6d883ea05aead20f571a0f63bfd63f1d432717ec7a0fb9ab29e01fcb061b3afc": { + "query": "\n UPDATE files\n SET is_primary = FALSE\n WHERE (version_id = $1)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "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": { @@ -1237,54 +1643,112 @@ "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 ", + "70cdf1b4a17405974909d89b1437a8425792d620f9ed67fd8e31e004e4609e83": { + "query": "\n UPDATE users\n SET username = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + } + }, + "71b0c906abfb16d399e23d1b6afbecb5e55c6e9b65cdc8de7d4fbf9ece682d03": { + "query": "\n SELECT title, description, downloads,\n icon_url, 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": "mod_id", - "type_info": "Int8" + "name": "title", + "type_info": "Varchar" }, { "ordinal": 1, - "name": "author_id", - "type_info": "Int8" + "name": "description", + "type_info": "Varchar" }, { "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": 3, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "body_url", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "published", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "updated", + "type_info": "Timestamptz" + }, { "ordinal": 7, - "name": "release_channel", + "name": "status", "type_info": "Int4" }, { "ordinal": 8, - "name": "accepted", - "type_info": "Bool" + "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": "discord_url", + "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "license_url", + "type_info": "Varchar" + }, + { + "ordinal": 13, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 14, + "name": "client_side", + "type_info": "Int4" + }, + { + "ordinal": 15, + "name": "server_side", + "type_info": "Int4" + }, + { + "ordinal": 16, + "name": "license", + "type_info": "Int4" + }, + { + "ordinal": 17, + "name": "slug", + "type_info": "Varchar" } ], "parameters": { @@ -1296,12 +1760,21 @@ false, false, false, - false, true, false, false, false, - false + false, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true ] } }, @@ -1337,6 +1810,19 @@ ] } }, + "7269f523a289e9d0dfe710074492e2eb547359e62d42234abed698871905edd6": { + "query": "\n UPDATE users\n SET avatar_url = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + } + }, "72c1e6de8f2c8d89be030454eeab6d5c9695164af2ebfb8d7e94b2deee4f130d": { "query": "\n SELECT c.category\n FROM mods_categories mc\n INNER JOIN categories c ON mc.joining_category_id=c.id\n WHERE mc.joining_mod_id = $1\n ", "describe": { @@ -1482,6 +1968,48 @@ ] } }, + "763eaff18057e579472960e9e8256c22ae275f24a45da96bc3e47385376faae3": { + "query": "\n UPDATE mods\n SET downloads = downloads + 1\n WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, + "76db1c204139e18002e5751c3dcefff79791a1dd852b62d34fcf008151e8945a": { + "query": "\n SELECT id, short, name FROM donation_platforms\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "short", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "name", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false + ] + } + }, "78c8b561e37e3aed48d3a4108ce7fd81866c6835ea91517ffc90c30e1284246e": { "query": "\n SELECT id FROM versions\n WHERE mod_id = $1\n ORDER BY date_published ASC\n ", "describe": { @@ -1565,6 +2093,27 @@ ] } }, + "82515e4e7e88f1193c956f032caabc70f535f925e212de30f974afd3ec126092": { + "query": "\n INSERT INTO licenses (short, name)\n VALUES ($1, $2)\n ON CONFLICT (short) DO NOTHING\n RETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Varchar" + ] + }, + "nullable": [ + false + ] + } + }, "8ba2b2c38958f1c542e514fc62ab4682f58b0b442ac1842d20625420698e34ec": { "query": "\n DELETE FROM team_members\n WHERE (team_id = $1 AND user_id = $2 AND NOT role = $3)\n ", "describe": { @@ -1591,6 +2140,19 @@ "nullable": [] } }, + "8fd5d332e9cd2f760f956bf4936350f29df414552643bcfb352ca8a8a0b98439": { + "query": "\n UPDATE mods\n SET icon_url = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + } + }, "96585aa2586e69eeae18f5a0c97a93d2c221c8a97470e5f59839b1a52b2e353a": { "query": "\n SELECT version_id FROM files\n INNER JOIN hashes ON hash = $1 AND algorithm = $2\n ", "describe": { @@ -1612,6 +2174,26 @@ ] } }, + "97143e41c18d191d09d244113b7b6cdf5bd6ab89c62ac46d0980d700ab288f48": { + "query": "\n SELECT name FROM side_types\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false + ] + } + }, "99a1eac69d7f5a5139703df431e6a5c3012a90143a8c635f93632f04d0bc41d4": { "query": "\n UPDATE mods\n SET wiki_url = $1\n WHERE (id = $2)\n ", "describe": { @@ -1625,8 +2207,249 @@ "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 ", + "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": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "username", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text", + "Int8" + ] + }, + "nullable": [ + false, + false + ] + } + }, + "9ef0577ee4845091a0d29bad6d1130223767e6d6140cafb9013fdb428d5159dd": { + "query": "\n INSERT INTO team_members (id, team_id, user_id, member_name, role, permissions, accepted)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Int8", + "Varchar", + "Varchar", + "Int8", + "Bool" + ] + }, + "nullable": [] + } + }, + "9ef9174fa003186a07fbf465e9ac083f6d452cf3c702c6ca05b1f3bbc0e30b5a": { + "query": "\n DELETE FROM files\n WHERE files.version_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, + "a28188c4840d0f3449379b3bba6b3c4af9483e01f50fd56785317398a59881ca": { + "query": "\n SELECT files.id, files.url, files.filename, files.is_primary FROM files\n WHERE files.version_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "url", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "filename", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "is_primary", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + } + }, + "a39ce28b656032f862b205cffa393a76b989f4803654a615477a94fda5f57354": { + "query": "\n DELETE FROM states\n WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, + "a40e4075ba1bff5b6fde104ed1557ad8d4a75d7d90d481decd222f31685c4981": { + "query": "\n DELETE FROM dependencies WHERE dependent_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, + "a647c282a276b63f36d2d8a253c32d0f627cea9cab8eb1b32b39875536bdfcbb": { + "query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, + "a90bb6904e1b790c0e29e060dac5ba4c2a6087e07c1197dc1f59f0aff31944c9": { + "query": "\n DELETE FROM states\n WHERE expires < CURRENT_DATE\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + } + }, + "a91fabe9e620bd700362c68631628725419183025c9699f4bd31c22b813b2824": { + "query": "\n SELECT files.id, files.url, files.filename, files.is_primary FROM files\n WHERE files.version_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "url", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "filename", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "is_primary", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + 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": { + "columns": [ + { + "ordinal": 0, + "name": "github_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "avatar_url", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "username", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "bio", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "created", + "type_info": "Timestamptz" + }, + { + "ordinal": 7, + "name": "role", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + true, + true, + true, + true, + false, + true, + false, + false + ] + } + }, + "aac56ba5384e3b2353c1adb359856fe149963488f751f1f7eb056455874e994c": { + "query": "\n SELECT id, title, description, downloads,\n icon_url, 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": [ { @@ -1691,8 +2514,38 @@ }, { "ordinal": 12, + "name": "discord_url", + "type_info": "Varchar" + }, + { + "ordinal": 13, + "name": "license_url", + "type_info": "Varchar" + }, + { + "ordinal": 14, "name": "team_id", "type_info": "Int8" + }, + { + "ordinal": 15, + "name": "client_side", + "type_info": "Int4" + }, + { + "ordinal": 16, + "name": "server_side", + "type_info": "Int4" + }, + { + "ordinal": 17, + "name": "license", + "type_info": "Int4" + }, + { + "ordinal": 18, + "name": "slug", + "type_info": "Varchar" } ], "parameters": { @@ -1713,78 +2566,18 @@ true, true, true, - false + true, + true, + false, + false, + false, + false, + true ] } }, - "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": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "username", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Text", - "Int8" - ] - }, - "nullable": [ - false, - false - ] - } - }, - "9ef0577ee4845091a0d29bad6d1130223767e6d6140cafb9013fdb428d5159dd": { - "query": "\n INSERT INTO team_members (id, team_id, user_id, member_name, role, permissions, accepted)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Int8", - "Varchar", - "Varchar", - "Int8", - "Bool" - ] - }, - "nullable": [] - } - }, - "9ef9174fa003186a07fbf465e9ac083f6d452cf3c702c6ca05b1f3bbc0e30b5a": { - "query": "\n DELETE FROM files\n WHERE files.version_id = $1\n ", + "acbafe265c4b7a1c95b0494a0a03c8bd2cd778ae561ef5a662fa931ca26cf603": { + "query": "\n DELETE FROM mods_donations\n WHERE joining_mod_id = $1\n ", "describe": { "columns": [], "parameters": { @@ -1795,159 +2588,6 @@ "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": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Varchar", - "Varchar", - "Varchar", - "Timestamptz", - "Int4", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Int4" - ] - }, - "nullable": [] - } - }, - "a39ce28b656032f862b205cffa393a76b989f4803654a615477a94fda5f57354": { - "query": "\n DELETE FROM states\n WHERE id = $1\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "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": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "url", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "filename", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false - ] - } - }, - "a647c282a276b63f36d2d8a253c32d0f627cea9cab8eb1b32b39875536bdfcbb": { - "query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - } - }, - "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": { - "columns": [ - { - "ordinal": 0, - "name": "github_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "name", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "email", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "avatar_url", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "username", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "bio", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "created", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "role", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - true, - true, - true, - true, - false, - true, - false, - false - ] - } - }, "ad273daadd249e93b500c339a62aac48a497ebcc15164776ad20860a4d232896": { "query": "\n SELECT DISTINCT gv.version, gv.created 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": { @@ -2212,6 +2852,19 @@ ] } }, + "c100a3be0e1b7bf449576c4052d87494979cb89d194805a5ce9e928eef796ae9": { + "query": "\n UPDATE mods\n SET license_url = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + } + }, "c1fddbf97350871b79cb0c235b1f7488c6616b7c1dfbde76a712fd57e91ba158": { "query": "\n SELECT id FROM game_versions\n WHERE version = $1\n ", "describe": { @@ -2232,6 +2885,33 @@ ] } }, + "c545a74e902c5c63bca1057b76e94b9547ee21fadbc61964f45837915d5f4608": { + "query": "\n INSERT INTO mods_donations (\n joining_mod_id, joining_platform_id, url\n )\n VALUES (\n $1, $2, $3\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int4", + "Varchar" + ] + }, + "nullable": [] + } + }, + "c5d44333c62223bd3e68185d1fb3f95152fafec593da8d06c9b2b665218a02be": { + "query": "\n UPDATE mods\n SET client_side = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int8" + ] + }, + "nullable": [] + } + }, "c64c487b56a25b252ff070fe03a7416e84260df8a6f938a018cc768598e9435b": { "query": "\n SELECT category FROM categories\n WHERE id = $1\n ", "describe": { @@ -2252,6 +2932,89 @@ ] } }, + "c6cec0987be23419fc721799df8063594458f0d63abd32550c2a2196f40487b7": { + "query": "SELECT EXISTS(SELECT 1 FROM downloads WHERE version_id = $1 AND date > (CURRENT_DATE - INTERVAL '30 minutes ago') AND identifier = $2)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [ + null + ] + } + }, + "c7c1d5629b128a70d415a74f07d56e12911c4ac6a394dd1fd83ffa88bd6851fa": { + "query": "\n SELECT u.id, u.github_id, u.name, u.email,\n u.avatar_url, u.bio,\n u.created, u.role\n FROM users u\n WHERE u.username = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "github_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "avatar_url", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "bio", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "created", + "type_info": "Timestamptz" + }, + { + "ordinal": 7, + "name": "role", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + true, + true, + true, + true, + true, + false, + false + ] + } + }, "c82eb1b059b62444ab1d17e5a0bd7ef8acea4b05c6f3576c07d20c4ca7635a11": { "query": "\n INSERT INTO dependencies (dependent_id, dependency_id)\n VALUES ($1, $2)\n ", "describe": { @@ -2350,6 +3113,32 @@ ] } }, + "cdf37a51c68eaa7401da19c5b1e34097592027ceb777bfa535116e3a52b28958": { + "query": "\n SELECT joining_platform_id, url FROM mods_donations\n WHERE joining_mod_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "joining_platform_id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "url", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false + ] + } + }, "cf031f19c7882833a8a30348ee90175a5d8b1fb7d9645c5deb2dc68c6eb33683": { "query": "\n SELECT id FROM release_channels\n WHERE channel = $1\n ", "describe": { @@ -2402,6 +3191,27 @@ ] } }, + "d5b00d6237b04018822db529995f0b001cd1cabf5ca93b4aff37f12c4feb83f6": { + "query": "\n INSERT INTO donation_platforms (short, name)\n VALUES ($1, $2)\n ON CONFLICT (short) DO NOTHING\n RETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Varchar" + ] + }, + "nullable": [ + false + ] + } + }, "d6453e50041b5521fa9e919a9162e533bb9426f8c584d98474c6ad414db715c8": { "query": "SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)", "describe": { @@ -2422,6 +3232,39 @@ ] } }, + "d6708313e6bc0704109eb9d0990b67b0c18fb0b06460bb177aaa7b2fa19f5b20": { + "query": "\n UPDATE mods\n SET slug = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + } + }, + "d8020ed838c032c2c287dc0f08989b3ab7156f2571bc75505e6f57b0caeef9c7": { + "query": "\n SELECT id FROM donation_platforms\n WHERE short = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + } + }, "d8b4e7e382c77a05395124d5a6a27cccb687d0e2c31b76d49b03aa364d099d42": { "query": "\n DELETE FROM files\n WHERE files.version_id = $1\n ", "describe": { @@ -2434,6 +3277,26 @@ "nullable": [] } }, + "d97203c84aa3818d20bb88671c3160ce701f9c40c143f9a8f2ec6239e3165d84": { + "query": "\n SELECT id FROM licenses\n WHERE short = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + 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": { @@ -2521,6 +3384,32 @@ ] } }, + "e2d93718c6f26cf739d25b56b6c5e8c295bb4af7e2b8076d6a3664019b57e01f": { + "query": "\n SELECT short, name FROM licenses\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "short", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false, + false + ] + } + }, "e35fa345b43725309b976efffbc8f9e20a62a5e90a86a82a77b55c39c168d2de": { "query": "\n SELECT id FROM versions\n WHERE mod_id = $1\n ", "describe": { @@ -2541,6 +3430,19 @@ ] } }, + "e48c85a2b2e11691afae3799aa126bdd8b7338a973308bbab2760c18bb9cb0b7": { + "query": "\n UPDATE versions\n SET featured = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Bool", + "Int8" + ] + }, + "nullable": [] + } + }, "e53013562a3a9659df5c56185b92cf9cc30ce7e409f4762be98662161af593b9": { "query": "\n UPDATE team_members\n SET member_name = $1\n WHERE (team_id = $2 AND user_id = $3 AND NOT role = $4)\n ", "describe": { @@ -2568,6 +3470,80 @@ "nullable": [] } }, + "e6a5d7cde6fa3103752065e2c5febee39afe548ab5ec8860fcc7e6ff1ad3f42f": { + "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, v.featured\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" + }, + { + "ordinal": 9, + "name": "featured", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + false, + false, + false + ] + } + }, "e7d0a64a08df6783c942f2fcadd94dd45f8d96ad3d3736e52ce90f68d396cdab": { "query": "SELECT EXISTS(SELECT 1 FROM team_members WHERE id=$1)", "describe": { @@ -2781,90 +3757,17 @@ ] } }, - "f78dac3d15be1ea0d0ed43a4beadc04ec00d8ba68be2bb68cbc3f2ebe5c93dbd": { - "query": "\n SELECT 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 = $1\n ", + "f453b43772c4d2d9d09dc389eb95482cc75e7f0eaf9dc7ff48cf40f22f1497cc": { + "query": "\n UPDATE users\n SET bio = $1\n WHERE (id = $2)\n ", "describe": { - "columns": [ - { - "ordinal": 0, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "description", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "icon_url", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "body_url", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "published", - "type_info": "Timestamptz" - }, - { - "ordinal": 6, - "name": "updated", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "status", - "type_info": "Int4" - }, - { - "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": "team_id", - "type_info": "Int8" - } - ], + "columns": [], "parameters": { "Left": [ + "Varchar", "Int8" ] }, - "nullable": [ - false, - false, - false, - true, - false, - false, - false, - false, - true, - true, - true, - false - ] + "nullable": [] } }, "f7bea04e8e279e27a24de1bdf3c413daa8677994df5131494b28691ed6611efc": { @@ -2913,6 +3816,32 @@ ] } }, + "f8c00875a7450c74423f9913cc3500898e9fcb6aa7eb8fc2f6fd16dc560773de": { + "query": "\n SELECT short, name FROM donation_platforms\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "short", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false, + false + ] + } + }, "fb6178b27856ff583039a974173efe5d6be4e347b6cc1d4904cf750a40d1b77f": { "query": "\n SELECT dependency_id id FROM dependencies\n WHERE dependent_id = $1\n ", "describe": { @@ -2973,5 +3902,15 @@ false ] } + }, + "fe73b6928f13955840e8df248688908fb6d82dd1d35dc803676639a6e0864ed5": { + "query": "\n DELETE FROM downloads\n WHERE date < (CURRENT_DATE - INTERVAL '30 minutes ago')\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + } } } \ No newline at end of file diff --git a/src/database/models/categories.rs b/src/database/models/categories.rs index 3c8529bc2..658f4f968 100644 --- a/src/database/models/categories.rs +++ b/src/database/models/categories.rs @@ -17,6 +17,18 @@ pub struct Category { pub category: String, } +pub struct License { + pub id: LicenseId, + pub short: String, + pub name: String, +} + +pub struct DonationPlatform { + pub id: DonationPlatformId, + pub short: String, + pub name: String, +} + pub struct CategoryBuilder<'a> { pub name: Option<&'a str>, } @@ -453,3 +465,293 @@ impl<'a> GameVersionBuilder<'a> { Ok(GameVersionId(result.id)) } } + +#[derive(Default)] +pub struct LicenseBuilder<'a> { + pub short: Option<&'a str>, + pub name: Option<&'a str>, +} + +impl License { + pub fn builder() -> LicenseBuilder<'static> { + LicenseBuilder::default() + } + + pub async fn get_id<'a, E>(id: &str, exec: E) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + SELECT id FROM licenses + WHERE short = $1 + ", + id + ) + .fetch_optional(exec) + .await?; + + Ok(result.map(|r| LicenseId(r.id))) + } + + pub async fn get<'a, E>(id: LicenseId, exec: E) -> Result + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + SELECT short, name FROM licenses + WHERE id = $1 + ", + id as LicenseId + ) + .fetch_one(exec) + .await?; + + Ok(License { + id, + short: result.short, + name: result.name, + }) + } + + pub async fn list<'a, E>(exec: E) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + SELECT id, short, name FROM licenses + " + ) + .fetch_many(exec) + .try_filter_map(|e| async { + Ok(e.right().map(|c| License { + id: LicenseId(c.id), + short: c.short, + name: c.name, + })) + }) + .try_collect::>() + .await?; + + Ok(result) + } + + pub async fn remove<'a, E>(short: &str, exec: E) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + use sqlx::Done; + + let result = sqlx::query!( + " + DELETE FROM licenses + WHERE short = $1 + ", + short + ) + .execute(exec) + .await?; + + if result.rows_affected() == 0 { + // Nothing was deleted + Ok(None) + } else { + Ok(Some(())) + } + } +} + +impl<'a> LicenseBuilder<'a> { + /// The license's short name/abbreviation. Spaces must be replaced with '_' for it to be valid + pub fn short(self, short: &'a str) -> Result, DatabaseError> { + if short + .chars() + .all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c)) + { + Ok(Self { + short: Some(short), + ..self + }) + } else { + Err(DatabaseError::InvalidIdentifier(short.to_string())) + } + } + + /// The license's long name + pub fn name(self, name: &'a str) -> Result, DatabaseError> { + Ok(Self { + name: Some(name), + ..self + }) + } + + pub async fn insert<'b, E>(self, exec: E) -> Result + where + E: sqlx::Executor<'b, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + INSERT INTO licenses (short, name) + VALUES ($1, $2) + ON CONFLICT (short) DO NOTHING + RETURNING id + ", + self.short, + self.name, + ) + .fetch_one(exec) + .await?; + + Ok(LicenseId(result.id)) + } +} + +#[derive(Default)] +pub struct DonationPlatformBuilder<'a> { + pub short: Option<&'a str>, + pub name: Option<&'a str>, +} + +impl DonationPlatform { + pub fn builder() -> DonationPlatformBuilder<'static> { + DonationPlatformBuilder::default() + } + + pub async fn get_id<'a, E>( + id: &str, + exec: E, + ) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + SELECT id FROM donation_platforms + WHERE short = $1 + ", + id + ) + .fetch_optional(exec) + .await?; + + Ok(result.map(|r| DonationPlatformId(r.id))) + } + + pub async fn get<'a, E>( + id: DonationPlatformId, + exec: E, + ) -> Result + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + SELECT short, name FROM donation_platforms + WHERE id = $1 + ", + id as DonationPlatformId + ) + .fetch_one(exec) + .await?; + + Ok(DonationPlatform { + id, + short: result.short, + name: result.name, + }) + } + + pub async fn list<'a, E>(exec: E) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + SELECT id, short, name FROM donation_platforms + " + ) + .fetch_many(exec) + .try_filter_map(|e| async { + Ok(e.right().map(|c| DonationPlatform { + id: DonationPlatformId(c.id), + short: c.short, + name: c.name, + })) + }) + .try_collect::>() + .await?; + + Ok(result) + } + + pub async fn remove<'a, E>(short: &str, exec: E) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + use sqlx::Done; + + let result = sqlx::query!( + " + DELETE FROM donation_platforms + WHERE short = $1 + ", + short + ) + .execute(exec) + .await?; + + if result.rows_affected() == 0 { + // Nothing was deleted + Ok(None) + } else { + Ok(Some(())) + } + } +} + +impl<'a> DonationPlatformBuilder<'a> { + /// The donation platform short name. Spaces must be replaced with '_' for it to be valid + pub fn short(self, short: &'a str) -> Result, DatabaseError> { + if short + .chars() + .all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c)) + { + Ok(Self { + short: Some(short), + ..self + }) + } else { + Err(DatabaseError::InvalidIdentifier(short.to_string())) + } + } + + /// The donation platform long name + pub fn name(self, name: &'a str) -> Result, DatabaseError> { + Ok(Self { + name: Some(name), + ..self + }) + } + + pub async fn insert<'b, E>(self, exec: E) -> Result + where + E: sqlx::Executor<'b, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + INSERT INTO donation_platforms (short, name) + VALUES ($1, $2) + ON CONFLICT (short) DO NOTHING + RETURNING id + ", + self.short, + self.name, + ) + .fetch_one(exec) + .await?; + + Ok(DonationPlatformId(result.id)) + } +} diff --git a/src/database/models/ids.rs b/src/database/models/ids.rs index 055c48e84..2bad552b2 100644 --- a/src/database/models/ids.rs +++ b/src/database/models/ids.rs @@ -104,6 +104,15 @@ pub struct ModId(pub i64); #[derive(Copy, Clone, Debug, Type)] #[sqlx(transparent)] pub struct StatusId(pub i32); +#[derive(Copy, Clone, Debug, Type)] +#[sqlx(transparent)] +pub struct SideTypeId(pub i32); +#[derive(Copy, Clone, Debug, Type)] +#[sqlx(transparent)] +pub struct LicenseId(pub i32); +#[derive(Copy, Clone, Debug, Type)] +#[sqlx(transparent)] +pub struct DonationPlatformId(pub i32); #[derive(Copy, Clone, Debug, Type)] #[sqlx(transparent)] diff --git a/src/database/models/mod.rs b/src/database/models/mod.rs index 6056c3278..b05327a65 100644 --- a/src/database/models/mod.rs +++ b/src/database/models/mod.rs @@ -79,3 +79,44 @@ impl ids::StatusId { Ok(result.map(|r| ids::StatusId(r.id))) } } + +impl ids::SideTypeId { + pub async fn get_id<'a, E>( + side: &crate::models::mods::SideType, + exec: E, + ) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + SELECT id FROM side_types + WHERE name = $1 + ", + side.as_str() + ) + .fetch_optional(exec) + .await?; + + Ok(result.map(|r| ids::SideTypeId(r.id))) + } +} + +impl ids::DonationPlatformId { + pub async fn get_id<'a, E>(id: &str, exec: E) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + SELECT id FROM donation_platforms + WHERE short = $1 + ", + id + ) + .fetch_optional(exec) + .await?; + + Ok(result.map(|r| ids::DonationPlatformId(r.id))) + } +} diff --git a/src/database/models/mod_item.rs b/src/database/models/mod_item.rs index 083f53a3b..1eef19489 100644 --- a/src/database/models/mod_item.rs +++ b/src/database/models/mod_item.rs @@ -1,5 +1,36 @@ use super::ids::*; +pub struct DonationUrl { + pub mod_id: ModId, + pub platform_id: DonationPlatformId, + pub url: String, +} + +impl DonationUrl { + pub async fn insert( + &self, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result<(), sqlx::error::Error> { + sqlx::query!( + " + INSERT INTO mods_donations ( + joining_mod_id, joining_platform_id, url + ) + VALUES ( + $1, $2, $3 + ) + ", + self.mod_id as ModId, + self.platform_id as DonationPlatformId, + self.url, + ) + .execute(&mut *transaction) + .await?; + + Ok(()) + } +} + pub struct ModBuilder { pub mod_id: ModId, pub team_id: TeamId, @@ -10,9 +41,16 @@ pub struct ModBuilder { pub issues_url: Option, pub source_url: Option, pub wiki_url: Option, + pub license_url: Option, + pub discord_url: Option, pub categories: Vec, pub initial_versions: Vec, pub status: StatusId, + pub client_side: SideTypeId, + pub server_side: SideTypeId, + pub license: LicenseId, + pub slug: Option, + pub donation_urls: Vec, } impl ModBuilder { @@ -34,6 +72,12 @@ impl ModBuilder { issues_url: self.issues_url, source_url: self.source_url, wiki_url: self.wiki_url, + license_url: self.license_url, + discord_url: self.discord_url, + client_side: self.client_side, + server_side: self.server_side, + license: self.license, + slug: self.slug, }; mod_struct.insert(&mut *transaction).await?; @@ -42,6 +86,11 @@ impl ModBuilder { version.insert(&mut *transaction).await?; } + for mut donation in self.donation_urls { + donation.mod_id = self.mod_id; + donation.insert(&mut *transaction).await?; + } + for category in self.categories { sqlx::query!( " @@ -73,6 +122,12 @@ pub struct Mod { pub issues_url: Option, pub source_url: Option, pub wiki_url: Option, + pub license_url: Option, + pub discord_url: Option, + pub client_side: SideTypeId, + pub server_side: SideTypeId, + pub license: LicenseId, + pub slug: Option, } impl Mod { @@ -85,12 +140,16 @@ impl Mod { INSERT INTO mods ( id, team_id, title, description, body_url, published, downloads, icon_url, issues_url, - source_url, wiki_url, status + source_url, wiki_url, status, discord_url, + client_side, server_side, license_url, license, + slug ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, - $10, $11, $12 + $10, $11, $12, $13, + $14, $15, $16, $17, + $18 ) ", self.id as ModId, @@ -104,7 +163,13 @@ impl Mod { self.issues_url.as_ref(), self.source_url.as_ref(), self.wiki_url.as_ref(), - self.status.0 + self.status.0, + self.discord_url.as_ref(), + self.client_side as SideTypeId, + self.server_side as SideTypeId, + self.license_url.as_ref(), + self.license as LicenseId, + self.slug.as_ref() ) .execute(&mut *transaction) .await?; @@ -121,8 +186,8 @@ impl Mod { SELECT title, description, downloads, icon_url, body_url, published, updated, status, - issues_url, source_url, wiki_url, - team_id + issues_url, source_url, wiki_url, discord_url, license_url, + team_id, client_side, server_side, license, slug FROM mods WHERE id = $1 ", @@ -145,7 +210,13 @@ impl Mod { issues_url: row.issues_url, source_url: row.source_url, wiki_url: row.wiki_url, + license_url: row.license_url, + discord_url: row.discord_url, + client_side: SideTypeId(row.client_side), status: StatusId(row.status), + server_side: SideTypeId(row.server_side), + license: LicenseId(row.license), + slug: row.slug, })) } else { Ok(None) @@ -164,8 +235,8 @@ impl Mod { SELECT id, title, description, downloads, icon_url, body_url, published, updated, status, - issues_url, source_url, wiki_url, - team_id + issues_url, source_url, wiki_url, discord_url, license_url, + team_id, client_side, server_side, license, slug FROM mods WHERE id IN (SELECT * FROM UNNEST($1::bigint[])) ", @@ -186,7 +257,13 @@ impl Mod { issues_url: m.issues_url, source_url: m.source_url, wiki_url: m.wiki_url, + license_url: m.license_url, + discord_url: m.discord_url, + client_side: SideTypeId(m.client_side), status: StatusId(m.status), + server_side: SideTypeId(m.server_side), + license: LicenseId(m.license), + slug: m.slug, })) }) .try_collect::>() @@ -227,6 +304,16 @@ impl Mod { .execute(exec) .await?; + sqlx::query!( + " + DELETE FROM mods_donations + WHERE joining_mod_id = $1 + ", + id as ModId, + ) + .execute(exec) + .await?; + use futures::TryStreamExt; let versions: Vec = sqlx::query!( " @@ -277,6 +364,30 @@ impl Mod { Ok(Some(())) } + pub async fn get_full_from_slug<'a, 'b, E>( + slug: String, + executor: E, + ) -> Result, sqlx::error::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + let id = sqlx::query!( + " + SELECT id FROM mods + WHERE slug = $1 + ", + slug + ) + .fetch_optional(executor) + .await?; + + if let Some(mod_id) = id { + Mod::get_full(ModId(mod_id.id), executor).await + } else { + Ok(None) + } + } + pub async fn get_full<'a, 'b, E>( id: ModId, executor: E, @@ -312,6 +423,24 @@ impl Mod { .try_collect::>() .await?; + let donations: Vec = sqlx::query!( + " + SELECT joining_platform_id, url FROM mods_donations + WHERE joining_mod_id = $1 + ", + id as ModId, + ) + .fetch_many(executor) + .try_filter_map(|e| async { + Ok(e.right().map(|c| DonationUrl { + mod_id: id, + platform_id: DonationPlatformId(c.joining_platform_id), + url: c.url, + })) + }) + .try_collect::>() + .await?; + let status = sqlx::query!( " SELECT status FROM statuses @@ -323,11 +452,48 @@ impl Mod { .await? .status; + let client_side = sqlx::query!( + " + SELECT name FROM side_types + WHERE id = $1 + ", + inner.client_side.0, + ) + .fetch_one(executor) + .await? + .name; + + let server_side = sqlx::query!( + " + SELECT name FROM side_types + WHERE id = $1 + ", + inner.server_side.0, + ) + .fetch_one(executor) + .await? + .name; + + let license = sqlx::query!( + " + SELECT short, name FROM licenses + WHERE id = $1 + ", + inner.license.0, + ) + .fetch_one(executor) + .await?; + Ok(Some(QueryMod { inner, categories, versions, + donation_urls: donations, status: crate::models::mods::ModStatus::from_str(&status), + license_id: license.short, + license_name: license.name, + client_side: crate::models::mods::SideType::from_str(&client_side), + server_side: crate::models::mods::SideType::from_str(&server_side) })) } else { Ok(None) @@ -351,5 +517,10 @@ pub struct QueryMod { pub categories: Vec, pub versions: Vec, + pub donation_urls: Vec, pub status: crate::models::mods::ModStatus, + pub license_id: String, + pub license_name: String, + pub client_side: crate::models::mods::SideType, + pub server_side: crate::models::mods::SideType, } diff --git a/src/database/models/team_item.rs b/src/database/models/team_item.rs index aea66188b..7c6b30632 100644 --- a/src/database/models/team_item.rs +++ b/src/database/models/team_item.rs @@ -260,7 +260,7 @@ impl TeamMember { name: m.member_name, role: m.role, permissions: Permissions::from_bits(m.permissions as u64) - .ok_or_else(|| super::DatabaseError::BitflagError)?, + .ok_or(super::DatabaseError::BitflagError)?, accepted: m.accepted, })) } else { @@ -297,7 +297,7 @@ impl TeamMember { name: m.member_name, role: m.role, permissions: Permissions::from_bits(m.permissions as u64) - .ok_or_else(|| super::DatabaseError::BitflagError)?, + .ok_or(super::DatabaseError::BitflagError)?, accepted: m.accepted, })) } else { diff --git a/src/database/models/user_item.rs b/src/database/models/user_item.rs index caae46089..6f8caff1e 100644 --- a/src/database/models/user_item.rs +++ b/src/database/models/user_item.rs @@ -113,6 +113,43 @@ impl User { } } + pub async fn get_from_username<'a, 'b, E>( + username: String, + executor: E, + ) -> Result, sqlx::error::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + SELECT u.id, u.github_id, u.name, u.email, + u.avatar_url, u.bio, + u.created, u.role + FROM users u + WHERE u.username = $1 + ", + username + ) + .fetch_optional(executor) + .await?; + + if let Some(row) = result { + Ok(Some(User { + id: UserId(row.id), + github_id: row.github_id, + name: row.name, + email: row.email, + avatar_url: row.avatar_url, + username, + bio: row.bio, + created: row.created, + role: row.role, + })) + } else { + Ok(None) + } + } + pub async fn get_many<'a, E>(user_ids: Vec, exec: E) -> Result, sqlx::Error> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, diff --git a/src/database/models/version_item.rs b/src/database/models/version_item.rs index cadd86e54..d335b1db8 100644 --- a/src/database/models/version_item.rs +++ b/src/database/models/version_item.rs @@ -13,12 +13,14 @@ pub struct VersionBuilder { pub game_versions: Vec, pub loaders: Vec, pub release_channel: ChannelId, + pub featured: bool, } pub struct VersionFileBuilder { pub url: String, pub filename: String, pub hashes: Vec, + pub primary: bool, } impl VersionFileBuilder { @@ -81,6 +83,7 @@ impl VersionBuilder { downloads: 0, release_channel: self.release_channel, accepted: false, + featured: self.featured, }; version.insert(&mut *transaction).await?; @@ -154,6 +157,7 @@ pub struct Version { pub downloads: i32, pub release_channel: ChannelId, pub accepted: bool, + pub featured: bool, } impl Version { @@ -166,13 +170,13 @@ impl Version { INSERT INTO versions ( id, mod_id, author_id, name, version_number, changelog_url, date_published, - downloads, release_channel, accepted + downloads, release_channel, accepted, featured ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, - $10 + $10, $11 ) ", self.id as VersionId, @@ -184,7 +188,8 @@ impl Version { self.date_published, self.downloads, self.release_channel as ChannelId, - self.accepted + self.accepted, + self.featured ) .execute(&mut *transaction) .await?; @@ -234,7 +239,7 @@ impl Version { let files = sqlx::query!( " - SELECT files.id, files.url, files.filename FROM files + SELECT files.id, files.url, files.filename, files.is_primary FROM files WHERE files.version_id = $1 ", id as VersionId, @@ -246,6 +251,7 @@ impl Version { version_id: id, url: c.url, filename: c.filename, + primary: c.is_primary, })) }) .try_collect::>() @@ -367,7 +373,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.accepted + v.release_channel, v.accepted, v.featured FROM versions v WHERE v.id = $1 ", @@ -388,6 +394,7 @@ impl Version { downloads: row.downloads, release_channel: ChannelId(row.release_channel), accepted: row.accepted, + featured: row.featured, })) } else { Ok(None) @@ -408,7 +415,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, accepted + v.release_channel, v.accepted, v.featured FROM versions v WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[])) ", @@ -427,6 +434,7 @@ impl Version { downloads: v.downloads, release_channel: ChannelId(v.release_channel), accepted: v.accepted, + featured: v.featured, })) }) .try_collect::>() @@ -446,7 +454,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, v.accepted + release_channels.channel, v.accepted, v.featured FROM versions v INNER JOIN release_channels ON v.release_channel = release_channels.id WHERE v.id = $1 @@ -487,7 +495,7 @@ impl Version { let mut files = sqlx::query!( " - SELECT files.id, files.url, files.filename FROM files + SELECT files.id, files.url, files.filename, files.is_primary FROM files WHERE files.version_id = $1 ", id as VersionId, @@ -499,6 +507,7 @@ impl Version { url: c.url, filename: c.filename, hashes: std::collections::HashMap::new(), + primary: c.is_primary, })) }) .try_collect::>() @@ -535,6 +544,7 @@ impl Version { loaders, game_versions, accepted: row.accepted, + featured: row.featured, })) } else { Ok(None) @@ -564,6 +574,7 @@ pub struct VersionFile { pub version_id: VersionId, pub url: String, pub filename: String, + pub primary: bool, } pub struct FileHash { @@ -587,6 +598,7 @@ pub struct QueryVersion { pub game_versions: Vec, pub loaders: Vec, pub accepted: bool, + pub featured: bool, } pub struct QueryFile { @@ -594,4 +606,5 @@ pub struct QueryFile { pub url: String, pub filename: String, pub hashes: std::collections::HashMap>, + pub primary: bool, } diff --git a/src/main.rs b/src/main.rs index 45a173bc4..36cdf68f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,6 +36,11 @@ struct Config { allow_missing_vars: bool, } +#[derive(Clone)] +pub struct Pepper { + pub pepper: String, +} + #[actix_rt::main] async fn main() -> std::io::Result<()> { dotenv::dotenv().ok(); @@ -155,6 +160,44 @@ async fn main() -> std::io::Result<()> { } }); + let pool_ref = pool.clone(); + scheduler.run(std::time::Duration::from_secs(15 * 60), move || { + let pool_ref = pool_ref.clone(); + // Use sqlx to delete records more than an hour old + info!("Deleting old records from temporary tables"); + + async move { + let downloads_result = sqlx::query!( + " + DELETE FROM downloads + WHERE date < (CURRENT_DATE - INTERVAL '30 minutes ago') + " + ) + .execute(&pool_ref) + .await; + + if let Err(e) = downloads_result { + warn!("Deleting old records from temporary table downloads failed: {:?}", e); + } + + let states_result = sqlx::query!( + " + DELETE FROM states + WHERE expires < CURRENT_DATE + " + ) + .execute(&pool_ref) + .await; + + if let Err(e) = states_result { + warn!("Deleting old records from temporary table states failed: {:?}", e); + } + + info!("Finished deleting old records from temporary tables"); + } + + }); + let indexing_queue = Arc::new(search::indexing::queue::CreationQueue::new()); let queue_ref = indexing_queue.clone(); @@ -216,6 +259,10 @@ async fn main() -> std::io::Result<()> { scheduler::schedule_versions(&mut scheduler, pool.clone(), skip_initial); + let ip_salt = Pepper { + pepper: crate::models::ids::Base62Id(crate::models::ids::random_base62(11)).to_string() + }; + let allowed_origins = dotenv::var("CORS_ORIGINS") .ok() .and_then(|s| serde_json::from_str::>(&s).ok()) @@ -247,6 +294,7 @@ async fn main() -> std::io::Result<()> { .data(file_host.clone()) .data(indexing_queue.clone()) .data(search_config.clone()) + .data(ip_salt.clone()) .service(routes::index_get) .service( web::scope("/api/v1/") diff --git a/src/models/mods.rs b/src/models/mods.rs index 5ddacfb30..bd81852ed 100644 --- a/src/models/mods.rs +++ b/src/models/mods.rs @@ -21,6 +21,8 @@ pub struct VersionId(pub u64); pub struct Mod { /// The ID of the mod, encoded as a base62 string. pub id: ModId, + /// The slug of a mod, used for vanity URLs + pub slug: Option, /// The team of people that has ownership of this mod. pub team: TeamId, /// The title or name of the mod. @@ -35,6 +37,13 @@ pub struct Mod { pub updated: DateTime, /// The status of the mod pub status: ModStatus, + /// The license of this mod + pub license: License, + + /// The support range for the client mod + pub client_side: SideType, + /// The support range for the server mod + pub server_side: SideType, /// The total number of downloads the mod has had. pub downloads: u32, @@ -42,7 +51,7 @@ pub struct Mod { pub categories: Vec, /// A list of ids for versions of the mod. pub versions: Vec, - ///The URL of the icon of the mod + /// The URL of the icon of the mod pub icon_url: Option, /// An optional link to where to submit bugs or issues with the mod. pub issues_url: Option, @@ -50,6 +59,60 @@ pub struct Mod { pub source_url: Option, /// An optional link to the mod's wiki page or other relevant information. pub wiki_url: Option, + /// An optional link to the mod's discord + pub discord_url: Option, + /// An optional list of all donation links the mod has + pub donation_urls: Option>, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "kebab-case")] +pub enum SideType { + Required, + NoFunctionality, + Unsupported, + Unknown, +} + +impl std::fmt::Display for SideType { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(fmt, "{}", self.as_str()) + } +} + +impl SideType { + // These are constant, so this can remove unneccessary allocations (`to_string`) + pub fn as_str(&self) -> &'static str { + match self { + SideType::Required => "required", + SideType::NoFunctionality => "no-functionality", + SideType::Unsupported => "unsupported", + SideType::Unknown => "unknown", + } + } + + pub fn from_str(string: &str) -> SideType { + match string { + "required" => SideType::Required, + "no-functionality" => SideType::NoFunctionality, + "unsupported" => SideType::Unsupported, + _ => SideType::Unknown, + } + } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct License { + pub id: String, + pub name: String, + pub url: Option, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct DonationLink { + pub id: String, + pub platform: String, + pub url: String, } /// A status decides the visbility of a mod in search, URLs, and the whole site itself. @@ -71,14 +134,7 @@ pub enum ModStatus { impl std::fmt::Display for ModStatus { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - ModStatus::Approved => write!(fmt, "approved"), - ModStatus::Rejected => write!(fmt, "rejected"), - ModStatus::Draft => write!(fmt, "draft"), - ModStatus::Unlisted => write!(fmt, "unlisted"), - ModStatus::Processing => write!(fmt, "processing"), - ModStatus::Unknown => write!(fmt, "unknown"), - } + write!(fmt, "{}", self.as_str()) } } @@ -116,10 +172,7 @@ impl ModStatus { } pub fn is_searchable(&self) -> bool { - match self { - ModStatus::Approved => true, - _ => false, - } + matches!(self, ModStatus::Approved) } } @@ -132,6 +185,8 @@ pub struct Version { pub mod_id: ModId, /// The ID of the author who published this version pub author_id: UserId, + /// Whether the version is featured or not + pub featured: bool, /// The name of this version pub name: String, @@ -166,6 +221,8 @@ pub struct VersionFile { pub url: String, /// The filename of the file. pub filename: String, + /// Whether the file is the primary file of a version + pub primary: bool, } #[derive(Serialize, Deserialize, Clone)] diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 3042da98a..bfa74c722 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -26,9 +26,11 @@ pub fn mods_config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("mod") + .service(mods::mod_slug_get) .service(mods::mod_get) .service(mods::mod_delete) .service(mods::mod_edit) + .service(mods::mod_icon_edit) .service(web::scope("{mod_id}").service(versions::version_list)), ); } @@ -46,7 +48,8 @@ pub fn versions_config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("version_file") .service(versions::delete_file) - .service(versions::get_version_from_hash), + .service(versions::get_version_from_hash) + .service(versions::download_version), ); } @@ -56,9 +59,12 @@ pub fn users_config(cfg: &mut web::ServiceConfig) { cfg.service(users::users_get); cfg.service( web::scope("user") + .service(users::user_username_get) .service(users::user_get) .service(users::mods_list) .service(users::user_delete) + .service(users::user_edit) + .service(users::user_icon_edit) .service(users::teams), ); } @@ -84,6 +90,8 @@ pub fn moderation_config(cfg: &mut web::ServiceConfig) { #[derive(thiserror::Error, Debug)] pub enum ApiError { + #[error("Environment Error")] + EnvError(#[from] dotenv::Error), #[error("Error while uploading file")] FileHostingError(#[from] FileHostingError), #[error("Internal server error")] @@ -103,6 +111,7 @@ pub enum ApiError { impl actix_web::ResponseError for ApiError { fn status_code(&self) -> actix_web::http::StatusCode { match self { + ApiError::EnvError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, ApiError::DatabaseError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, ApiError::AuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED, ApiError::CustomAuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED, @@ -117,6 +126,7 @@ impl actix_web::ResponseError for ApiError { actix_web::web::HttpResponse::build(self.status_code()).json( crate::models::error::ApiError { error: match self { + ApiError::EnvError(..) => "environment_error", ApiError::DatabaseError(..) => "database_error", ApiError::AuthenticationError(..) => "unauthorized", ApiError::CustomAuthenticationError(..) => "unauthorized", diff --git a/src/routes/mod_creation.rs b/src/routes/mod_creation.rs index 7e9261767..b67bb4c67 100644 --- a/src/routes/mod_creation.rs +++ b/src/routes/mod_creation.rs @@ -2,7 +2,7 @@ use crate::auth::{get_user_from_headers, AuthenticationError}; use crate::database::models; use crate::file_hosting::{FileHost, FileHostingError}; use crate::models::error::ApiError; -use crate::models::mods::{ModId, ModStatus, VersionId}; +use crate::models::mods::{DonationLink, License, ModId, ModStatus, SideType, VersionId}; use crate::models::users::UserId; use crate::routes::version_creation::InitialVersionData; use crate::search::indexing::{queue::CreationQueue, IndexingError}; @@ -99,6 +99,8 @@ impl actix_web::ResponseError for CreateError { struct ModCreateData { /// The title or name of the mod. pub mod_name: String, + /// The slug of a mod, used for vanity URLs + pub mod_slug: Option, /// A short description of the mod. pub mod_description: String, /// A long description of the mod, in markdown. @@ -113,8 +115,20 @@ 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 link to the mod's license page + pub license_url: Option, + /// An optional link to the mod's discord. + pub discord_url: Option, /// An optional boolean. If true, the mod will be created as a draft. pub is_draft: Option, + /// The support range for the client mod + pub client_side: SideType, + /// The support range for the server mod + pub server_side: SideType, + /// The license id that the mod follows + pub license_id: String, + /// An optional list of all donation links the mod has + pub donation_urls: Option>, } pub struct UploadedFile { @@ -461,7 +475,53 @@ async fn mod_create_inner( let status_id = models::StatusId::get_id(&status, &mut *transaction) .await? - .expect("No database entry found for status"); + .ok_or_else(|| { + CreateError::InvalidInput(format!("Status {} does not exist.", status.clone())) + })?; + let client_side_id = + models::SideTypeId::get_id(&mod_create_data.client_side, &mut *transaction) + .await? + .ok_or_else(|| { + CreateError::InvalidInput( + "Client side type specified does not exist.".to_string(), + ) + })?; + + let server_side_id = + models::SideTypeId::get_id(&mod_create_data.server_side, &mut *transaction) + .await? + .ok_or_else(|| { + CreateError::InvalidInput( + "Server side type specified does not exist.".to_string(), + ) + })?; + + let license_id = + models::categories::License::get_id(&mod_create_data.license_id, &mut *transaction) + .await? + .ok_or_else(|| { + CreateError::InvalidInput("License specified does not exist.".to_string()) + })?; + let mut donation_urls = vec![]; + + if let Some(urls) = &mod_create_data.donation_urls { + for url in urls { + let platform_id = models::DonationPlatformId::get_id(&url.id, &mut *transaction) + .await? + .ok_or_else(|| { + CreateError::InvalidInput(format!( + "Donation platform {} does not exist.", + url.id.clone() + )) + })?; + + donation_urls.push(models::mod_item::DonationUrl { + mod_id: mod_id.into(), + platform_id, + url: url.url.clone(), + }) + } + } let mod_builder = models::mod_item::ModBuilder { mod_id: mod_id.into(), @@ -474,15 +534,23 @@ async fn mod_create_inner( source_url: mod_create_data.source_url, wiki_url: mod_create_data.wiki_url, + license_url: mod_create_data.license_url, + discord_url: mod_create_data.discord_url, categories, initial_versions: versions, status: status_id, + client_side: client_side_id, + server_side: server_side_id, + license: license_id, + slug: mod_create_data.mod_slug, + donation_urls, }; let now = chrono::Utc::now(); let response = crate::models::mods::Mod { id: mod_id, + slug: mod_builder.slug.clone(), team: team_id.into(), title: mod_builder.title.clone(), description: mod_builder.description.clone(), @@ -490,6 +558,13 @@ async fn mod_create_inner( published: now, updated: now, status, + license: License { + id: mod_create_data.license_id.clone(), + name: "".to_string(), + url: mod_builder.license_url.clone(), + }, + client_side: mod_create_data.client_side, + server_side: mod_create_data.server_side, downloads: 0, categories: mod_create_data.categories, versions: mod_builder @@ -501,6 +576,8 @@ async fn mod_create_inner( issues_url: mod_builder.issues_url.clone(), source_url: mod_builder.source_url.clone(), wiki_url: mod_builder.wiki_url.clone(), + discord_url: mod_builder.discord_url.clone(), + donation_urls: mod_create_data.donation_urls.clone(), }; let _mod_id = mod_builder.insert(&mut *transaction).await?; @@ -598,6 +675,7 @@ async fn create_initial_version( game_versions, loaders, release_channel, + featured: version_data.featured, }; Ok(version) @@ -642,7 +720,7 @@ async fn process_icon_upload( } } -fn get_image_content_type(extension: &str) -> Option<&'static str> { +pub fn get_image_content_type(extension: &str) -> Option<&'static str> { let content_type = match &*extension { "bmp" => "image/bmp", "gif" => "image/gif", diff --git a/src/routes/moderation.rs b/src/routes/moderation.rs index 932722bea..e9a1bb64f 100644 --- a/src/routes/moderation.rs +++ b/src/routes/moderation.rs @@ -2,10 +2,12 @@ use super::ApiError; use crate::auth::check_is_moderator_from_headers; use crate::database; use crate::models; -use crate::models::mods::{ModStatus, VersionType}; +use crate::models::mods::{ModStatus, VersionType, ModId}; use actix_web::{get, web, HttpRequest, HttpResponse}; -use serde::Deserialize; +use serde::{Serialize, Deserialize}; use sqlx::PgPool; +use sqlx::types::chrono::{DateTime, Utc}; +use crate::models::teams::TeamId; #[derive(Deserialize)] pub struct ResultCount { @@ -17,6 +19,42 @@ fn default_count() -> i16 { 100 } +/// A mod returned from the API moderation routes +#[derive(Serialize)] +pub struct ModerationMod { + /// The ID of the mod, encoded as a base62 string. + pub id: ModId, + /// The slug of a mod, used for vanity URLs + pub slug: Option, + /// The team of people that has ownership of this mod. + pub team: TeamId, + /// The title or name of the mod. + pub title: String, + /// A short description of the mod. + pub description: String, + /// The link to the long description of the mod. + pub body_url: String, + /// The date at which the mod was first published. + pub published: DateTime, + /// The date at which the mod was first published. + pub updated: DateTime, + /// The status of the mod + pub status: ModStatus, + + /// The total number of downloads the mod has had. + pub downloads: u32, + /// The URL of the icon of the mod + pub icon_url: Option, + /// An optional link to where to submit bugs or issues with the mod. + pub issues_url: Option, + /// An optional link to the source code for the mod. + pub source_url: Option, + /// An optional link to the mod's wiki page or other relevant information. + pub wiki_url: Option, + /// An optional link to the mod's discord + pub discord_url: Option, +} + #[get("mods")] pub async fn mods( req: HttpRequest, @@ -41,15 +79,14 @@ pub async fn mods( ) .fetch_many(&**pool) .try_filter_map(|e| async { - Ok(e.right().map(|m| models::mods::Mod { + Ok(e.right().map(|m| ModerationMod { id: database::models::ids::ModId(m.id).into(), + slug: m.slug, 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, @@ -57,9 +94,10 @@ pub async fn mods( updated: m.updated, downloads: m.downloads as u32, wiki_url: m.wiki_url, + discord_url: m.discord_url, })) }) - .try_collect::>() + .try_collect::>() .await .map_err(|e| ApiError::DatabaseError(e.into()))?; @@ -92,6 +130,7 @@ pub async fn versions( 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(), + featured: m.featured, name: m.name, version_number: m.version_number, changelog_url: m.changelog_url, diff --git a/src/routes/mods.rs b/src/routes/mods.rs index 8b91db6f9..00d1dc585 100644 --- a/src/routes/mods.rs +++ b/src/routes/mods.rs @@ -3,10 +3,11 @@ use crate::auth::get_user_from_headers; use crate::database; use crate::file_hosting::FileHost; use crate::models; -use crate::models::mods::{ModStatus, SearchRequest}; +use crate::models::mods::{DonationLink, License, ModStatus, SearchRequest, SideType}; use crate::models::teams::Permissions; use crate::search::{search_for_mod, SearchConfig, SearchError}; use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse}; +use futures::StreamExt; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use std::sync::Arc; @@ -80,6 +81,53 @@ pub async fn mods_get( Ok(HttpResponse::Ok().json(mods)) } +#[get("@{id}")] +pub async fn mod_slug_get( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, +) -> Result { + let id = info.into_inner().0; + let mod_data = database::models::Mod::get_full_from_slug(id, &**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 { + 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("")) + } +} + #[get("{id}")] pub async fn mod_get( req: HttpRequest, @@ -132,6 +180,7 @@ fn convert_mod(data: database::models::mod_item::QueryMod) -> models::mods::Mod models::mods::Mod { id: m.id.into(), + slug: m.slug, team: m.team_id.into(), title: m.title, description: m.description, @@ -139,6 +188,13 @@ fn convert_mod(data: database::models::mod_item::QueryMod) -> models::mods::Mod published: m.published, updated: m.updated, status: data.status, + license: License { + id: data.license_id, + name: data.license_name, + url: m.license_url, + }, + client_side: data.client_side, + server_side: data.server_side, downloads: m.downloads as u32, categories: data.categories, versions: data.versions.into_iter().map(|v| v.into()).collect(), @@ -146,6 +202,8 @@ fn convert_mod(data: database::models::mod_item::QueryMod) -> models::mods::Mod issues_url: m.issues_url, source_url: m.source_url, wiki_url: m.wiki_url, + discord_url: m.discord_url, + donation_urls: None, } } @@ -175,6 +233,28 @@ pub struct EditMod { with = "::serde_with::rust::double_option" )] pub wiki_url: Option>, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "::serde_with::rust::double_option" + )] + pub license_url: Option>, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "::serde_with::rust::double_option" + )] + pub discord_url: Option>, + pub donation_urls: Option>, + pub license_id: Option, + pub client_side: Option, + pub server_side: Option, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "::serde_with::rust::double_option" + )] + pub slug: Option>, } #[patch("{id}")] @@ -270,12 +350,10 @@ pub async fn mod_edit( )); } - 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(), - )); - } + if (status == &ModStatus::Rejected || status == &ModStatus::Approved) && !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) @@ -421,6 +499,199 @@ pub async fn mod_edit( .map_err(|e| ApiError::DatabaseError(e.into()))?; } + if let Some(license_url) = &new_mod.license_url { + if !perms.contains(Permissions::EDIT_DETAILS) { + return Err(ApiError::CustomAuthenticationError( + "You do not have the permissions to edit the license URL of this mod!" + .to_string(), + )); + } + + sqlx::query!( + " + UPDATE mods + SET license_url = $1 + WHERE (id = $2) + ", + license_url.as_deref(), + id as database::models::ids::ModId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + + if let Some(discord_url) = &new_mod.discord_url { + if !perms.contains(Permissions::EDIT_DETAILS) { + return Err(ApiError::CustomAuthenticationError( + "You do not have the permissions to edit the discord URL of this mod!" + .to_string(), + )); + } + + sqlx::query!( + " + UPDATE mods + SET discord_url = $1 + WHERE (id = $2) + ", + discord_url.as_deref(), + id as database::models::ids::ModId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + + if let Some(slug) = &new_mod.slug { + if !perms.contains(Permissions::EDIT_DETAILS) { + return Err(ApiError::CustomAuthenticationError( + "You do not have the permissions to edit the slug of this mod!".to_string(), + )); + } + + sqlx::query!( + " + UPDATE mods + SET slug = $1 + WHERE (id = $2) + ", + slug.as_deref(), + id as database::models::ids::ModId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + + if let Some(new_side) = &new_mod.client_side { + if !perms.contains(Permissions::EDIT_DETAILS) { + return Err(ApiError::CustomAuthenticationError( + "You do not have the permissions to edit the side type of this mod!" + .to_string(), + )); + } + + let side_type_id = + database::models::SideTypeId::get_id(new_side, &mut *transaction) + .await? + .expect("No database entry found for side type"); + + sqlx::query!( + " + UPDATE mods + SET client_side = $1 + WHERE (id = $2) + ", + side_type_id as database::models::SideTypeId, + id as database::models::ids::ModId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + + if let Some(new_side) = &new_mod.server_side { + if !perms.contains(Permissions::EDIT_DETAILS) { + return Err(ApiError::CustomAuthenticationError( + "You do not have the permissions to edit the side type of this mod!" + .to_string(), + )); + } + + let side_type_id = + database::models::SideTypeId::get_id(new_side, &mut *transaction) + .await? + .expect("No database entry found for side type"); + + sqlx::query!( + " + UPDATE mods + SET server_side = $1 + WHERE (id = $2) + ", + side_type_id as database::models::SideTypeId, + id as database::models::ids::ModId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + + if let Some(license) = &new_mod.license_id { + if !perms.contains(Permissions::EDIT_DETAILS) { + return Err(ApiError::CustomAuthenticationError( + "You do not have the permissions to edit the license of this mod!" + .to_string(), + )); + } + + let license_id = + database::models::categories::License::get_id(license, &mut *transaction) + .await? + .expect("No database entry found for license"); + + sqlx::query!( + " + UPDATE mods + SET license = $1 + WHERE (id = $2) + ", + license_id as database::models::LicenseId, + id as database::models::ids::ModId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + + if let Some(donations) = &new_mod.donation_urls { + if !perms.contains(Permissions::EDIT_DETAILS) { + return Err(ApiError::CustomAuthenticationError( + "You do not have the permissions to edit the donation links of this mod!" + .to_string(), + )); + } + + sqlx::query!( + " + DELETE FROM mods_donations + WHERE joining_mod_id = $1 + ", + id as database::models::ids::ModId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + for donation in donations { + let platform_id = database::models::DonationPlatformId::get_id( + &donation.id, + &mut *transaction, + ) + .await? + .ok_or_else(|| { + ApiError::InvalidInputError(format!( + "Platform {} does not exist.", + donation.id.clone() + )) + })?; + + sqlx::query!( + " + INSERT INTO mods_donations (joining_mod_id, joining_platform_id, url) + VALUES ($1, $2, $3) + ", + id as database::models::ids::ModId, + platform_id as database::models::ids::DonationPlatformId, + donation.url + ) + .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( @@ -452,6 +723,99 @@ pub async fn mod_edit( } } +#[derive(Serialize, Deserialize)] +pub struct Extension { + pub ext: String, +} + +#[patch("{id}/icon")] +pub async fn mod_icon_edit( + web::Query(ext): web::Query, + req: HttpRequest, + info: web::Path<(models::ids::ModId,)>, + pool: web::Data, + file_host: web::Data>, + mut payload: web::Payload, +) -> Result { + if let Some(content_type) = super::mod_creation::get_image_content_type(&*ext.ext) { + let cdn_url = dotenv::var("CDN_URL")?; + let user = get_user_from_headers(req.headers(), &**pool).await?; + let id = info.into_inner().0; + + let mod_item = database::models::Mod::get(id.into(), &**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))? + .ok_or_else(|| ApiError::InvalidInputError("Invalid Mod ID specified!".to_string()))?; + + if !user.role.is_mod() { + let team_member = database::models::TeamMember::get_from_user_id( + mod_item.team_id, + user.id.into(), + &**pool, + ) + .await + .map_err(ApiError::DatabaseError)? + .ok_or_else(|| ApiError::InvalidInputError("Invalid Mod ID specified!".to_string()))?; + + if !team_member.permissions.contains(Permissions::EDIT_DETAILS) { + return Err(ApiError::CustomAuthenticationError( + "You don't have permission to edit this mod's icon.".to_string(), + )); + } + } + + if let Some(icon) = mod_item.icon_url { + let name = icon.split('/').next(); + + if let Some(icon_path) = name { + file_host.delete_file_version("", icon_path).await?; + } + } + + let mut bytes = web::BytesMut::new(); + while let Some(item) = payload.next().await { + bytes.extend_from_slice(&item.map_err(|_| { + ApiError::InvalidInputError("Unable to parse bytes in payload sent!".to_string()) + })?); + } + + if bytes.len() >= 262144 { + return Err(ApiError::InvalidInputError(String::from( + "Icons must be smaller than 256KiB", + ))); + } + + let upload_data = file_host + .upload_file( + content_type, + &format!("data/{}/icon.{}", id, ext.ext), + bytes.to_vec(), + ) + .await?; + + let mod_id: database::models::ids::ModId = id.into(); + sqlx::query!( + " + UPDATE mods + SET icon_url = $1 + WHERE (id = $2) + ", + format!("{}/{}", cdn_url, upload_data.file_name), + mod_id as database::models::ids::ModId, + ) + .execute(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + Ok(HttpResponse::Ok().body("")) + } else { + Err(ApiError::InvalidInputError(format!( + "Invalid format for mod icon: {}", + ext.ext + ))) + } +} + #[delete("{id}")] pub async fn mod_delete( req: HttpRequest, diff --git a/src/routes/tags.rs b/src/routes/tags.rs index 1aae1c019..4cbb857df 100644 --- a/src/routes/tags.rs +++ b/src/routes/tags.rs @@ -1,6 +1,7 @@ use super::ApiError; use crate::auth::check_is_admin_from_headers; use crate::database::models; +use crate::database::models::categories::{DonationPlatform, License}; use actix_web::{delete, get, put, web, HttpRequest, HttpResponse}; use models::categories::{Category, GameVersion, Loader}; use sqlx::PgPool; @@ -16,7 +17,13 @@ pub fn config(cfg: &mut web::ServiceConfig) { .service(loader_delete) .service(game_version_list) .service(game_version_create) - .service(game_version_delete), + .service(game_version_delete) + .service(license_create) + .service(license_delete) + .service(license_list) + .service(donation_platform_create) + .service(donation_platform_list) + .service(donation_platform_delete), ); } @@ -34,14 +41,7 @@ pub async fn category_create( pool: web::Data, category: web::Path<(String,)>, ) -> Result { - check_is_admin_from_headers( - req.headers(), - &mut *pool - .acquire() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?, - ) - .await?; + check_is_admin_from_headers(req.headers(), &**pool).await?; let name = category.into_inner().0; @@ -56,14 +56,7 @@ pub async fn category_delete( pool: web::Data, category: web::Path<(String,)>, ) -> Result { - check_is_admin_from_headers( - req.headers(), - &mut *pool - .acquire() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?, - ) - .await?; + check_is_admin_from_headers(req.headers(), &**pool).await?; let name = category.into_inner().0; let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?; @@ -94,14 +87,7 @@ pub async fn loader_create( pool: web::Data, loader: web::Path<(String,)>, ) -> Result { - check_is_admin_from_headers( - req.headers(), - &mut *pool - .acquire() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?, - ) - .await?; + check_is_admin_from_headers(req.headers(), &**pool).await?; let name = loader.into_inner().0; @@ -116,14 +102,7 @@ pub async fn loader_delete( pool: web::Data, loader: web::Path<(String,)>, ) -> Result { - check_is_admin_from_headers( - req.headers(), - &mut *pool - .acquire() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?, - ) - .await?; + check_is_admin_from_headers(req.headers(), &**pool).await?; let name = loader.into_inner().0; let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?; @@ -176,14 +155,7 @@ pub async fn game_version_create( game_version: web::Path<(String,)>, version_data: web::Json, ) -> Result { - check_is_admin_from_headers( - req.headers(), - &mut *pool - .acquire() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?, - ) - .await?; + check_is_admin_from_headers(req.headers(), &**pool).await?; let name = game_version.into_inner().0; @@ -209,14 +181,7 @@ pub async fn game_version_delete( pool: web::Data, game_version: web::Path<(String,)>, ) -> Result { - check_is_admin_from_headers( - req.headers(), - &mut *pool - .acquire() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?, - ) - .await?; + check_is_admin_from_headers(req.headers(), &**pool).await?; let name = game_version.into_inner().0; let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?; @@ -234,3 +199,141 @@ pub async fn game_version_delete( Ok(HttpResponse::NotFound().body("")) } } + +#[derive(serde::Serialize)] +pub struct LicenseQueryData { + short: String, + name: String, +} + +#[get("license")] +pub async fn license_list(pool: web::Data) -> Result { + let results: Vec = License::list(&**pool) + .await? + .into_iter() + .map(|x| LicenseQueryData { + short: x.short, + name: x.name, + }) + .collect(); + Ok(HttpResponse::Ok().json(results)) +} + +#[derive(serde::Deserialize)] +pub struct LicenseData { + name: String, +} + +#[put("license/{name}")] +pub async fn license_create( + req: HttpRequest, + pool: web::Data, + license: web::Path<(String,)>, + license_data: web::Json, +) -> Result { + check_is_admin_from_headers(req.headers(), &**pool).await?; + + let short = license.into_inner().0; + + let _id = License::builder() + .short(&short)? + .name(&license_data.name)? + .insert(&**pool) + .await?; + + Ok(HttpResponse::Ok().body("")) +} + +#[delete("license/{name}")] +pub async fn license_delete( + req: HttpRequest, + pool: web::Data, + license: web::Path<(String,)>, +) -> Result { + check_is_admin_from_headers(req.headers(), &**pool).await?; + + let name = license.into_inner().0; + let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?; + + let result = License::remove(&name, &mut transaction).await?; + + transaction + .commit() + .await + .map_err(models::DatabaseError::from)?; + + if result.is_some() { + Ok(HttpResponse::Ok().body("")) + } else { + Ok(HttpResponse::NotFound().body("")) + } +} + +#[derive(serde::Serialize)] +pub struct DonationPlatformQueryData { + short: String, + name: String, +} + +#[get("donation_platform")] +pub async fn donation_platform_list(pool: web::Data) -> Result { + let results: Vec = DonationPlatform::list(&**pool) + .await? + .into_iter() + .map(|x| DonationPlatformQueryData { + short: x.short, + name: x.name, + }) + .collect(); + Ok(HttpResponse::Ok().json(results)) +} + +#[derive(serde::Deserialize)] +pub struct DonationPlatformData { + name: String, +} + +#[put("donation_platform/{name}")] +pub async fn donation_platform_create( + req: HttpRequest, + pool: web::Data, + license: web::Path<(String,)>, + license_data: web::Json, +) -> Result { + check_is_admin_from_headers(req.headers(), &**pool).await?; + + let short = license.into_inner().0; + + let _id = DonationPlatform::builder() + .short(&short)? + .name(&license_data.name)? + .insert(&**pool) + .await?; + + Ok(HttpResponse::Ok().body("")) +} + +#[delete("donation_platform/{name}")] +pub async fn donation_platform_delete( + req: HttpRequest, + pool: web::Data, + loader: web::Path<(String,)>, +) -> Result { + check_is_admin_from_headers(req.headers(), &**pool).await?; + + let name = loader.into_inner().0; + let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?; + + let result = DonationPlatform::remove(&name, &mut transaction).await?; + + transaction + .commit() + .await + .map_err(models::DatabaseError::from)?; + + if result.is_some() { + Ok(HttpResponse::Ok().body("")) + } else { + Ok(HttpResponse::NotFound().body("")) + } +} diff --git a/src/routes/users.rs b/src/routes/users.rs index 37af7b377..e8c4cb9ae 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -1,10 +1,13 @@ use crate::auth::{check_is_moderator_from_headers, get_user_from_headers}; use crate::database::models::{TeamMember, User}; +use crate::file_hosting::FileHost; use crate::models::users::{Role, UserId}; use crate::routes::ApiError; -use actix_web::{delete, get, web, HttpRequest, HttpResponse}; +use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse}; +use futures::StreamExt; use serde::{Deserialize, Serialize}; use sqlx::PgPool; +use std::sync::Arc; #[get("user")] pub async fn user_auth_get( @@ -44,22 +47,30 @@ pub async fn users_get( let users: Vec = users_data .into_iter() - .map(|data| crate::models::users::User { - id: data.id.into(), - github_id: data.github_id.map(|i| i as u64), - username: data.username, - name: data.name, - email: None, - avatar_url: data.avatar_url, - bio: data.bio, - created: data.created, - role: Role::from_string(&*data.role), - }) + .map(convert_user) .collect(); Ok(HttpResponse::Ok().json(users)) } +#[get("@{id}")] +pub async fn user_username_get( + info: web::Path<(String,)>, + pool: web::Data, +) -> Result { + let id = info.into_inner().0; + let user_data = User::get_from_username(id, &**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + if let Some(data) = user_data { + let response = convert_user(data); + Ok(HttpResponse::Ok().json(response)) + } else { + Ok(HttpResponse::NotFound().body("")) + } +} + #[get("{id}")] pub async fn user_get( info: web::Path<(UserId,)>, @@ -71,23 +82,27 @@ pub async fn user_get( .map_err(|e| ApiError::DatabaseError(e.into()))?; if let Some(data) = user_data { - let response = crate::models::users::User { - id: data.id.into(), - github_id: data.github_id.map(|i| i as u64), - username: data.username, - name: data.name, - email: None, - avatar_url: data.avatar_url, - bio: data.bio, - created: data.created, - role: Role::from_string(&*data.role), - }; + let response = convert_user(data); Ok(HttpResponse::Ok().json(response)) } else { Ok(HttpResponse::NotFound().body("")) } } +fn convert_user(data: crate::database::models::user_item::User) -> crate::models::users::User { + crate::models::users::User { + id: data.id.into(), + github_id: data.github_id.map(|i| i as u64), + username: data.username, + name: data.name, + email: None, + avatar_url: data.avatar_url, + bio: data.bio, + created: data.created, + role: Role::from_string(&*data.role), + } +} + #[get("{user_id}/mods")] pub async fn mods_list( info: web::Path<(UserId,)>, @@ -161,6 +176,236 @@ pub async fn teams( Ok(HttpResponse::Ok().json(team_members)) } +#[derive(Serialize, Deserialize)] +pub struct EditUser { + pub username: Option, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "::serde_with::rust::double_option" + )] + pub name: Option>, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "::serde_with::rust::double_option" + )] + pub email: Option>, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "::serde_with::rust::double_option" + )] + pub bio: Option>, + pub role: Option, +} + +#[patch("{id}")] +pub async fn user_edit( + req: HttpRequest, + info: web::Path<(UserId,)>, + pool: web::Data, + new_user: web::Json, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + + let user_id = info.into_inner().0; + let id: crate::database::models::ids::UserId = user_id.into(); + + if user.id == user_id || user.role.is_mod() { + let mut transaction = pool + .begin() + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + if let Some(username) = &new_user.username { + sqlx::query!( + " + UPDATE users + SET username = $1 + WHERE (id = $2) + ", + username, + id as crate::database::models::ids::UserId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + + if let Some(name) = &new_user.name { + sqlx::query!( + " + UPDATE users + SET name = $1 + WHERE (id = $2) + ", + name.as_deref(), + id as crate::database::models::ids::UserId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + + if let Some(bio) = &new_user.bio { + sqlx::query!( + " + UPDATE users + SET bio = $1 + WHERE (id = $2) + ", + bio.as_deref(), + id as crate::database::models::ids::UserId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + + if let Some(email) = &new_user.email { + sqlx::query!( + " + UPDATE users + SET email = $1 + WHERE (id = $2) + ", + email.as_deref(), + id as crate::database::models::ids::UserId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + + if let Some(role) = &new_user.role { + if !user.role.is_mod() { + return Err(ApiError::CustomAuthenticationError( + "You do not have the permissions to edit the role of this user!".to_string(), + )); + } + + let role = Role::from_string(role).to_string(); + + sqlx::query!( + " + UPDATE users + SET role = $1 + WHERE (id = $2) + ", + role, + id as crate::database::models::ids::UserId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + + 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 user!".to_string(), + )) + } +} + +#[derive(Serialize, Deserialize)] +pub struct Extension { + pub ext: String, +} + +#[patch("{id}/icon")] +pub async fn user_icon_edit( + web::Query(ext): web::Query, + req: HttpRequest, + info: web::Path<(UserId,)>, + pool: web::Data, + file_host: web::Data>, + mut payload: web::Payload, +) -> Result { + if let Some(content_type) = super::mod_creation::get_image_content_type(&*ext.ext) { + let cdn_url = dotenv::var("CDN_URL")?; + let user = get_user_from_headers(req.headers(), &**pool).await?; + let id = info.into_inner().0; + + if user.id != id && !user.role.is_mod() { + return Err(ApiError::CustomAuthenticationError( + "You don't have permission to edit this user's icon.".to_string(), + )); + } + + let mut icon_url = user.avatar_url; + + if user.id != id { + let new_user = User::get(id.into(), &**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + if let Some(new) = new_user { + icon_url = new.avatar_url; + } else { + return Ok(HttpResponse::NotFound().body("")); + } + } + + if let Some(icon) = icon_url { + if icon.starts_with(&cdn_url) { + let name = icon.split('/').next(); + + if let Some(icon_path) = name { + file_host.delete_file_version("", icon_path).await?; + } + } + } + + let mut bytes = web::BytesMut::new(); + while let Some(item) = payload.next().await { + bytes.extend_from_slice(&item.map_err(|_| { + ApiError::InvalidInputError("Unable to parse bytes in payload sent!".to_string()) + })?); + } + + if bytes.len() >= 262144 { + return Err(ApiError::InvalidInputError(String::from( + "Icons must be smaller than 256KiB", + ))); + } + + let upload_data = file_host + .upload_file( + content_type, + &format!("user/{}/icon.{}", id, ext.ext), + bytes.to_vec(), + ) + .await?; + + let mod_id: crate::database::models::ids::UserId = id.into(); + sqlx::query!( + " + UPDATE users + SET avatar_url = $1 + WHERE (id = $2) + ", + format!("{}/{}", cdn_url, upload_data.file_name), + mod_id as crate::database::models::ids::UserId, + ) + .execute(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + Ok(HttpResponse::Ok().body("")) + } else { + Err(ApiError::InvalidInputError(format!( + "Invalid format for user icon: {}", + ext.ext + ))) + } +} + // TODO: Make this actually do stuff #[delete("{id}")] pub async fn user_delete( diff --git a/src/routes/version_creation.rs b/src/routes/version_creation.rs index fdf7d9d93..0de69eec1 100644 --- a/src/routes/version_creation.rs +++ b/src/routes/version_creation.rs @@ -24,6 +24,7 @@ pub struct InitialVersionData { pub game_versions: Vec, pub release_channel: VersionType, pub loaders: Vec, + pub featured: bool, } #[derive(Serialize, Deserialize, Clone)] @@ -265,6 +266,7 @@ async fn version_create_inner( game_versions, loaders, release_channel, + featured: version_create_data.featured, }); continue; @@ -298,6 +300,7 @@ async fn version_create_inner( id: builder.version_id.into(), mod_id: builder.mod_id.into(), author_id: user.id, + featured: builder.featured, name: builder.name.clone(), version_number: builder.version_number.clone(), changelog_url: builder.changelog_url.clone(), @@ -324,6 +327,7 @@ async fn version_create_inner( .collect(), url: file.url.clone(), filename: file.filename.clone(), + primary: file.primary, }) .collect::>(), dependencies: version_data.dependencies, @@ -528,6 +532,7 @@ pub async fn upload_file( // bytes, but this is the string version. hash: upload_data.content_sha1.into_bytes(), }], + primary: uploaded_files.len() == 1, }) } diff --git a/src/routes/versions.rs b/src/routes/versions.rs index dd36af9dc..06694de10 100644 --- a/src/routes/versions.rs +++ b/src/routes/versions.rs @@ -1,6 +1,6 @@ use super::ApiError; use crate::auth::{check_is_moderator_from_headers, get_user_from_headers}; -use crate::database; +use crate::{database, Pepper}; use crate::file_hosting::FileHost; use crate::models; use crate::models::teams::Permissions; @@ -151,6 +151,7 @@ fn convert_version(data: database::models::version_item::QueryVersion) -> models mod_id: data.mod_id.into(), author_id: data.author_id.into(), + featured: data.featured, name: data.name, version_number: data.version_number, changelog_url: data.changelog_url, @@ -178,6 +179,7 @@ fn convert_version(data: database::models::version_item::QueryVersion) -> models .map(|(k, v)| Some((k, String::from_utf8(v).ok()?))) .collect::>() .unwrap_or_else(Default::default), + primary: f.primary, } }) .collect(), @@ -204,6 +206,8 @@ pub struct EditVersion { pub game_versions: Option>, pub loaders: Option>, pub accepted: Option, + pub featured: Option, + pub primary_file: Option<(String, String)>, } #[patch("{id}")] @@ -388,6 +392,65 @@ pub async fn version_edit( } } + if let Some(featured) = &new_version.featured { + sqlx::query!( + " + UPDATE versions + SET featured = $1 + WHERE (id = $2) + ", + featured, + id as database::models::ids::VersionId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + + if let Some(primary_file) = &new_version.primary_file { + let result = sqlx::query!( + " + SELECT id FROM files + INNER JOIN hashes ON hash = $1 AND algorithm = $2 + ", + primary_file.1.as_bytes(), + primary_file.0 + ) + .fetch_optional(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))? + .ok_or_else(|| { + ApiError::InvalidInputError(format!( + "Specified file with hash {} does not exist.", + primary_file.1.clone() + )) + })?; + + sqlx::query!( + " + UPDATE files + SET is_primary = FALSE + WHERE (version_id = $1) + ", + id as database::models::ids::VersionId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + sqlx::query!( + " + UPDATE files + SET is_primary = TRUE + WHERE (id = $1) + ", + result.id, + ) + .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!( @@ -518,6 +581,102 @@ pub async fn get_version_from_hash( } } +#[derive(Serialize, Deserialize)] +pub struct DownloadRedirect { + pub url: String, +} + +// under /api/v1/version_file/{hash}/download +#[get("{version_id}/download")] +pub async fn download_version( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + algorithm: web::Query, + pepper: web::Data, +) -> Result { + let hash = info.into_inner().0; + + let result = sqlx::query!( + " + SELECT f.url url, f.id id, f.version_id version_id, v.mod_id mod_id FROM files f + INNER JOIN versions v ON v.id = f.version_id + 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 real_ip = req.connection_info(); + let ip_option = real_ip.realip_remote_addr(); + + if let Some(ip) = ip_option { + let hash = sha1::Sha1::from(format!("{}{}", ip, pepper.pepper)).hexdigest(); + + let download_exists = sqlx::query!( + "SELECT EXISTS(SELECT 1 FROM downloads WHERE version_id = $1 AND date > (CURRENT_DATE - INTERVAL '30 minutes ago') AND identifier = $2)", + id.version_id, + hash, + ) + .fetch_one(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))? + .exists.unwrap_or(false); + + if !download_exists { + sqlx::query!( + " + INSERT INTO downloads ( + version_id, identifier + ) + VALUES ( + $1, $2 + ) + ", + id.version_id, + hash + ) + .execute(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + sqlx::query!( + " + UPDATE versions + SET downloads = downloads + 1 + WHERE id = $1 + ", + id.version_id, + ) + .execute(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + sqlx::query!( + " + UPDATE mods + SET downloads = downloads + 1 + WHERE id = $1 + ", + id.mod_id, + ) + .execute(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + } + Ok(HttpResponse::TemporaryRedirect() + .header("Location", &*id.url) + .json(DownloadRedirect { url: id.url })) + } else { + Ok(HttpResponse::NotFound().body("")) + } +} + // under /api/v1/version_file/{hash} #[delete("{version_id}")] pub async fn delete_file(