diff --git a/Cargo.lock b/Cargo.lock index ae401b28e..ded4d97ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1072,6 +1072,37 @@ dependencies = [ "syn", ] +[[package]] +name = "curl" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e268162af1a5fe89917ae25ba3b0a77c8da752bdc58e7dbb4f15b91fbd33756e" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2", + "winapi 0.3.9", +] + +[[package]] +name = "curl-sys" +version = "0.4.40+curl-7.75.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffafc1c35958318bd7fdd0582995ce4c72f4f461a8e70499ccee83a619fd562" +dependencies = [ + "cc", + "libc", + "libnghttp2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "winapi 0.3.9", +] + [[package]] name = "darling" version = "0.10.2" @@ -1298,6 +1329,18 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531a685ab99b8f60a271b44d5dd1a76e55124a8c9fa0407b7a8e9cd172d5b588" +dependencies = [ + "futures-core", + "futures-sink", + "pin-project 1.0.4", + "spinning_top", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1856,6 +1899,30 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" +[[package]] +name = "isahc" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3d0a62435883f745c825ec06a03a38d24bf5fa65c43e2c083b6a60ce0058ae" +dependencies = [ + "crossbeam-utils 0.8.1", + "curl", + "curl-sys", + "encoding_rs", + "flume", + "futures-lite", + "http", + "log", + "mime", + "once_cell", + "slab", + "sluice", + "tracing", + "tracing-futures", + "url", + "waker-fn", +] + [[package]] name = "itoa" version = "0.4.7" @@ -1957,6 +2024,28 @@ version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929" +[[package]] +name = "libnghttp2-sys" +version = "0.1.6+1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0af55541a8827e138d59ec9e5877fb6095ece63fb6f4da45e7491b4fbd262855" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "libz-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linked-hash-map" version = "0.5.4" @@ -2034,12 +2123,12 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "meilisearch-sdk" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb2081610089deb10290747b8782049f9cb64a70a4d305a28970db8b780d1448" +checksum = "f8972f69aef330566ece2a76e61ebb9383565b03c45aeb95dbd66ad672186497" dependencies = [ + "isahc", "log", - "reqwest 0.10.10", "serde", "serde_json", "wasm-bindgen", @@ -3179,6 +3268,17 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +[[package]] +name = "sluice" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fa0333a60ff2e3474a6775cc611840c2a55610c831dd366503474c02f1a28f5" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", +] + [[package]] name = "smallvec" version = "1.6.1" @@ -3202,6 +3302,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spinning_top" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e529d73e80d64b5f2631f9035113347c578a1c9c7774b83a2b880788459ab36" +dependencies = [ + "lock_api", +] + [[package]] name = "sqlformat" version = "0.1.5" @@ -3701,9 +3810,21 @@ dependencies = [ "cfg-if 1.0.0", "log", "pin-project-lite 0.2.4", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a9bd1db7706f2373a190b0d067146caa39350c486f3d455b0e33b431f94c07" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.17" diff --git a/Cargo.toml b/Cargo.toml index a76bec688..5e81cbaae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ actix-multipart = "0.3.0" actix-cors = "0.4.1" actix-ratelimit = "0.3.0" -meilisearch-sdk = "0.4.0" +meilisearch-sdk = "0.6.0" reqwest = { version = "0.10.8", features = ["json"] } serde_json = "1.0" diff --git a/migrations/20210224174945_notifications.sql b/migrations/20210224174945_notifications.sql new file mode 100644 index 000000000..a8ea8efe7 --- /dev/null +++ b/migrations/20210224174945_notifications.sql @@ -0,0 +1,17 @@ +-- Add migration script here +CREATE TABLE notifications ( + id bigint PRIMARY KEY, + user_id bigint REFERENCES users NOT NULL, + title varchar(255) NOT NULL, + text varchar(2048) NOT NULL, + link varchar(2048) NOT NULL, + created timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL, + read boolean DEFAULT FALSE NOT NULL +); + +CREATE TABLE notifications_actions ( + id serial PRIMARY KEY, + notification_id bigint REFERENCES notifications NOT NULL, + title varchar(255) NOT NULL, + action_route varchar(2048) NOT NULL +); \ No newline at end of file diff --git a/migrations/20210301041252_follows.sql b/migrations/20210301041252_follows.sql new file mode 100644 index 000000000..6c822465e --- /dev/null +++ b/migrations/20210301041252_follows.sql @@ -0,0 +1,9 @@ +CREATE TABLE mod_follows( + follower_id bigint REFERENCES users NOT NULL, + mod_id bigint REFERENCES mods NOT NULL, + created timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL, + PRIMARY KEY (follower_id, mod_id) +); + +ALTER TABLE mods + ADD COLUMN follows integer NOT NULL default 0; \ No newline at end of file diff --git a/sqlx-data.json b/sqlx-data.json index 79a47ed76..8ae4aafcb 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -56,20 +56,13 @@ "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 ", + "03c196a6b0c287b9d913559442b1ea679c35634e33f94197f587532757cb7385": { + "query": "\n DELETE FROM notifications_actions\n WHERE notification_id IN (SELECT * FROM UNNEST($1::bigint[]))\n ", "describe": { "columns": [], "parameters": { "Left": [ - "Int8", - "Int8", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Timestamptz" + "Int8Array" ] }, "nullable": [] @@ -255,6 +248,62 @@ ] } }, + "105a0184a875e7b9c5aa38527816bace9d9136ffc22627a1695ca337bf7009dd": { + "query": "\n SELECT n.user_id, n.title, n.text, n.link, n.created, n.read,\n STRING_AGG(DISTINCT na.id || ', ' || na.title || ', ' || na.action_route, ' ,') actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.id = $1\n GROUP BY n.id, n.user_id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "text", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "link", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "created", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "read", + "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "actions", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + null + ] + } + }, "1220d15a56dbf823eaa452fbafa17442ab0568bc81a31fa38e16e3df3278e5f9": { "query": "SELECT EXISTS(SELECT 1 FROM users WHERE id = $1)", "describe": { @@ -275,100 +324,16 @@ ] } }, - "1305fabd44e54fb9f4a78e698507f19cbcd9cf52bb193f3188679918979f28a8": { - "query": "\n SELECT m.id, m.title, m.description, m.downloads, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.status, m.slug, m.license, m.client_side, m.server_side FROM mods m\n ", + "15b8ea323c2f6d03c2e385d9c46d7f13460764f2f106fd638226c42ae0217f75": { + "query": "\n DELETE FROM notifications\n WHERE user_id = $1\n ", "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "description", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 4, - "name": "icon_url", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "body_url", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "published", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "updated", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "team_id", - "type_info": "Int8" - }, - { - "ordinal": 9, - "name": "status", - "type_info": "Int4" - }, - { - "ordinal": 10, - "name": "slug", - "type_info": "Varchar" - }, - { - "ordinal": 11, - "name": "license", - "type_info": "Int4" - }, - { - "ordinal": 12, - "name": "client_side", - "type_info": "Int4" - }, - { - "ordinal": 13, - "name": "server_side", - "type_info": "Int4" - } - ], + "columns": [], "parameters": { - "Left": [] + "Left": [ + "Int8" + ] }, - "nullable": [ - false, - false, - false, - false, - true, - true, - false, - false, - false, - false, - true, - false, - false, - false - ] + "nullable": [] } }, "16049957962ded08751d5a4ddce2ffac17ecd486f61210c51a952508425d83e6": { @@ -465,8 +430,8 @@ ] } }, - "182d2466e978b9ec25ce205a6670f9c994e55650742153f6d5735a8771c55226": { - "query": "\n SELECT m.id id, m.title title, m.description description, m.downloads downloads,\n m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,\n m.updated updated, m.status status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug,\n s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name,\n STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id\n INNER JOIN statuses s ON s.id = m.status\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN licenses l ON m.license = l.id\n WHERE m.id IN (SELECT * FROM UNNEST($1::bigint[]))\n GROUP BY m.id, s.id, cs.id, ss.id, l.id;\n ", + "18f9188a2c89707ceea46b1ebc66c0cc09da21240dbf36c1c7c32736aed44a38": { + "query": "\n SELECT m.id id, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,\n m.updated updated, m.status status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug,\n s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name,\n STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id\n INNER JOIN statuses s ON s.id = m.status\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN licenses l ON m.license = l.id\n WHERE m.id IN (SELECT * FROM UNNEST($1::bigint[]))\n GROUP BY m.id, s.id, cs.id, ss.id, l.id;\n ", "describe": { "columns": [ { @@ -491,116 +456,121 @@ }, { "ordinal": 4, + "name": "follows", + "type_info": "Int4" + }, + { + "ordinal": 5, "name": "icon_url", "type_info": "Varchar" }, { - "ordinal": 5, + "ordinal": 6, "name": "body", "type_info": "Varchar" }, { - "ordinal": 6, + "ordinal": 7, "name": "body_url", "type_info": "Varchar" }, { - "ordinal": 7, + "ordinal": 8, "name": "published", "type_info": "Timestamptz" }, { - "ordinal": 8, + "ordinal": 9, "name": "updated", "type_info": "Timestamptz" }, { - "ordinal": 9, + "ordinal": 10, "name": "status", "type_info": "Int4" }, { - "ordinal": 10, + "ordinal": 11, "name": "issues_url", "type_info": "Varchar" }, { - "ordinal": 11, + "ordinal": 12, "name": "source_url", "type_info": "Varchar" }, { - "ordinal": 12, + "ordinal": 13, "name": "wiki_url", "type_info": "Varchar" }, { - "ordinal": 13, + "ordinal": 14, "name": "discord_url", "type_info": "Varchar" }, { - "ordinal": 14, + "ordinal": 15, "name": "license_url", "type_info": "Varchar" }, { - "ordinal": 15, + "ordinal": 16, "name": "team_id", "type_info": "Int8" }, { - "ordinal": 16, + "ordinal": 17, "name": "client_side", "type_info": "Int4" }, { - "ordinal": 17, + "ordinal": 18, "name": "server_side", "type_info": "Int4" }, { - "ordinal": 18, + "ordinal": 19, "name": "license", "type_info": "Int4" }, { - "ordinal": 19, + "ordinal": 20, "name": "slug", "type_info": "Varchar" }, { - "ordinal": 20, + "ordinal": 21, "name": "status_name", "type_info": "Varchar" }, { - "ordinal": 21, + "ordinal": 22, "name": "client_side_type", "type_info": "Varchar" }, { - "ordinal": 22, + "ordinal": 23, "name": "server_side_type", "type_info": "Varchar" }, { - "ordinal": 23, + "ordinal": 24, "name": "short", "type_info": "Varchar" }, { - "ordinal": 24, + "ordinal": 25, "name": "license_name", "type_info": "Varchar" }, { - "ordinal": 25, + "ordinal": 26, "name": "categories", "type_info": "Text" }, { - "ordinal": 26, + "ordinal": 27, "name": "versions", "type_info": "Text" } @@ -615,6 +585,7 @@ false, false, false, + false, true, false, true, @@ -808,8 +779,8 @@ ] } }, - "2439ae8db5ae81aad03351e9a2e19259ef033110ed1c2d71bc0a643104ca32d0": { - "query": "\n UPDATE versions\n SET downloads = downloads + 1\n WHERE id = $1\n ", + "24dd644d22542e4d4493ce1e0ea57ce73e47a1a433ffcedf2631ce2f21bfd2c0": { + "query": "\n UPDATE mods\n SET follows = follows - 1\n WHERE id = $1\n ", "describe": { "columns": [], "parameters": { @@ -866,6 +837,108 @@ ] } }, + "255ebd215adf4e2a5a837b8a682b7396d185c656e1151fcec24eddca1b7fb942": { + "query": "\n SELECT m.id, m.title, m.description, m.downloads, m.follows, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.status, m.slug, m.license, m.client_side, m.server_side FROM mods m\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "follows", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "body_url", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "published", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "updated", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 10, + "name": "status", + "type_info": "Int4" + }, + { + "ordinal": 11, + "name": "slug", + "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "license", + "type_info": "Int4" + }, + { + "ordinal": 13, + "name": "client_side", + "type_info": "Int4" + }, + { + "ordinal": 14, + "name": "server_side", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + true, + false, + false, + false + ] + } + }, "29e657d26f0fb24a766f5b5eb6a94d01d1616884d8ca10e91536e974d5b585a6": { "query": "\n INSERT INTO loaders_versions (loader_id, version_id)\n VALUES ($1, $2)\n ", "describe": { @@ -921,92 +994,8 @@ "nullable": [] } }, - "2fa070eef3fe8f708a1495104f78eda2bfa0fe19ada2bf66ac35fb2468631774": { - "query": "\n SELECT category FROM categories\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "category", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false - ] - } - }, - "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": { - "columns": [ - { - "ordinal": 0, - "name": "status", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [ - false - ] - } - }, - "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": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - } - }, - "36603c3b34a13d5b57d302e54159279bca9a918bb804ead378bd86e0512a9ef5": { - "query": "\n SELECT m.id id, m.title title, m.description description, m.downloads downloads,\n m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,\n m.updated updated, m.status status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug,\n s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name,\n STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id\n INNER JOIN statuses s ON s.id = m.status\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN licenses l ON m.license = l.id\n WHERE m.id = $1\n GROUP BY m.id, s.id, cs.id, ss.id, l.id;\n ", + "2d5af30eec91c9086fe1beffdb648766986cb3e0ff00c0f3fc96f119f0a67c00": { + "query": "\n SELECT id, title, description, downloads, follows,\n icon_url, body, body_url, published,\n updated, status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug\n FROM mods\n WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))\n ", "describe": { "columns": [ { @@ -1031,118 +1020,166 @@ }, { "ordinal": 4, + "name": "follows", + "type_info": "Int4" + }, + { + "ordinal": 5, "name": "icon_url", "type_info": "Varchar" }, { - "ordinal": 5, + "ordinal": 6, "name": "body", "type_info": "Varchar" }, { - "ordinal": 6, + "ordinal": 7, "name": "body_url", "type_info": "Varchar" }, { - "ordinal": 7, + "ordinal": 8, "name": "published", "type_info": "Timestamptz" }, { - "ordinal": 8, + "ordinal": 9, "name": "updated", "type_info": "Timestamptz" }, { - "ordinal": 9, + "ordinal": 10, "name": "status", "type_info": "Int4" }, { - "ordinal": 10, + "ordinal": 11, "name": "issues_url", "type_info": "Varchar" }, { - "ordinal": 11, + "ordinal": 12, "name": "source_url", "type_info": "Varchar" }, { - "ordinal": 12, + "ordinal": 13, "name": "wiki_url", "type_info": "Varchar" }, { - "ordinal": 13, + "ordinal": 14, "name": "discord_url", "type_info": "Varchar" }, { - "ordinal": 14, + "ordinal": 15, "name": "license_url", "type_info": "Varchar" }, { - "ordinal": 15, + "ordinal": 16, "name": "team_id", "type_info": "Int8" }, { - "ordinal": 16, + "ordinal": 17, "name": "client_side", "type_info": "Int4" }, { - "ordinal": 17, + "ordinal": 18, "name": "server_side", "type_info": "Int4" }, { - "ordinal": 18, + "ordinal": 19, "name": "license", "type_info": "Int4" }, { - "ordinal": 19, + "ordinal": 20, "name": "slug", "type_info": "Varchar" - }, + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + false, + true, + false, + false, + false, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true + ] + } + }, + "2fa070eef3fe8f708a1495104f78eda2bfa0fe19ada2bf66ac35fb2468631774": { + "query": "\n SELECT category FROM categories\n ", + "describe": { + "columns": [ { - "ordinal": 20, - "name": "status_name", + "ordinal": 0, + "name": "category", "type_info": "Varchar" - }, + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + } + }, + "3135db1c5309dac7580a731b2829397ae7bdd6c9a67b21e813f26a4f5aa251a9": { + "query": "\n SELECT status FROM statuses\n WHERE id = $1\n ", + "describe": { + "columns": [ { - "ordinal": 21, - "name": "client_side_type", + "ordinal": 0, + "name": "status", "type_info": "Varchar" - }, + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false + ] + } + }, + "31853f131eaeb2aaedc2bcc27da387462408409af810337f6a8ef397f674fb44": { + "query": "\n SELECT DISTINCT loaders.loader FROM versions\n INNER JOIN loaders_versions lv ON lv.version_id = versions.id\n INNER JOIN loaders ON loaders.id = lv.loader_id\n WHERE versions.mod_id = $1\n ", + "describe": { + "columns": [ { - "ordinal": 22, - "name": "server_side_type", + "ordinal": 0, + "name": "loader", "type_info": "Varchar" - }, - { - "ordinal": 23, - "name": "short", - "type_info": "Varchar" - }, - { - "ordinal": 24, - "name": "license_name", - "type_info": "Varchar" - }, - { - "ordinal": 25, - "name": "categories", - "type_info": "Text" - }, - { - "ordinal": 26, - "name": "versions", - "type_info": "Text" } ], "parameters": { @@ -1151,36 +1188,35 @@ ] }, "nullable": [ - false, - false, - false, - false, - true, - false, - true, - false, - false, - false, - true, - true, - true, - true, - true, - false, - false, - false, - false, - true, - false, - false, - false, - false, - false, - null, - null + false ] } }, + "3387447d6dd9c63448f021c670457270d0f2c00a270d3c1dd7160f8ed99805d3": { + "query": "\n DELETE FROM mod_follows\n WHERE follower_id = $1 AND mod_id = $2\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [] + } + }, + "33fc96ac71cfa382991cfb153e89da1e9f43ebf5367c28b30c336b758222307b": { + "query": "\n DELETE FROM loaders_versions\n WHERE loaders_versions.version_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, "381974f80a890a59f89c46b0c709e4511c0216eb8059ee47bb1e1456caf68fd7": { "query": "\n INSERT INTO dependencies (dependent_id, dependency_id, dependency_type)\n VALUES ($1, $2, $3)\n ", "describe": { @@ -1336,6 +1372,18 @@ "nullable": [] } }, + "413762398111e04074a2d8a1e4e03ed362b9167d397947f8d14e5ae330e3de0b": { + "query": "\n UPDATE versions\n SET downloads = downloads + 1\n WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, "43b793e2df30a6ace9e037e38bb4ea456656cfbe276c151e3a9e0a408d2c249f": { "query": "\n UPDATE versions\n SET release_channel = $1\n WHERE (id = $2)\n ", "describe": { @@ -1377,6 +1425,26 @@ "nullable": [] } }, + "48294a4e0c594e80fff8d14a705aa7282f55e47cf3772e77f1d4bf4849008b60": { + "query": "\n SELECT follower_id FROM mod_follows\n WHERE mod_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "follower_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + } + }, "49e36828e3a0214b48234435e34311735ae32e08d8be1270f8f0db4b27e708ba": { "query": "\n INSERT INTO loaders (loader)\n VALUES ($1)\n ON CONFLICT (loader) DO NOTHING\n RETURNING id\n ", "describe": { @@ -1542,6 +1610,35 @@ "nullable": [] } }, + "4fb5bd341369b4beb6b4a88de296b608ea5441a96db9f7360fbdccceb4628202": { + "query": "\n UPDATE mods\n SET slug = LOWER($1)\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Int8" + ] + }, + "nullable": [] + } + }, + "4ff46a178f181a11c6c57c628770ef3b80e7344cff5d8bf268876a863cbfda36": { + "query": "\n INSERT INTO notifications (\n id, user_id, title, text, link\n )\n VALUES (\n $1, $2, $3, $4, $5\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Varchar", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + } + }, "507314fdcacaa3c7751738c9d0baee2b90aec719b6b203f922824eced5ea8369": { "query": "\n DELETE FROM game_versions_versions WHERE joining_version_id = $1\n ", "describe": { @@ -1773,26 +1870,6 @@ ] } }, - "56fc196cbe33032b699348d7a2f3366100bc54decb1d18bb6aad865a88096c67": { - "query": "\n SELECT id FROM mods\n WHERE slug = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false - ] - } - }, "57bb3db92e6a8fb8606005be955e2379f13a04f101f91358322a591a860a7f9e": { "query": "\n SELECT id FROM reports\n ORDER BY created ASC\n LIMIT $1;\n ", "describe": { @@ -1918,6 +1995,18 @@ "nullable": [] } }, + "5d7d7e33c2952199225d7b93f5a74f3436ba18aa24e6ef1840becbf236447fd6": { + "query": "\n DELETE FROM mod_follows\n WHERE mod_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, "5eb2795d25d6d03e22564048c198d821cd5ff22eb4e39b9dd7f198c9113d4f87": { "query": "\n UPDATE users\n SET name = $1\n WHERE (id = $2)\n ", "describe": { @@ -1983,6 +2072,250 @@ ] } }, + "64b2c8a208eaef033bf555c4a4962746d2c3706d24ae60e67a8b732c9a4b0edc": { + "query": "\n SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read,\n STRING_AGG(DISTINCT na.id || ', ' || na.title || ', ' || na.action_route, ' ,') actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.user_id = $1\n GROUP BY n.id, n.user_id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "text", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "link", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "created", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "read", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "actions", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + null + ] + } + }, + "6612afe698ec5ad3a9a98294d12fa5b04ccfeac4b31365b3821ac8e2ae6c5768": { + "query": "\n SELECT m.id id, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,\n m.updated updated, m.status status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug,\n s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name,\n STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id\n INNER JOIN statuses s ON s.id = m.status\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN licenses l ON m.license = l.id\n WHERE m.id = $1\n GROUP BY m.id, s.id, cs.id, ss.id, l.id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "follows", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "body", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "body_url", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "published", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "updated", + "type_info": "Timestamptz" + }, + { + "ordinal": 10, + "name": "status", + "type_info": "Int4" + }, + { + "ordinal": 11, + "name": "issues_url", + "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "source_url", + "type_info": "Varchar" + }, + { + "ordinal": 13, + "name": "wiki_url", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "discord_url", + "type_info": "Varchar" + }, + { + "ordinal": 15, + "name": "license_url", + "type_info": "Varchar" + }, + { + "ordinal": 16, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 17, + "name": "client_side", + "type_info": "Int4" + }, + { + "ordinal": 18, + "name": "server_side", + "type_info": "Int4" + }, + { + "ordinal": 19, + "name": "license", + "type_info": "Int4" + }, + { + "ordinal": 20, + "name": "slug", + "type_info": "Varchar" + }, + { + "ordinal": 21, + "name": "status_name", + "type_info": "Varchar" + }, + { + "ordinal": 22, + "name": "client_side_type", + "type_info": "Varchar" + }, + { + "ordinal": 23, + "name": "server_side_type", + "type_info": "Varchar" + }, + { + "ordinal": 24, + "name": "short", + "type_info": "Varchar" + }, + { + "ordinal": 25, + "name": "license_name", + "type_info": "Varchar" + }, + { + "ordinal": 26, + "name": "categories", + "type_info": "Text" + }, + { + "ordinal": 27, + "name": "versions", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + false, + true, + false, + false, + false, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + null, + null + ] + } + }, "67d021f0776276081d3c50ca97afa6b78b98860bf929009e845e9c00a192e3b5": { "query": "\n SELECT id FROM report_types\n WHERE name = $1\n ", "describe": { @@ -2055,6 +2388,19 @@ "nullable": [] } }, + "6e5ddd4069e59426636cf59a67d19cee449bc7de0762a68327181a5009118ca6": { + "query": "\n INSERT INTO mod_follows (follower_id, mod_id)\n VALUES ($1, $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [] + } + }, "6f1fb4c3269b2a8190f328df025be76241eae757d9c4f3e5eb1cc01b191837df": { "query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1\n ", "describe": { @@ -2216,18 +2562,6 @@ "nullable": [] } }, - "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": { @@ -2332,6 +2666,60 @@ ] } }, + "7e73d3a17807f57ba6def5ff718e6dcb3a65ef8da653d839560b24635334cf05": { + "query": "\n SELECT m.title FROM mods m\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "title", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + } + }, + "7f1696cee355c03f474fda2283669c60046833db88b3e2befd62a1fea7a12c70": { + "query": "\n INSERT INTO downloads (\n version_id, identifier\n )\n VALUES (\n $1, $2\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Varchar" + ] + }, + "nullable": [] + } + }, + "80e253ea5c9846a8ce479bb2617773109a8e2583af74bfc30ddbfe3a7c5da8ae": { + "query": "\n SELECT f.id FROM files f\n INNER JOIN hashes h ON h.hash = $1 AND h.algorithm = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Bytea", + "Text" + ] + }, + "nullable": [ + false + ] + } + }, "82515e4e7e88f1193c956f032caabc70f535f925e212de30f974afd3ec126092": { "query": "\n INSERT INTO licenses (short, name)\n VALUES ($1, $2)\n ON CONFLICT (short) DO NOTHING\n RETURNING id\n ", "describe": { @@ -2462,55 +2850,56 @@ "nullable": [] } }, - "8f706d78ac4235ea04c59e2c220a4791e1d08fdf287b783b4aaef36fd2445467": { - "query": "\n DELETE FROM loaders\n WHERE loader = $1\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [] - } - }, - "8fd5d332e9cd2f760f956bf4936350f29df414552643bcfb352ca8a8a0b98439": { - "query": "\n UPDATE mods\n SET icon_url = $1\n WHERE (id = $2)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Int8" - ] - }, - "nullable": [] - } - }, - "904b88f0b2ae62a31c79e11c63e1ca42fd6c61c330de33b0f62f6d675c3e3df0": { - "query": "\n SELECT id, title, description, downloads,\n icon_url, body, body_url, published,\n updated, status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug\n FROM mods\n WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))\n ", + "8c25a870b9306d653caaa4c324122ecd928796107b9d2fcdeaba82c7fcbbbebc": { + "query": "\n SELECT m.title, m.id FROM mods m\n WHERE m.team_id = $1\n ", "describe": { "columns": [ { "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, "name": "title", "type_info": "Varchar" }, { - "ordinal": 2, + "ordinal": 1, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false + ] + } + }, + "8f02d5c2b9095c21498802f01cefdbc57a5f1c2a7aee717ba19daaffc498c5cd": { + "query": "\n SELECT title, description, downloads, follows,\n icon_url, body, body_url, published,\n updated, status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug\n FROM mods\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 1, "name": "description", "type_info": "Varchar" }, { - "ordinal": 3, + "ordinal": 2, "name": "downloads", "type_info": "Int4" }, + { + "ordinal": 3, + "name": "follows", + "type_info": "Int4" + }, { "ordinal": 4, "name": "icon_url", @@ -2594,7 +2983,7 @@ ], "parameters": { "Left": [ - "Int8Array" + "Int8" ] }, "nullable": [ @@ -2621,6 +3010,31 @@ ] } }, + "8f706d78ac4235ea04c59e2c220a4791e1d08fdf287b783b4aaef36fd2445467": { + "query": "\n DELETE FROM loaders\n WHERE loader = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + } + }, + "8fd5d332e9cd2f760f956bf4936350f29df414552643bcfb352ca8a8a0b98439": { + "query": "\n UPDATE mods\n SET icon_url = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + } + }, "94c49b879a95a9068d93e01e270755c8acedfcfad178b0ed1efbf253b23431aa": { "query": "\n INSERT INTO versions (\n id, mod_id, author_id, name, version_number,\n changelog, changelog_url, date_published,\n downloads, release_channel, featured\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7,\n $8, $9,\n $10, $11\n )\n ", "describe": { @@ -2663,98 +3077,6 @@ ] } }, - "975616f51d8d179a34e92b445c8ed3ac62de17dbbc44a7d42ca5476615b4f04d": { - "query": "\n SELECT m.id, m.title, m.description, m.downloads, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.slug, m.license, m.client_side, m.server_side\n FROM mods m\n WHERE id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "description", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 4, - "name": "icon_url", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "body_url", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "published", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "updated", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "team_id", - "type_info": "Int8" - }, - { - "ordinal": 9, - "name": "slug", - "type_info": "Varchar" - }, - { - "ordinal": 10, - "name": "license", - "type_info": "Int4" - }, - { - "ordinal": 11, - "name": "client_side", - "type_info": "Int4" - }, - { - "ordinal": 12, - "name": "server_side", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - true, - true, - false, - false, - false, - true, - false, - false, - false - ] - } - }, "97690dda7edea8c985891cae5ad405f628ed81e333bc88df5493c928a4324d43": { "query": "SELECT EXISTS(SELECT 1 FROM reports WHERE id=$1)", "describe": { @@ -2815,134 +3137,6 @@ ] } }, - "a2369858ff21b3fa491685168624e9eb22aca34ef0fefc774675b38066c8beba": { - "query": "\n SELECT title, description, downloads,\n icon_url, body, body_url, published,\n updated, status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug\n FROM mods\n WHERE id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "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", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "body_url", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "published", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "updated", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "status", - "type_info": "Int4" - }, - { - "ordinal": 9, - "name": "issues_url", - "type_info": "Varchar" - }, - { - "ordinal": 10, - "name": "source_url", - "type_info": "Varchar" - }, - { - "ordinal": 11, - "name": "wiki_url", - "type_info": "Varchar" - }, - { - "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": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - true, - false, - true, - false, - false, - false, - true, - true, - true, - true, - true, - false, - false, - false, - false, - true - ] - } - }, "a39ce28b656032f862b205cffa393a76b989f4803654a615477a94fda5f57354": { "query": "\n DELETE FROM states\n WHERE id = $1\n ", "describe": { @@ -3278,26 +3472,6 @@ "nullable": [] } }, - "b34577335d30ffe30327cdd5b3c029a187a1cae27bea99ff0bcf062f87468fe7": { - "query": "\n SELECT loaders.loader FROM versions\n INNER JOIN loaders_versions lv ON lv.version_id = versions.id\n INNER JOIN loaders ON loaders.id = lv.loader_id\n WHERE versions.mod_id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "loader", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false - ] - } - }, "b3c1b38d2e72c5ec9e6f34d497fb6eb5d01d6cdd07f38ee4a2bbae3b92911df7": { "query": "\n SELECT version FROM game_versions\n WHERE major = $1 AND type = $2\n ORDER BY created DESC\n ", "describe": { @@ -3319,6 +3493,25 @@ ] } }, + "b446a7ddda6860b72517501332a51e64e76f6ab6de6527ad86b4c77db71f6037": { + "query": "\n INSERT INTO users (\n id, github_id, username, name, email,\n avatar_url, bio, created\n )\n VALUES (\n $1, $2, LOWER($3), $4, $5,\n $6, $7, $8\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Text", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Timestamptz" + ] + }, + "nullable": [] + } + }, "b69a6f42965b3e7103fcbf46e39528466926789ff31e9ed2591bb175527ec169": { "query": "\n DELETE FROM users\n WHERE id = $1\n ", "describe": { @@ -3331,6 +3524,68 @@ "nullable": [] } }, + "b6d377746ceeb0e52ed55255fb294cfce4873739e3277a79038bcaf1e9796c68": { + "query": "\n SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read,\n STRING_AGG(DISTINCT na.id || ', ' || na.title || ', ' || na.action_route, ' ,') actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.id IN (SELECT * FROM UNNEST($1::bigint[]))\n GROUP BY n.id, n.user_id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "text", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "link", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "created", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "read", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "actions", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + null + ] + } + }, "b7b2b5b99340c7601de53cc33dc56af054b50b2fe4d1d212901c958115a42baa": { "query": "\n UPDATE versions\n SET author_id = $1\n WHERE (author_id = $2)\n ", "describe": { @@ -3344,6 +3599,18 @@ "nullable": [] } }, + "b8091122d243912e628b06e244bb8ac47cd36903185a50d15ad2368b257ab0e2": { + "query": "\n DELETE FROM notifications\n WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, "b903ac4e686ef85ba28d698c668da07860e7f276b261d8f2cebb74e73b094970": { "query": "\n DELETE FROM hashes\n WHERE EXISTS(\n SELECT 1 FROM files WHERE\n (files.version_id = $1) AND\n (hashes.file_id = files.id)\n )\n ", "describe": { @@ -3405,6 +3672,18 @@ "nullable": [] } }, + "bc41b72640b63a9eb09ed92adc119b7119a7173d758d9541e06672c4b2f977d7": { + "query": "\n UPDATE mods\n SET downloads = downloads + 1\n WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, "bc91841f9672608a28bd45a862919f2bd34fac0b3479e3b4b67a9f6bea2a562a": { "query": "\n UPDATE mods\n SET issues_url = $1\n WHERE (id = $2)\n ", "describe": { @@ -3475,6 +3754,18 @@ ] } }, + "bdde6a7e476933c109c5b0d7236e033ccb7bf242266f77815a387a370365a10e": { + "query": "\n DELETE FROM notifications_actions\n WHERE notification_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, "bec1612d4929d143bc5d6860a57cc036c5ab23e69d750ca5791c620297953c50": { "query": "\n SELECT team_id FROM mods WHERE id = $1\n ", "describe": { @@ -3580,6 +3871,18 @@ ] } }, + "c201a7a7198fe2a083fc556b408b8b700e81759f4aa5966a4a3874a46aafb6b2": { + "query": "\n DELETE FROM mod_follows\n WHERE follower_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, "c3dcb5a8b798ea6c0922698a007dbc8ab549f5f85bad780da59163f4d6371238": { "query": "\n SELECT id FROM mods\n WHERE status = (\n SELECT id FROM statuses WHERE status = $1\n )\n ORDER BY updated ASC\n LIMIT $2;\n ", "describe": { @@ -3687,68 +3990,6 @@ ] } }, - "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 - ] - } - }, "c9d63ed46799db7c30a7e917d97a5d4b2b78b0234cce49e136fa57526b38c1ca": { "query": "\n SELECT EXISTS(SELECT 1 FROM versions WHERE id = $1)\n ", "describe": { @@ -3769,6 +4010,40 @@ ] } }, + "ca1574164dfb16ccee0a1f0468760903453632a5c38dbc64aa667a8ee6086c47": { + "query": "\n INSERT INTO notifications_actions (\n notification_id, title, action_route\n )\n VALUES (\n $1, $2, $3\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + } + }, + "ca52197b89fcc61f131b0937d642133ae19903d183f84513601e16ee7f3df7d8": { + "query": "\n SELECT id FROM mods\n WHERE LOWER(slug) = LOWER($1)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + } + }, "ca5e6d56640069ea9b1c32881d59be8eb1c2ee06ececf223ad378d0422a6f198": { "query": "\n SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,\n v.changelog changelog, v.changelog_url changelog_url, v.date_published date_published, v.downloads downloads,\n rc.channel release_channel, v.featured featured,\n STRING_AGG(DISTINCT gv.version, ',') game_versions, STRING_AGG(DISTINCT l.loader, ',') loaders,\n STRING_AGG(DISTINCT f.id || ', ' || f.filename || ', ' || f.is_primary || ', ' || f.url, ' ,') files,\n STRING_AGG(DISTINCT h.algorithm || ', ' || encode(h.hash, 'escape') || ', ' || h.file_id, ' ,') hashes,\n STRING_AGG(DISTINCT d.dependency_id || ', ' || d.dependency_type, ' ,') dependencies\n FROM versions v\n INNER JOIN release_channels rc on v.release_channel = rc.id\n LEFT OUTER JOIN game_versions_versions gvv on v.id = gvv.joining_version_id\n LEFT OUTER JOIN game_versions gv on gvv.game_version_id = gv.id\n LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id\n LEFT OUTER JOIN loaders l on lv.loader_id = l.id\n LEFT OUTER JOIN files f on v.id = f.version_id\n LEFT OUTER JOIN hashes h on f.id = h.file_id\n LEFT OUTER JOIN dependencies d on v.id = d.dependent_id\n WHERE v.id = $1\n GROUP BY v.id, rc.id;\n ", "describe": { @@ -3970,6 +4245,35 @@ "nullable": [] } }, + "ce4e3569e69bb87b28af75f6f836715357b290704896edc661185e6a9c3f778e": { + "query": "\n INSERT INTO mods (\n id, team_id, title, description, body,\n published, downloads, icon_url, issues_url,\n source_url, wiki_url, status, 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 LOWER($18)\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Varchar", + "Varchar", + "Varchar", + "Timestamptz", + "Int4", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Int4", + "Varchar", + "Int4", + "Int4", + "Varchar", + "Int4", + "Text" + ] + }, + "nullable": [] + } + }, "cf031f19c7882833a8a30348ee90175a5d8b1fb7d9645c5deb2dc68c6eb33683": { "query": "\n SELECT id FROM release_channels\n WHERE channel = $1\n ", "describe": { @@ -4022,6 +4326,26 @@ ] } }, + "d44c747044d9b1f6424e6a0eece324fd3e0964efdcb9ec7d18e70a2f67787914": { + "query": "\n SELECT DISTINCT loaders.loader FROM versions\n INNER JOIN loaders_versions lv ON lv.version_id = versions.id\n INNER JOIN loaders ON loaders.id = lv.loader_id\n WHERE versions.mod_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "loader", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + } + }, "d5b00d6237b04018822db529995f0b001cd1cabf5ca93b4aff37f12c4feb83f6": { "query": "\n INSERT INTO donation_platforms (short, name)\n VALUES ($1, $2)\n ON CONFLICT (short) DO NOTHING\n RETURNING id\n ", "describe": { @@ -4063,19 +4387,6 @@ ] } }, - "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": { @@ -4128,6 +4439,68 @@ ] } }, + "d97246f46e85cd99356468cdf36f00d86d1d576f94e4a259bf8b43cf20463f0e": { + "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 LOWER(u.username) = LOWER($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 + ] + } + }, "dc6aa2e7bfd5d5004620ddd4cd6a47ecc56159e1489054e0652d56df802fb5e5": { "query": "\n UPDATE mods\n SET body = $1\n WHERE (id = $2)\n ", "describe": { @@ -4319,14 +4692,14 @@ ] } }, - "ebf2d1fbcd12816799b60be6e8dec606eadd96edc26a840a411b44a19dc0497c": { - "query": "\n SELECT loaders.loader FROM versions\n INNER JOIN loaders_versions lv ON lv.version_id = versions.id\n INNER JOIN loaders ON loaders.id = lv.loader_id\n WHERE versions.mod_id = $1\n ", + "ebef881a0dae70e990814e567ed3de9565bb29b772782bc974c953af195fd6d7": { + "query": "\n SELECT n.id FROM notifications n\n WHERE n.user_id = $1\n ", "describe": { "columns": [ { "ordinal": 0, - "name": "loader", - "type_info": "Varchar" + "name": "id", + "type_info": "Int8" } ], "parameters": { @@ -4357,6 +4730,18 @@ ] } }, + "ef994a6ba13ac2b24cd035d5e31302f2a53a63d1baa42d0d7ac84418a82fbb9b": { + "query": "\n UPDATE mods\n SET follows = follows + 1\n WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, "f0db9d8606ccc2196a9cfafe0e7090dab42bf790f25e0469b8947fac1cf043d5": { "query": "\n SELECT version FROM game_versions\n WHERE id = $1\n ", "describe": { @@ -4422,35 +4807,6 @@ "nullable": [] } }, - "f481818cf577979ca751681155dada6ffee92ac6179e39c38f9c0442a33eac2e": { - "query": "\n INSERT INTO mods (\n id, team_id, title, description, body,\n published, downloads, icon_url, issues_url,\n source_url, wiki_url, status, 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": [] - } - }, "f7bea04e8e279e27a24de1bdf3c413daa8677994df5131494b28691ed6611efc": { "query": "\n SELECT url,expires FROM states\n WHERE id = $1\n ", "describe": { @@ -4503,6 +4859,104 @@ ] } }, + "fa5fe155fafd3b10c12fe5cf9cbbe4a4b3eb59f0f2e6584753a72517bb2d4574": { + "query": "\n SELECT m.id, m.title, m.description, m.downloads, m.follows, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.slug, m.license, m.client_side, m.server_side\n FROM mods m\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "follows", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "body_url", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "published", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "updated", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 10, + "name": "slug", + "type_info": "Varchar" + }, + { + "ordinal": 11, + "name": "license", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "client_side", + "type_info": "Int4" + }, + { + "ordinal": 13, + "name": "server_side", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + true, + false, + false, + false + ] + } + }, "fa911efc808e726c13659d3ce6baf61dc562e6f1e73fd65537a4ab1dad17120e": { "query": "\n DELETE FROM downloads\n WHERE downloads.version_id = $1\n ", "describe": { @@ -4535,6 +4989,26 @@ ] } }, + "fb955ca41b95120f66c98c0b528b1db10c4be4a55e9641bb104d772e390c9bb7": { + "query": "SELECT EXISTS(SELECT 1 FROM notifications WHERE id=$1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + null + ] + } + }, "fbd55f89e9bd8c67605f67944cd585abf8a475b83f0b926d7dbcb26478df4da0": { "query": "\n INSERT INTO dependencies (dependent_id, dependency_id, dependency_type)\n VALUES ($1, $2, $3)\n ", "describe": { diff --git a/src/database/models/ids.rs b/src/database/models/ids.rs index f61c3c191..e5875c313 100644 --- a/src/database/models/ids.rs +++ b/src/database/models/ids.rs @@ -94,6 +94,14 @@ generate_ids!( ReportId ); +generate_ids!( + pub generate_notification_id, + NotificationId, + 8, + "SELECT EXISTS(SELECT 1 FROM notifications WHERE id=$1)", + NotificationId +); + #[derive(Copy, Clone, Debug, PartialEq, Eq, Type)] #[sqlx(transparent)] pub struct UserId(pub i64); @@ -152,6 +160,13 @@ pub struct FileId(pub i64); #[sqlx(transparent)] pub struct StateId(pub i64); +#[derive(Copy, Clone, Debug, Type)] +#[sqlx(transparent)] +pub struct NotificationId(pub i64); +#[derive(Copy, Clone, Debug, Type)] +#[sqlx(transparent)] +pub struct NotificationActionId(pub i32); + use crate::models::ids; impl From for ModId { @@ -204,3 +219,13 @@ impl From for ids::ReportId { ids::ReportId(id.0 as u64) } } +impl From for NotificationId { + fn from(id: ids::NotificationId) -> Self { + NotificationId(id.0 as i64) + } +} +impl From for ids::NotificationId { + fn from(id: NotificationId) -> Self { + ids::NotificationId(id.0 as u64) + } +} diff --git a/src/database/models/mod.rs b/src/database/models/mod.rs index 2dd2e8c3e..820df48d4 100644 --- a/src/database/models/mod.rs +++ b/src/database/models/mod.rs @@ -6,6 +6,7 @@ use thiserror::Error; pub mod categories; pub mod ids; pub mod mod_item; +pub mod notification_item; pub mod report_item; pub mod team_item; pub mod user_item; diff --git a/src/database/models/mod_item.rs b/src/database/models/mod_item.rs index 793635261..93fc027b9 100644 --- a/src/database/models/mod_item.rs +++ b/src/database/models/mod_item.rs @@ -71,6 +71,7 @@ impl ModBuilder { updated: chrono::Utc::now(), status: self.status, downloads: 0, + follows: 0, icon_url: self.icon_url, issues_url: self.issues_url, source_url: self.source_url, @@ -122,6 +123,7 @@ pub struct Mod { pub updated: chrono::DateTime, pub status: StatusId, pub downloads: i32, + pub follows: i32, pub icon_url: Option, pub issues_url: Option, pub source_url: Option, @@ -153,7 +155,7 @@ impl Mod { $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, - $18 + LOWER($18) ) ", self.id as ModId, @@ -187,7 +189,7 @@ impl Mod { { let result = sqlx::query!( " - SELECT title, description, downloads, + SELECT title, description, downloads, follows, icon_url, body, body_url, published, updated, status, issues_url, source_url, wiki_url, discord_url, license_url, @@ -222,6 +224,7 @@ impl Mod { license: LicenseId(row.license), slug: row.slug, body: row.body, + follows: row.follows, })) } else { Ok(None) @@ -237,7 +240,7 @@ impl Mod { let mod_ids_parsed: Vec = mod_ids.into_iter().map(|x| x.0).collect(); let mods = sqlx::query!( " - SELECT id, title, description, downloads, + SELECT id, title, description, downloads, follows, icon_url, body, body_url, published, updated, status, issues_url, source_url, wiki_url, discord_url, license_url, @@ -270,6 +273,7 @@ impl Mod { license: LicenseId(m.license), slug: m.slug, body: m.body, + follows: m.follows, })) }) .try_collect::>() @@ -300,6 +304,26 @@ impl Mod { return Ok(None); }; + sqlx::query!( + " + DELETE FROM mod_follows + WHERE mod_id = $1 + ", + id as ModId + ) + .execute(exec) + .await?; + + sqlx::query!( + " + DELETE FROM mod_follows + WHERE mod_id = $1 + ", + id as ModId, + ) + .execute(exec) + .await?; + sqlx::query!( " DELETE FROM reports @@ -390,7 +414,7 @@ impl Mod { let id = sqlx::query!( " SELECT id FROM mods - WHERE slug = $1 + WHERE LOWER(slug) = LOWER($1) ", slug ) @@ -413,7 +437,7 @@ impl Mod { { let result = sqlx::query!( " - SELECT m.id id, m.title title, m.description description, m.downloads downloads, + SELECT m.id id, m.title title, m.description description, m.downloads downloads, m.follows follows, m.icon_url icon_url, m.body body, m.body_url body_url, m.published published, m.updated updated, m.status status, m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url, @@ -459,6 +483,7 @@ impl Mod { license: LicenseId(m.license), slug: m.slug.clone(), body: m.body.clone(), + follows: m.follows, }, categories: m .categories @@ -496,7 +521,7 @@ impl Mod { let mod_ids_parsed: Vec = mod_ids.into_iter().map(|x| x.0).collect(); sqlx::query!( " - SELECT m.id id, m.title title, m.description description, m.downloads downloads, + SELECT m.id id, m.title title, m.description description, m.downloads downloads, m.follows follows, m.icon_url icon_url, m.body body, m.body_url body_url, m.published published, m.updated updated, m.status status, m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url, @@ -540,6 +565,7 @@ impl Mod { license: LicenseId(m.license), slug: m.slug.clone(), body: m.body.clone(), + follows: m.follows }, categories: m.categories.unwrap_or_default().split(',').map(|x| x.to_string()).collect(), versions: m.versions.unwrap_or_default().split(',').map(|x| VersionId(x.parse().unwrap_or_default())).collect(), diff --git a/src/database/models/notification_item.rs b/src/database/models/notification_item.rs new file mode 100644 index 000000000..3a0c7fc53 --- /dev/null +++ b/src/database/models/notification_item.rs @@ -0,0 +1,327 @@ +use super::ids::*; +use crate::database::models::DatabaseError; + +pub struct NotificationBuilder { + pub title: String, + pub text: String, + pub link: String, + pub actions: Vec, +} + +pub struct NotificationActionBuilder { + pub title: String, + pub action_route: String, +} + +pub struct Notification { + pub id: NotificationId, + pub user_id: UserId, + pub title: String, + pub text: String, + pub link: String, + pub read: bool, + pub created: chrono::DateTime, + pub actions: Vec, +} + +pub struct NotificationAction { + pub id: NotificationActionId, + pub notification_id: NotificationId, + pub title: String, + pub action_route: String, +} + +impl NotificationBuilder { + pub async fn insert( + &self, + user: UserId, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result<(), DatabaseError> { + self.insert_many(vec![user], transaction).await + } + + pub async fn insert_many( + &self, + users: Vec, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result<(), DatabaseError> { + for user in users { + let id = generate_notification_id(&mut *transaction).await?; + + let mut actions = Vec::new(); + + for action in &self.actions { + actions.push(NotificationAction { + id: NotificationActionId(0), + notification_id: id, + title: action.title.clone(), + action_route: action.action_route.clone(), + }) + } + + Notification { + id, + user_id: user, + title: self.title.clone(), + text: self.text.clone(), + link: self.link.clone(), + read: false, + created: chrono::Utc::now(), + actions, + } + .insert(&mut *transaction) + .await?; + } + + Ok(()) + } +} + +impl Notification { + pub async fn insert( + &self, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result<(), sqlx::error::Error> { + sqlx::query!( + " + INSERT INTO notifications ( + id, user_id, title, text, link + ) + VALUES ( + $1, $2, $3, $4, $5 + ) + ", + self.id as NotificationId, + self.user_id as UserId, + &self.title, + &self.text, + &self.link + ) + .execute(&mut *transaction) + .await?; + + for action in &self.actions { + action.insert(&mut *transaction).await?; + } + + Ok(()) + } + + pub async fn get<'a, 'b, E>( + id: NotificationId, + executor: E, + ) -> Result, sqlx::error::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + SELECT n.user_id, n.title, n.text, n.link, n.created, n.read, + STRING_AGG(DISTINCT na.id || ', ' || na.title || ', ' || na.action_route, ' ,') actions + FROM notifications n + LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id + WHERE n.id = $1 + GROUP BY n.id, n.user_id; + ", + id as NotificationId, + ) + .fetch_optional(executor) + .await?; + + if let Some(row) = result { + let mut actions: Vec = Vec::new(); + + row.actions.unwrap_or_default().split(" ,").for_each(|x| { + let action: Vec<&str> = x.split(", ").collect(); + + if action.len() >= 3 { + actions.push(NotificationAction { + id: NotificationActionId(action[0].parse().unwrap_or(0)), + notification_id: id, + title: action[1].to_string(), + action_route: action[2].to_string(), + }); + } + }); + + Ok(Some(Notification { + id, + user_id: UserId(row.user_id), + title: row.title, + text: row.text, + link: row.link, + read: row.read, + created: row.created, + actions, + })) + } else { + Ok(None) + } + } + + pub async fn get_many<'a, E>( + notification_ids: Vec, + exec: E, + ) -> Result, sqlx::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + use futures::stream::TryStreamExt; + + let notification_ids_parsed: Vec = notification_ids.into_iter().map(|x| x.0).collect(); + sqlx::query!( + " + SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, + STRING_AGG(DISTINCT na.id || ', ' || na.title || ', ' || na.action_route, ' ,') actions + FROM notifications n + LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id + WHERE n.id IN (SELECT * FROM UNNEST($1::bigint[])) + GROUP BY n.id, n.user_id; + ", + ¬ification_ids_parsed + ) + .fetch_many(exec) + .try_filter_map(|e| async { + Ok(e.right().map(|row| { + let id = NotificationId(row.id); + let mut actions: Vec = Vec::new(); + + row.actions.unwrap_or_default().split(" ,").for_each(|x| { + let action: Vec<&str> = x.split(", ").collect(); + + if action.len() >= 3 { + actions.push(NotificationAction { + id: NotificationActionId(action[0].parse().unwrap_or(0)), + notification_id: id, + title: action[1].to_string(), + action_route: action[2].to_string(), + }); + } + }); + + Notification { + id, + user_id: UserId(row.user_id), + title: row.title, + text: row.text, + link: row.link, + read: row.read, + created: row.created, + actions, + } + })) + }) + .try_collect::>() + .await + } + + pub async fn get_many_user<'a, E>( + user_id: UserId, + exec: E, + ) -> Result, sqlx::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + use futures::stream::TryStreamExt; + + sqlx::query!( + " + SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, + STRING_AGG(DISTINCT na.id || ', ' || na.title || ', ' || na.action_route, ' ,') actions + FROM notifications n + LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id + WHERE n.user_id = $1 + GROUP BY n.id, n.user_id; + ", + user_id as UserId + ) + .fetch_many(exec) + .try_filter_map(|e| async { + Ok(e.right().map(|row| { + let id = NotificationId(row.id); + let mut actions: Vec = Vec::new(); + + row.actions.unwrap_or_default().split(" ,").for_each(|x| { + let action: Vec<&str> = x.split(", ").collect(); + + if action.len() >= 3 { + actions.push(NotificationAction { + id: NotificationActionId(action[0].parse().unwrap_or(0)), + notification_id: id, + title: action[1].to_string(), + action_route: action[2].to_string(), + }); + } + }); + + Notification { + id, + user_id: UserId(row.user_id), + title: row.title, + text: row.text, + link: row.link, + read: row.read, + created: row.created, + actions, + } + })) + }) + .try_collect::>() + .await + } + + pub async fn remove<'a, 'b, E>( + id: NotificationId, + exec: E, + ) -> Result, sqlx::error::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + sqlx::query!( + " + DELETE FROM notifications_actions + WHERE notification_id = $1 + ", + id as NotificationId, + ) + .execute(exec) + .await?; + + sqlx::query!( + " + DELETE FROM notifications + WHERE id = $1 + ", + id as NotificationId, + ) + .execute(exec) + .await?; + + Ok(Some(())) + } +} + +impl NotificationAction { + pub async fn insert( + &self, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result<(), sqlx::error::Error> { + sqlx::query!( + " + INSERT INTO notifications_actions ( + notification_id, title, action_route + ) + VALUES ( + $1, $2, $3 + ) + ", + self.notification_id as NotificationId, + &self.title, + &self.action_route, + ) + .execute(&mut *transaction) + .await?; + + Ok(()) + } +} diff --git a/src/database/models/user_item.rs b/src/database/models/user_item.rs index e4b0a6d9c..564b07a2b 100644 --- a/src/database/models/user_item.rs +++ b/src/database/models/user_item.rs @@ -24,7 +24,7 @@ impl User { avatar_url, bio, created ) VALUES ( - $1, $2, $3, $4, $5, + $1, $2, LOWER($3), $4, $5, $6, $7, $8 ) ", @@ -126,7 +126,7 @@ impl User { u.avatar_url, u.bio, u.created, u.role FROM users u - WHERE u.username = $1 + WHERE LOWER(u.username) = LOWER($1) ", username ) @@ -239,6 +239,29 @@ impl User { .execute(exec) .await?; + use futures::TryStreamExt; + let notifications: Vec = sqlx::query!( + " + SELECT n.id FROM notifications n + WHERE n.user_id = $1 + ", + id as UserId, + ) + .fetch_many(exec) + .try_filter_map(|e| async { Ok(e.right().map(|m| m.id as i64)) }) + .try_collect::>() + .await?; + + sqlx::query!( + " + DELETE FROM notifications + WHERE user_id = $1 + ", + id as UserId, + ) + .execute(exec) + .await?; + sqlx::query!( " DELETE FROM reports @@ -249,6 +272,26 @@ impl User { .execute(exec) .await?; + sqlx::query!( + " + DELETE FROM mod_follows + WHERE follower_id = $1 + ", + id as UserId, + ) + .execute(exec) + .await?; + + sqlx::query!( + " + DELETE FROM notifications_actions + WHERE notification_id IN (SELECT * FROM UNNEST($1::bigint[])) + ", + ¬ifications + ) + .execute(exec) + .await?; + sqlx::query!( " DELETE FROM team_members @@ -298,6 +341,38 @@ impl User { let _result = super::mod_item::Mod::remove_full(mod_id, exec).await?; } + let notifications: Vec = sqlx::query!( + " + SELECT n.id FROM notifications n + WHERE n.user_id = $1 + ", + id as UserId, + ) + .fetch_many(exec) + .try_filter_map(|e| async { Ok(e.right().map(|m| m.id as i64)) }) + .try_collect::>() + .await?; + + sqlx::query!( + " + DELETE FROM notifications + WHERE user_id = $1 + ", + id as UserId, + ) + .execute(exec) + .await?; + + sqlx::query!( + " + DELETE FROM notifications_actions + WHERE notification_id IN (SELECT * FROM UNNEST($1::bigint[])) + ", + ¬ifications + ) + .execute(exec) + .await?; + let deleted_user: UserId = crate::models::users::DELETED_USER.into(); sqlx::query!( diff --git a/src/models/ids.rs b/src/models/ids.rs index c74d7249f..5563a3702 100644 --- a/src/models/ids.rs +++ b/src/models/ids.rs @@ -1,6 +1,7 @@ use thiserror::Error; pub use super::mods::{ModId, VersionId}; +pub use super::notifications::NotificationId; pub use super::reports::ReportId; pub use super::teams::TeamId; pub use super::users::UserId; @@ -109,6 +110,7 @@ base62_id_impl!(UserId, UserId); base62_id_impl!(VersionId, VersionId); base62_id_impl!(TeamId, TeamId); base62_id_impl!(ReportId, ReportId); +base62_id_impl!(NotificationId, NotificationId); pub mod base62_impl { use serde::de::{self, Deserializer, Visitor}; diff --git a/src/models/mod.rs b/src/models/mod.rs index 238294dcf..8295104af 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,6 +1,7 @@ pub mod error; pub mod ids; pub mod mods; +pub mod notifications; pub mod reports; pub mod teams; pub mod users; diff --git a/src/models/mods.rs b/src/models/mods.rs index 66dffe12c..df607873c 100644 --- a/src/models/mods.rs +++ b/src/models/mods.rs @@ -49,6 +49,9 @@ pub struct Mod { /// The total number of downloads the mod has had. pub downloads: u32, + /// The total number of followers this mod has accumulated + pub followers: u32, + /// A list of the categories that the mod is in. pub categories: Vec, /// A list of ids for versions of the mod. diff --git a/src/models/notifications.rs b/src/models/notifications.rs new file mode 100644 index 000000000..8e5688fff --- /dev/null +++ b/src/models/notifications.rs @@ -0,0 +1,27 @@ +use super::ids::Base62Id; +use super::users::UserId; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(from = "Base62Id")] +#[serde(into = "Base62Id")] +pub struct NotificationId(pub u64); + +#[derive(Serialize, Deserialize)] +pub struct Notification { + pub id: NotificationId, + pub user_id: UserId, + pub title: String, + pub text: String, + pub link: String, + pub read: bool, + pub created: DateTime, + pub actions: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct NotificationAction { + pub title: String, + pub action_route: String, +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index ce4751dec..c3c89491f 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -6,6 +6,7 @@ mod mod_creation; mod moderation; mod mods; mod not_found; +mod notifications; mod reports; mod tags; mod teams; @@ -65,8 +66,7 @@ pub fn users_config(cfg: &mut web::ServiceConfig) { .service(users::mods_list) .service(users::user_delete) .service(users::user_edit) - .service(users::user_icon_edit) - .service(users::teams), + .service(users::user_icon_edit), ); } diff --git a/src/routes/mod_creation.rs b/src/routes/mod_creation.rs index 971653cbd..cbe67d6ee 100644 --- a/src/routes/mod_creation.rs +++ b/src/routes/mod_creation.rs @@ -299,6 +299,7 @@ async fn mod_create_inner( check_length(3..=256, "mod name", &create_data.mod_name)?; check_length(3..=2048, "mod description", &create_data.mod_description)?; + check_length(3..=64, "mod slug", &create_data.mod_slug)?; check_length(..65536, "mod body", &create_data.mod_body)?; if create_data.categories.len() > 3 { @@ -321,6 +322,12 @@ async fn mod_create_inner( if let Some(url) = &create_data.source_url { check_length(..=2048, "url", url)?; } + if let Some(url) = &create_data.discord_url { + check_length(..=2048, "url", url)?; + } + if let Some(url) = &create_data.license_url { + check_length(..=2048, "url", url)?; + } create_data .initial_versions @@ -565,6 +572,7 @@ async fn mod_create_inner( client_side: mod_create_data.client_side, server_side: mod_create_data.server_side, downloads: 0, + followers: 0, categories: mod_create_data.categories, versions: mod_builder .initial_versions diff --git a/src/routes/mods.rs b/src/routes/mods.rs index 926dcd1c1..cf46e7488 100644 --- a/src/routes/mods.rs +++ b/src/routes/mods.rs @@ -1,10 +1,10 @@ -use super::ApiError; use crate::auth::get_user_from_headers; use crate::database; use crate::file_hosting::FileHost; use crate::models; use crate::models::mods::{DonationLink, License, ModId, ModStatus, SearchRequest, SideType}; use crate::models::teams::Permissions; +use crate::routes::ApiError; use crate::search::indexing::queue::CreationQueue; use crate::search::{search_for_mod, SearchConfig, SearchError}; use actix_web::web::Data; @@ -214,6 +214,7 @@ pub fn convert_mod(data: database::models::mod_item::QueryMod) -> models::mods:: client_side: data.client_side, server_side: data.server_side, downloads: m.downloads as u32, + followers: m.follows as u32, categories: data.categories, versions: data.versions.into_iter().map(|v| v.into()).collect(), icon_url: m.icon_url, @@ -333,6 +334,12 @@ pub async fn mod_edit( )); } + if title.len() > 256 || title.len() < 3 { + return Err(ApiError::InvalidInputError( + "The mod's title must be within 3-256 characters!".to_string(), + )); + } + sqlx::query!( " UPDATE mods @@ -355,6 +362,12 @@ pub async fn mod_edit( )); } + if description.len() > 2048 || description.len() < 3 { + return Err(ApiError::InvalidInputError( + "The mod's description must be within 3-256 characters!".to_string(), + )); + } + sqlx::query!( " UPDATE mods @@ -479,6 +492,14 @@ pub async fn mod_edit( )); } + if let Some(issues) = issues_url { + if issues.len() > 2048 { + return Err(ApiError::InvalidInputError( + "The mod's issues url must be less than 2048 characters!".to_string(), + )); + } + } + sqlx::query!( " UPDATE mods @@ -501,6 +522,14 @@ pub async fn mod_edit( )); } + if let Some(source) = source_url { + if source.len() > 2048 { + return Err(ApiError::InvalidInputError( + "The mod's source url must be less than 2048 characters!".to_string(), + )); + } + } + sqlx::query!( " UPDATE mods @@ -523,6 +552,14 @@ pub async fn mod_edit( )); } + if let Some(wiki) = wiki_url { + if wiki.len() > 2048 { + return Err(ApiError::InvalidInputError( + "The mod's wiki url must be less than 2048 characters!".to_string(), + )); + } + } + sqlx::query!( " UPDATE mods @@ -545,6 +582,14 @@ pub async fn mod_edit( )); } + if let Some(license) = license_url { + if license.len() > 2048 { + return Err(ApiError::InvalidInputError( + "The mod's license url must be less than 2048 characters!".to_string(), + )); + } + } + sqlx::query!( " UPDATE mods @@ -567,6 +612,14 @@ pub async fn mod_edit( )); } + if let Some(discord) = discord_url { + if discord.len() > 2048 { + return Err(ApiError::InvalidInputError( + "The mod's discord url must be less than 2048 characters!".to_string(), + )); + } + } + sqlx::query!( " UPDATE mods @@ -589,6 +642,12 @@ pub async fn mod_edit( } if let Some(slug) = slug { + if slug.len() > 64 || slug.len() < 3 { + return Err(ApiError::InvalidInputError( + "The mod's slug must be within 3-64 characters!".to_string(), + )); + } + let slug_modid_option: Option = serde_json::from_str(&*format!("\"{}\"", slug)).ok(); if let Some(slug_modid) = slug_modid_option { @@ -614,7 +673,7 @@ pub async fn mod_edit( sqlx::query!( " UPDATE mods - SET slug = $1 + SET slug = LOWER($1) WHERE (id = $2) ", slug.as_deref(), @@ -760,6 +819,12 @@ pub async fn mod_edit( )); } + if body.len() > 65536 { + return Err(ApiError::InvalidInputError( + "The mod's body must be less than 65536 characters!".to_string(), + )); + } + sqlx::query!( " UPDATE mods @@ -921,6 +986,89 @@ pub async fn mod_delete( } } +#[get("{id}/follow")] +pub async fn mod_follow( + req: HttpRequest, + info: web::Path<(models::ids::ModId,)>, + pool: web::Data, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + let id = info.into_inner().0; + + let _result = 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()))?; + + let user_id: database::models::ids::UserId = user.id.into(); + let mod_id: database::models::ids::ModId = id.into(); + + sqlx::query!( + " + UPDATE mods + SET follows = follows + 1 + WHERE id = $1 + ", + mod_id as database::models::ids::ModId, + ) + .execute(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + sqlx::query!( + " + INSERT INTO mod_follows (follower_id, mod_id) + VALUES ($1, $2) + ", + user_id as database::models::ids::UserId, + mod_id as database::models::ids::ModId + ) + .execute(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + Ok(HttpResponse::Ok().body("")) +} + +#[delete("{id}/follow")] +pub async fn mod_unfollow( + req: HttpRequest, + info: web::Path<(models::ids::ModId,)>, + pool: web::Data, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + let id = info.into_inner().0; + + let user_id: database::models::ids::UserId = user.id.into(); + let mod_id: database::models::ids::ModId = id.into(); + + sqlx::query!( + " + UPDATE mods + SET follows = follows - 1 + WHERE id = $1 + ", + mod_id as database::models::ids::ModId, + ) + .execute(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + sqlx::query!( + " + DELETE FROM mod_follows + WHERE follower_id = $1 AND mod_id = $2 + ", + user_id as database::models::ids::UserId, + mod_id as database::models::ids::ModId + ) + .execute(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + Ok(HttpResponse::Ok().body("")) +} + pub async fn delete_from_index( id: crate::models::mods::ModId, config: web::Data, diff --git a/src/routes/notifications.rs b/src/routes/notifications.rs new file mode 100644 index 000000000..753dcf05e --- /dev/null +++ b/src/routes/notifications.rs @@ -0,0 +1,122 @@ +use crate::auth::get_user_from_headers; +use crate::database; +use crate::models::ids::NotificationId; +use crate::models::notifications::{Notification, NotificationAction}; +use crate::routes::ApiError; +use actix_web::{delete, get, web, HttpRequest, HttpResponse}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; + +#[derive(Serialize, Deserialize)] +pub struct NotificationIds { + pub ids: String, +} + +#[get("notifications")] +pub async fn notifications_get( + req: HttpRequest, + web::Query(ids): web::Query, + pool: web::Data, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + + let notification_ids = serde_json::from_str::>(&*ids.ids)? + .into_iter() + .map(|x| x.into()) + .collect(); + + let notifications_data = + database::models::notification_item::Notification::get_many(notification_ids, &**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + let mut notifications: Vec = Vec::new(); + + for notification in notifications_data { + if notification.user_id == user.id.into() || user.role.is_mod() { + notifications.push(convert_notification(notification)); + } + } + + Ok(HttpResponse::Ok().json(notifications)) +} + +#[get("{id}")] +pub async fn notification_get( + req: HttpRequest, + info: web::Path<(NotificationId,)>, + pool: web::Data, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + + let id = info.into_inner().0; + + let notification_data = + database::models::notification_item::Notification::get(id.into(), &**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + if let Some(data) = notification_data { + if user.id == data.user_id.into() || user.role.is_mod() { + Ok(HttpResponse::Ok().json(convert_notification(data))) + } else { + Ok(HttpResponse::NotFound().body("")) + } + } else { + Ok(HttpResponse::NotFound().body("")) + } +} + +pub fn convert_notification( + notif: database::models::notification_item::Notification, +) -> Notification { + Notification { + id: notif.id.into(), + user_id: notif.user_id.into(), + title: notif.title, + text: notif.text, + link: notif.link, + read: notif.read, + created: notif.created, + actions: notif + .actions + .into_iter() + .map(|x| NotificationAction { + title: x.title, + action_route: x.action_route, + }) + .collect(), + } +} + +#[delete("{id}")] +pub async fn notification_delete( + req: HttpRequest, + info: web::Path<(NotificationId,)>, + pool: web::Data, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + + let id = info.into_inner().0; + + let notification_data = + database::models::notification_item::Notification::get(id.into(), &**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + if let Some(data) = notification_data { + if data.user_id == user.id.into() || user.role.is_mod() { + database::models::notification_item::Notification::remove(id.into(), &**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + Ok(HttpResponse::Ok().body("")) + } else { + Err(ApiError::CustomAuthenticationError( + "You are not authorized to delete this notification!".to_string(), + )) + } + } else { + Ok(HttpResponse::NotFound().body("")) + } +} diff --git a/src/routes/teams.rs b/src/routes/teams.rs index d3aee4a9c..43c2d4354 100644 --- a/src/routes/teams.rs +++ b/src/routes/teams.rs @@ -1,5 +1,7 @@ use crate::auth::get_user_from_headers; +use crate::database::models::notification_item::{NotificationActionBuilder, NotificationBuilder}; use crate::database::models::TeamMember; +use crate::models::ids::ModId; use crate::models::teams::{Permissions, TeamId}; use crate::models::users::UserId; use crate::routes::ApiError; @@ -196,6 +198,39 @@ pub async fn add_team_member( .await .map_err(|e| ApiError::DatabaseError(e.into()))?; + let result = sqlx::query!( + " + SELECT m.title, m.id FROM mods m + WHERE m.team_id = $1 + ", + team_id as crate::database::models::ids::TeamId + ) + .fetch_one(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + let team: TeamId = team_id.into(); + NotificationBuilder { + title: "You have been invited to join a team!".to_string(), + text: format!( + "Team invite from {} to join the team for mod {}", + current_user.username, result.title + ), + link: format!("mod/{}", ModId(result.id as u64)), + actions: vec![ + NotificationActionBuilder { + title: "Accept".to_string(), + action_route: format!("team/{}/join", team), + }, + NotificationActionBuilder { + title: "Deny".to_string(), + action_route: format!("team/{}/members/{}", team, new_member.user_id), + }, + ], + } + .insert(new_member.user_id.into(), &mut transaction) + .await?; + transaction .commit() .await diff --git a/src/routes/users.rs b/src/routes/users.rs index e70f14eb7..0b38e1543 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -1,7 +1,10 @@ use crate::auth::get_user_from_headers; -use crate::database::models::{TeamMember, User}; +use crate::database::models::User; use crate::file_hosting::FileHost; +use crate::models::ids::NotificationId; +use crate::models::notifications::Notification; use crate::models::users::{Role, UserId}; +use crate::routes::notifications::convert_notification; use crate::routes::ApiError; use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse}; use futures::StreamExt; @@ -148,48 +151,6 @@ pub async fn mods_list( } } -#[get("{user_id}/teams")] -pub async fn teams( - req: HttpRequest, - info: web::Path<(UserId,)>, - pool: web::Data, -) -> Result { - let id: crate::database::models::UserId = info.into_inner().0.into(); - - let current_user = get_user_from_headers(req.headers(), &**pool).await.ok(); - - let results; - let mut same_user = false; - - if let Some(user) = current_user { - if user.id.0 == id.0 as u64 { - results = TeamMember::get_from_user_private(id, &**pool).await?; - same_user = true; - } else { - results = TeamMember::get_from_user_public(id, &**pool).await?; - } - } else { - results = TeamMember::get_from_user_public(id, &**pool).await?; - } - - let team_members: Vec = results - .into_iter() - .map(|data| crate::models::teams::TeamMember { - team_id: data.team_id.into(), - user_id: data.user_id.into(), - role: data.role, - permissions: if same_user { - Some(data.permissions) - } else { - None - }, - accepted: data.accepted, - }) - .collect(); - - Ok(HttpResponse::Ok().json(team_members)) -} - #[derive(Serialize, Deserialize)] pub struct EditUser { pub username: Option, @@ -463,3 +424,63 @@ pub async fn user_delete( Ok(HttpResponse::NotFound().body("")) } } + +#[get("{id}/follows")] +pub async fn user_follows( + req: HttpRequest, + info: web::Path<(UserId,)>, + pool: web::Data, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + let id = info.into_inner().0; + + if !user.role.is_mod() && user.id != id { + return Err(ApiError::CustomAuthenticationError( + "You do not have permission to see the mods this user follows!".to_string(), + )); + } + + use futures::TryStreamExt; + + let user_id: crate::database::models::UserId = id.into(); + let notifications: Vec = sqlx::query!( + " + SELECT n.id FROM notifications n + WHERE n.user_id = $1 + ", + user_id as crate::database::models::ids::UserId, + ) + .fetch_many(&**pool) + .try_filter_map(|e| async { Ok(e.right().map(|m| NotificationId(m.id as u64))) }) + .try_collect::>() + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + Ok(HttpResponse::Ok().json(notifications)) +} + +#[get("{id}/notifications")] +pub async fn user_notifications( + req: HttpRequest, + info: web::Path<(UserId,)>, + pool: web::Data, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + let id = info.into_inner().0; + + if !user.role.is_mod() && user.id != id { + return Err(ApiError::CustomAuthenticationError( + "You do not have permission to see the mods this user follows!".to_string(), + )); + } + + let notifications: Vec = + crate::database::models::notification_item::Notification::get_many_user(id.into(), &**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))? + .into_iter() + .map(convert_notification) + .collect(); + + Ok(HttpResponse::Ok().json(notifications)) +} diff --git a/src/routes/version_creation.rs b/src/routes/version_creation.rs index 520660052..0ea035bc4 100644 --- a/src/routes/version_creation.rs +++ b/src/routes/version_creation.rs @@ -1,5 +1,6 @@ use crate::auth::get_user_from_headers; use crate::database::models; +use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::version_item::{VersionBuilder, VersionFileBuilder}; use crate::file_hosting::FileHost; use crate::models::mods::{ @@ -272,6 +273,49 @@ async fn version_create_inner( let builder = version_builder .ok_or_else(|| CreateError::InvalidInput("`data` field is required".to_string()))?; + let result = sqlx::query!( + " + SELECT m.title FROM mods m + WHERE id = $1 + ", + builder.mod_id as crate::database::models::ids::ModId + ) + .fetch_one(&mut *transaction) + .await?; + + use futures::stream::TryStreamExt; + + let users = sqlx::query!( + " + SELECT follower_id FROM mod_follows + WHERE mod_id = $1 + ", + builder.mod_id as crate::database::models::ids::ModId + ) + .fetch_many(&mut *transaction) + .try_filter_map(|e| async { + Ok(e.right() + .map(|m| crate::database::models::ids::UserId(m.follower_id))) + }) + .try_collect::>() + .await?; + + let mod_id: ModId = builder.mod_id.into(); + let version_id: VersionId = builder.version_id.into(); + + NotificationBuilder { + title: "A mod you followed has been updated!".to_string(), + text: format!( + "Mod {} has been updated to version {}", + result.title, + version_data.version_number.clone() + ), + link: format!("mod/{}/version/{}", mod_id, version_id), + actions: vec![], + } + .insert_many(users, &mut *transaction) + .await?; + let response = Version { id: builder.version_id.into(), mod_id: builder.mod_id.into(), diff --git a/src/routes/versions.rs b/src/routes/versions.rs index d01043174..0f217a264 100644 --- a/src/routes/versions.rs +++ b/src/routes/versions.rs @@ -257,6 +257,12 @@ pub async fn version_edit( .map_err(|e| ApiError::DatabaseError(e.into()))?; if let Some(name) = &new_version.name { + if name.len() > 256 || name.len() < 3 { + return Err(ApiError::InvalidInputError( + "The version name must be within 3-256 characters!".to_string(), + )); + } + sqlx::query!( " UPDATE versions @@ -272,6 +278,12 @@ pub async fn version_edit( } if let Some(number) = &new_version.version_number { + if number.len() > 64 || number.is_empty() { + return Err(ApiError::InvalidInputError( + "The version number must be within 1-64 characters!".to_string(), + )); + } + sqlx::query!( " UPDATE versions @@ -432,8 +444,8 @@ pub async fn version_edit( 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 + SELECT f.id FROM files f + INNER JOIN hashes h ON h.hash = $1 AND h.algorithm = $2 ", primary_file.1.as_bytes(), primary_file.0 @@ -474,6 +486,13 @@ pub async fn version_edit( } if let Some(body) = &new_version.changelog { + if body.len() > 65536 { + return Err(ApiError::InvalidInputError( + "The version changelog must be less than 65536 characters long!" + .to_string(), + )); + } + sqlx::query!( " UPDATE versions @@ -648,13 +667,13 @@ pub async fn download_version( if !download_exists { sqlx::query!( " - INSERT INTO downloads ( - version_id, identifier - ) - VALUES ( - $1, $2 - ) - ", + INSERT INTO downloads ( + version_id, identifier + ) + VALUES ( + $1, $2 + ) + ", id.version_id, hash ) @@ -664,10 +683,10 @@ pub async fn download_version( sqlx::query!( " - UPDATE versions - SET downloads = downloads + 1 - WHERE id = $1 - ", + UPDATE versions + SET downloads = downloads + 1 + WHERE id = $1 + ", id.version_id, ) .execute(&**pool) @@ -676,10 +695,10 @@ pub async fn download_version( sqlx::query!( " - UPDATE mods - SET downloads = downloads + 1 - WHERE id = $1 - ", + UPDATE mods + SET downloads = downloads + 1 + WHERE id = $1 + ", id.mod_id, ) .execute(&**pool) diff --git a/src/search/indexing/local_import.rs b/src/search/indexing/local_import.rs index 9f6cdac26..8b8729248 100644 --- a/src/search/indexing/local_import.rs +++ b/src/search/indexing/local_import.rs @@ -15,7 +15,7 @@ pub async fn index_local(pool: PgPool) -> Result, IndexingE let mut mods = sqlx::query!( " - SELECT m.id, m.title, m.description, m.downloads, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.status, m.slug, m.license, m.client_side, m.server_side FROM mods m + SELECT m.id, m.title, m.description, m.downloads, m.follows, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.status, m.slug, m.license, m.client_side, m.server_side FROM mods m " ).fetch(&pool); @@ -55,7 +55,7 @@ pub async fn index_local(pool: PgPool) -> Result, IndexingE let loaders = sqlx::query!( " - SELECT loaders.loader FROM versions + SELECT DISTINCT loaders.loader FROM versions INNER JOIN loaders_versions lv ON lv.version_id = versions.id INNER JOIN loaders ON loaders.id = lv.loader_id WHERE versions.mod_id = $1 @@ -151,6 +151,7 @@ pub async fn index_local(pool: PgPool) -> Result, IndexingE description: mod_data.description, categories, versions, + follows: mod_data.follows, downloads: mod_data.downloads, page_url: format!("https://modrinth.com/mod/{}", mod_id), icon_url, @@ -179,7 +180,7 @@ pub async fn query_one( ) -> Result { let mod_data = sqlx::query!( " - SELECT m.id, m.title, m.description, m.downloads, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.slug, m.license, m.client_side, m.server_side + SELECT m.id, m.title, m.description, m.downloads, m.follows, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.slug, m.license, m.client_side, m.server_side FROM mods m WHERE id = $1 ", @@ -203,7 +204,7 @@ pub async fn query_one( let loaders = sqlx::query!( " - SELECT loaders.loader FROM versions + SELECT DISTINCT loaders.loader FROM versions INNER JOIN loaders_versions lv ON lv.version_id = versions.id INNER JOIN loaders ON loaders.id = lv.loader_id WHERE versions.mod_id = $1 @@ -299,6 +300,7 @@ pub async fn query_one( description: mod_data.description, categories, versions, + follows: mod_data.follows, downloads: mod_data.downloads, page_url: format!("https://modrinth.com/mod/{}", mod_id), icon_url, diff --git a/src/search/indexing/mod.rs b/src/search/indexing/mod.rs index 9e66cc5c9..1862d5daf 100644 --- a/src/search/indexing/mod.rs +++ b/src/search/indexing/mod.rs @@ -8,7 +8,7 @@ use meilisearch_sdk::client::Client; use meilisearch_sdk::indexes::Index; use meilisearch_sdk::settings::Settings; use sqlx::postgres::PgPool; -use std::collections::{HashMap, VecDeque}; +use std::collections::VecDeque; use thiserror::Error; #[derive(Error, Debug)] @@ -100,6 +100,14 @@ pub async fn reconfigure_indices(config: &SearchConfig) -> Result<(), IndexingEr }) .await?; + // Follows Index + update_index(&client, "follows_mods", { + let mut follows_rules = default_rules(); + follows_rules.push_front("desc(follows)".to_string()); + follows_rules.into() + }) + .await?; + // Updated Index update_index(&client, "updated_mods", { let mut updated_rules = default_rules(); @@ -242,6 +250,7 @@ fn default_settings() -> Settings { "categories".to_string(), "versions".to_string(), "downloads".to_string(), + "follows".to_string(), "page_url".to_string(), "icon_url".to_string(), "author_url".to_string(), @@ -262,8 +271,6 @@ fn default_settings() -> Settings { Settings::new() .with_displayed_attributes(displayed_attributes) .with_searchable_attributes(searchable_attributes) - .with_stop_words(vec![]) - .with_synonyms(HashMap::new()) .with_attributes_for_faceting(vec![ String::from("categories"), String::from("host"), diff --git a/src/search/mod.rs b/src/search/mod.rs index f52cb6be5..44612265e 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -68,6 +68,7 @@ pub struct UploadSearchMod { pub description: String, pub categories: Vec>, pub versions: Vec, + pub follows: i32, pub downloads: i32, pub page_url: String, pub icon_url: String, @@ -160,6 +161,7 @@ pub async fn search_for_mod( let index = match index { "relevance" => "relevance_mods", "downloads" => "downloads_mods", + "follows" => "follows_mods", "updated" => "updated_mods", "newest" => "newest_mods", i => return Err(SearchError::InvalidIndex(i.to_string())),