From e809f77461b134e308ae87b0f64d9292db275d66 Mon Sep 17 00:00:00 2001 From: Geometrically <18202329+Geometrically@users.noreply.github.com> Date: Tue, 6 Dec 2022 19:51:03 -0700 Subject: [PATCH] Public discord webhook (#492) --- .env | 1 + Cargo.toml | 2 +- migrations/20221206221021_webhook-sent.sql | 9 + sqlx-data.json | 1758 +++++++++++--------- src/database/models/categories.rs | 24 +- src/database/models/ids.rs | 3 +- src/database/models/project_item.rs | 16 +- src/models/projects.rs | 9 +- src/routes/mod.rs | 6 + src/routes/project_creation.rs | 5 +- src/routes/projects.rs | 32 +- src/routes/tags.rs | 4 +- src/routes/v1/mods.rs | 1 + src/routes/version_creation.rs | 5 + src/util/webhook.rs | 417 ++++- src/validate/mod.rs | 4 +- 16 files changed, 1391 insertions(+), 905 deletions(-) create mode 100644 migrations/20221206221021_webhook-sent.sql diff --git a/.env b/.env index f4dd1732a..8d033da92 100644 --- a/.env +++ b/.env @@ -9,6 +9,7 @@ LABRINTH_ADMIN_KEY=feedbeef RATE_LIMIT_IGNORE_KEY=feedbeef MODERATION_DISCORD_WEBHOOK= +PUBLIC_DISCORD_WEBHOOK= CLOUDFLARE_INTEGRATION=false DATABASE_URL=postgresql://labrinth:labrinth@localhost/labrinth diff --git a/Cargo.toml b/Cargo.toml index 9e8a7fbe7..12cbb2e1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,7 +66,7 @@ log = "0.4.17" env_logger = "0.9.1" thiserror = "1.0.37" -sqlx = { version = "0.6.2", features = ["runtime-actix-rustls", "postgres", "chrono", "offline", "macros", "migrate", "decimal"] } +sqlx = { version = "0.6.2", features = ["runtime-actix-rustls", "postgres", "chrono", "offline", "macros", "migrate", "decimal", "json"] } rust_decimal = { version = "1.26", features = ["serde-with-float", "serde-with-str"] } sentry = "0.28.0" diff --git a/migrations/20221206221021_webhook-sent.sql b/migrations/20221206221021_webhook-sent.sql new file mode 100644 index 000000000..0dbe4eaa1 --- /dev/null +++ b/migrations/20221206221021_webhook-sent.sql @@ -0,0 +1,9 @@ +-- Add migration script here +ALTER TABLE mods ADD COLUMN webhook_sent BOOL NOT NULL DEFAULT FALSE; + +UPDATE mods +SET webhook_sent = (status = 'approved'); + +UPDATE mods +SET status = 'withheld' +WHERE status = 'unlisted'; \ No newline at end of file diff --git a/sqlx-data.json b/sqlx-data.json index 73a54b573..7399f92c2 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -363,6 +363,18 @@ }, "query": "\n UPDATE mods_gallery\n SET description = $2\n WHERE id = $1\n " }, + "124fbf0544ea6989d6dc5e840405dbc76d7385276a38ad79d9093c53c73bbde2": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n UPDATE mods\n SET webhook_sent = TRUE\n WHERE id = $1\n " + }, "15b8ea323c2f6d03c2e385d9c46d7f13460764f2f106fd638226c42ae0217f75": { "describe": { "columns": [], @@ -553,231 +565,6 @@ }, "query": "\n UPDATE users\n SET avatar_url = $1\n WHERE (id = $2)\n " }, - "1b5bba3116e4ba5b00c19927a4dc0e3688e3cc737610bcaef67129f281cc78af": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "project_type", - "ordinal": 1, - "type_info": "Int4" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "description", - "ordinal": 3, - "type_info": "Varchar" - }, - { - "name": "downloads", - "ordinal": 4, - "type_info": "Int4" - }, - { - "name": "follows", - "ordinal": 5, - "type_info": "Int4" - }, - { - "name": "icon_url", - "ordinal": 6, - "type_info": "Varchar" - }, - { - "name": "body", - "ordinal": 7, - "type_info": "Varchar" - }, - { - "name": "body_url", - "ordinal": 8, - "type_info": "Varchar" - }, - { - "name": "published", - "ordinal": 9, - "type_info": "Timestamptz" - }, - { - "name": "updated", - "ordinal": 10, - "type_info": "Timestamptz" - }, - { - "name": "approved", - "ordinal": 11, - "type_info": "Timestamptz" - }, - { - "name": "status", - "ordinal": 12, - "type_info": "Varchar" - }, - { - "name": "requested_status", - "ordinal": 13, - "type_info": "Varchar" - }, - { - "name": "issues_url", - "ordinal": 14, - "type_info": "Varchar" - }, - { - "name": "source_url", - "ordinal": 15, - "type_info": "Varchar" - }, - { - "name": "wiki_url", - "ordinal": 16, - "type_info": "Varchar" - }, - { - "name": "discord_url", - "ordinal": 17, - "type_info": "Varchar" - }, - { - "name": "license_url", - "ordinal": 18, - "type_info": "Varchar" - }, - { - "name": "team_id", - "ordinal": 19, - "type_info": "Int8" - }, - { - "name": "client_side", - "ordinal": 20, - "type_info": "Int4" - }, - { - "name": "server_side", - "ordinal": 21, - "type_info": "Int4" - }, - { - "name": "license", - "ordinal": 22, - "type_info": "Varchar" - }, - { - "name": "slug", - "ordinal": 23, - "type_info": "Varchar" - }, - { - "name": "moderation_message", - "ordinal": 24, - "type_info": "Varchar" - }, - { - "name": "moderation_message_body", - "ordinal": 25, - "type_info": "Varchar" - }, - { - "name": "client_side_type", - "ordinal": 26, - "type_info": "Varchar" - }, - { - "name": "server_side_type", - "ordinal": 27, - "type_info": "Varchar" - }, - { - "name": "project_type_name", - "ordinal": 28, - "type_info": "Varchar" - }, - { - "name": "flame_anvil_project", - "ordinal": 29, - "type_info": "Int4" - }, - { - "name": "flame_anvil_user", - "ordinal": 30, - "type_info": "Int8" - }, - { - "name": "categories", - "ordinal": 31, - "type_info": "TextArray" - }, - { - "name": "versions", - "ordinal": 32, - "type_info": "TextArray" - }, - { - "name": "gallery", - "ordinal": 33, - "type_info": "TextArray" - }, - { - "name": "donations", - "ordinal": 34, - "type_info": "TextArray" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - false, - true, - false, - false, - true, - false, - true, - true, - true, - true, - true, - true, - false, - false, - false, - false, - true, - true, - true, - false, - false, - false, - true, - true, - null, - null, - null, - null - ], - "parameters": { - "Left": [ - "Int8", - "TextArray" - ] - } - }, - "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,\n m.updated updated, m.approved approved, m.status status, m.requested_status requested_status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user,\n ARRAY_AGG(DISTINCT c.category || ' |||| ' || mc.is_additional) filter (where c.category is not null) categories,\n ARRAY_AGG(DISTINCT v.id || ' |||| ' || v.date_published) filter (where v.id is not null) versions,\n ARRAY_AGG(DISTINCT mg.image_url || ' |||| ' || mg.featured || ' |||| ' || mg.created || ' |||| ' || COALESCE(mg.title, ' ') || ' |||| ' || COALESCE(mg.description, ' ')) filter (where mg.image_url is not null) gallery,\n ARRAY_AGG(DISTINCT md.joining_platform_id || ' |||| ' || dp.short || ' |||| ' || dp.name || ' |||| ' || md.url) filter (where md.joining_platform_id is not null) donations\n FROM mods m\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n LEFT JOIN mods_donations md ON md.joining_mod_id = m.id\n LEFT JOIN donation_platforms dp ON md.joining_platform_id = dp.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n LEFT JOIN versions v ON v.mod_id = m.id AND v.status = ANY($2)\n LEFT JOIN mods_gallery mg ON mg.mod_id = m.id\n WHERE m.id = $1\n GROUP BY pt.id, cs.id, ss.id, m.id;\n " - }, "1c7b0eb4341af5a7942e52f632cf582561f10b4b6a41a082fb8a60f04ac17c6e": { "describe": { "columns": [ @@ -1906,182 +1693,6 @@ }, "query": "\n INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id)\n VALUES ($1, $2)\n " }, - "463da1159ff5265f9f4a53ecac28415328e0f1677f6671f31f24aadb66773900": { - "describe": { - "columns": [ - { - "name": "project_type", - "ordinal": 0, - "type_info": "Int4" - }, - { - "name": "title", - "ordinal": 1, - "type_info": "Varchar" - }, - { - "name": "description", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "downloads", - "ordinal": 3, - "type_info": "Int4" - }, - { - "name": "follows", - "ordinal": 4, - "type_info": "Int4" - }, - { - "name": "icon_url", - "ordinal": 5, - "type_info": "Varchar" - }, - { - "name": "body", - "ordinal": 6, - "type_info": "Varchar" - }, - { - "name": "body_url", - "ordinal": 7, - "type_info": "Varchar" - }, - { - "name": "published", - "ordinal": 8, - "type_info": "Timestamptz" - }, - { - "name": "updated", - "ordinal": 9, - "type_info": "Timestamptz" - }, - { - "name": "approved", - "ordinal": 10, - "type_info": "Timestamptz" - }, - { - "name": "status", - "ordinal": 11, - "type_info": "Varchar" - }, - { - "name": "requested_status", - "ordinal": 12, - "type_info": "Varchar" - }, - { - "name": "issues_url", - "ordinal": 13, - "type_info": "Varchar" - }, - { - "name": "source_url", - "ordinal": 14, - "type_info": "Varchar" - }, - { - "name": "wiki_url", - "ordinal": 15, - "type_info": "Varchar" - }, - { - "name": "discord_url", - "ordinal": 16, - "type_info": "Varchar" - }, - { - "name": "license_url", - "ordinal": 17, - "type_info": "Varchar" - }, - { - "name": "team_id", - "ordinal": 18, - "type_info": "Int8" - }, - { - "name": "client_side", - "ordinal": 19, - "type_info": "Int4" - }, - { - "name": "server_side", - "ordinal": 20, - "type_info": "Int4" - }, - { - "name": "license", - "ordinal": 21, - "type_info": "Varchar" - }, - { - "name": "slug", - "ordinal": 22, - "type_info": "Varchar" - }, - { - "name": "moderation_message", - "ordinal": 23, - "type_info": "Varchar" - }, - { - "name": "moderation_message_body", - "ordinal": 24, - "type_info": "Varchar" - }, - { - "name": "flame_anvil_project", - "ordinal": 25, - "type_info": "Int4" - }, - { - "name": "flame_anvil_user", - "ordinal": 26, - "type_info": "Int8" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - true, - false, - true, - false, - false, - true, - false, - true, - true, - true, - true, - true, - true, - false, - false, - false, - false, - true, - true, - true, - true, - true - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT project_type, title, description, downloads, follows,\n icon_url, body, body_url, published,\n updated, approved, status, requested_status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug,\n moderation_message, moderation_message_body, flame_anvil_project,\n flame_anvil_user\n FROM mods\n WHERE id = $1\n " - }, "4778d2f5994fda2f978fa53e0840c1a9a2582ef0434a5ff7f21706f1dc4edcf4": { "describe": { "columns": [], @@ -3680,6 +3291,237 @@ }, "query": "\n UPDATE users\n SET payout_wallet = $1, payout_wallet_type = $2, payout_address = $3\n WHERE (id = $4)\n " }, + "8e82de58229da984e8414158df148c625ce23fdb41fbe42672837c36eada823f": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "project_type", + "ordinal": 1, + "type_info": "Int4" + }, + { + "name": "title", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "description", + "ordinal": 3, + "type_info": "Varchar" + }, + { + "name": "downloads", + "ordinal": 4, + "type_info": "Int4" + }, + { + "name": "follows", + "ordinal": 5, + "type_info": "Int4" + }, + { + "name": "icon_url", + "ordinal": 6, + "type_info": "Varchar" + }, + { + "name": "body", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "body_url", + "ordinal": 8, + "type_info": "Varchar" + }, + { + "name": "published", + "ordinal": 9, + "type_info": "Timestamptz" + }, + { + "name": "updated", + "ordinal": 10, + "type_info": "Timestamptz" + }, + { + "name": "approved", + "ordinal": 11, + "type_info": "Timestamptz" + }, + { + "name": "status", + "ordinal": 12, + "type_info": "Varchar" + }, + { + "name": "requested_status", + "ordinal": 13, + "type_info": "Varchar" + }, + { + "name": "issues_url", + "ordinal": 14, + "type_info": "Varchar" + }, + { + "name": "source_url", + "ordinal": 15, + "type_info": "Varchar" + }, + { + "name": "wiki_url", + "ordinal": 16, + "type_info": "Varchar" + }, + { + "name": "discord_url", + "ordinal": 17, + "type_info": "Varchar" + }, + { + "name": "license_url", + "ordinal": 18, + "type_info": "Varchar" + }, + { + "name": "team_id", + "ordinal": 19, + "type_info": "Int8" + }, + { + "name": "client_side", + "ordinal": 20, + "type_info": "Int4" + }, + { + "name": "server_side", + "ordinal": 21, + "type_info": "Int4" + }, + { + "name": "license", + "ordinal": 22, + "type_info": "Varchar" + }, + { + "name": "slug", + "ordinal": 23, + "type_info": "Varchar" + }, + { + "name": "moderation_message", + "ordinal": 24, + "type_info": "Varchar" + }, + { + "name": "moderation_message_body", + "ordinal": 25, + "type_info": "Varchar" + }, + { + "name": "client_side_type", + "ordinal": 26, + "type_info": "Varchar" + }, + { + "name": "server_side_type", + "ordinal": 27, + "type_info": "Varchar" + }, + { + "name": "project_type_name", + "ordinal": 28, + "type_info": "Varchar" + }, + { + "name": "flame_anvil_project", + "ordinal": 29, + "type_info": "Int4" + }, + { + "name": "flame_anvil_user", + "ordinal": 30, + "type_info": "Int8" + }, + { + "name": "webhook_sent", + "ordinal": 31, + "type_info": "Bool" + }, + { + "name": "categories", + "ordinal": 32, + "type_info": "TextArray" + }, + { + "name": "versions", + "ordinal": 33, + "type_info": "TextArray" + }, + { + "name": "gallery", + "ordinal": 34, + "type_info": "TextArray" + }, + { + "name": "donations", + "ordinal": 35, + "type_info": "TextArray" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + true, + false, + false, + true, + false, + true, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true, + true, + true, + false, + false, + false, + true, + true, + false, + null, + null, + null, + null + ], + "parameters": { + "Left": [ + "Int8Array", + "TextArray" + ] + } + }, + "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,\n m.updated updated, m.approved approved, m.status status, m.requested_status requested_status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user, m.webhook_sent,\n ARRAY_AGG(DISTINCT c.category || ' |||| ' || mc.is_additional) filter (where c.category is not null) categories,\n ARRAY_AGG(DISTINCT v.id || ' |||| ' || v.date_published) filter (where v.id is not null) versions,\n ARRAY_AGG(DISTINCT mg.image_url || ' |||| ' || mg.featured || ' |||| ' || mg.created || ' |||| ' || COALESCE(mg.title, ' ') || ' |||| ' || COALESCE(mg.description, ' ')) filter (where mg.image_url is not null) gallery,\n ARRAY_AGG(DISTINCT md.joining_platform_id || ' |||| ' || dp.short || ' |||| ' || dp.name || ' |||| ' || md.url) filter (where md.joining_platform_id is not null) donations\n FROM mods m\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n LEFT JOIN mods_donations md ON md.joining_mod_id = m.id\n LEFT JOIN donation_platforms dp ON md.joining_platform_id = dp.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n LEFT JOIN versions v ON v.mod_id = m.id AND v.status = ANY($2)\n LEFT JOIN mods_gallery mg ON mg.mod_id = m.id\n WHERE m.id = ANY($1)\n GROUP BY pt.id, cs.id, ss.id, m.id;\n " + }, "8e9127af96108ec5a1da1a75abcaa1e810625d89232a4e7c0dffec77896c87ba": { "describe": { "columns": [ @@ -3936,6 +3778,237 @@ }, "query": "\n SELECT id FROM mods\n WHERE slug = LOWER($1)\n " }, + "955e977dfb7173a55fbda80f070725828870efd42ac01a5cc1ced1f30d8d60fd": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "project_type", + "ordinal": 1, + "type_info": "Int4" + }, + { + "name": "title", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "description", + "ordinal": 3, + "type_info": "Varchar" + }, + { + "name": "downloads", + "ordinal": 4, + "type_info": "Int4" + }, + { + "name": "follows", + "ordinal": 5, + "type_info": "Int4" + }, + { + "name": "icon_url", + "ordinal": 6, + "type_info": "Varchar" + }, + { + "name": "body", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "body_url", + "ordinal": 8, + "type_info": "Varchar" + }, + { + "name": "published", + "ordinal": 9, + "type_info": "Timestamptz" + }, + { + "name": "updated", + "ordinal": 10, + "type_info": "Timestamptz" + }, + { + "name": "approved", + "ordinal": 11, + "type_info": "Timestamptz" + }, + { + "name": "status", + "ordinal": 12, + "type_info": "Varchar" + }, + { + "name": "requested_status", + "ordinal": 13, + "type_info": "Varchar" + }, + { + "name": "issues_url", + "ordinal": 14, + "type_info": "Varchar" + }, + { + "name": "source_url", + "ordinal": 15, + "type_info": "Varchar" + }, + { + "name": "wiki_url", + "ordinal": 16, + "type_info": "Varchar" + }, + { + "name": "discord_url", + "ordinal": 17, + "type_info": "Varchar" + }, + { + "name": "license_url", + "ordinal": 18, + "type_info": "Varchar" + }, + { + "name": "team_id", + "ordinal": 19, + "type_info": "Int8" + }, + { + "name": "client_side", + "ordinal": 20, + "type_info": "Int4" + }, + { + "name": "server_side", + "ordinal": 21, + "type_info": "Int4" + }, + { + "name": "license", + "ordinal": 22, + "type_info": "Varchar" + }, + { + "name": "slug", + "ordinal": 23, + "type_info": "Varchar" + }, + { + "name": "moderation_message", + "ordinal": 24, + "type_info": "Varchar" + }, + { + "name": "moderation_message_body", + "ordinal": 25, + "type_info": "Varchar" + }, + { + "name": "client_side_type", + "ordinal": 26, + "type_info": "Varchar" + }, + { + "name": "server_side_type", + "ordinal": 27, + "type_info": "Varchar" + }, + { + "name": "project_type_name", + "ordinal": 28, + "type_info": "Varchar" + }, + { + "name": "flame_anvil_project", + "ordinal": 29, + "type_info": "Int4" + }, + { + "name": "flame_anvil_user", + "ordinal": 30, + "type_info": "Int8" + }, + { + "name": "webhook_sent", + "ordinal": 31, + "type_info": "Bool" + }, + { + "name": "categories", + "ordinal": 32, + "type_info": "TextArray" + }, + { + "name": "versions", + "ordinal": 33, + "type_info": "TextArray" + }, + { + "name": "gallery", + "ordinal": 34, + "type_info": "TextArray" + }, + { + "name": "donations", + "ordinal": 35, + "type_info": "TextArray" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + true, + false, + false, + true, + false, + true, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true, + true, + true, + false, + false, + false, + true, + true, + false, + null, + null, + null, + null + ], + "parameters": { + "Left": [ + "Int8", + "TextArray" + ] + } + }, + "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,\n m.updated updated, m.approved approved, m.status status, m.requested_status requested_status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user, m.webhook_sent webhook_sent,\n ARRAY_AGG(DISTINCT c.category || ' |||| ' || mc.is_additional) filter (where c.category is not null) categories,\n ARRAY_AGG(DISTINCT v.id || ' |||| ' || v.date_published) filter (where v.id is not null) versions,\n ARRAY_AGG(DISTINCT mg.image_url || ' |||| ' || mg.featured || ' |||| ' || mg.created || ' |||| ' || COALESCE(mg.title, ' ') || ' |||| ' || COALESCE(mg.description, ' ')) filter (where mg.image_url is not null) gallery,\n ARRAY_AGG(DISTINCT md.joining_platform_id || ' |||| ' || dp.short || ' |||| ' || dp.name || ' |||| ' || md.url) filter (where md.joining_platform_id is not null) donations\n FROM mods m\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n LEFT JOIN mods_donations md ON md.joining_mod_id = m.id\n LEFT JOIN donation_platforms dp ON md.joining_platform_id = dp.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n LEFT JOIN versions v ON v.mod_id = m.id AND v.status = ANY($2)\n LEFT JOIN mods_gallery mg ON mg.mod_id = m.id\n WHERE m.id = $1\n GROUP BY pt.id, cs.id, ss.id, m.id;\n " + }, "96b2f4e0e619e7ed312d191dc90d64113235d72254fbda8f528ce866d1795cb5": { "describe": { "columns": [ @@ -4193,188 +4266,6 @@ }, "query": "\n UPDATE mods\n SET status = $1, approved = $2\n WHERE (id = $3)\n " }, - "a44e3e69d342d7e60e3e9e25843788011b08f3230564597f100a495cc97b9c0c": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "project_type", - "ordinal": 1, - "type_info": "Int4" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "description", - "ordinal": 3, - "type_info": "Varchar" - }, - { - "name": "downloads", - "ordinal": 4, - "type_info": "Int4" - }, - { - "name": "follows", - "ordinal": 5, - "type_info": "Int4" - }, - { - "name": "icon_url", - "ordinal": 6, - "type_info": "Varchar" - }, - { - "name": "body", - "ordinal": 7, - "type_info": "Varchar" - }, - { - "name": "body_url", - "ordinal": 8, - "type_info": "Varchar" - }, - { - "name": "published", - "ordinal": 9, - "type_info": "Timestamptz" - }, - { - "name": "updated", - "ordinal": 10, - "type_info": "Timestamptz" - }, - { - "name": "approved", - "ordinal": 11, - "type_info": "Timestamptz" - }, - { - "name": "status", - "ordinal": 12, - "type_info": "Varchar" - }, - { - "name": "requested_status", - "ordinal": 13, - "type_info": "Varchar" - }, - { - "name": "issues_url", - "ordinal": 14, - "type_info": "Varchar" - }, - { - "name": "source_url", - "ordinal": 15, - "type_info": "Varchar" - }, - { - "name": "wiki_url", - "ordinal": 16, - "type_info": "Varchar" - }, - { - "name": "discord_url", - "ordinal": 17, - "type_info": "Varchar" - }, - { - "name": "license_url", - "ordinal": 18, - "type_info": "Varchar" - }, - { - "name": "team_id", - "ordinal": 19, - "type_info": "Int8" - }, - { - "name": "client_side", - "ordinal": 20, - "type_info": "Int4" - }, - { - "name": "server_side", - "ordinal": 21, - "type_info": "Int4" - }, - { - "name": "license", - "ordinal": 22, - "type_info": "Varchar" - }, - { - "name": "slug", - "ordinal": 23, - "type_info": "Varchar" - }, - { - "name": "moderation_message", - "ordinal": 24, - "type_info": "Varchar" - }, - { - "name": "moderation_message_body", - "ordinal": 25, - "type_info": "Varchar" - }, - { - "name": "flame_anvil_project", - "ordinal": 26, - "type_info": "Int4" - }, - { - "name": "flame_anvil_user", - "ordinal": 27, - "type_info": "Int8" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - false, - true, - false, - false, - true, - false, - true, - true, - true, - true, - true, - true, - false, - false, - false, - false, - true, - true, - true, - true, - true - ], - "parameters": { - "Left": [ - "Int8Array" - ] - } - }, - "query": "\n SELECT id, project_type, title, description, downloads, follows,\n icon_url, body, body_url, published,\n updated, approved, status, requested_status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug,\n moderation_message, moderation_message_body, flame_anvil_project,\n flame_anvil_user\n FROM mods\n WHERE id = ANY($1)\n " - }, "a647c282a276b63f36d2d8a253c32d0f627cea9cab8eb1b32b39875536bdfcbb": { "describe": { "columns": [], @@ -4680,6 +4571,194 @@ }, "query": "\n DELETE FROM teams\n WHERE id = $1\n " }, + "b13eba8e7fa3863b3788efb5a9286831125c2d2ff18a0996ec8d291a3bb30520": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "project_type", + "ordinal": 1, + "type_info": "Int4" + }, + { + "name": "title", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "description", + "ordinal": 3, + "type_info": "Varchar" + }, + { + "name": "downloads", + "ordinal": 4, + "type_info": "Int4" + }, + { + "name": "follows", + "ordinal": 5, + "type_info": "Int4" + }, + { + "name": "icon_url", + "ordinal": 6, + "type_info": "Varchar" + }, + { + "name": "body", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "body_url", + "ordinal": 8, + "type_info": "Varchar" + }, + { + "name": "published", + "ordinal": 9, + "type_info": "Timestamptz" + }, + { + "name": "updated", + "ordinal": 10, + "type_info": "Timestamptz" + }, + { + "name": "approved", + "ordinal": 11, + "type_info": "Timestamptz" + }, + { + "name": "status", + "ordinal": 12, + "type_info": "Varchar" + }, + { + "name": "requested_status", + "ordinal": 13, + "type_info": "Varchar" + }, + { + "name": "issues_url", + "ordinal": 14, + "type_info": "Varchar" + }, + { + "name": "source_url", + "ordinal": 15, + "type_info": "Varchar" + }, + { + "name": "wiki_url", + "ordinal": 16, + "type_info": "Varchar" + }, + { + "name": "discord_url", + "ordinal": 17, + "type_info": "Varchar" + }, + { + "name": "license_url", + "ordinal": 18, + "type_info": "Varchar" + }, + { + "name": "team_id", + "ordinal": 19, + "type_info": "Int8" + }, + { + "name": "client_side", + "ordinal": 20, + "type_info": "Int4" + }, + { + "name": "server_side", + "ordinal": 21, + "type_info": "Int4" + }, + { + "name": "license", + "ordinal": 22, + "type_info": "Varchar" + }, + { + "name": "slug", + "ordinal": 23, + "type_info": "Varchar" + }, + { + "name": "moderation_message", + "ordinal": 24, + "type_info": "Varchar" + }, + { + "name": "moderation_message_body", + "ordinal": 25, + "type_info": "Varchar" + }, + { + "name": "flame_anvil_project", + "ordinal": 26, + "type_info": "Int4" + }, + { + "name": "flame_anvil_user", + "ordinal": 27, + "type_info": "Int8" + }, + { + "name": "webhook_sent", + "ordinal": 28, + "type_info": "Bool" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + true, + false, + false, + true, + false, + true, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true, + true, + true, + true, + true, + false + ], + "parameters": { + "Left": [ + "Int8Array" + ] + } + }, + "query": "\n SELECT id, project_type, title, description, downloads, follows,\n icon_url, body, body_url, published,\n updated, approved, status, requested_status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug,\n moderation_message, moderation_message_body, flame_anvil_project,\n flame_anvil_user, webhook_sent\n FROM mods\n WHERE id = ANY($1)\n " + }, "b1e77dbaf4b190ab361f4fa203c442e5905cef6c1a135011a59ebd6e2dc0a92a": { "describe": { "columns": [], @@ -4816,6 +4895,112 @@ }, "query": "\n DELETE FROM notifications_actions\n WHERE notification_id = ANY($1)\n " }, + "ba46da8af8591ac26f173700cdd36bdf63ed8817c58565d49ea1f1a20c7280d2": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "title", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "description", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "icon_url", + "ordinal": 3, + "type_info": "Varchar" + }, + { + "name": "slug", + "ordinal": 4, + "type_info": "Varchar" + }, + { + "name": "client_side_type", + "ordinal": 5, + "type_info": "Varchar" + }, + { + "name": "server_side_type", + "ordinal": 6, + "type_info": "Varchar" + }, + { + "name": "project_type", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "username", + "ordinal": 8, + "type_info": "Varchar" + }, + { + "name": "avatar_url", + "ordinal": 9, + "type_info": "Varchar" + }, + { + "name": "categories", + "ordinal": 10, + "type_info": "VarcharArray" + }, + { + "name": "loaders", + "ordinal": 11, + "type_info": "VarcharArray" + }, + { + "name": "versions", + "ordinal": 12, + "type_info": "Jsonb" + }, + { + "name": "all_game_versions", + "ordinal": 13, + "type_info": "Jsonb" + }, + { + "name": "gallery", + "ordinal": 14, + "type_info": "VarcharArray" + } + ], + "nullable": [ + false, + false, + false, + true, + true, + false, + false, + false, + false, + true, + null, + null, + null, + null, + null + ], + "parameters": { + "Left": [ + "Int8", + "TextArray", + "Text" + ] + } + }, + "query": "\n SELECT m.id id, m.title title, m.description description,\n m.icon_url icon_url, m.slug slug, cs.name client_side_type, ss.name server_side_type,\n pt.name project_type, u.username username, u.avatar_url avatar_url,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null) categories,\n ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,\n JSONB_AGG(DISTINCT TO_JSONB(gv)) filter (where gv.version is not null) versions,\n JSONB_AGG(DISTINCT TO_JSONB(agv)) filter (where gv.version is not null) all_game_versions,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null) gallery\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id AND mc.is_additional = FALSE\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 AND v.status != ANY($2)\n LEFT OUTER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id\n LEFT OUTER JOIN game_versions gv ON gvv.game_version_id = gv.id\n LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id\n LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id\n LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id\n LEFT OUTER JOIN game_versions agv ON 1=1\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE\n INNER JOIN users u ON tm.user_id = u.id\n WHERE m.id = $1\n GROUP BY m.id, cs.id, ss.id, pt.id, u.id;\n " + }, "bb30f376f5ea8ddb66371b937812511c1685871c1efccfca0d190bbb5103eaa7": { "describe": { "columns": [ @@ -5407,6 +5592,188 @@ }, "query": "SELECT id, mod_id FROM versions\n WHERE ((version_number = $1 OR id = $3) AND mod_id = $2)" }, + "c771a0d517feda271f5a759394834ab4ae4d9611b361bc242cd96a6e4dfae1c7": { + "describe": { + "columns": [ + { + "name": "project_type", + "ordinal": 0, + "type_info": "Int4" + }, + { + "name": "title", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "description", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "downloads", + "ordinal": 3, + "type_info": "Int4" + }, + { + "name": "follows", + "ordinal": 4, + "type_info": "Int4" + }, + { + "name": "icon_url", + "ordinal": 5, + "type_info": "Varchar" + }, + { + "name": "body", + "ordinal": 6, + "type_info": "Varchar" + }, + { + "name": "body_url", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "published", + "ordinal": 8, + "type_info": "Timestamptz" + }, + { + "name": "updated", + "ordinal": 9, + "type_info": "Timestamptz" + }, + { + "name": "approved", + "ordinal": 10, + "type_info": "Timestamptz" + }, + { + "name": "status", + "ordinal": 11, + "type_info": "Varchar" + }, + { + "name": "requested_status", + "ordinal": 12, + "type_info": "Varchar" + }, + { + "name": "issues_url", + "ordinal": 13, + "type_info": "Varchar" + }, + { + "name": "source_url", + "ordinal": 14, + "type_info": "Varchar" + }, + { + "name": "wiki_url", + "ordinal": 15, + "type_info": "Varchar" + }, + { + "name": "discord_url", + "ordinal": 16, + "type_info": "Varchar" + }, + { + "name": "license_url", + "ordinal": 17, + "type_info": "Varchar" + }, + { + "name": "team_id", + "ordinal": 18, + "type_info": "Int8" + }, + { + "name": "client_side", + "ordinal": 19, + "type_info": "Int4" + }, + { + "name": "server_side", + "ordinal": 20, + "type_info": "Int4" + }, + { + "name": "license", + "ordinal": 21, + "type_info": "Varchar" + }, + { + "name": "slug", + "ordinal": 22, + "type_info": "Varchar" + }, + { + "name": "moderation_message", + "ordinal": 23, + "type_info": "Varchar" + }, + { + "name": "moderation_message_body", + "ordinal": 24, + "type_info": "Varchar" + }, + { + "name": "flame_anvil_project", + "ordinal": 25, + "type_info": "Int4" + }, + { + "name": "flame_anvil_user", + "ordinal": 26, + "type_info": "Int8" + }, + { + "name": "webhook_sent", + "ordinal": 27, + "type_info": "Bool" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + true, + false, + true, + false, + false, + true, + false, + true, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true, + true, + true, + true, + true, + false + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT project_type, title, description, downloads, follows,\n icon_url, body, body_url, published,\n updated, approved, status, requested_status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug,\n moderation_message, moderation_message_body, flame_anvil_project,\n flame_anvil_user, webhook_sent\n FROM mods\n WHERE id = $1\n " + }, "c8a27a122160a0896914c786deef9e8193eb240501d30d5ffb4129e2103efd3d": { "describe": { "columns": [], @@ -7026,231 +7393,6 @@ }, "query": "\n UPDATE users\n SET midas_expires = $1, is_overdue = FALSE\n WHERE (stripe_customer_id = $2)\n " }, - "f831fad03729b3aea44a3b46ebd13abf50ee1f26f02512f65b8f66492b229533": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "project_type", - "ordinal": 1, - "type_info": "Int4" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "description", - "ordinal": 3, - "type_info": "Varchar" - }, - { - "name": "downloads", - "ordinal": 4, - "type_info": "Int4" - }, - { - "name": "follows", - "ordinal": 5, - "type_info": "Int4" - }, - { - "name": "icon_url", - "ordinal": 6, - "type_info": "Varchar" - }, - { - "name": "body", - "ordinal": 7, - "type_info": "Varchar" - }, - { - "name": "body_url", - "ordinal": 8, - "type_info": "Varchar" - }, - { - "name": "published", - "ordinal": 9, - "type_info": "Timestamptz" - }, - { - "name": "updated", - "ordinal": 10, - "type_info": "Timestamptz" - }, - { - "name": "approved", - "ordinal": 11, - "type_info": "Timestamptz" - }, - { - "name": "status", - "ordinal": 12, - "type_info": "Varchar" - }, - { - "name": "requested_status", - "ordinal": 13, - "type_info": "Varchar" - }, - { - "name": "issues_url", - "ordinal": 14, - "type_info": "Varchar" - }, - { - "name": "source_url", - "ordinal": 15, - "type_info": "Varchar" - }, - { - "name": "wiki_url", - "ordinal": 16, - "type_info": "Varchar" - }, - { - "name": "discord_url", - "ordinal": 17, - "type_info": "Varchar" - }, - { - "name": "license_url", - "ordinal": 18, - "type_info": "Varchar" - }, - { - "name": "team_id", - "ordinal": 19, - "type_info": "Int8" - }, - { - "name": "client_side", - "ordinal": 20, - "type_info": "Int4" - }, - { - "name": "server_side", - "ordinal": 21, - "type_info": "Int4" - }, - { - "name": "license", - "ordinal": 22, - "type_info": "Varchar" - }, - { - "name": "slug", - "ordinal": 23, - "type_info": "Varchar" - }, - { - "name": "moderation_message", - "ordinal": 24, - "type_info": "Varchar" - }, - { - "name": "moderation_message_body", - "ordinal": 25, - "type_info": "Varchar" - }, - { - "name": "client_side_type", - "ordinal": 26, - "type_info": "Varchar" - }, - { - "name": "server_side_type", - "ordinal": 27, - "type_info": "Varchar" - }, - { - "name": "project_type_name", - "ordinal": 28, - "type_info": "Varchar" - }, - { - "name": "flame_anvil_project", - "ordinal": 29, - "type_info": "Int4" - }, - { - "name": "flame_anvil_user", - "ordinal": 30, - "type_info": "Int8" - }, - { - "name": "categories", - "ordinal": 31, - "type_info": "TextArray" - }, - { - "name": "versions", - "ordinal": 32, - "type_info": "TextArray" - }, - { - "name": "gallery", - "ordinal": 33, - "type_info": "TextArray" - }, - { - "name": "donations", - "ordinal": 34, - "type_info": "TextArray" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - false, - true, - false, - false, - true, - false, - true, - true, - true, - true, - true, - true, - false, - false, - false, - false, - true, - true, - true, - false, - false, - false, - true, - true, - null, - null, - null, - null - ], - "parameters": { - "Left": [ - "Int8Array", - "TextArray" - ] - } - }, - "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,\n m.updated updated, m.approved approved, m.status status, m.requested_status requested_status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user,\n ARRAY_AGG(DISTINCT c.category || ' |||| ' || mc.is_additional) filter (where c.category is not null) categories,\n ARRAY_AGG(DISTINCT v.id || ' |||| ' || v.date_published) filter (where v.id is not null) versions,\n ARRAY_AGG(DISTINCT mg.image_url || ' |||| ' || mg.featured || ' |||| ' || mg.created || ' |||| ' || COALESCE(mg.title, ' ') || ' |||| ' || COALESCE(mg.description, ' ')) filter (where mg.image_url is not null) gallery,\n ARRAY_AGG(DISTINCT md.joining_platform_id || ' |||| ' || dp.short || ' |||| ' || dp.name || ' |||| ' || md.url) filter (where md.joining_platform_id is not null) donations\n FROM mods m\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n LEFT JOIN mods_donations md ON md.joining_mod_id = m.id\n LEFT JOIN donation_platforms dp ON md.joining_platform_id = dp.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n LEFT JOIN versions v ON v.mod_id = m.id AND v.status = ANY($2)\n LEFT JOIN mods_gallery mg ON mg.mod_id = m.id\n WHERE m.id = ANY($1)\n GROUP BY pt.id, cs.id, ss.id, m.id;\n " - }, "f8be3053274b00ee9743e798886696062009c5f681baaf29dfc24cfbbda93742": { "describe": { "columns": [ diff --git a/src/database/models/categories.rs b/src/database/models/categories.rs index d2b8c2fff..6ddca4298 100644 --- a/src/database/models/categories.rs +++ b/src/database/models/categories.rs @@ -3,6 +3,7 @@ use super::DatabaseError; use chrono::DateTime; use chrono::Utc; use futures::TryStreamExt; +use serde::Deserialize; pub struct ProjectType { pub id: ProjectTypeId, @@ -16,12 +17,13 @@ pub struct Loader { pub supported_project_types: Vec, } -#[derive(Clone)] +#[derive(Clone, Deserialize, Debug)] pub struct GameVersion { pub id: GameVersionId, pub version: String, - pub version_type: String, - pub date: DateTime, + #[serde(rename = "type")] + pub type_: String, + pub created: DateTime, pub major: bool, } @@ -507,8 +509,8 @@ impl GameVersion { .try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion { id: GameVersionId(c.id), version: c.version_, - version_type: c.type_, - date: c.created, + type_: c.type_, + created: c.created, major: c.major })) }) .try_collect::>() @@ -542,8 +544,8 @@ impl GameVersion { .try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion { id: GameVersionId(c.id), version: c.version_, - version_type: c.type_, - date: c.created, + type_: c.type_, + created: c.created, major: c.major, })) }) .try_collect::>() @@ -561,8 +563,8 @@ impl GameVersion { .try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion { id: GameVersionId(c.id), version: c.version_, - version_type: c.type_, - date: c.created, + type_: c.type_, + created: c.created, major: c.major, })) }) .try_collect::>() @@ -581,8 +583,8 @@ impl GameVersion { .try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion { id: GameVersionId(c.id), version: c.version_, - version_type: c.type_, - date: c.created, + type_: c.type_, + created: c.created, major: c.major, })) }) .try_collect::>() diff --git a/src/database/models/ids.rs b/src/database/models/ids.rs index 5d25031b3..5b85ad8c8 100644 --- a/src/database/models/ids.rs +++ b/src/database/models/ids.rs @@ -2,6 +2,7 @@ use super::DatabaseError; use crate::models::ids::base62_impl::to_base62; use crate::models::ids::random_base62_rng; use censor::Censor; +use serde::Deserialize; use sqlx::sqlx_macros::Type; const ID_RETRY_COUNT: usize = 20; @@ -136,7 +137,7 @@ pub struct DonationPlatformId(pub i32); #[derive(Copy, Clone, Debug, Type, PartialEq, Eq, Hash)] #[sqlx(transparent)] pub struct VersionId(pub i64); -#[derive(Copy, Clone, Debug, Type)] +#[derive(Copy, Clone, Debug, Type, Deserialize)] #[sqlx(transparent)] pub struct GameVersionId(pub i32); #[derive(Copy, Clone, Debug, Type)] diff --git a/src/database/models/project_item.rs b/src/database/models/project_item.rs index fba9f7c5e..0d2ae5a9e 100644 --- a/src/database/models/project_item.rs +++ b/src/database/models/project_item.rs @@ -134,6 +134,7 @@ impl ProjectBuilder { moderation_message_body: None, flame_anvil_project: None, flame_anvil_user: None, + webhook_sent: false, }; project_struct.insert(&mut *transaction).await?; @@ -211,6 +212,7 @@ pub struct Project { pub moderation_message_body: Option, pub flame_anvil_project: Option, pub flame_anvil_user: Option, + pub webhook_sent: bool, } impl Project { @@ -277,7 +279,7 @@ impl Project { issues_url, source_url, wiki_url, discord_url, license_url, team_id, client_side, server_side, license, slug, moderation_message, moderation_message_body, flame_anvil_project, - flame_anvil_user + flame_anvil_user, webhook_sent FROM mods WHERE id = $1 ", @@ -318,6 +320,7 @@ impl Project { approved: row.approved, flame_anvil_project: row.flame_anvil_project, flame_anvil_user: row.flame_anvil_user.map(UserId), + webhook_sent: row.webhook_sent, })) } else { Ok(None) @@ -343,7 +346,7 @@ impl Project { issues_url, source_url, wiki_url, discord_url, license_url, team_id, client_side, server_side, license, slug, moderation_message, moderation_message_body, flame_anvil_project, - flame_anvil_user + flame_anvil_user, webhook_sent FROM mods WHERE id = ANY($1) ", @@ -384,6 +387,7 @@ impl Project { approved: m.approved, flame_anvil_project: m.flame_anvil_project, flame_anvil_user: m.flame_anvil_user.map(UserId), + webhook_sent: m.webhook_sent, })) }) .try_collect::>() @@ -662,7 +666,7 @@ impl Project { m.updated updated, m.approved approved, m.status status, m.requested_status requested_status, m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url, m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body, - cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user, + cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user, m.webhook_sent webhook_sent, ARRAY_AGG(DISTINCT c.category || ' |||| ' || mc.is_additional) filter (where c.category is not null) categories, ARRAY_AGG(DISTINCT v.id || ' |||| ' || v.date_published) filter (where v.id is not null) versions, ARRAY_AGG(DISTINCT mg.image_url || ' |||| ' || mg.featured || ' |||| ' || mg.created || ' |||| ' || COALESCE(mg.title, ' ') || ' |||| ' || COALESCE(mg.description, ' ')) filter (where mg.image_url is not null) gallery, @@ -736,6 +740,7 @@ impl Project { approved: m.approved, flame_anvil_project: m.flame_anvil_project, flame_anvil_user: m.flame_anvil_user.map(UserId), + webhook_sent: m.webhook_sent, }, project_type: m.project_type_name, categories, @@ -848,7 +853,7 @@ impl Project { m.updated updated, m.approved approved, m.status status, m.requested_status requested_status, m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url, m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body, - cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user, + cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user, m.webhook_sent, ARRAY_AGG(DISTINCT c.category || ' |||| ' || mc.is_additional) filter (where c.category is not null) categories, ARRAY_AGG(DISTINCT v.id || ' |||| ' || v.date_published) filter (where v.id is not null) versions, ARRAY_AGG(DISTINCT mg.image_url || ' |||| ' || mg.featured || ' |||| ' || mg.created || ' |||| ' || COALESCE(mg.title, ' ') || ' |||| ' || COALESCE(mg.description, ' ')) filter (where mg.image_url is not null) gallery, @@ -925,7 +930,8 @@ impl Project { moderation_message_body: m.moderation_message_body, approved: m.approved, flame_anvil_project: m.flame_anvil_project, - flame_anvil_user: m.flame_anvil_user.map(UserId) + flame_anvil_user: m.flame_anvil_user.map(UserId), + webhook_sent: m.webhook_sent, }, project_type: m.project_type_name, categories, diff --git a/src/models/projects.rs b/src/models/projects.rs index a9b1fdcbf..df303221b 100644 --- a/src/models/projects.rs +++ b/src/models/projects.rs @@ -493,6 +493,7 @@ impl From for Version { /// A status decides the visibility of a project in search, URLs, and the whole site itself. /// Listed - Version is displayed on project, and accessible by URL +/// Archived - Identical to listed but has a message displayed stating version is unsupported /// Draft - Version is not displayed on project, and not accessible by URL /// Unlisted - Version is not displayed on project, and accessible by URL /// Scheduled - Version is scheduled to be released in the future @@ -500,6 +501,7 @@ impl From for Version { #[serde(rename_all = "lowercase")] pub enum VersionStatus { Listed, + Archived, Draft, Unlisted, Scheduled, @@ -518,12 +520,14 @@ impl VersionStatus { "listed" => VersionStatus::Listed, "draft" => VersionStatus::Draft, "unlisted" => VersionStatus::Unlisted, + "scheduled" => VersionStatus::Scheduled, _ => VersionStatus::Unknown, } } pub fn as_str(&self) -> &'static str { match self { VersionStatus::Listed => "listed", + VersionStatus::Archived => "archived", VersionStatus::Draft => "draft", VersionStatus::Unlisted => "unlisted", VersionStatus::Unknown => "unknown", @@ -534,6 +538,7 @@ impl VersionStatus { pub fn iterator() -> impl Iterator { [ VersionStatus::Listed, + VersionStatus::Archived, VersionStatus::Draft, VersionStatus::Unlisted, VersionStatus::Scheduled, @@ -547,6 +552,7 @@ impl VersionStatus { pub fn is_hidden(&self) -> bool { match self { VersionStatus::Listed => false, + VersionStatus::Archived => false, VersionStatus::Unlisted => false, VersionStatus::Draft => true, @@ -557,13 +563,14 @@ impl VersionStatus { // Whether version is listed on project / returned in aggregate routes pub fn is_listed(&self) -> bool { - matches!(self, VersionStatus::Listed) + matches!(self, VersionStatus::Listed | VersionStatus::Archived) } // Whether a version status can be requested pub fn can_be_requested(&self) -> bool { match self { VersionStatus::Listed => true, + VersionStatus::Archived => true, VersionStatus::Draft => true, VersionStatus::Unlisted => true, VersionStatus::Scheduled => false, diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 54184d299..4ec16818a 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -226,6 +226,8 @@ pub enum ApiError { Crypto(String), #[error("Payments Error: {0}")] Payments(String), + #[error("Discord Error: {0}")] + DiscordError(String), } impl actix_web::ResponseError for ApiError { @@ -272,6 +274,9 @@ impl actix_web::ResponseError for ApiError { ApiError::Payments(..) => { actix_web::http::StatusCode::FAILED_DEPENDENCY } + ApiError::DiscordError(..) => { + actix_web::http::StatusCode::FAILED_DEPENDENCY + } } } @@ -294,6 +299,7 @@ impl actix_web::ResponseError for ApiError { ApiError::Analytics(..) => "analytics_error", ApiError::Crypto(..) => "crypto_error", ApiError::Payments(..) => "payments_error", + ApiError::DiscordError(..) => "discord_error", }, description: &self.to_string(), }, diff --git a/src/routes/project_creation.rs b/src/routes/project_creation.rs index 42762dc22..21ad7ed3e 100644 --- a/src/routes/project_creation.rs +++ b/src/routes/project_creation.rs @@ -277,6 +277,7 @@ pub async fn project_create( &***file_host, &flame_anvil_queue, &mut uploaded_files, + &*client, ) .await; @@ -334,6 +335,7 @@ pub async fn project_create_inner( file_host: &dyn FileHost, flame_anvil_queue: &Mutex, uploaded_files: &mut Vec, + pool: &PgPool, ) -> Result { // The base URL for files uploaded to backblaze let cdn_url = dotenvy::var("CDN_URL")?; @@ -817,7 +819,8 @@ pub async fn project_create_inner( if let Ok(webhook_url) = dotenvy::var("MODERATION_DISCORD_WEBHOOK") { crate::util::webhook::send_discord_webhook( - response.clone(), + response.id, + pool, webhook_url, ) .await diff --git a/src/routes/projects.rs b/src/routes/projects.rs index b48409073..a8db6ad81 100644 --- a/src/routes/projects.rs +++ b/src/routes/projects.rs @@ -499,7 +499,8 @@ pub async fn project_edit( dotenvy::var("MODERATION_DISCORD_WEBHOOK") { crate::util::webhook::send_discord_webhook( - Project::from(project_item.clone()), + project_item.inner.id.into(), + &*pool, webhook_url, ) .await @@ -507,7 +508,9 @@ pub async fn project_edit( } } - if status.is_approved() { + if status.is_approved() + && !project_item.inner.status.is_approved() + { sqlx::query!( " UPDATE mods @@ -520,6 +523,31 @@ pub async fn project_edit( .await?; } + if status.is_searchable() && !project_item.inner.webhook_sent { + if let Ok(webhook_url) = + dotenvy::var("PUBLIC_DISCORD_WEBHOOK") + { + crate::util::webhook::send_discord_webhook( + project_item.inner.id.into(), + &*pool, + webhook_url, + ) + .await + .ok(); + + sqlx::query!( + " + UPDATE mods + SET webhook_sent = TRUE + WHERE id = $1 + ", + id as database::models::ids::ProjectId, + ) + .execute(&mut *transaction) + .await?; + } + } + sqlx::query!( " UPDATE mods diff --git a/src/routes/tags.rs b/src/routes/tags.rs index 9dfc6148a..d0f1b22ae 100644 --- a/src/routes/tags.rs +++ b/src/routes/tags.rs @@ -231,8 +231,8 @@ pub async fn game_version_list( .into_iter() .map(|x| GameVersionQueryData { version: x.version, - version_type: x.version_type, - date: x.date, + version_type: x.type_, + date: x.created, major: x.major, }) .collect(); diff --git a/src/routes/v1/mods.rs b/src/routes/v1/mods.rs index d331918ff..596c32abf 100644 --- a/src/routes/v1/mods.rs +++ b/src/routes/v1/mods.rs @@ -133,6 +133,7 @@ pub async fn mod_create( &***file_host, &flame_anvil_queue, &mut uploaded_files, + &*client, ) .await; diff --git a/src/routes/version_creation.rs b/src/routes/version_creation.rs index d6de13a49..177ed6418 100644 --- a/src/routes/version_creation.rs +++ b/src/routes/version_creation.rs @@ -29,6 +29,10 @@ use std::sync::Arc; use tokio::sync::Mutex; use validator::Validate; +fn default_requested_status() -> VersionStatus { + VersionStatus::Listed +} + #[derive(Serialize, Deserialize, Validate, Clone)] pub struct InitialVersionData { #[serde(alias = "mod_id")] @@ -59,6 +63,7 @@ pub struct InitialVersionData { pub loaders: Vec, pub featured: bool, pub primary_file: Option, + #[serde(default = "default_requested_status")] pub status: VersionStatus, } diff --git a/src/util/webhook.rs b/src/util/webhook.rs index 3744a6f50..f72731346 100644 --- a/src/util/webhook.rs +++ b/src/util/webhook.rs @@ -1,16 +1,30 @@ -use crate::models::projects::Project; +use crate::database::models::categories::GameVersion; +use crate::models::projects::ProjectId; +use crate::routes::ApiError; use chrono::{DateTime, Utc}; use serde::Serialize; +use sqlx::PgPool; +use std::usize; #[derive(Serialize)] struct DiscordEmbed { + pub author: Option, pub title: String, pub description: String, pub url: String, pub timestamp: DateTime, pub color: u32, pub fields: Vec, - pub image: DiscordEmbedImage, + pub thumbnail: DiscordEmbedThumbnail, + pub image: Option, + pub footer: Option, +} + +#[derive(Serialize)] +struct DiscordEmbedAuthor { + pub name: String, + pub url: Option, + pub icon_url: Option, } #[derive(Serialize)] @@ -25,83 +39,344 @@ struct DiscordEmbedImage { pub url: Option, } +#[derive(Serialize)] +struct DiscordEmbedThumbnail { + pub url: Option, +} + +#[derive(Serialize)] +struct DiscordEmbedFooter { + pub text: String, + pub icon_url: Option, +} + #[derive(Serialize)] struct DiscordWebhook { + pub avatar_url: Option, + pub username: Option, pub embeds: Vec, } pub async fn send_discord_webhook( - project: Project, + project_id: ProjectId, + pool: &PgPool, webhook_url: String, -) -> Result<(), reqwest::Error> { - let mut fields = vec![ - DiscordEmbedField { - name: "id", - value: project.id.to_string(), - inline: true, - }, - DiscordEmbedField { - name: "project_type", - value: project.project_type.clone(), - inline: true, - }, - DiscordEmbedField { - name: "client_side", - value: project.client_side.to_string(), - inline: true, - }, - DiscordEmbedField { - name: "server_side", - value: project.server_side.to_string(), - inline: true, - }, - ]; - - if !project.categories.is_empty() { - fields.push(DiscordEmbedField { - name: "categories", - value: project.categories.join(", "), - inline: true, - }); - } - - if let Some(ref slug) = project.slug { - fields.push(DiscordEmbedField { - name: "slug", - value: slug.clone(), - inline: true, - }); - } - - let embed = DiscordEmbed { - url: format!( - "{}/{}/{}", - dotenvy::var("SITE_URL").unwrap_or_default(), - project.project_type, - project - .clone() - .slug - .unwrap_or_else(|| project.id.to_string()) - ), - title: project.title, - description: project.description, - timestamp: project.published, - color: 0x1bd96a, - fields, - image: DiscordEmbedImage { - url: project.icon_url, - }, - }; - - let client = reqwest::Client::new(); - - client - .post(&webhook_url) - .json(&DiscordWebhook { - embeds: vec![embed], - }) - .send() +) -> Result<(), ApiError> { + let row = + sqlx::query!( + " + SELECT m.id id, m.title title, m.description description, + m.icon_url icon_url, m.slug slug, cs.name client_side_type, ss.name server_side_type, + pt.name project_type, u.username username, u.avatar_url avatar_url, + ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null) categories, + ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders, + JSONB_AGG(DISTINCT TO_JSONB(gv)) filter (where gv.version is not null) versions, + JSONB_AGG(DISTINCT TO_JSONB(agv)) filter (where gv.version is not null) all_game_versions, + ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null) gallery + FROM mods m + LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id AND mc.is_additional = FALSE + LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id + LEFT OUTER JOIN versions v ON v.mod_id = m.id AND v.status != ANY($2) + LEFT OUTER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id + LEFT OUTER JOIN game_versions gv ON gvv.game_version_id = gv.id + LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id + LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id + LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id + LEFT OUTER JOIN game_versions agv ON 1=1 + INNER JOIN project_types pt ON pt.id = m.project_type + INNER JOIN side_types cs ON m.client_side = cs.id + INNER JOIN side_types ss ON m.server_side = ss.id + INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE + INNER JOIN users u ON tm.user_id = u.id + WHERE m.id = $1 + GROUP BY m.id, cs.id, ss.id, pt.id, u.id; + ", + project_id.0 as i64, + &*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::>(), + crate::models::teams::OWNER_ROLE, + ) + .fetch_optional(&*pool) .await?; + if let Some(project) = row { + let mut fields = vec![]; + + let categories = project.categories.unwrap_or_default(); + let loaders = project.loaders.unwrap_or_default(); + + let versions: Vec = + serde_json::from_value(project.versions.unwrap_or_default()) + .map_err(|err| { + ApiError::DiscordError( + "Error while sending projects webhook".to_string(), + ) + })?; + let all_game_versions: Vec = serde_json::from_value( + project.all_game_versions.unwrap_or_default(), + ) + .map_err(|err| { + ApiError::DiscordError( + "Error while sending projects webhook".to_string(), + ) + })?; + + if !categories.is_empty() { + fields.push(DiscordEmbedField { + name: "Categories", + value: categories + .into_iter() + .map(|mut x| format!("{}{x}", x.remove(0).to_uppercase())) + .collect::>() + .join("\n"), + inline: true, + }); + } + + if !loaders.is_empty() { + let mut formatted_loaders: String = String::new(); + + for loader in loaders { + let emoji_id: i64 = match &*loader { + "bukkit" => 1049793345481883689, + "bungeecord" => 1049793347067314220, + "fabric" => 1049793348719890532, + "forge" => 1049793350498275358, + "liteloader" => 1049793351630733333, + "minecraft" => 1049793352964526100, + "modloader" => 1049793353962762382, + "paper" => 1049793355598540810, + "purpur" => 1049793357351751772, + "quilt" => 1049793857681887342, + "rift" => 1049793359373414502, + "spigot" => 1049793413886779413, + "sponge" => 1049793416969605231, + "velocity" => 1049793419108700170, + "waterfall" => 1049793420937412638, + _ => 1049805243866681424, + }; + + let mut x = loader.clone(); + formatted_loaders.push_str(&format!( + "<:{loader}:{emoji_id}> {}{x}\n", + x.remove(0).to_uppercase() + )); + } + + fields.push(DiscordEmbedField { + name: "Loaders", + value: formatted_loaders, + inline: true, + }); + } + + if !versions.is_empty() { + let mut formatted_game_versions: String = + get_gv_range(versions, all_game_versions); + + fields.push(DiscordEmbedField { + name: "Versions", + value: formatted_game_versions, + inline: true, + }); + } + + let embed = DiscordEmbed { + author: Some(DiscordEmbedAuthor { + name: project.username.clone(), + url: Some(format!( + "{}/user/{}", + dotenvy::var("SITE_URL").unwrap_or_default(), + project.username + )), + icon_url: project.avatar_url, + }), + url: format!( + "{}/{}/{}", + dotenvy::var("SITE_URL").unwrap_or_default(), + project.project_type, + project.slug.unwrap_or_else(|| project_id.to_string()) + ), + title: project.title, + description: project.description, + timestamp: Utc::now(), + color: 0x1bd96a, + fields, + thumbnail: DiscordEmbedThumbnail { + url: project.icon_url, + }, + image: project.gallery.unwrap_or_default().first().map(|x| { + DiscordEmbedImage { + url: Some(x.to_string()), + } + }), + footer: Some(DiscordEmbedFooter { + text: "Modrinth".to_string(), + icon_url: Some( + "https://cdn-raw.modrinth.com/modrinth-new.png".to_string(), + ), + }), + }; + + let client = reqwest::Client::new(); + + client + .post(&webhook_url) + .json(&DiscordWebhook { + avatar_url: Some( + "https://cdn.modrinth.com/Modrinth_Dark_Logo.png" + .to_string(), + ), + username: Some("Modrinth Release".to_string()), + embeds: vec![embed], + }) + .send() + .await + .map_err(|err| { + ApiError::DiscordError( + "Error while sending projects webhook".to_string(), + ) + })?; + } + Ok(()) } + +fn get_gv_range( + mut game_versions: Vec, + mut all_game_versions: Vec, +) -> String { + // both -> least to greatest + game_versions.sort_by(|a, b| a.created.cmp(&b.created)); + + all_game_versions.sort_by(|a, b| a.created.cmp(&b.created)); + + let all_releases = all_game_versions + .iter() + .filter(|x| &*x.type_ == "release") + .cloned() + .collect::>(); + + let mut intervals = Vec::new(); + let mut current_interval = 0; + + const MAX_VALUE: usize = 1000000; + + for i in 0..game_versions.len() { + let current_version = &*game_versions[i].version; + + let index = all_game_versions + .iter() + .position(|x| &*x.version == current_version) + .unwrap_or(MAX_VALUE); + let release_index = all_releases + .iter() + .position(|x| &*x.version == current_version) + .unwrap_or(MAX_VALUE); + + if i == 0 { + intervals.push(vec![vec![i, index, release_index]]) + } else { + let interval_base = &intervals[current_interval]; + + if ((index as i32) + - (interval_base[interval_base.len() - 1][1] as i32) + == 1 + || (release_index as i32) + - (interval_base[interval_base.len() - 1][2] as i32) + == 1) + && (all_game_versions[interval_base[0][1]].type_ == "release" + || all_game_versions[index].type_ != "release") + { + if intervals[current_interval].get(1).is_some() { + intervals[current_interval][1] = + vec![i, index, release_index]; + } else { + intervals[current_interval] + .insert(1, vec![i, index, release_index]); + } + } else { + current_interval += 1; + intervals.push(vec![vec![i, index, release_index]]); + } + } + } + + let mut new_intervals = Vec::new(); + + for interval in intervals { + if interval.len() == 2 + && interval[0][2] != MAX_VALUE + && interval[1][2] == MAX_VALUE + { + let mut last_snapshot: Option = None; + + for j in ((interval[0][1] + 1)..=interval[1][1]).rev() { + if all_game_versions[j].type_ == "release" { + new_intervals.push(vec![ + interval[0].clone(), + vec![ + game_versions + .iter() + .position(|x| { + x.version == all_game_versions[j].version + }) + .unwrap_or(MAX_VALUE), + j, + all_releases + .iter() + .position(|x| { + x.version == all_game_versions[j].version + }) + .unwrap_or(MAX_VALUE), + ], + ]); + + if let Some(last_snapshot) = last_snapshot { + if last_snapshot != j + 1 { + new_intervals.push(vec![ + vec![ + game_versions + .iter() + .position(|x| { + x.version + == all_game_versions + [last_snapshot] + .version + }) + .unwrap_or(MAX_VALUE), + last_snapshot, + MAX_VALUE, + ], + interval[1].clone(), + ]) + } + } else { + new_intervals.push(vec![interval[1].clone()]) + } + + break; + } else { + last_snapshot = Some(j); + } + } + } else { + new_intervals.push(interval); + } + } + + let mut output = Vec::new(); + + for interval in new_intervals { + if interval.len() == 2 { + output.push(format!( + "{}—{}", + &game_versions[interval[0][0]].version, + &game_versions[interval[1][0]].version + )) + } else { + output.push(game_versions[interval[0][0]].version.clone()) + } + } + + output.join("\n") +} diff --git a/src/validate/mod.rs b/src/validate/mod.rs index dc80f77f5..3e6cb41b9 100644 --- a/src/validate/mod.rs +++ b/src/validate/mod.rs @@ -151,7 +151,7 @@ fn game_version_supported( all_game_versions .iter() .find(|y| y.version == x.0) - .map(|x| x.date > date) + .map(|x| x.created > date) .unwrap_or(false) }) } @@ -160,7 +160,7 @@ fn game_version_supported( all_game_versions .iter() .find(|y| y.version == x.0) - .map(|x| x.date > before && x.date < after) + .map(|x| x.created > before && x.created < after) .unwrap_or(false) }) }