From 95ae981698606f5966ee204917d6742d2d9a742d Mon Sep 17 00:00:00 2001 From: Geometrically <18202329+Geometrically@users.noreply.github.com> Date: Sat, 15 Apr 2023 19:48:21 -0700 Subject: [PATCH] Overhaul notifs + threads fixes (#573) * Overhaul notifs + threads fixes * fix lang --- migrations/20230414203933_threads-fix.sql | 10 + sqlx-data.json | 757 ++++++++++------------ src/database/models/notification_item.rs | 133 ++-- src/database/models/thread_item.rs | 27 +- src/models/notifications.rs | 167 ++++- src/models/threads.rs | 5 + src/routes/v2/project_creation.rs | 2 + src/routes/v2/projects.rs | 24 +- src/routes/v2/reports.rs | 11 +- src/routes/v2/teams.rs | 39 +- src/routes/v2/threads.rs | 134 +++- src/routes/v2/version_creation.rs | 29 +- 12 files changed, 719 insertions(+), 619 deletions(-) create mode 100644 migrations/20230414203933_threads-fix.sql diff --git a/migrations/20230414203933_threads-fix.sql b/migrations/20230414203933_threads-fix.sql new file mode 100644 index 00000000..20acc02d --- /dev/null +++ b/migrations/20230414203933_threads-fix.sql @@ -0,0 +1,10 @@ +ALTER TABLE threads ADD COLUMN show_in_mod_inbox BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE threads_messages DROP COLUMN show_in_mod_inbox; + +ALTER TABLE notifications ADD COLUMN body jsonb NULL; +ALTER TABLE notifications ALTER COLUMN title DROP NOT NULL; +ALTER TABLE notifications ALTER COLUMN text DROP NOT NULL; +ALTER TABLE notifications ALTER COLUMN link DROP NOT NULL; + +ALTER TABLE threads ADD COLUMN report_id bigint REFERENCES reports ON UPDATE CASCADE; +ALTER TABLE threads ADD COLUMN project_id bigint REFERENCES mods ON UPDATE CASCADE; \ No newline at end of file diff --git a/sqlx-data.json b/sqlx-data.json index 8194f929..1809fe09 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -287,56 +287,6 @@ }, "query": "\n INSERT INTO files (id, version_id, url, filename, is_primary, size, file_type)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n " }, - "0eee1b0969f3ee800ccf5105d878c8c417f09bdced6bbbefcdc339534d1baf2a": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "author_id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "thread_id", - "ordinal": 2, - "type_info": "Int8" - }, - { - "name": "body", - "ordinal": 3, - "type_info": "Jsonb" - }, - { - "name": "created", - "ordinal": 4, - "type_info": "Timestamptz" - }, - { - "name": "show_in_mod_inbox", - "ordinal": 5, - "type_info": "Bool" - } - ], - "nullable": [ - false, - true, - false, - false, - false, - false - ], - "parameters": { - "Left": [ - "Int8Array" - ] - } - }, - "query": "\n SELECT tm.id, tm.author_id, tm.thread_id, tm.body, tm.created, tm.show_in_mod_inbox\n FROM threads_messages tm\n WHERE tm.id = ANY($1)\n " - }, "0f0244e77f60e69b3ab1320265749656e25da0b021b3df9013a2da470dbc8d46": { "describe": { "columns": [], @@ -410,23 +360,6 @@ }, "query": "\n SELECT EXISTS(SELECT 1 FROM mod_follows mf WHERE mf.follower_id = $1 AND mf.mod_id = $2)\n " }, - "114df19aa81498b77022bd7347dd4449c7cc48efdab19003bde62c2f2f837d3c": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Varchar", - "Varchar", - "Varchar", - "Varchar" - ] - } - }, - "query": "\n INSERT INTO notifications (\n id, user_id, title, text, link, type\n )\n VALUES (\n $1, $2, $3, $4, $5, $6\n )\n " - }, "1209ffc1ffbea89f7060573275dc7325ac4d7b4885b6c1d1ec92998e6012e455": { "describe": { "columns": [], @@ -695,74 +628,6 @@ }, "query": "\n UPDATE users\n SET email = $1\n WHERE (id = $2)\n " }, - "1762798f2b3221292a0152723beda6b4888cf2d15793cca13c3bebc320b2f23e": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "user_id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "text", - "ordinal": 3, - "type_info": "Varchar" - }, - { - "name": "link", - "ordinal": 4, - "type_info": "Varchar" - }, - { - "name": "created", - "ordinal": 5, - "type_info": "Timestamptz" - }, - { - "name": "read", - "ordinal": 6, - "type_info": "Bool" - }, - { - "name": "notification_type", - "ordinal": 7, - "type_info": "Varchar" - }, - { - "name": "actions", - "ordinal": 8, - "type_info": "Jsonb" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - true, - null - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type,\n JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'title', na.title, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) 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 " - }, "1931ff3846345c0af4e15c3a84dcbfc7c9cbb92c98d2e73634f611a1e5358c7a": { "describe": { "columns": [ @@ -1197,6 +1062,19 @@ }, "query": "SELECT EXISTS(SELECT 1 FROM versions WHERE id=$1)" }, + "2007ac2b16a1d3d8fd053d962ba8548613535255fa197059e86959adf372948d": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Jsonb" + ] + } + }, + "query": "\n UPDATE threads_messages\n SET body = $2\n WHERE id = $1\n " + }, "20413fce27fe9c1dec71900f9563e787acc11e7789b5294786e0ea6f20d7d958": { "describe": { "columns": [], @@ -1661,6 +1539,50 @@ }, "query": "DELETE FROM banned_users WHERE github_id = $1;" }, + "320d73cd900a6e00f0e74b7a8c34a7658d16034b01a35558cb42fa9c16185eb5": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "author_id", + "ordinal": 1, + "type_info": "Int8" + }, + { + "name": "thread_id", + "ordinal": 2, + "type_info": "Int8" + }, + { + "name": "body", + "ordinal": 3, + "type_info": "Jsonb" + }, + { + "name": "created", + "ordinal": 4, + "type_info": "Timestamptz" + } + ], + "nullable": [ + false, + true, + false, + false, + false + ], + "parameters": { + "Left": [ + "Int8Array" + ] + } + }, + "query": "\n SELECT tm.id, tm.author_id, tm.thread_id, tm.body, tm.created\n FROM threads_messages tm\n WHERE tm.id = ANY($1)\n " + }, "33a965c7dc615d3b701c05299889357db8dd36d378850625d2602ba471af4885": { "describe": { "columns": [], @@ -2145,6 +2067,62 @@ }, "query": "\n SELECT gv.id id, gv.version version_, gv.type type_, gv.created created, gv.major major FROM game_versions gv\n WHERE major = $1 AND type = $2\n ORDER BY created DESC\n " }, + "447c21dd66f9ac85f9f9d39402d2ceb8440d5951e2c7277d329b65412d330f4e": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "thread_type", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "show_in_mod_inbox", + "ordinal": 2, + "type_info": "Bool" + }, + { + "name": "project_id", + "ordinal": 3, + "type_info": "Int8" + }, + { + "name": "report_id", + "ordinal": 4, + "type_info": "Int8" + }, + { + "name": "members", + "ordinal": 5, + "type_info": "Int8Array" + }, + { + "name": "messages", + "ordinal": 6, + "type_info": "Jsonb" + } + ], + "nullable": [ + false, + false, + false, + true, + true, + null, + null + ], + "parameters": { + "Left": [ + "Int8Array" + ] + } + }, + "query": "\n SELECT t.id, t.thread_type, t.show_in_mod_inbox, t.project_id, t.report_id,\n ARRAY_AGG(DISTINCT tm.user_id) filter (where tm.user_id is not null) members,\n JSONB_AGG(DISTINCT jsonb_build_object('id', tmsg.id, 'author_id', tmsg.author_id, 'thread_id', tmsg.thread_id, 'body', tmsg.body, 'created', tmsg.created)) filter (where tmsg.id is not null) messages\n FROM threads t\n LEFT OUTER JOIN threads_messages tmsg ON tmsg.thread_id = t.id\n LEFT OUTER JOIN threads_members tm ON tm.thread_id = t.id\n WHERE t.id = ANY($1)\n GROUP BY t.id\n " + }, "4567790f0dc98ff20b596a33161d1f6ac8af73da67fe8c54192724626c6bf670": { "describe": { "columns": [], @@ -2203,6 +2181,19 @@ }, "query": "\n SELECT d.dependency_id, COALESCE(vd.mod_id, 0) mod_id, d.mod_dependency_id\n FROM versions v\n INNER JOIN dependencies d ON d.dependent_id = v.id\n LEFT JOIN versions vd ON d.dependency_id = vd.id\n WHERE v.mod_id = $1\n " }, + "49813a96f007216072d69468aae705d73d5b85dcdd64a22060009b12d947ed5a": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Bool", + "Int8" + ] + } + }, + "query": "\n UPDATE threads\n SET show_in_mod_inbox = $1\n WHERE id = $2\n " + }, "49a5d21a1454afc6383b78e468fd0decc75b9163e7286f34ceab22d563a0d3f7": { "describe": { "columns": [], @@ -2326,18 +2317,6 @@ }, "query": "\n SELECT id FROM mods\n WHERE id = $1\n " }, - "4d093a0f6c87e07f1db3889c05d961c0ead1391fe3e7e9d770de271edee53eb5": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n DELETE FROM threads_messages\n WHERE id = $1\n " - }, "4d54032b02c860f4facec39eacb4548a0701d4505e7a80b4834650696df69c2b": { "describe": { "columns": [], @@ -2530,53 +2509,25 @@ }, "query": "\n UPDATE versions\n SET version_number = $1\n WHERE (id = $2)\n " }, - "59859b611ac75b266671daacee0ae3eb0eff1808a046dd0c7b70154f590c28eb": { + "599a7966e054d7892c6c48c6f303872bb51f2b5eb387a3967bf8aebb5d33f627": { "describe": { "columns": [ { "name": "id", "ordinal": 0, "type_info": "Int8" - }, - { - "name": "thread_id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "author_id", - "ordinal": 2, - "type_info": "Int8" - }, - { - "name": "body", - "ordinal": 3, - "type_info": "Jsonb" - }, - { - "name": "created", - "ordinal": 4, - "type_info": "Timestamptz" - }, - { - "name": "project_id", - "ordinal": 5, - "type_info": "Int8" } ], "nullable": [ - false, - false, - true, - false, - false, false ], "parameters": { - "Left": [] + "Left": [ + "Int8" + ] } }, - "query": "\n SELECT tm.id, tm.thread_id, tm.author_id, tm.body, tm.created, m.id project_id FROM threads_messages tm\n INNER JOIN mods m ON m.thread_id = tm.thread_id\n WHERE tm.show_in_mod_inbox = TRUE\n " + "query": "\n SELECT m.id\n FROM mods m\n WHERE m.team_id = $1\n " }, "599df07263a2705e57fc70a7c4f5dc606e1730c281e3b573d2f2a2030bed04e0": { "describe": { @@ -3051,44 +3002,6 @@ }, "query": "\n UPDATE files\n SET is_primary = FALSE\n WHERE (version_id = $1)\n " }, - "6df09f8d633b6d6f481c858755ff577d5d8f2ddf26c8235e4e7685364ae91460": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "thread_type", - "ordinal": 1, - "type_info": "Varchar" - }, - { - "name": "members", - "ordinal": 2, - "type_info": "Int8Array" - }, - { - "name": "messages", - "ordinal": 3, - "type_info": "Jsonb" - } - ], - "nullable": [ - false, - false, - null, - null - ], - "parameters": { - "Left": [ - "Int8Array" - ] - } - }, - "query": "\n SELECT t.id, t.thread_type,\n ARRAY_AGG(DISTINCT tm.user_id) filter (where tm.user_id is not null) members,\n JSONB_AGG(DISTINCT jsonb_build_object('id', tmsg.id, 'author_id', tmsg.author_id, 'thread_id', tmsg.thread_id, 'body', tmsg.body, 'created', tmsg.created, 'show_in_mod_inbox', tmsg.show_in_mod_inbox)) filter (where tmsg.id is not null) messages\n FROM threads t\n LEFT OUTER JOIN threads_messages tmsg ON tmsg.thread_id = t.id\n LEFT OUTER JOIN threads_members tm ON tm.thread_id = t.id\n WHERE t.id = ANY($1)\n GROUP BY t.id\n " - }, "6e07cc68675d0f583182eaa9f50853fa5996b9f83543fe8b6c2a073cf6a9cb5d": { "describe": { "columns": [ @@ -3492,18 +3405,6 @@ }, "query": "\n SELECT v.mod_id project_id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n INNER JOIN versions v ON v.id = f.version_id AND v.status != ANY($1)\n INNER JOIN mods m on v.mod_id = m.id\n WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ANY($4)\n ORDER BY v.date_published ASC\n " }, - "7a4b588622729603f4c8d0c6e471baa67d19e9b4c333b2a79c28d2dd84fd869a": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n UPDATE threads_messages\n SET show_in_mod_inbox = FALSE\n WHERE id = $1\n " - }, "7ab21e7613dd88e97cf602e76bff62170c13ceef8104a4ce4cb2d101f8ce4f48": { "describe": { "columns": [], @@ -3782,20 +3683,31 @@ }, "query": "\n INSERT INTO mods_categories (joining_mod_id, joining_category_id, is_additional)\n VALUES ($1, $2, FALSE)\n " }, - "8129255d25bf0624d83f50558b668ed7b7f9c264e380d276522fc82bc871939b": { + "7ee877b149288e53a14b5f0f3417806aee7a8954fa0fc17da80d59bebbe067d2": { "describe": { - "columns": [], - "nullable": [], + "columns": [ + { + "name": "status", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "team_id", + "ordinal": 1, + "type_info": "Int8" + } + ], + "nullable": [ + false, + false + ], "parameters": { "Left": [ - "Int8", - "Varchar", - "Varchar", - "Varchar" + "Int8" ] } }, - "query": "\n INSERT INTO notifications_actions (\n notification_id, title, action_route, action_route_method\n )\n VALUES (\n $1, $2, $3, $4\n )\n " + "query": "SELECT m.status, m.team_id FROM mods m WHERE thread_id = $1" }, "83d428e1c07d16e356ef26bdf1d707940b1683b5f631ded1f6674a081453d67b": { "describe": { @@ -3988,6 +3900,18 @@ }, "query": "\n UPDATE users\n SET payout_wallet = $1, payout_wallet_type = $2, payout_address = $3\n WHERE (id = $4)\n " }, + "8f5e2a570cf35b2d158182bac37fd40bcec277bbdeddaece5efaa88600048a70": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n UPDATE threads\n SET show_in_mod_inbox = FALSE\n WHERE id = $1\n " + }, "9284d7f22617e0a7daf91540ff31791d0921ec5d4eb4809846dc67567bec1a81": { "describe": { "columns": [ @@ -4017,38 +3941,6 @@ }, "query": "\n SELECT h.hash, v.mod_id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n INNER JOIN versions v ON v.id = f.version_id AND v.status != ANY($1)\n INNER JOIN mods m on v.mod_id = m.id\n WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ANY($4)\n " }, - "9348309884811e8b22f33786ae7c0f259f37f3c90e545f00761a641570107160": { - "describe": { - "columns": [ - { - "name": "title", - "ordinal": 0, - "type_info": "Varchar" - }, - { - "name": "id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "project_type", - "ordinal": 2, - "type_info": "Varchar" - } - ], - "nullable": [ - false, - false, - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT m.title title, m.id id, pt.name project_type\n FROM mods m\n INNER JOIN project_types pt ON pt.id = m.project_type\n WHERE m.team_id = $1\n " - }, "9381c483b29d364f14c46d5e73bc14b1ec5d0525e27b9e9b099cb0786934fe78": { "describe": { "columns": [ @@ -4069,26 +3961,6 @@ }, "query": "\n SELECT id FROM mods\n WHERE slug = LOWER($1)\n " }, - "965a8a34ae559d402f0112ce5f04f1ad18de1eebc92299f4d2ce7250e3fa12d5": { - "describe": { - "columns": [ - { - "name": "status", - "ordinal": 0, - "type_info": "Varchar" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "SELECT m.status FROM mods m WHERE thread_id = $1" - }, "96b2f4e0e619e7ed312d191dc90d64113235d72254fbda8f528ce866d1795cb5": { "describe": { "columns": [ @@ -4245,22 +4117,6 @@ }, "query": "\n INSERT INTO team_members (id, team_id, user_id, role, permissions, accepted, payouts_split, ordering)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n " }, - "9d975d279fa869aad310922653de2fcbe0d2bfb896588067721e5f5560a57593": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Jsonb", - "Int8", - "Bool" - ] - } - }, - "query": "\n INSERT INTO threads_messages (\n id, author_id, body, thread_id, show_in_mod_inbox\n )\n VALUES (\n $1, $2, $3, $4, $5\n )\n " - }, "9dc32a9ef59f57fbad862520b6d3a4795a95d7d0db17e05eb8aedc3a2fe600dc": { "describe": { "columns": [ @@ -4606,32 +4462,6 @@ }, "query": "\n SELECT loader_id id FROM loaders_versions\n WHERE version_id = $1\n " }, - "adbe17a5ad3cea333b30b5d6111aff713a8f7dc79ded21f5ba942c4f1108aa8f": { - "describe": { - "columns": [ - { - "name": "title", - "ordinal": 0, - "type_info": "Varchar" - }, - { - "name": "project_type", - "ordinal": 1, - "type_info": "Varchar" - } - ], - "nullable": [ - false, - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT m.title title, pt.name project_type\n FROM mods m\n INNER JOIN project_types pt ON pt.id = m.project_type\n WHERE m.id = $1\n " - }, "ae1686b8b566dd7ecc57c653c9313a4b324a2ec3a63aa6a44ed1d8ea7999b115": { "describe": { "columns": [], @@ -4642,6 +4472,21 @@ }, "query": "\n DELETE FROM dependencies WHERE mod_dependency_id = NULL AND dependency_id = NULL AND dependency_file_name = NULL\n " }, + "b0c29c51bd3ae5b93d487471a98ee9bbb43a4df468ba781852b137dd315b9608": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Jsonb", + "Int8" + ] + } + }, + "query": "\n INSERT INTO threads_messages (\n id, author_id, body, thread_id\n )\n VALUES (\n $1, $2, $3, $4\n )\n " + }, "b0e3d1c70b87bb54819e3fac04b684a9b857aeedb4dcb7cb400c2af0dbb12922": { "describe": { "columns": [], @@ -5426,6 +5271,80 @@ }, "query": "SELECT id FROM versions WHERE mod_id = $1 AND (version_number = $2 OR id = $3) ORDER BY date_published ASC" }, + "c49cda8215982b699d7aee14614763c9b5b997489581293fc2ae3604697867fe": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "user_id", + "ordinal": 1, + "type_info": "Int8" + }, + { + "name": "title", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "text", + "ordinal": 3, + "type_info": "Varchar" + }, + { + "name": "link", + "ordinal": 4, + "type_info": "Varchar" + }, + { + "name": "created", + "ordinal": 5, + "type_info": "Timestamptz" + }, + { + "name": "read", + "ordinal": 6, + "type_info": "Bool" + }, + { + "name": "notification_type", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "body", + "ordinal": 8, + "type_info": "Jsonb" + }, + { + "name": "actions", + "ordinal": 9, + "type_info": "Jsonb" + } + ], + "nullable": [ + false, + false, + true, + true, + true, + false, + false, + true, + true, + null + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type, n.body,\n JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'title', na.title, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) 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 " + }, "c4b167ec7452cc92be0e33f7e4f3908f0c4109291511c94909e9105fc62a432f": { "describe": { "columns": [], @@ -5835,6 +5754,20 @@ }, "query": "\n DELETE FROM threads_messages\n WHERE thread_id = $1\n " }, + "d2c046d4bedeb7181ece4e94d7de90c97bd3dd1b0c16070704028923a0c2834a": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Jsonb" + ] + } + }, + "query": "\n INSERT INTO notifications (\n id, user_id, body\n )\n VALUES (\n $1, $2, $3\n )\n " + }, "d331ca8f22da418cf654985c822ce4466824beaa00dea64cde90dc651a03024b": { "describe": { "columns": [], @@ -6420,6 +6353,24 @@ }, "query": "\n UPDATE mods\n SET status = $1\n WHERE (id = $2)\n " }, + "e9d863c1793939d5ae7137d810f23d06460c28a9058b251448e3786c436f80cd": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [] + } + }, + "query": "\n SELECT id\n FROM threads\n WHERE show_in_mod_inbox = TRUE\n " + }, "ea1525cbe7460d0d9e9da8f448c661f7209bc1a7a04e2ea0026fa69c3f550a14": { "describe": { "columns": [ @@ -6538,74 +6489,6 @@ }, "query": "\n SELECT id FROM reports\n WHERE closed = FALSE AND reporter = $1\n ORDER BY created ASC\n LIMIT $2;\n " }, - "f191675ebb8f77548ba7e4385288a2f1899c7c611395249db85bea9a3ce9a54a": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "user_id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "text", - "ordinal": 3, - "type_info": "Varchar" - }, - { - "name": "link", - "ordinal": 4, - "type_info": "Varchar" - }, - { - "name": "created", - "ordinal": 5, - "type_info": "Timestamptz" - }, - { - "name": "read", - "ordinal": 6, - "type_info": "Bool" - }, - { - "name": "notification_type", - "ordinal": 7, - "type_info": "Varchar" - }, - { - "name": "actions", - "ordinal": 8, - "type_info": "Jsonb" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - true, - null - ], - "parameters": { - "Left": [ - "Int8Array" - ] - } - }, - "query": "\n SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type,\n JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'title', na.title, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.id = ANY($1)\n GROUP BY n.id, n.user_id\n ORDER BY n.created DESC;\n " - }, "f22e9aee090f9952cf795a3540c03b0a5036dab0b740847d05e03d4565756283": { "describe": { "columns": [], @@ -6895,6 +6778,80 @@ }, "query": "\n INSERT INTO mods_categories (joining_mod_id, joining_category_id, is_additional)\n VALUES ($1, $2, TRUE)\n " }, + "fce67ce3d0c27c64af85fb7d36661513bc5ea2e96fcf12f3a51c97999b01b83c": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "user_id", + "ordinal": 1, + "type_info": "Int8" + }, + { + "name": "title", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "text", + "ordinal": 3, + "type_info": "Varchar" + }, + { + "name": "link", + "ordinal": 4, + "type_info": "Varchar" + }, + { + "name": "created", + "ordinal": 5, + "type_info": "Timestamptz" + }, + { + "name": "read", + "ordinal": 6, + "type_info": "Bool" + }, + { + "name": "notification_type", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "body", + "ordinal": 8, + "type_info": "Jsonb" + }, + { + "name": "actions", + "ordinal": 9, + "type_info": "Jsonb" + } + ], + "nullable": [ + false, + false, + true, + true, + true, + false, + false, + true, + true, + null + ], + "parameters": { + "Left": [ + "Int8Array" + ] + } + }, + "query": "\n SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type, n.body,\n JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'title', na.title, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.id = ANY($1)\n GROUP BY n.id, n.user_id\n ORDER BY n.created DESC;\n " + }, "fdfe36dcb85347a3a8228b5d5fc2d017b9baa307b5ae0ae9deaafab9dcdcb74a": { "describe": { "columns": [ diff --git a/src/database/models/notification_item.rs b/src/database/models/notification_item.rs index 9250fbe0..7c9d0a07 100644 --- a/src/database/models/notification_item.rs +++ b/src/database/models/notification_item.rs @@ -1,31 +1,19 @@ use super::ids::*; use crate::database::models::DatabaseError; +use crate::models::notifications::NotificationBody; use chrono::{DateTime, Utc}; use serde::Deserialize; pub struct NotificationBuilder { - pub notification_type: Option, - pub title: String, - pub text: String, - pub link: String, - pub actions: Vec, -} - -pub struct NotificationActionBuilder { - pub title: String, - pub action_route: (String, String), + pub body: NotificationBody, } pub struct Notification { pub id: NotificationId, pub user_id: UserId, - pub notification_type: Option, - pub title: String, - pub text: String, - pub link: String, + pub body: NotificationBody, pub read: bool, pub created: DateTime, - pub actions: Vec, } #[derive(Deserialize)] @@ -54,28 +42,12 @@ impl NotificationBuilder { 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_method: action.action_route.0.clone(), - action_route: action.action_route.1.clone(), - }) - } - Notification { id, user_id: user, - notification_type: self.notification_type.clone(), - title: self.title.clone(), - text: self.text.clone(), - link: self.link.clone(), + body: self.body.clone(), read: false, created: Utc::now(), - actions, } .insert(&mut *transaction) .await?; @@ -89,30 +61,23 @@ impl Notification { pub async fn insert( &self, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, - ) -> Result<(), sqlx::error::Error> { + ) -> Result<(), DatabaseError> { sqlx::query!( " INSERT INTO notifications ( - id, user_id, title, text, link, type + id, user_id, body ) VALUES ( - $1, $2, $3, $4, $5, $6 + $1, $2, $3 ) ", self.id as NotificationId, self.user_id as UserId, - &self.title, - &self.text, - &self.link, - self.notification_type + serde_json::value::to_value(self.body.clone())? ) .execute(&mut *transaction) .await?; - for action in &self.actions { - action.insert(&mut *transaction).await?; - } - Ok(()) } @@ -141,7 +106,7 @@ impl Notification { notification_ids.iter().map(|x| x.0).collect(); sqlx::query!( " - SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type, + SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type, n.body, JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'title', na.title, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) actions FROM notifications n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id @@ -159,17 +124,25 @@ impl Notification { Notification { id, user_id: UserId(row.user_id), - notification_type: row.notification_type, - title: row.title, - text: row.text, - link: row.link, read: row.read, created: row.created, - actions: serde_json::from_value( - row.actions.unwrap_or_default(), - ) - .ok() - .unwrap_or_default(), + body: row.body.clone().and_then(|x| serde_json::from_value(x).ok()).unwrap_or_else(|| { + if let Some(title) = row.title { + NotificationBody::LegacyMarkdown { + notification_type: row.notification_type, + title, + text: row.text.unwrap_or_default(), + link: row.link.unwrap_or_default(), + actions: serde_json::from_value( + row.actions.unwrap_or_default(), + ) + .ok() + .unwrap_or_default(), + } + } else { + NotificationBody::Unknown + } + }), } })) }) @@ -188,7 +161,7 @@ impl Notification { sqlx::query!( " - SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type, + SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type, n.body, JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'title', na.title, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) actions FROM notifications n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id @@ -205,17 +178,25 @@ impl Notification { Notification { id, user_id: UserId(row.user_id), - notification_type: row.notification_type, - title: row.title, - text: row.text, - link: row.link, read: row.read, created: row.created, - actions: serde_json::from_value( - row.actions.unwrap_or_default(), - ) - .ok() - .unwrap_or_default(), + body: row.body.clone().and_then(|x| serde_json::from_value(x).ok()).unwrap_or_else(|| { + if let Some(title) = row.title { + NotificationBody::LegacyMarkdown { + notification_type: row.notification_type, + title, + text: row.text.unwrap_or_default(), + link: row.link.unwrap_or_default(), + actions: serde_json::from_value( + row.actions.unwrap_or_default(), + ) + .ok() + .unwrap_or_default(), + } + } else { + NotificationBody::Unknown + } + }), } })) }) @@ -260,29 +241,3 @@ impl Notification { 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, action_route_method - ) - VALUES ( - $1, $2, $3, $4 - ) - ", - self.notification_id as NotificationId, - &self.title, - &self.action_route, - &self.action_route_method - ) - .execute(&mut *transaction) - .await?; - - Ok(()) - } -} diff --git a/src/database/models/thread_item.rs b/src/database/models/thread_item.rs index d3bfaf4d..2740b8b6 100644 --- a/src/database/models/thread_item.rs +++ b/src/database/models/thread_item.rs @@ -7,6 +7,8 @@ use serde::Deserialize; pub struct ThreadBuilder { pub type_: ThreadType, pub members: Vec, + pub project_id: Option, + pub report_id: Option, } #[derive(Clone)] @@ -15,13 +17,15 @@ pub struct Thread { pub type_: ThreadType, pub messages: Vec, pub members: Vec, + pub show_in_mod_inbox: bool, + pub project_id: Option, + pub report_id: Option, } pub struct ThreadMessageBuilder { pub author_id: Option, pub body: MessageBody, pub thread_id: ThreadId, - pub show_in_mod_inbox: bool, } #[derive(Deserialize, Clone)] @@ -31,7 +35,6 @@ pub struct ThreadMessage { pub author_id: Option, pub body: MessageBody, pub created: DateTime, - pub show_in_mod_inbox: bool, } impl ThreadMessageBuilder { @@ -45,17 +48,16 @@ impl ThreadMessageBuilder { sqlx::query!( " INSERT INTO threads_messages ( - id, author_id, body, thread_id, show_in_mod_inbox + id, author_id, body, thread_id ) VALUES ( - $1, $2, $3, $4, $5 + $1, $2, $3, $4 ) ", thread_message_id as ThreadMessageId, self.author_id.map(|x| x.0), serde_json::value::to_value(self.body.clone())?, self.thread_id as ThreadId, - self.show_in_mod_inbox, ) .execute(&mut *transaction) .await?; @@ -133,9 +135,9 @@ impl Thread { thread_ids.iter().map(|x| x.0).collect(); let threads = sqlx::query!( " - SELECT t.id, t.thread_type, + SELECT t.id, t.thread_type, t.show_in_mod_inbox, t.project_id, t.report_id, ARRAY_AGG(DISTINCT tm.user_id) filter (where tm.user_id is not null) members, - JSONB_AGG(DISTINCT jsonb_build_object('id', tmsg.id, 'author_id', tmsg.author_id, 'thread_id', tmsg.thread_id, 'body', tmsg.body, 'created', tmsg.created, 'show_in_mod_inbox', tmsg.show_in_mod_inbox)) filter (where tmsg.id is not null) messages + JSONB_AGG(DISTINCT jsonb_build_object('id', tmsg.id, 'author_id', tmsg.author_id, 'thread_id', tmsg.thread_id, 'body', tmsg.body, 'created', tmsg.created)) filter (where tmsg.id is not null) messages FROM threads t LEFT OUTER JOIN threads_messages tmsg ON tmsg.thread_id = t.id LEFT OUTER JOIN threads_members tm ON tm.thread_id = t.id @@ -159,6 +161,9 @@ impl Thread { messages }, members: x.members.unwrap_or_default().into_iter().map(UserId).collect(), + show_in_mod_inbox: x.show_in_mod_inbox, + project_id: x.project_id.map(ProjectId), + report_id: x.report_id.map(ReportId), })) }) .try_collect::>() @@ -229,7 +234,7 @@ impl ThreadMessage { message_ids.iter().map(|x| x.0).collect(); let messages = sqlx::query!( " - SELECT tm.id, tm.author_id, tm.thread_id, tm.body, tm.created, tm.show_in_mod_inbox + SELECT tm.id, tm.author_id, tm.thread_id, tm.body, tm.created FROM threads_messages tm WHERE tm.id = ANY($1) ", @@ -244,7 +249,6 @@ impl ThreadMessage { body: serde_json::from_value(x.body) .unwrap_or(MessageBody::Deleted), created: x.created, - show_in_mod_inbox: x.show_in_mod_inbox, })) }) .try_collect::>() @@ -259,10 +263,13 @@ impl ThreadMessage { ) -> Result, sqlx::error::Error> { sqlx::query!( " - DELETE FROM threads_messages + UPDATE threads_messages + SET body = $2 WHERE id = $1 ", id as ThreadMessageId, + serde_json::to_value(MessageBody::Deleted) + .unwrap_or(serde_json::json!({})) ) .execute(&mut *transaction) .await?; diff --git a/src/models/notifications.rs b/src/models/notifications.rs index f327e388..253ebf30 100644 --- a/src/models/notifications.rs +++ b/src/models/notifications.rs @@ -1,5 +1,11 @@ use super::ids::Base62Id; use super::users::UserId; +use crate::database::models::notification_item::Notification as DBNotification; +use crate::database::models::notification_item::NotificationAction as DBNotificationAction; +use crate::models::ids::{ + ProjectId, ReportId, TeamId, ThreadId, ThreadMessageId, VersionId, +}; +use crate::models::projects::ProjectStatus; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -13,35 +19,176 @@ pub struct Notification { pub id: NotificationId, pub user_id: UserId, #[serde(rename = "type")] + pub read: bool, + pub created: DateTime, + pub body: NotificationBody, + + // DEPRECATED: use body field instead pub type_: Option, pub title: String, pub text: String, pub link: String, - pub read: bool, - pub created: DateTime, pub actions: Vec, } -use crate::database::models::notification_item::Notification as DBNotification; -use crate::database::models::notification_item::NotificationAction as DBNotificationAction; +#[derive(Serialize, Deserialize, Clone)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum NotificationBody { + ProjectUpdate { + project_id: ProjectId, + version_id: VersionId, + }, + TeamInvite { + project_id: ProjectId, + team_id: TeamId, + invited_by: UserId, + role: String, + }, + StatusChange { + project_id: ProjectId, + old_status: ProjectStatus, + new_status: ProjectStatus, + }, + ModeratorMessage { + thread_id: ThreadId, + message_id: ThreadMessageId, + + project_id: Option, + report_id: Option, + }, + LegacyMarkdown { + notification_type: Option, + title: String, + text: String, + link: String, + actions: Vec, + }, + Unknown, +} impl From for Notification { fn from(notif: DBNotification) -> Self { + let (type_, title, text, link, actions) = { + match ¬if.body { + NotificationBody::ProjectUpdate { + project_id, + version_id, + } => ( + Some("project_update".to_string()), + "A project you follow has been updated!".to_string(), + format!( + "The project {} has released a new version: {}", + project_id, version_id + ), + format!("/project/{}/version/{}", project_id, version_id), + vec![], + ), + NotificationBody::TeamInvite { + project_id, + role, + team_id, + .. + } => ( + Some("team_invite".to_string()), + "You have been invited to join a team!".to_string(), + format!( + "An invite has been sent for you to be {} of a team", + role + ), + format!("/project/{}", project_id), + vec![ + NotificationAction { + title: "Accept".to_string(), + action_route: ( + "POST".to_string(), + format!("team/{team_id}/join"), + ), + }, + NotificationAction { + title: "Deny".to_string(), + action_route: ( + "DELETE".to_string(), + format!( + "team/{team_id}/members/{}", + UserId::from(notif.user_id) + ), + ), + }, + ], + ), + NotificationBody::StatusChange { + old_status, + new_status, + project_id, + } => ( + Some("status_change".to_string()), + "Project status has changed".to_string(), + format!( + "Status has changed from {} to {}", + old_status.as_friendly_str(), + new_status.as_friendly_str() + ), + format!("/project/{}", project_id), + vec![], + ), + NotificationBody::ModeratorMessage { + project_id, + report_id, + .. + } => ( + Some("moderator_message".to_string()), + "A moderator has sent you a message!".to_string(), + "Click on the link to read more.".to_string(), + if let Some(project_id) = project_id { + format!("/project/{}", project_id) + } else if let Some(report_id) = report_id { + format!("/project/{}", report_id) + } else { + "#".to_string() + }, + vec![], + ), + NotificationBody::LegacyMarkdown { + notification_type, + title, + text, + link, + actions, + } => ( + notification_type.clone(), + title.clone(), + text.clone(), + link.clone(), + actions.clone().into_iter().map(Into::into).collect(), + ), + NotificationBody::Unknown => ( + None, + "".to_string(), + "".to_string(), + "#".to_string(), + vec![], + ), + } + }; + Self { id: notif.id.into(), user_id: notif.user_id.into(), - type_: notif.notification_type, - title: notif.title, - text: notif.text, - link: notif.link, + body: notif.body, read: notif.read, created: notif.created, - actions: notif.actions.into_iter().map(Into::into).collect(), + + // DEPRECATED + type_, + title, + text, + link, + actions, } } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct NotificationAction { pub title: String, /// The route to call when this notification action is called. Formatted HTTP Method, route diff --git a/src/models/threads.rs b/src/models/threads.rs index 2892a2cd..22ff821a 100644 --- a/src/models/threads.rs +++ b/src/models/threads.rs @@ -1,4 +1,5 @@ use super::ids::Base62Id; +use crate::models::ids::{ProjectId, ReportId}; use crate::models::projects::ProjectStatus; use crate::models::users::{User, UserId}; use chrono::{DateTime, Utc}; @@ -21,6 +22,9 @@ pub struct Thread { pub type_: ThreadType, pub messages: Vec, pub members: Vec, + + pub project_id: Option, + pub report_id: Option, } #[derive(Serialize, Deserialize)] @@ -45,6 +49,7 @@ pub enum MessageBody { old_status: ProjectStatus, }, ThreadClosure, + ThreadReopen, Deleted, } diff --git a/src/routes/v2/project_creation.rs b/src/routes/v2/project_creation.rs index 1da9c13a..2c604642 100644 --- a/src/routes/v2/project_creation.rs +++ b/src/routes/v2/project_creation.rs @@ -759,6 +759,8 @@ async fn project_create_inner( let thread_id = ThreadBuilder { type_: ThreadType::Project, members: vec![], + project_id: Some(project_id.into()), + report_id: None, } .insert(&mut *transaction) .await?; diff --git a/src/routes/v2/projects.rs b/src/routes/v2/projects.rs index d5c8383c..897f49e7 100644 --- a/src/routes/v2/projects.rs +++ b/src/routes/v2/projects.rs @@ -4,6 +4,7 @@ use crate::database::models::thread_item::ThreadMessageBuilder; use crate::file_hosting::FileHost; use crate::models; use crate::models::ids::base62_impl::parse_base62; +use crate::models::notifications::NotificationBody; use crate::models::projects::{ DonationLink, Project, ProjectId, ProjectStatus, SearchRequest, SideType, }; @@ -615,23 +616,11 @@ pub async fn project_edit( .await?; NotificationBuilder { - notification_type: Some("status_update".to_string()), - title: format!( - "**{}**'s status has changed!", - project_item.inner.title - ), - text: format!( - "The project {}'s status has changed from {} to {}", - project_item.inner.title, - project_item.inner.status.as_friendly_str(), - status.as_friendly_str() - ), - link: format!( - "/{}/{}", - project_item.project_type, - ProjectId::from(id) - ), - actions: vec![], + body: NotificationBody::StatusChange { + project_id: project_item.inner.id.into(), + old_status: project_item.inner.status, + new_status: *status, + }, } .insert_many(notified_members, &mut transaction) .await?; @@ -645,7 +634,6 @@ pub async fn project_edit( old_status: project_item.inner.status, }, thread_id: thread, - show_in_mod_inbox: false, } .insert(&mut transaction) .await?; diff --git a/src/routes/v2/reports.rs b/src/routes/v2/reports.rs index cc947a43..8ac22aec 100644 --- a/src/routes/v2/reports.rs +++ b/src/routes/v2/reports.rs @@ -71,6 +71,8 @@ pub async fn report_create( let thread_id = ThreadBuilder { type_: ThreadType::Report, members: vec![], + project_id: None, + report_id: Some(id), } .insert(&mut transaction) .await?; @@ -314,7 +316,7 @@ pub async fn report_edit( } if let Some(edit_closed) = edit_report.closed { - if report.closed && !edit_closed && !user.role.is_mod() { + if !user.role.is_mod() { return Err(ApiError::InvalidInput( "You cannot reopen a report!".to_string(), )); @@ -323,9 +325,12 @@ pub async fn report_edit( if let Some(thread) = report.thread_id { ThreadMessageBuilder { author_id: Some(user.id.into()), - body: MessageBody::ThreadClosure, + body: if !edit_closed && report.closed { + MessageBody::ThreadReopen + } else { + MessageBody::ThreadClosure + }, thread_id: thread, - show_in_mod_inbox: false, } .insert(&mut transaction) .await?; diff --git a/src/routes/v2/teams.rs b/src/routes/v2/teams.rs index c2ce692c..bdcbc0d5 100644 --- a/src/routes/v2/teams.rs +++ b/src/routes/v2/teams.rs @@ -1,8 +1,7 @@ -use crate::database::models::notification_item::{ - NotificationActionBuilder, NotificationBuilder, -}; +use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::TeamMember; use crate::models::ids::ProjectId; +use crate::models::notifications::NotificationBody; use crate::models::teams::{Permissions, TeamId}; use crate::models::users::UserId; use crate::routes::ApiError; @@ -333,9 +332,8 @@ pub async fn add_team_member( let result = sqlx::query!( " - SELECT m.title title, m.id id, pt.name project_type + SELECT m.id FROM mods m - INNER JOIN project_types pt ON pt.id = m.project_type WHERE m.team_id = $1 ", team_id as crate::database::models::ids::TeamId @@ -343,32 +341,13 @@ pub async fn add_team_member( .fetch_one(&**pool) .await?; - let team: TeamId = team_id.into(); NotificationBuilder { - notification_type: Some("team_invite".to_string()), - title: "You have been invited to join a team!".to_string(), - text: format!( - "Team invite from {} to join the team for project {}", - current_user.username, result.title - ), - link: format!( - "/{}/{}", - result.project_type, - ProjectId(result.id as u64) - ), - actions: vec![ - NotificationActionBuilder { - title: "Accept".to_string(), - action_route: ("POST".to_string(), format!("team/{team}/join")), - }, - NotificationActionBuilder { - title: "Deny".to_string(), - action_route: ( - "DELETE".to_string(), - format!("team/{team}/members/{}", new_member.user_id), - ), - }, - ], + body: NotificationBody::TeamInvite { + project_id: ProjectId(result.id as u64), + team_id: team_id.into(), + invited_by: current_user.id, + role: new_member.role.clone(), + }, } .insert(new_member.user_id.into(), &mut transaction) .await?; diff --git a/src/routes/v2/threads.rs b/src/routes/v2/threads.rs index 47bf4a8a..b5c6be51 100644 --- a/src/routes/v2/threads.rs +++ b/src/routes/v2/threads.rs @@ -1,7 +1,9 @@ use crate::database; +use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::thread_item::ThreadMessageBuilder; -use crate::models::ids::ThreadMessageId; -use crate::models::projects::ProjectStatus; +use crate::models::ids::{ReportId, ThreadMessageId}; +use crate::models::notifications::NotificationBody; +use crate::models::projects::{ProjectId, ProjectStatus}; use crate::models::threads::{ MessageBody, Thread, ThreadId, ThreadMessage, ThreadType, }; @@ -11,6 +13,7 @@ use crate::util::auth::{ check_is_moderator_from_headers, get_user_from_headers, }; use actix_web::{delete, get, post, web, HttpRequest, HttpResponse}; +use futures::TryStreamExt; use serde::Deserialize; use sqlx::PgPool; @@ -19,7 +22,8 @@ pub fn config(cfg: &mut web::ServiceConfig) { web::scope("thread") .service(moderation_inbox) .service(thread_get) - .service(thread_send_message), + .service(thread_send_message) + .service(thread_read), ); cfg.service(web::scope("message").service(message_delete)); cfg.service(threads_get); @@ -86,8 +90,6 @@ pub async fn filter_authorized_threads( } if !check_threads.is_empty() { - use futures::TryStreamExt; - let project_thread_ids = check_threads .iter() .filter(|x| x.type_ == ThreadType::Project) @@ -248,6 +250,8 @@ fn convert_thread( .into_iter() .filter(|x| !x.role.is_mod() || user.role.is_mod()) .collect(), + project_id: data.project_id.map(ProjectId::from), + report_id: data.report_id.map(ReportId::from), } } @@ -384,30 +388,93 @@ pub async fn thread_send_message( return Ok(HttpResponse::NotFound().body("")); } - let mod_notif = if thread.type_ == ThreadType::Project { - let status = sqlx::query!( - "SELECT m.status FROM mods m WHERE thread_id = $1", + let report = if let Some(report) = thread.report_id { + database::models::report_item::Report::get(report, &**pool).await? + } else { + None + }; + + if report.as_ref().map(|x| x.closed).unwrap_or(false) + && !user.role.is_mod() + { + return Err(ApiError::InvalidInput( + "You may not reply to a closed report".to_string(), + )); + } + + let (mod_notif, (user_notif, team_id)) = if thread.type_ + == ThreadType::Project + { + let record = sqlx::query!( + "SELECT m.status, m.team_id FROM mods m WHERE thread_id = $1", thread.id as database::models::ids::ThreadId, ) .fetch_one(&**pool) .await?; - let status = ProjectStatus::from_str(&status.status); + let status = ProjectStatus::from_str(&record.status); - status == ProjectStatus::Processing && !user.role.is_mod() + ( + status == ProjectStatus::Processing && !user.role.is_mod(), + ( + status != ProjectStatus::Processing && user.role.is_mod(), + Some(record.team_id), + ), + ) } else { - false + (false, (thread.type_ == ThreadType::Report, None)) }; let mut transaction = pool.begin().await?; - ThreadMessageBuilder { + sqlx::query!( + " + UPDATE threads + SET show_in_mod_inbox = $1 + WHERE id = $2 + ", + mod_notif, + thread.id.0, + ) + .execute(&mut *transaction) + .await?; + + let id = ThreadMessageBuilder { author_id: Some(user.id.into()), body: new_message.body.clone(), thread_id: thread.id, - show_in_mod_inbox: mod_notif, } .insert(&mut transaction) .await?; + + if user_notif { + let users_to_notify = if let Some(team_id) = team_id { + let members = database::models::TeamMember::get_from_team_full( + database::models::TeamId(team_id), + &**pool, + ) + .await?; + + members.into_iter().map(|x| x.user.id).collect() + } else if let Some(report) = report { + vec![report.reporter] + } else { + vec![] + }; + + if !users_to_notify.is_empty() { + NotificationBuilder { + body: NotificationBody::ModeratorMessage { + thread_id: thread.id.into(), + message_id: id.into(), + project_id: thread.project_id.map(ProjectId::from), + report_id: thread.report_id.map(ReportId::from), + }, + } + .insert_many(users_to_notify, &mut transaction) + .await?; + } + } + transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) @@ -421,36 +488,33 @@ pub async fn moderation_inbox( req: HttpRequest, pool: web::Data, ) -> Result { - check_is_moderator_from_headers(req.headers(), &**pool).await?; + let user = check_is_moderator_from_headers(req.headers(), &**pool).await?; - let messages = sqlx::query!( + let ids = sqlx::query!( " - SELECT tm.id, tm.thread_id, tm.author_id, tm.body, tm.created, m.id project_id FROM threads_messages tm - INNER JOIN mods m ON m.thread_id = tm.thread_id - WHERE tm.show_in_mod_inbox = TRUE + SELECT id + FROM threads + WHERE show_in_mod_inbox = TRUE " ) - .fetch_all(&**pool) - .await? - .into_iter() - .map(|x| serde_json::json! ({ - "message": ThreadMessage { - id: ThreadMessageId(x.id as u64), - author_id: x.author_id.map(|x| crate::models::users::UserId(x as u64)), - body: serde_json::from_value(x.body).unwrap_or(MessageBody::Deleted), - created: x.created - }, - "project_id": crate::models::projects::ProjectId(x.project_id as u64), - })) - .collect::>(); + .fetch_many(&**pool) + .try_filter_map(|e| async { + Ok(e.right().map(|m| database::models::ThreadId(m.id))) + }) + .try_collect::>() + .await?; - Ok(HttpResponse::Ok().json(messages)) + let threads_data = + database::models::Thread::get_many(&ids, &**pool).await?; + let threads = filter_authorized_threads(threads_data, &user, &pool).await?; + + Ok(HttpResponse::Ok().json(threads)) } #[post("{id}/read")] -pub async fn read_message( +pub async fn thread_read( req: HttpRequest, - info: web::Path<(ThreadMessageId,)>, + info: web::Path<(ThreadId,)>, pool: web::Data, ) -> Result { check_is_moderator_from_headers(req.headers(), &**pool).await?; @@ -460,7 +524,7 @@ pub async fn read_message( sqlx::query!( " - UPDATE threads_messages + UPDATE threads SET show_in_mod_inbox = FALSE WHERE id = $1 ", diff --git a/src/routes/v2/version_creation.rs b/src/routes/v2/version_creation.rs index 9d037140..77016e92 100644 --- a/src/routes/v2/version_creation.rs +++ b/src/routes/v2/version_creation.rs @@ -5,6 +5,7 @@ use crate::database::models::version_item::{ DependencyBuilder, VersionBuilder, VersionFileBuilder, }; use crate::file_hosting::FileHost; +use crate::models::notifications::NotificationBody; use crate::models::pack::PackFileHash; use crate::models::projects::{ Dependency, DependencyType, FileType, GameVersion, Loader, ProjectId, @@ -377,18 +378,6 @@ async fn version_create_inner( )); } - let result = sqlx::query!( - " - SELECT m.title title, pt.name project_type - FROM mods m - INNER JOIN project_types pt ON pt.id = m.project_type - WHERE m.id = $1 - ", - builder.project_id as crate::database::models::ids::ProjectId - ) - .fetch_one(&mut *transaction) - .await?; - use futures::stream::TryStreamExt; let users = sqlx::query!( @@ -409,18 +398,10 @@ async fn version_create_inner( let version_id: VersionId = builder.version_id.into(); NotificationBuilder { - notification_type: Some("project_update".to_string()), - title: format!("**{}** has been updated!", result.title), - text: format!( - "The project {} has released a new version: {}", - result.title, - version_data.version_number.clone() - ), - link: format!( - "/{}/{}/version/{}", - result.project_type, project_id, version_id - ), - actions: vec![], + body: NotificationBody::ProjectUpdate { + project_id, + version_id, + }, } .insert_many(users, &mut *transaction) .await?;