Add dependencies to search (#578)
* Add dependencies to search * add attrs for faceting * run prepare * Add user data route from token * update to 24hrs * Fix report bugs
This commit is contained in:
parent
5c559af936
commit
59f24df294
@ -1,2 +0,0 @@
|
||||
edition = "2018"
|
||||
max_width = 80
|
||||
13
shell.nix
13
shell.nix
@ -1,13 +0,0 @@
|
||||
# TODO: Move to flake
|
||||
{pkgs ? import <nixpkgs> {},
|
||||
fenix ? import (fetchTarball "https://github.com/nix-community/fenix/archive/main.tar.gz") {}
|
||||
}:
|
||||
pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
fenix.default.toolchain
|
||||
docker docker-compose
|
||||
git
|
||||
openssl pkg-config
|
||||
sqlx-cli
|
||||
];
|
||||
}
|
||||
444
sqlx-data.json
444
sqlx-data.json
@ -525,26 +525,6 @@
|
||||
},
|
||||
"query": "\n SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)\n "
|
||||
},
|
||||
"0f6469055265ad8b114136368001aa927b587df9f64f0e19fd37d1f4b4adab60": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n SELECT id FROM mods\n WHERE status = $1 AND queued < NOW() - INTERVAL '1 day'\n ORDER BY updated ASC\n "
|
||||
},
|
||||
"0fb1cca8a2a37107104244953371fe2f8a5e6edd57f4b325c5842c6571eb16b4": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@ -566,6 +546,178 @@
|
||||
},
|
||||
"query": "\n SELECT EXISTS(SELECT 1 FROM mod_follows mf WHERE mf.follower_id = $1 AND mf.mod_id = $2)\n "
|
||||
},
|
||||
"113bffbd003f0f32eef61468148a51dd9437be841c5b79fdb52dd6c12ebaba61": {
|
||||
"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": "published",
|
||||
"ordinal": 7,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "approved",
|
||||
"ordinal": 8,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "updated",
|
||||
"ordinal": 9,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "team_id",
|
||||
"ordinal": 10,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "license",
|
||||
"ordinal": 11,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "slug",
|
||||
"ordinal": 12,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "status_name",
|
||||
"ordinal": 13,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "color",
|
||||
"ordinal": 14,
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"name": "client_side_type",
|
||||
"ordinal": 15,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "server_side_type",
|
||||
"ordinal": 16,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "project_type_name",
|
||||
"ordinal": 17,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "username",
|
||||
"ordinal": 18,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "categories",
|
||||
"ordinal": 19,
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"name": "additional_categories",
|
||||
"ordinal": 20,
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"name": "loaders",
|
||||
"ordinal": 21,
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"name": "versions",
|
||||
"ordinal": 22,
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"name": "gallery",
|
||||
"ordinal": 23,
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"name": "featured_gallery",
|
||||
"ordinal": 24,
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"name": "dependencies",
|
||||
"ordinal": 25,
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"TextArray",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"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.published published, m.approved approved, m.updated updated,\n m.team_id team_id, m.license license, m.slug slug, m.status status_name, m.color color,\n cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, u.username username,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,\n ARRAY_AGG(DISTINCT gv.version) filter (where gv.version is not null) versions,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery,\n JSONB_AGG(DISTINCT jsonb_build_object('id', mdep.id, 'dep_type', d.dependency_type)) filter (where mdep.id is not null) dependencies\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id AND v.status != ANY($1)\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 dependencies d ON d.dependent_id = v.id\n LEFT OUTER JOIN mods mdep ON mdep.id = d.mod_dependency_id\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.status = ANY($2)\n GROUP BY m.id, cs.id, ss.id, pt.id, u.id;\n "
|
||||
},
|
||||
"1209ffc1ffbea89f7060573275dc7325ac4d7b4885b6c1d1ec92998e6012e455": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -611,6 +763,18 @@
|
||||
},
|
||||
"query": "\n UPDATE mods\n SET webhook_sent = TRUE\n WHERE id = $1\n "
|
||||
},
|
||||
"127691940ca7e542e246dd2a1c9cb391041b30ddf0547d73b49c1dd9dc59d2ae": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE notifications\n SET read = TRUE\n WHERE id = ANY($1)\n "
|
||||
},
|
||||
"1411c9ae3af067679aa21d7f45937cd94d457e4eb17a108566776a9bd1ee77e2": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -1898,19 +2062,6 @@
|
||||
},
|
||||
"query": "\n SELECT r.id, rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created, r.thread_id, r.closed\n FROM reports r\n INNER JOIN report_types rt ON rt.id = r.report_type_id\n WHERE r.id = ANY($1)\n ORDER BY r.created DESC\n "
|
||||
},
|
||||
"3ae7c4a29dab8bce0e84a9c47a4a4f50a3be4bcb86e5b13d7dd60975d62e9ea3": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Varchar"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n INSERT INTO threads (\n id, thread_type\n )\n VALUES (\n $1, $2\n )\n "
|
||||
},
|
||||
"3af747b5543a5a9b10dcce0a1eb9c2a1926dd5a507fe0d8b7f52d8ccc7fcd0af": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -2673,6 +2824,26 @@
|
||||
},
|
||||
"query": "\n UPDATE versions\n SET version_number = $1\n WHERE (id = $2)\n "
|
||||
},
|
||||
"5586d60c8f3d58a31e6635ffb3cb30bac389bf21b190dfd1e64a44e837f3879c": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n SELECT id FROM mods\n WHERE status = $1 AND queued < NOW() - INTERVAL '40 hours'\n ORDER BY updated ASC\n "
|
||||
},
|
||||
"57743e20646dab2bcc02fe555d6b8ddb999697b7e95ec732d1a1a9e2bfdb8181": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@ -4374,6 +4545,32 @@
|
||||
},
|
||||
"query": "\n SELECT u.id, u.github_id, u.name, u.email,\n u.avatar_url, u.username, u.bio,\n u.created, u.role, u.badges,\n u.balance, u.payout_wallet, u.payout_wallet_type,\n u.payout_address\n FROM users u\n WHERE u.id = ANY($1)\n "
|
||||
},
|
||||
"a962f21969bba402258fca169c45f3d71bc1b71f754cdcc1f5c968e4948653b2": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "notifs_count",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "followed_projects",
|
||||
"ordinal": 1,
|
||||
"type_info": "Int8Array"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
null,
|
||||
null
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n SELECT COUNT(DISTINCT n.id) notifs_count, ARRAY_AGG(mf.mod_id) followed_projects FROM notifications n\n LEFT OUTER JOIN mod_follows mf ON mf.follower_id = $1\n WHERE user_id = $1 AND read = FALSE\n "
|
||||
},
|
||||
"aa59f79136ef87dd4121d5f367f5dbdbca80e936c1b986ec99c09c3e95daa756": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -4752,172 +4949,6 @@
|
||||
},
|
||||
"query": "\n DELETE FROM game_versions_versions gvv\n WHERE gvv.joining_version_id = $1\n "
|
||||
},
|
||||
"bf4afeda41a54e09a80a4cc505d1fbb72124c442ebaca731a291f022524daf1a": {
|
||||
"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": "published",
|
||||
"ordinal": 7,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "approved",
|
||||
"ordinal": 8,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "updated",
|
||||
"ordinal": 9,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "team_id",
|
||||
"ordinal": 10,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "license",
|
||||
"ordinal": 11,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "slug",
|
||||
"ordinal": 12,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "status_name",
|
||||
"ordinal": 13,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "color",
|
||||
"ordinal": 14,
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"name": "client_side_type",
|
||||
"ordinal": 15,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "server_side_type",
|
||||
"ordinal": 16,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "project_type_name",
|
||||
"ordinal": 17,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "username",
|
||||
"ordinal": 18,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "categories",
|
||||
"ordinal": 19,
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"name": "additional_categories",
|
||||
"ordinal": 20,
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"name": "loaders",
|
||||
"ordinal": 21,
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"name": "versions",
|
||||
"ordinal": 22,
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"name": "gallery",
|
||||
"ordinal": 23,
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"name": "featured_gallery",
|
||||
"ordinal": 24,
|
||||
"type_info": "VarcharArray"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"TextArray",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"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.published published, m.approved approved, m.updated updated,\n m.team_id team_id, m.license license, m.slug slug, m.status status_name, m.color color,\n cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, u.username username,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,\n ARRAY_AGG(DISTINCT gv.version) filter (where gv.version is not null) versions,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id AND v.status != ANY($1)\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 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.status = ANY($2)\n GROUP BY m.id, cs.id, ss.id, pt.id, u.id;\n "
|
||||
},
|
||||
"bf7f721664f5e0ed41adc41b5483037256635f28ff6c4e5d3cbcec4387f9c8ef": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@ -5373,6 +5404,21 @@
|
||||
},
|
||||
"query": "\n SELECT v.id version_id, v.mod_id project_id, h.hash hash FROM hashes h\n INNER JOIN files f on h.file_id = f.id\n INNER JOIN versions v on f.version_id = v.id\n WHERE h.algorithm = 'sha1' AND h.hash = ANY($1)\n "
|
||||
},
|
||||
"d0a65443aef9d3781000d0a59d8d81d1a8e51613dfee343d079c233375cebd12": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n INSERT INTO threads (\n id, thread_type, report_id, project_id\n )\n VALUES (\n $1, $2, $3, $4\n )\n "
|
||||
},
|
||||
"d12bc07adb4dc8147d0ddccd72a4f23ed38cd31d7db3d36ebbe2c9b627130f0b": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
|
||||
@ -52,10 +52,7 @@ pub struct DonationPlatform {
|
||||
}
|
||||
|
||||
impl Category {
|
||||
pub async fn get_id<'a, E>(
|
||||
name: &str,
|
||||
exec: E,
|
||||
) -> Result<Option<CategoryId>, DatabaseError>
|
||||
pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<CategoryId>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
@ -124,10 +121,7 @@ impl Category {
|
||||
}
|
||||
|
||||
impl Loader {
|
||||
pub async fn get_id<'a, E>(
|
||||
name: &str,
|
||||
exec: E,
|
||||
) -> Result<Option<LoaderId>, DatabaseError>
|
||||
pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<LoaderId>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
@ -315,10 +309,7 @@ impl GameVersion {
|
||||
|
||||
impl<'a> GameVersionBuilder<'a> {
|
||||
/// The game version. Spaces must be replaced with '_' for it to be valid
|
||||
pub fn version(
|
||||
self,
|
||||
version: &'a str,
|
||||
) -> Result<GameVersionBuilder<'a>, DatabaseError> {
|
||||
pub fn version(self, version: &'a str) -> Result<GameVersionBuilder<'a>, DatabaseError> {
|
||||
Ok(Self {
|
||||
version: Some(version),
|
||||
..self
|
||||
@ -342,10 +333,7 @@ impl<'a> GameVersionBuilder<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn insert<'b, E>(
|
||||
self,
|
||||
exec: E,
|
||||
) -> Result<GameVersionId, DatabaseError>
|
||||
pub async fn insert<'b, E>(self, exec: E) -> Result<GameVersionId, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'b, Database = sqlx::Postgres>,
|
||||
{
|
||||
@ -393,9 +381,7 @@ impl DonationPlatform {
|
||||
Ok(result.map(|r| DonationPlatformId(r.id)))
|
||||
}
|
||||
|
||||
pub async fn list<'a, E>(
|
||||
exec: E,
|
||||
) -> Result<Vec<DonationPlatform>, DatabaseError>
|
||||
pub async fn list<'a, E>(exec: E) -> Result<Vec<DonationPlatform>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
@ -420,10 +406,7 @@ impl DonationPlatform {
|
||||
}
|
||||
|
||||
impl ReportType {
|
||||
pub async fn get_id<'a, E>(
|
||||
name: &str,
|
||||
exec: E,
|
||||
) -> Result<Option<ReportTypeId>, DatabaseError>
|
||||
pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<ReportTypeId>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
@ -459,10 +442,7 @@ impl ReportType {
|
||||
}
|
||||
|
||||
impl ProjectType {
|
||||
pub async fn get_id<'a, E>(
|
||||
name: &str,
|
||||
exec: E,
|
||||
) -> Result<Option<ProjectTypeId>, DatabaseError>
|
||||
pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<ProjectTypeId>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
@ -498,10 +478,7 @@ impl ProjectType {
|
||||
}
|
||||
|
||||
impl SideType {
|
||||
pub async fn get_id<'a, E>(
|
||||
name: &str,
|
||||
exec: E,
|
||||
) -> Result<Option<SideTypeId>, DatabaseError>
|
||||
pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<SideTypeId>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
|
||||
@ -102,8 +102,7 @@ impl Notification {
|
||||
{
|
||||
use futures::stream::TryStreamExt;
|
||||
|
||||
let notification_ids_parsed: Vec<i64> =
|
||||
notification_ids.iter().map(|x| x.0).collect();
|
||||
let notification_ids_parsed: Vec<i64> = 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, n.body,
|
||||
@ -204,6 +203,33 @@ impl Notification {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn read(
|
||||
id: NotificationId,
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<Option<()>, sqlx::error::Error> {
|
||||
Self::read_many(&[id], transaction).await
|
||||
}
|
||||
|
||||
pub async fn read_many(
|
||||
notification_ids: &[NotificationId],
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<Option<()>, sqlx::error::Error> {
|
||||
let notification_ids_parsed: Vec<i64> = notification_ids.iter().map(|x| x.0).collect();
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE notifications
|
||||
SET read = TRUE
|
||||
WHERE id = ANY($1)
|
||||
",
|
||||
¬ification_ids_parsed
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
pub async fn remove(
|
||||
id: NotificationId,
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
@ -215,8 +241,7 @@ impl Notification {
|
||||
notification_ids: &[NotificationId],
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<Option<()>, sqlx::error::Error> {
|
||||
let notification_ids_parsed: Vec<i64> =
|
||||
notification_ids.iter().map(|x| x.0).collect();
|
||||
let notification_ids_parsed: Vec<i64> = notification_ids.iter().map(|x| x.0).collect();
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
|
||||
@ -186,12 +186,11 @@ impl ProjectBuilder {
|
||||
self.project_id as ProjectId,
|
||||
category as CategoryId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Project::update_game_versions(self.project_id, &mut *transaction)
|
||||
.await?;
|
||||
Project::update_game_versions(self.project_id, &mut *transaction).await?;
|
||||
Project::update_loaders(self.project_id, &mut *transaction).await?;
|
||||
|
||||
Ok(self.project_id)
|
||||
@ -307,8 +306,7 @@ impl Project {
|
||||
{
|
||||
use futures::stream::TryStreamExt;
|
||||
|
||||
let project_ids_parsed: Vec<i64> =
|
||||
project_ids.iter().map(|x| x.0).collect();
|
||||
let project_ids_parsed: Vec<i64> = project_ids.iter().map(|x| x.0).collect();
|
||||
let projects = sqlx::query!(
|
||||
"
|
||||
SELECT id, project_type, title, description, downloads, follows,
|
||||
@ -342,12 +340,8 @@ impl Project {
|
||||
license_url: m.license_url,
|
||||
discord_url: m.discord_url,
|
||||
client_side: SideTypeId(m.client_side),
|
||||
status: ProjectStatus::from_str(
|
||||
&m.status,
|
||||
),
|
||||
requested_status: m.requested_status.map(|x| ProjectStatus::from_str(
|
||||
&x,
|
||||
)),
|
||||
status: ProjectStatus::from_str(&m.status),
|
||||
requested_status: m.requested_status.map(|x| ProjectStatus::from_str(&x)),
|
||||
server_side: SideTypeId(m.server_side),
|
||||
license: m.license,
|
||||
slug: m.slug,
|
||||
@ -402,11 +396,7 @@ impl Project {
|
||||
|
||||
if let Some(thread_id) = thread_id {
|
||||
if let Some(id) = thread_id.thread_id {
|
||||
crate::database::models::Thread::remove_full(
|
||||
ThreadId(id),
|
||||
transaction,
|
||||
)
|
||||
.await?;
|
||||
crate::database::models::Thread::remove_full(ThreadId(id), transaction).await?;
|
||||
}
|
||||
}
|
||||
|
||||
@ -595,23 +585,18 @@ impl Project {
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
let id_option =
|
||||
crate::models::ids::base62_impl::parse_base62(slug_or_project_id)
|
||||
.ok();
|
||||
let id_option = crate::models::ids::base62_impl::parse_base62(slug_or_project_id).ok();
|
||||
|
||||
if let Some(id) = id_option {
|
||||
let mut project =
|
||||
Project::get(ProjectId(id as i64), executor).await?;
|
||||
let mut project = Project::get(ProjectId(id as i64), executor).await?;
|
||||
|
||||
if project.is_none() {
|
||||
project = Project::get_from_slug(slug_or_project_id, executor)
|
||||
.await?;
|
||||
project = Project::get_from_slug(slug_or_project_id, executor).await?;
|
||||
}
|
||||
|
||||
Ok(project)
|
||||
} else {
|
||||
let project =
|
||||
Project::get_from_slug(slug_or_project_id, executor).await?;
|
||||
let project = Project::get_from_slug(slug_or_project_id, executor).await?;
|
||||
|
||||
Ok(project)
|
||||
}
|
||||
@ -624,25 +609,18 @@ impl Project {
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
let id_option =
|
||||
crate::models::ids::base62_impl::parse_base62(slug_or_project_id)
|
||||
.ok();
|
||||
let id_option = crate::models::ids::base62_impl::parse_base62(slug_or_project_id).ok();
|
||||
|
||||
if let Some(id) = id_option {
|
||||
let mut project =
|
||||
Project::get_full(ProjectId(id as i64), executor).await?;
|
||||
let mut project = Project::get_full(ProjectId(id as i64), executor).await?;
|
||||
|
||||
if project.is_none() {
|
||||
project =
|
||||
Project::get_full_from_slug(slug_or_project_id, executor)
|
||||
.await?;
|
||||
project = Project::get_full_from_slug(slug_or_project_id, executor).await?;
|
||||
}
|
||||
|
||||
Ok(project)
|
||||
} else {
|
||||
let project =
|
||||
Project::get_full_from_slug(slug_or_project_id, executor)
|
||||
.await?;
|
||||
let project = Project::get_full_from_slug(slug_or_project_id, executor).await?;
|
||||
Ok(project)
|
||||
}
|
||||
}
|
||||
@ -668,8 +646,7 @@ impl Project {
|
||||
{
|
||||
use futures::TryStreamExt;
|
||||
|
||||
let project_ids_parsed: Vec<i64> =
|
||||
project_ids.iter().map(|x| x.0).collect();
|
||||
let project_ids_parsed: Vec<i64> = project_ids.iter().map(|x| x.0).collect();
|
||||
sqlx::query!(
|
||||
"
|
||||
SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,
|
||||
|
||||
@ -58,10 +58,7 @@ impl Report {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get<'a, E>(
|
||||
id: ReportId,
|
||||
exec: E,
|
||||
) -> Result<Option<QueryReport>, sqlx::Error>
|
||||
pub async fn get<'a, E>(id: ReportId, exec: E) -> Result<Option<QueryReport>, sqlx::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
@ -79,8 +76,7 @@ impl Report {
|
||||
{
|
||||
use futures::stream::TryStreamExt;
|
||||
|
||||
let report_ids_parsed: Vec<i64> =
|
||||
report_ids.iter().map(|x| x.0).collect();
|
||||
let report_ids_parsed: Vec<i64> = report_ids.iter().map(|x| x.0).collect();
|
||||
let reports = sqlx::query!(
|
||||
"
|
||||
SELECT r.id, rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created, r.thread_id, r.closed
|
||||
@ -141,11 +137,7 @@ impl Report {
|
||||
|
||||
if let Some(thread_id) = thread_id {
|
||||
if let Some(id) = thread_id.thread_id {
|
||||
crate::database::models::Thread::remove_full(
|
||||
ThreadId(id),
|
||||
transaction,
|
||||
)
|
||||
.await?;
|
||||
crate::database::models::Thread::remove_full(ThreadId(id), transaction).await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -36,8 +36,7 @@ impl TeamBuilder {
|
||||
.await?;
|
||||
|
||||
for member in self.members {
|
||||
let team_member_id =
|
||||
generate_team_member_id(&mut *transaction).await?;
|
||||
let team_member_id = generate_team_member_id(&mut *transaction).await?;
|
||||
let team_member = TeamMember {
|
||||
id: team_member_id,
|
||||
team_id,
|
||||
@ -224,16 +223,16 @@ impl TeamMember {
|
||||
.fetch_many(executor)
|
||||
.try_filter_map(|e| async {
|
||||
if let Some(m) = e.right() {
|
||||
Ok(Some(Ok(TeamMember {
|
||||
id: TeamMemberId(m.id),
|
||||
team_id: TeamId(m.team_id),
|
||||
user_id,
|
||||
role: m.role,
|
||||
permissions: Permissions::from_bits(m.permissions as u64).unwrap_or_default(),
|
||||
accepted: m.accepted,
|
||||
payouts_split: m.payouts_split,
|
||||
ordering: m.ordering,
|
||||
})))
|
||||
Ok(Some(Ok(TeamMember {
|
||||
id: TeamMemberId(m.id),
|
||||
team_id: TeamId(m.team_id),
|
||||
user_id,
|
||||
role: m.role,
|
||||
permissions: Permissions::from_bits(m.permissions as u64).unwrap_or_default(),
|
||||
accepted: m.accepted,
|
||||
payouts_split: m.payouts_split,
|
||||
ordering: m.ordering,
|
||||
})))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
@ -275,8 +274,7 @@ impl TeamMember {
|
||||
team_id: id,
|
||||
user_id,
|
||||
role: m.role,
|
||||
permissions: Permissions::from_bits(m.permissions as u64)
|
||||
.unwrap_or_default(),
|
||||
permissions: Permissions::from_bits(m.permissions as u64).unwrap_or_default(),
|
||||
accepted: m.accepted,
|
||||
payouts_split: m.payouts_split,
|
||||
ordering: m.ordering,
|
||||
@ -448,8 +446,7 @@ impl TeamMember {
|
||||
team_id: TeamId(m.team_id),
|
||||
user_id,
|
||||
role: m.role,
|
||||
permissions: Permissions::from_bits(m.permissions as u64)
|
||||
.unwrap_or_default(),
|
||||
permissions: Permissions::from_bits(m.permissions as u64).unwrap_or_default(),
|
||||
accepted: m.accepted,
|
||||
payouts_split: m.payouts_split,
|
||||
ordering: m.ordering,
|
||||
@ -486,8 +483,7 @@ impl TeamMember {
|
||||
team_id: TeamId(m.team_id),
|
||||
user_id,
|
||||
role: m.role,
|
||||
permissions: Permissions::from_bits(m.permissions as u64)
|
||||
.unwrap_or_default(),
|
||||
permissions: Permissions::from_bits(m.permissions as u64).unwrap_or_default(),
|
||||
accepted: m.accepted,
|
||||
payouts_split: m.payouts_split,
|
||||
ordering: m.ordering,
|
||||
|
||||
@ -42,8 +42,7 @@ impl ThreadMessageBuilder {
|
||||
&self,
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<ThreadMessageId, DatabaseError> {
|
||||
let thread_message_id =
|
||||
generate_thread_message_id(&mut *transaction).await?;
|
||||
let thread_message_id = generate_thread_message_id(&mut *transaction).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
@ -76,14 +75,16 @@ impl ThreadBuilder {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO threads (
|
||||
id, thread_type
|
||||
id, thread_type, report_id, project_id
|
||||
)
|
||||
VALUES (
|
||||
$1, $2
|
||||
$1, $2, $3, $4
|
||||
)
|
||||
",
|
||||
thread_id as ThreadId,
|
||||
self.type_.as_str(),
|
||||
self.report_id.map(|x| x.0),
|
||||
self.project_id.map(|x| x.0),
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
@ -110,10 +111,7 @@ impl ThreadBuilder {
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
pub async fn get<'a, E>(
|
||||
id: ThreadId,
|
||||
exec: E,
|
||||
) -> Result<Option<Thread>, sqlx::Error>
|
||||
pub async fn get<'a, E>(id: ThreadId, exec: E) -> Result<Option<Thread>, sqlx::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
@ -131,8 +129,7 @@ impl Thread {
|
||||
{
|
||||
use futures::stream::TryStreamExt;
|
||||
|
||||
let thread_ids_parsed: Vec<i64> =
|
||||
thread_ids.iter().map(|x| x.0).collect();
|
||||
let thread_ids_parsed: Vec<i64> = thread_ids.iter().map(|x| x.0).collect();
|
||||
let threads = sqlx::query!(
|
||||
"
|
||||
SELECT t.id, t.thread_type, t.show_in_mod_inbox, t.project_id, t.report_id,
|
||||
@ -230,8 +227,7 @@ impl ThreadMessage {
|
||||
{
|
||||
use futures::stream::TryStreamExt;
|
||||
|
||||
let message_ids_parsed: Vec<i64> =
|
||||
message_ids.iter().map(|x| x.0).collect();
|
||||
let message_ids_parsed: Vec<i64> = 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
|
||||
@ -246,8 +242,7 @@ impl ThreadMessage {
|
||||
id: ThreadMessageId(x.id),
|
||||
thread_id: ThreadId(x.thread_id),
|
||||
author_id: x.author_id.map(UserId),
|
||||
body: serde_json::from_value(x.body)
|
||||
.unwrap_or(MessageBody::Deleted),
|
||||
body: serde_json::from_value(x.body).unwrap_or(MessageBody::Deleted),
|
||||
created: x.created,
|
||||
}))
|
||||
})
|
||||
@ -268,8 +263,7 @@ impl ThreadMessage {
|
||||
WHERE id = $1
|
||||
",
|
||||
id as ThreadMessageId,
|
||||
serde_json::to_value(MessageBody::Deleted)
|
||||
.unwrap_or(serde_json::json!({}))
|
||||
serde_json::to_value(MessageBody::Deleted).unwrap_or(serde_json::json!({}))
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
@ -50,10 +50,7 @@ impl User {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub async fn get<'a, 'b, E>(
|
||||
id: UserId,
|
||||
executor: E,
|
||||
) -> Result<Option<Self>, sqlx::error::Error>
|
||||
pub async fn get<'a, 'b, E>(id: UserId, executor: E) -> Result<Option<Self>, sqlx::error::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
@ -95,12 +92,9 @@ impl User {
|
||||
bio: row.bio,
|
||||
created: row.created,
|
||||
role: row.role,
|
||||
badges: Badges::from_bits(row.badges as u64)
|
||||
.unwrap_or_default(),
|
||||
badges: Badges::from_bits(row.badges as u64).unwrap_or_default(),
|
||||
balance: row.balance,
|
||||
payout_wallet: row
|
||||
.payout_wallet
|
||||
.map(|x| RecipientWallet::from_string(&x)),
|
||||
payout_wallet: row.payout_wallet.map(|x| RecipientWallet::from_string(&x)),
|
||||
payout_wallet_type: row
|
||||
.payout_wallet_type
|
||||
.map(|x| RecipientType::from_string(&x)),
|
||||
@ -144,12 +138,9 @@ impl User {
|
||||
bio: row.bio,
|
||||
created: row.created,
|
||||
role: row.role,
|
||||
badges: Badges::from_bits(row.badges as u64)
|
||||
.unwrap_or_default(),
|
||||
badges: Badges::from_bits(row.badges as u64).unwrap_or_default(),
|
||||
balance: row.balance,
|
||||
payout_wallet: row
|
||||
.payout_wallet
|
||||
.map(|x| RecipientWallet::from_string(&x)),
|
||||
payout_wallet: row.payout_wallet.map(|x| RecipientWallet::from_string(&x)),
|
||||
payout_wallet_type: row
|
||||
.payout_wallet_type
|
||||
.map(|x| RecipientType::from_string(&x)),
|
||||
@ -160,10 +151,7 @@ impl User {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_many<'a, E>(
|
||||
user_ids: &[UserId],
|
||||
exec: E,
|
||||
) -> Result<Vec<User>, sqlx::Error>
|
||||
pub async fn get_many<'a, E>(user_ids: &[UserId], exec: E) -> Result<Vec<User>, sqlx::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
@ -196,12 +184,8 @@ impl User {
|
||||
role: u.role,
|
||||
badges: Badges::from_bits(u.badges as u64).unwrap_or_default(),
|
||||
balance: u.balance,
|
||||
payout_wallet: u
|
||||
.payout_wallet
|
||||
.map(|x| RecipientWallet::from_string(&x)),
|
||||
payout_wallet_type: u
|
||||
.payout_wallet_type
|
||||
.map(|x| RecipientType::from_string(&x)),
|
||||
payout_wallet: u.payout_wallet.map(|x| RecipientWallet::from_string(&x)),
|
||||
payout_wallet_type: u.payout_wallet_type.map(|x| RecipientType::from_string(&x)),
|
||||
payout_address: u.payout_address,
|
||||
}))
|
||||
})
|
||||
@ -384,11 +368,8 @@ impl User {
|
||||
.await?;
|
||||
|
||||
for project_id in projects {
|
||||
let _result = super::project_item::Project::remove_full(
|
||||
project_id,
|
||||
transaction,
|
||||
)
|
||||
.await?;
|
||||
let _result =
|
||||
super::project_item::Project::remove_full(project_id, transaction).await?;
|
||||
}
|
||||
|
||||
let notifications: Vec<i64> = sqlx::query!(
|
||||
@ -489,8 +470,7 @@ impl User {
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
let id_option =
|
||||
crate::models::ids::base62_impl::parse_base62(username_or_id).ok();
|
||||
let id_option = crate::models::ids::base62_impl::parse_base62(username_or_id).ok();
|
||||
|
||||
if let Some(id) = id_option {
|
||||
let id = UserId(id as i64);
|
||||
|
||||
@ -499,8 +499,7 @@ impl Version {
|
||||
{
|
||||
use futures::stream::TryStreamExt;
|
||||
|
||||
let version_ids_parsed: Vec<i64> =
|
||||
version_ids.iter().map(|x| x.0).collect();
|
||||
let version_ids_parsed: Vec<i64> = version_ids.iter().map(|x| x.0).collect();
|
||||
sqlx::query!(
|
||||
"
|
||||
SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,
|
||||
@ -648,8 +647,7 @@ impl Version {
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
let project_id_opt =
|
||||
parse_base62(project_id_or_slug).ok().map(|x| x as i64);
|
||||
let project_id_opt = parse_base62(project_id_or_slug).ok().map(|x| x as i64);
|
||||
let id_opt = parse_base62(slug).ok().map(|x| x as i64);
|
||||
let id = sqlx::query!(
|
||||
"
|
||||
|
||||
@ -6,8 +6,7 @@ use std::time::Duration;
|
||||
|
||||
pub async fn connect() -> Result<PgPool, sqlx::Error> {
|
||||
info!("Initializing database connection");
|
||||
let database_url =
|
||||
dotenvy::var("DATABASE_URL").expect("`DATABASE_URL` not in .env");
|
||||
let database_url = dotenvy::var("DATABASE_URL").expect("`DATABASE_URL` not in .env");
|
||||
let pool = PgPoolOptions::new()
|
||||
.min_connections(
|
||||
dotenvy::var("DATABASE_MIN_CONNECTIONS")
|
||||
|
||||
@ -16,12 +16,10 @@ pub struct BackblazeHost {
|
||||
|
||||
impl BackblazeHost {
|
||||
pub async fn new(key_id: &str, key: &str, bucket_id: &str) -> Self {
|
||||
let authorization_data =
|
||||
authorization::authorize_account(key_id, key).await.unwrap();
|
||||
let upload_url_data =
|
||||
authorization::get_upload_url(&authorization_data, bucket_id)
|
||||
.await
|
||||
.unwrap();
|
||||
let authorization_data = authorization::authorize_account(key_id, key).await.unwrap();
|
||||
let upload_url_data = authorization::get_upload_url(&authorization_data, bucket_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
BackblazeHost {
|
||||
upload_url_data,
|
||||
@ -40,13 +38,8 @@ impl FileHost for BackblazeHost {
|
||||
) -> Result<UploadFileData, FileHostingError> {
|
||||
let content_sha512 = format!("{:x}", sha2::Sha512::digest(&file_bytes));
|
||||
|
||||
let upload_data = upload::upload_file(
|
||||
&self.upload_url_data,
|
||||
content_type,
|
||||
file_name,
|
||||
file_bytes,
|
||||
)
|
||||
.await?;
|
||||
let upload_data =
|
||||
upload::upload_file(&self.upload_url_data, content_type, file_name, file_bytes).await?;
|
||||
Ok(UploadFileData {
|
||||
file_id: upload_data.file_id,
|
||||
file_name: upload_data.file_name,
|
||||
@ -81,12 +74,8 @@ impl FileHost for BackblazeHost {
|
||||
file_id: &str,
|
||||
file_name: &str,
|
||||
) -> Result<DeleteFileData, FileHostingError> {
|
||||
let delete_data = delete::delete_file_version(
|
||||
&self.authorization_data,
|
||||
file_id,
|
||||
file_name,
|
||||
)
|
||||
.await?;
|
||||
let delete_data =
|
||||
delete::delete_file_version(&self.authorization_data, file_id, file_name).await?;
|
||||
Ok(DeleteFileData {
|
||||
file_id: delete_data.file_id,
|
||||
file_name: delete_data.file_name,
|
||||
@ -94,9 +83,7 @@ impl FileHost for BackblazeHost {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn process_response<T>(
|
||||
response: Response,
|
||||
) -> Result<T, FileHostingError>
|
||||
pub async fn process_response<T>(response: Response) -> Result<T, FileHostingError>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
|
||||
@ -56,13 +56,7 @@ pub async fn get_upload_url(
|
||||
bucket_id: &str,
|
||||
) -> Result<UploadUrlData, FileHostingError> {
|
||||
let response = reqwest::Client::new()
|
||||
.post(
|
||||
&format!(
|
||||
"{}/b2api/v2/b2_get_upload_url",
|
||||
authorization_data.api_url
|
||||
)
|
||||
.to_string(),
|
||||
)
|
||||
.post(&format!("{}/b2api/v2/b2_get_upload_url", authorization_data.api_url).to_string())
|
||||
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
||||
.header(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
|
||||
@ -20,12 +20,9 @@ impl FileHost for MockHost {
|
||||
file_name: &str,
|
||||
file_bytes: Bytes,
|
||||
) -> Result<UploadFileData, FileHostingError> {
|
||||
let path =
|
||||
std::path::Path::new(&dotenvy::var("MOCK_FILE_PATH").unwrap())
|
||||
.join(file_name.replace("../", ""));
|
||||
std::fs::create_dir_all(
|
||||
path.parent().ok_or(FileHostingError::InvalidFilename)?,
|
||||
)?;
|
||||
let path = std::path::Path::new(&dotenvy::var("MOCK_FILE_PATH").unwrap())
|
||||
.join(file_name.replace("../", ""));
|
||||
std::fs::create_dir_all(path.parent().ok_or(FileHostingError::InvalidFilename)?)?;
|
||||
let content_sha1 = sha1::Sha1::from(&file_bytes).hexdigest();
|
||||
let content_sha512 = format!("{:x}", sha2::Sha512::digest(&file_bytes));
|
||||
|
||||
@ -47,9 +44,8 @@ impl FileHost for MockHost {
|
||||
file_id: &str,
|
||||
file_name: &str,
|
||||
) -> Result<DeleteFileData, FileHostingError> {
|
||||
let path =
|
||||
std::path::Path::new(&dotenvy::var("MOCK_FILE_PATH").unwrap())
|
||||
.join(file_name.replace("../", ""));
|
||||
let path = std::path::Path::new(&dotenvy::var("MOCK_FILE_PATH").unwrap())
|
||||
.join(file_name.replace("../", ""));
|
||||
std::fs::remove_file(path)?;
|
||||
|
||||
Ok(DeleteFileData {
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
use crate::file_hosting::{
|
||||
DeleteFileData, FileHost, FileHostingError, UploadFileData,
|
||||
};
|
||||
use crate::file_hosting::{DeleteFileData, FileHost, FileHostingError, UploadFileData};
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use chrono::Utc;
|
||||
@ -33,23 +31,12 @@ impl S3Host {
|
||||
endpoint: url.to_string(),
|
||||
}
|
||||
},
|
||||
Credentials::new(
|
||||
Some(access_token),
|
||||
Some(secret),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map_err(|_| {
|
||||
FileHostingError::S3Error(
|
||||
"Error while creating credentials".to_string(),
|
||||
)
|
||||
Credentials::new(Some(access_token), Some(secret), None, None, None).map_err(|_| {
|
||||
FileHostingError::S3Error("Error while creating credentials".to_string())
|
||||
})?,
|
||||
)
|
||||
.map_err(|_| {
|
||||
FileHostingError::S3Error(
|
||||
"Error while creating Bucket instance".to_string(),
|
||||
)
|
||||
FileHostingError::S3Error("Error while creating Bucket instance".to_string())
|
||||
})?;
|
||||
|
||||
Ok(S3Host { bucket })
|
||||
@ -68,16 +55,10 @@ impl FileHost for S3Host {
|
||||
let content_sha512 = format!("{:x}", sha2::Sha512::digest(&file_bytes));
|
||||
|
||||
self.bucket
|
||||
.put_object_with_content_type(
|
||||
format!("/{file_name}"),
|
||||
&file_bytes,
|
||||
content_type,
|
||||
)
|
||||
.put_object_with_content_type(format!("/{file_name}"), &file_bytes, content_type)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
FileHostingError::S3Error(
|
||||
"Error while uploading file to S3".to_string(),
|
||||
)
|
||||
FileHostingError::S3Error("Error while uploading file to S3".to_string())
|
||||
})?;
|
||||
|
||||
Ok(UploadFileData {
|
||||
@ -101,9 +82,7 @@ impl FileHost for S3Host {
|
||||
.delete_object(format!("/{file_name}"))
|
||||
.await
|
||||
.map_err(|_| {
|
||||
FileHostingError::S3Error(
|
||||
"Error while deleting file from S3".to_string(),
|
||||
)
|
||||
FileHostingError::S3Error("Error while deleting file from S3".to_string())
|
||||
})?;
|
||||
|
||||
Ok(DeleteFileData {
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
use actix_web::web;
|
||||
use sqlx::PgPool;
|
||||
|
||||
pub async fn test_database(
|
||||
postgres: web::Data<PgPool>,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
pub async fn test_database(postgres: web::Data<PgPool>) -> Result<(), sqlx::Error> {
|
||||
let mut transaction = postgres.acquire().await?;
|
||||
sqlx::query(
|
||||
"
|
||||
|
||||
153
src/main.rs
153
src/main.rs
@ -35,8 +35,7 @@ pub struct Pepper {
|
||||
#[actix_rt::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
dotenvy::dotenv().ok();
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("info"))
|
||||
.init();
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
|
||||
|
||||
if check_env_vars() {
|
||||
error!("Some environment variables are missing!");
|
||||
@ -75,40 +74,37 @@ async fn main() -> std::io::Result<()> {
|
||||
.await
|
||||
.expect("Database connection failed");
|
||||
|
||||
let storage_backend =
|
||||
dotenvy::var("STORAGE_BACKEND").unwrap_or_else(|_| "local".to_string());
|
||||
let storage_backend = dotenvy::var("STORAGE_BACKEND").unwrap_or_else(|_| "local".to_string());
|
||||
|
||||
let file_host: Arc<dyn file_hosting::FileHost + Send + Sync> =
|
||||
match storage_backend.as_str() {
|
||||
"backblaze" => Arc::new(
|
||||
file_hosting::BackblazeHost::new(
|
||||
&dotenvy::var("BACKBLAZE_KEY_ID").unwrap(),
|
||||
&dotenvy::var("BACKBLAZE_KEY").unwrap(),
|
||||
&dotenvy::var("BACKBLAZE_BUCKET_ID").unwrap(),
|
||||
)
|
||||
.await,
|
||||
),
|
||||
"s3" => Arc::new(
|
||||
S3Host::new(
|
||||
&dotenvy::var("S3_BUCKET_NAME").unwrap(),
|
||||
&dotenvy::var("S3_REGION").unwrap(),
|
||||
&dotenvy::var("S3_URL").unwrap(),
|
||||
&dotenvy::var("S3_ACCESS_TOKEN").unwrap(),
|
||||
&dotenvy::var("S3_SECRET").unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
"local" => Arc::new(file_hosting::MockHost::new()),
|
||||
_ => panic!("Invalid storage backend specified. Aborting startup!"),
|
||||
};
|
||||
let file_host: Arc<dyn file_hosting::FileHost + Send + Sync> = match storage_backend.as_str() {
|
||||
"backblaze" => Arc::new(
|
||||
file_hosting::BackblazeHost::new(
|
||||
&dotenvy::var("BACKBLAZE_KEY_ID").unwrap(),
|
||||
&dotenvy::var("BACKBLAZE_KEY").unwrap(),
|
||||
&dotenvy::var("BACKBLAZE_BUCKET_ID").unwrap(),
|
||||
)
|
||||
.await,
|
||||
),
|
||||
"s3" => Arc::new(
|
||||
S3Host::new(
|
||||
&dotenvy::var("S3_BUCKET_NAME").unwrap(),
|
||||
&dotenvy::var("S3_REGION").unwrap(),
|
||||
&dotenvy::var("S3_URL").unwrap(),
|
||||
&dotenvy::var("S3_ACCESS_TOKEN").unwrap(),
|
||||
&dotenvy::var("S3_SECRET").unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
"local" => Arc::new(file_hosting::MockHost::new()),
|
||||
_ => panic!("Invalid storage backend specified. Aborting startup!"),
|
||||
};
|
||||
|
||||
let mut scheduler = scheduler::Scheduler::new();
|
||||
|
||||
// The interval in seconds at which the local database is indexed
|
||||
// for searching. Defaults to 1 hour if unset.
|
||||
let local_index_interval = std::time::Duration::from_secs(
|
||||
parse_var("LOCAL_INDEX_INTERVAL").unwrap_or(3600),
|
||||
);
|
||||
let local_index_interval =
|
||||
std::time::Duration::from_secs(parse_var("LOCAL_INDEX_INTERVAL").unwrap_or(3600));
|
||||
|
||||
let pool_ref = pool.clone();
|
||||
let search_config_ref = search_config.clone();
|
||||
@ -118,8 +114,7 @@ async fn main() -> std::io::Result<()> {
|
||||
async move {
|
||||
info!("Indexing local database");
|
||||
let settings = IndexingSettings { index_local: true };
|
||||
let result =
|
||||
index_projects(pool_ref, settings, &search_config_ref).await;
|
||||
let result = index_projects(pool_ref, settings, &search_config_ref).await;
|
||||
if let Err(e) = result {
|
||||
warn!("Local project indexing failed: {:?}", e);
|
||||
}
|
||||
@ -170,14 +165,11 @@ async fn main() -> std::io::Result<()> {
|
||||
",
|
||||
crate::models::projects::ProjectStatus::Scheduled.as_str(),
|
||||
)
|
||||
.execute(&pool_ref)
|
||||
.await;
|
||||
.execute(&pool_ref)
|
||||
.await;
|
||||
|
||||
if let Err(e) = projects_results {
|
||||
warn!(
|
||||
"Syncing scheduled releases for projects failed: {:?}",
|
||||
e
|
||||
);
|
||||
warn!("Syncing scheduled releases for projects failed: {:?}", e);
|
||||
}
|
||||
|
||||
let versions_results = sqlx::query!(
|
||||
@ -188,21 +180,18 @@ async fn main() -> std::io::Result<()> {
|
||||
",
|
||||
crate::models::projects::VersionStatus::Scheduled.as_str(),
|
||||
)
|
||||
.execute(&pool_ref)
|
||||
.await;
|
||||
.execute(&pool_ref)
|
||||
.await;
|
||||
|
||||
if let Err(e) = versions_results {
|
||||
warn!(
|
||||
"Syncing scheduled releases for versions failed: {:?}",
|
||||
e
|
||||
);
|
||||
warn!("Syncing scheduled releases for versions failed: {:?}", e);
|
||||
}
|
||||
|
||||
info!("Finished releasing scheduled versions/projects");
|
||||
}
|
||||
});
|
||||
|
||||
// Reminding moderators to review projects which have been in the queue longer than 24hr
|
||||
// Reminding moderators to review projects which have been in the queue longer than 40hr
|
||||
let pool_ref = pool.clone();
|
||||
let webhook_message_sent = Arc::new(Mutex::new(Vec::<(
|
||||
database::models::ProjectId,
|
||||
@ -212,7 +201,7 @@ async fn main() -> std::io::Result<()> {
|
||||
scheduler.run(std::time::Duration::from_secs(10 * 60), move || {
|
||||
let pool_ref = pool_ref.clone();
|
||||
let webhook_message_sent_ref = webhook_message_sent.clone();
|
||||
info!("Checking reviewed projects submitted more than 24hrs ago");
|
||||
info!("Checking reviewed projects submitted more than 40hrs ago");
|
||||
|
||||
async move {
|
||||
let do_steps = async {
|
||||
@ -221,7 +210,7 @@ async fn main() -> std::io::Result<()> {
|
||||
let project_ids = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM mods
|
||||
WHERE status = $1 AND queued < NOW() - INTERVAL '1 day'
|
||||
WHERE status = $1 AND queued < NOW() - INTERVAL '40 hours'
|
||||
ORDER BY updated ASC
|
||||
",
|
||||
crate::models::projects::ProjectStatus::Processing.as_str(),
|
||||
@ -247,7 +236,7 @@ async fn main() -> std::io::Result<()> {
|
||||
project.into(),
|
||||
&pool_ref,
|
||||
webhook_url,
|
||||
Some("<@&783155186491195394> This project has been in the queue for over 24 hours!".to_string()),
|
||||
Some("<@&783155186491195394> This project has been in the queue for over 40 hours!".to_string()),
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
@ -261,12 +250,12 @@ async fn main() -> std::io::Result<()> {
|
||||
|
||||
if let Err(e) = do_steps.await {
|
||||
warn!(
|
||||
"Checking reviewed projects submitted more than 24hrs ago failed: {:?}",
|
||||
"Checking reviewed projects submitted more than 40hrs ago failed: {:?}",
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
info!("Finished checking reviewed projects submitted more than 24hrs ago");
|
||||
info!("Finished checking reviewed projects submitted more than 40hrs ago");
|
||||
}
|
||||
});
|
||||
|
||||
@ -291,8 +280,7 @@ async fn main() -> std::io::Result<()> {
|
||||
});
|
||||
|
||||
let ip_salt = Pepper {
|
||||
pepper: models::ids::Base62Id(models::ids::random_base62(11))
|
||||
.to_string(),
|
||||
pepper: models::ids::Base62Id(models::ids::random_base62(11)).to_string(),
|
||||
};
|
||||
|
||||
let payouts_queue = Arc::new(Mutex::new(PayoutsQueue::new()));
|
||||
@ -317,48 +305,43 @@ async fn main() -> std::io::Result<()> {
|
||||
RateLimiter::new(MemoryStoreActor::from(store.clone()).start())
|
||||
.with_identifier(|req| {
|
||||
let connection_info = req.connection_info();
|
||||
let ip = String::from(
|
||||
if parse_var("CLOUDFLARE_INTEGRATION")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
if let Some(header) =
|
||||
req.headers().get("CF-Connecting-IP")
|
||||
{
|
||||
header
|
||||
.to_str()
|
||||
.map_err(|_| ARError::Identification)?
|
||||
let ip =
|
||||
String::from(if parse_var("CLOUDFLARE_INTEGRATION").unwrap_or(false) {
|
||||
if let Some(header) = req.headers().get("CF-Connecting-IP") {
|
||||
header.to_str().map_err(|_| ARError::Identification)?
|
||||
} else {
|
||||
connection_info
|
||||
.peer_addr()
|
||||
.ok_or(ARError::Identification)?
|
||||
connection_info.peer_addr().ok_or(ARError::Identification)?
|
||||
}
|
||||
} else {
|
||||
connection_info
|
||||
.peer_addr()
|
||||
.ok_or(ARError::Identification)?
|
||||
},
|
||||
);
|
||||
connection_info.peer_addr().ok_or(ARError::Identification)?
|
||||
});
|
||||
|
||||
Ok(ip)
|
||||
})
|
||||
.with_interval(std::time::Duration::from_secs(60))
|
||||
.with_max_requests(300)
|
||||
.with_ignore_key(
|
||||
dotenvy::var("RATE_LIMIT_IGNORE_KEY").ok(),
|
||||
),
|
||||
.with_ignore_key(dotenvy::var("RATE_LIMIT_IGNORE_KEY").ok()),
|
||||
)
|
||||
.app_data(
|
||||
web::FormConfig::default().error_handler(|err, _req| {
|
||||
routes::ApiError::Validation(err.to_string()).into()
|
||||
}),
|
||||
)
|
||||
.app_data(
|
||||
web::PathConfig::default().error_handler(|err, _req| {
|
||||
routes::ApiError::Validation(err.to_string()).into()
|
||||
}),
|
||||
)
|
||||
.app_data(
|
||||
web::QueryConfig::default().error_handler(|err, _req| {
|
||||
routes::ApiError::Validation(err.to_string()).into()
|
||||
}),
|
||||
)
|
||||
.app_data(
|
||||
web::JsonConfig::default().error_handler(|err, _req| {
|
||||
routes::ApiError::Validation(err.to_string()).into()
|
||||
}),
|
||||
)
|
||||
.app_data(web::FormConfig::default().error_handler(|err, _req| {
|
||||
routes::ApiError::Validation(err.to_string()).into()
|
||||
}))
|
||||
.app_data(web::PathConfig::default().error_handler(|err, _req| {
|
||||
routes::ApiError::Validation(err.to_string()).into()
|
||||
}))
|
||||
.app_data(web::QueryConfig::default().error_handler(|err, _req| {
|
||||
routes::ApiError::Validation(err.to_string()).into()
|
||||
}))
|
||||
.app_data(web::JsonConfig::default().error_handler(|err, _req| {
|
||||
routes::ApiError::Validation(err.to_string()).into()
|
||||
}))
|
||||
.app_data(web::Data::new(pool.clone()))
|
||||
.app_data(web::Data::new(file_host.clone()))
|
||||
.app_data(web::Data::new(search_config.clone()))
|
||||
|
||||
@ -131,10 +131,7 @@ pub mod base62_impl {
|
||||
impl<'de> Visitor<'de> for Base62Visitor {
|
||||
type Value = Base62Id;
|
||||
|
||||
fn expecting(
|
||||
&self,
|
||||
formatter: &mut std::fmt::Formatter,
|
||||
) -> std::fmt::Result {
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a base62 string id")
|
||||
}
|
||||
|
||||
@ -190,9 +187,7 @@ pub mod base62_impl {
|
||||
}
|
||||
|
||||
// We don't want this panicking or wrapping on integer overflow
|
||||
if let Some(n) =
|
||||
num.checked_mul(62).and_then(|n| n.checked_add(next_digit))
|
||||
{
|
||||
if let Some(n) = num.checked_mul(62).and_then(|n| n.checked_add(next_digit)) {
|
||||
num = n;
|
||||
} else {
|
||||
return Err(DecodingError::Overflow);
|
||||
|
||||
@ -2,9 +2,7 @@ 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::ids::{ProjectId, ReportId, TeamId, ThreadId, ThreadMessageId, VersionId};
|
||||
use crate::models::projects::ProjectStatus;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -91,27 +89,18 @@ impl From<DBNotification> for Notification {
|
||||
} => (
|
||||
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!("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"),
|
||||
),
|
||||
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)
|
||||
),
|
||||
format!("team/{team_id}/members/{}", UserId::from(notif.user_id)),
|
||||
),
|
||||
},
|
||||
],
|
||||
|
||||
@ -30,9 +30,7 @@ pub struct PackFile {
|
||||
pub file_size: u32,
|
||||
}
|
||||
|
||||
fn validate_download_url(
|
||||
values: &[String],
|
||||
) -> Result<(), validator::ValidationError> {
|
||||
fn validate_download_url(values: &[String]) -> Result<(), validator::ValidationError> {
|
||||
for value in values {
|
||||
let url = url::Url::parse(value)
|
||||
.ok()
|
||||
@ -42,8 +40,7 @@ fn validate_download_url(
|
||||
return Err(validator::ValidationError::new("invalid URL"));
|
||||
}
|
||||
|
||||
let domains = parse_strings_from_var("WHITELISTED_MODPACK_DOMAINS")
|
||||
.unwrap_or_default();
|
||||
let domains = parse_strings_from_var("WHITELISTED_MODPACK_DOMAINS").unwrap_or_default();
|
||||
if !domains.contains(
|
||||
&url.domain()
|
||||
.ok_or_else(|| validator::ValidationError::new("invalid URL"))?
|
||||
|
||||
@ -534,16 +534,10 @@ impl From<QueryVersion> for Version {
|
||||
version_id: d.version_id.map(|i| VersionId(i.0 as u64)),
|
||||
project_id: d.project_id.map(|i| ProjectId(i.0 as u64)),
|
||||
file_name: d.file_name,
|
||||
dependency_type: DependencyType::from_str(
|
||||
d.dependency_type.as_str(),
|
||||
),
|
||||
dependency_type: DependencyType::from_str(d.dependency_type.as_str()),
|
||||
})
|
||||
.collect(),
|
||||
game_versions: data
|
||||
.game_versions
|
||||
.into_iter()
|
||||
.map(GameVersion)
|
||||
.collect(),
|
||||
game_versions: data.game_versions.into_iter().map(GameVersion).collect(),
|
||||
loaders: data.loaders.into_iter().map(Loader).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,36 +68,25 @@ impl PayoutsQueue {
|
||||
.form(&form)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ApiError::Payments(
|
||||
"Error while authenticating with PayPal".to_string(),
|
||||
)
|
||||
})?
|
||||
.map_err(|_| ApiError::Payments("Error while authenticating with PayPal".to_string()))?
|
||||
.json()
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ApiError::Payments(
|
||||
"Error while authenticating with PayPal (deser error)"
|
||||
.to_string(),
|
||||
"Error while authenticating with PayPal (deser error)".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
self.credential_expires =
|
||||
Utc::now() + Duration::seconds(credential.expires_in);
|
||||
self.credential_expires = Utc::now() + Duration::seconds(credential.expires_in);
|
||||
self.credential = credential;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_payout(
|
||||
&mut self,
|
||||
mut payout: PayoutItem,
|
||||
) -> Result<Decimal, ApiError> {
|
||||
pub async fn send_payout(&mut self, mut payout: PayoutItem) -> Result<Decimal, ApiError> {
|
||||
if self.credential_expires < Utc::now() {
|
||||
self.refresh_token().await.map_err(|_| {
|
||||
ApiError::Payments(
|
||||
"Error while authenticating with PayPal".to_string(),
|
||||
)
|
||||
ApiError::Payments("Error while authenticating with PayPal".to_string())
|
||||
})?;
|
||||
}
|
||||
|
||||
@ -109,8 +98,7 @@ impl PayoutsQueue {
|
||||
std::cmp::min(
|
||||
std::cmp::max(
|
||||
Decimal::ONE / Decimal::from(4),
|
||||
(Decimal::from(2) / Decimal::ONE_HUNDRED)
|
||||
* payout.amount.value,
|
||||
(Decimal::from(2) / Decimal::ONE_HUNDRED) * payout.amount.value,
|
||||
),
|
||||
Decimal::from(20),
|
||||
)
|
||||
@ -151,9 +139,7 @@ impl PayoutsQueue {
|
||||
}
|
||||
|
||||
let body: PayPalError = res.json().await.map_err(|_| {
|
||||
ApiError::Payments(
|
||||
"Error while registering payment in PayPal!".to_string(),
|
||||
)
|
||||
ApiError::Payments("Error while registering payment in PayPal!".to_string())
|
||||
})?;
|
||||
|
||||
return Err(ApiError::Payments(format!(
|
||||
@ -190,8 +176,7 @@ impl PayoutsQueue {
|
||||
"Authorization",
|
||||
format!(
|
||||
"{} {}",
|
||||
self.credential.token_type,
|
||||
self.credential.access_token
|
||||
self.credential.token_type, self.credential.access_token
|
||||
),
|
||||
)
|
||||
.send()
|
||||
@ -199,9 +184,7 @@ impl PayoutsQueue {
|
||||
{
|
||||
if let Ok(res) = res.json::<PayoutData>().await {
|
||||
if let Some(data) = res.items.first() {
|
||||
if (fee - data.payout_item_fee.value)
|
||||
> Decimal::ZERO
|
||||
{
|
||||
if (fee - data.payout_item_fee.value) > Decimal::ZERO {
|
||||
return Ok(fee - data.payout_item_fee.value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,27 +35,18 @@ impl ResponseError for ARError {
|
||||
reset,
|
||||
} => {
|
||||
let mut response = actix_web::HttpResponse::TooManyRequests();
|
||||
response.insert_header((
|
||||
"x-ratelimit-limit",
|
||||
max_requests.to_string(),
|
||||
));
|
||||
response.insert_header((
|
||||
"x-ratelimit-remaining",
|
||||
remaining.to_string(),
|
||||
));
|
||||
response
|
||||
.insert_header(("x-ratelimit-reset", reset.to_string()));
|
||||
response.insert_header(("x-ratelimit-limit", max_requests.to_string()));
|
||||
response.insert_header(("x-ratelimit-remaining", remaining.to_string()));
|
||||
response.insert_header(("x-ratelimit-reset", reset.to_string()));
|
||||
response.json(ApiError {
|
||||
error: "ratelimit_error",
|
||||
description: &self.to_string(),
|
||||
})
|
||||
}
|
||||
_ => actix_web::HttpResponse::build(self.status_code()).json(
|
||||
ApiError {
|
||||
error: "ratelimit_error",
|
||||
description: &self.to_string(),
|
||||
},
|
||||
),
|
||||
_ => actix_web::HttpResponse::build(self.status_code()).json(ApiError {
|
||||
error: "ratelimit_error",
|
||||
description: &self.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,9 +36,9 @@ impl MemoryStore {
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
debug!("Creating new MemoryStore");
|
||||
MemoryStore {
|
||||
inner: Arc::new(
|
||||
DashMap::<String, (usize, Duration)>::with_capacity(capacity),
|
||||
),
|
||||
inner: Arc::new(DashMap::<String, (usize, Duration)>::with_capacity(
|
||||
capacity,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -74,18 +74,10 @@ impl Supervised for MemoryStoreActor {
|
||||
|
||||
impl Handler<ActorMessage> for MemoryStoreActor {
|
||||
type Result = ActorResponse;
|
||||
fn handle(
|
||||
&mut self,
|
||||
msg: ActorMessage,
|
||||
ctx: &mut Self::Context,
|
||||
) -> Self::Result {
|
||||
fn handle(&mut self, msg: ActorMessage, ctx: &mut Self::Context) -> Self::Result {
|
||||
match msg {
|
||||
ActorMessage::Set { key, value, expiry } => {
|
||||
debug!(
|
||||
"Inserting key {} with expiry {}",
|
||||
&key,
|
||||
&expiry.as_secs()
|
||||
);
|
||||
debug!("Inserting key {} with expiry {}", &key, &expiry.as_secs());
|
||||
let future_key = String::from(&key);
|
||||
let now = SystemTime::now();
|
||||
let now = now.duration_since(UNIX_EPOCH).unwrap();
|
||||
@ -93,10 +85,7 @@ impl Handler<ActorMessage> for MemoryStoreActor {
|
||||
ctx.notify_later(ActorMessage::Remove(future_key), expiry);
|
||||
ActorResponse::Set(Box::pin(future::ready(Ok(()))))
|
||||
}
|
||||
ActorMessage::Update { key, value } => match self
|
||||
.inner
|
||||
.get_mut(&key)
|
||||
{
|
||||
ActorMessage::Update { key, value } => match self.inner.get_mut(&key) {
|
||||
Some(mut c) => {
|
||||
let val_mut: &mut (usize, Duration) = c.value_mut();
|
||||
if val_mut.0 > value {
|
||||
@ -107,22 +96,18 @@ impl Handler<ActorMessage> for MemoryStoreActor {
|
||||
let new_val = val_mut.0;
|
||||
ActorResponse::Update(Box::pin(future::ready(Ok(new_val))))
|
||||
}
|
||||
None => ActorResponse::Update(Box::pin(future::ready(Err(
|
||||
ARError::ReadWrite(
|
||||
"memory store: read failed!".to_string(),
|
||||
),
|
||||
)))),
|
||||
None => ActorResponse::Update(Box::pin(future::ready(Err(ARError::ReadWrite(
|
||||
"memory store: read failed!".to_string(),
|
||||
))))),
|
||||
},
|
||||
ActorMessage::Get(key) => {
|
||||
if self.inner.contains_key(&key) {
|
||||
let val = match self.inner.get(&key) {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
return ActorResponse::Get(Box::pin(future::ready(
|
||||
Err(ARError::ReadWrite(
|
||||
"memory store: read failed!".to_string(),
|
||||
)),
|
||||
)))
|
||||
return ActorResponse::Get(Box::pin(future::ready(Err(
|
||||
ARError::ReadWrite("memory store: read failed!".to_string()),
|
||||
))))
|
||||
}
|
||||
};
|
||||
let val = val.value().0;
|
||||
@ -135,17 +120,14 @@ impl Handler<ActorMessage> for MemoryStoreActor {
|
||||
let c = match self.inner.get(&key) {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
return ActorResponse::Expire(Box::pin(future::ready(
|
||||
Err(ARError::ReadWrite(
|
||||
"memory store: read failed!".to_string(),
|
||||
)),
|
||||
)))
|
||||
return ActorResponse::Expire(Box::pin(future::ready(Err(
|
||||
ARError::ReadWrite("memory store: read failed!".to_string()),
|
||||
))))
|
||||
}
|
||||
};
|
||||
let dur = c.value().1;
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
let res =
|
||||
dur.checked_sub(now).unwrap_or_else(|| Duration::new(0, 0));
|
||||
let res = dur.checked_sub(now).unwrap_or_else(|| Duration::new(0, 0));
|
||||
ActorResponse::Expire(Box::pin(future::ready(Ok(res))))
|
||||
}
|
||||
ActorMessage::Remove(key) => {
|
||||
@ -153,11 +135,9 @@ impl Handler<ActorMessage> for MemoryStoreActor {
|
||||
let val = match self.inner.remove::<String>(&key) {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
return ActorResponse::Remove(Box::pin(future::ready(
|
||||
Err(ARError::ReadWrite(
|
||||
"memory store: remove failed!".to_string(),
|
||||
)),
|
||||
)))
|
||||
return ActorResponse::Remove(Box::pin(future::ready(Err(
|
||||
ARError::ReadWrite("memory store: remove failed!".to_string()),
|
||||
))))
|
||||
}
|
||||
};
|
||||
let val = val.1;
|
||||
|
||||
@ -18,8 +18,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
type RateLimiterIdentifier =
|
||||
Rc<Box<dyn Fn(&ServiceRequest) -> Result<String, ARError> + 'static>>;
|
||||
type RateLimiterIdentifier = Rc<Box<dyn Fn(&ServiceRequest) -> Result<String, ARError> + 'static>>;
|
||||
|
||||
pub struct RateLimiter<T>
|
||||
where
|
||||
@ -42,8 +41,7 @@ where
|
||||
pub fn new(store: Addr<T>) -> Self {
|
||||
let identifier = |req: &ServiceRequest| {
|
||||
let connection_info = req.connection_info();
|
||||
let ip =
|
||||
connection_info.peer_addr().ok_or(ARError::Identification)?;
|
||||
let ip = connection_info.peer_addr().ok_or(ARError::Identification)?;
|
||||
Ok(String::from(ip))
|
||||
};
|
||||
RateLimiter {
|
||||
@ -74,9 +72,7 @@ where
|
||||
}
|
||||
|
||||
/// Function to get the identifier for the client request
|
||||
pub fn with_identifier<
|
||||
F: Fn(&ServiceRequest) -> Result<String, ARError> + 'static,
|
||||
>(
|
||||
pub fn with_identifier<F: Fn(&ServiceRequest) -> Result<String, ARError> + 'static>(
|
||||
mut self,
|
||||
identifier: F,
|
||||
) -> Self {
|
||||
@ -89,8 +85,7 @@ impl<T, S, B> Transform<S, ServiceRequest> for RateLimiter<T>
|
||||
where
|
||||
T: Handler<ActorMessage> + Send + Sync + 'static,
|
||||
T::Context: ToEnvelope<T, ActorMessage>,
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = AWError>
|
||||
+ 'static,
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = AWError> + 'static,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
@ -130,21 +125,16 @@ where
|
||||
impl<T, S, B> Service<ServiceRequest> for RateLimitMiddleware<S, T>
|
||||
where
|
||||
T: Handler<ActorMessage> + 'static,
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = AWError>
|
||||
+ 'static,
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = AWError> + 'static,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
T::Context: ToEnvelope<T, ActorMessage>,
|
||||
{
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = S::Error;
|
||||
type Future =
|
||||
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
|
||||
|
||||
fn poll_ready(
|
||||
&self,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), Self::Error>> {
|
||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.service.borrow_mut().poll_ready(cx)
|
||||
}
|
||||
|
||||
@ -178,15 +168,9 @@ where
|
||||
if let Some(c) = opt {
|
||||
// Existing entry in store
|
||||
let expiry = store
|
||||
.send(ActorMessage::Expire(String::from(
|
||||
&identifier,
|
||||
)))
|
||||
.send(ActorMessage::Expire(String::from(&identifier)))
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ARError::ReadWrite(
|
||||
"Setting timeout".to_string(),
|
||||
)
|
||||
})?;
|
||||
.map_err(|_| ARError::ReadWrite("Setting timeout".to_string()))?;
|
||||
let reset: Duration = match expiry {
|
||||
ActorResponse::Expire(dur) => dur.await?,
|
||||
_ => unreachable!(),
|
||||
@ -208,9 +192,7 @@ where
|
||||
})
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ARError::ReadWrite(
|
||||
"Decrementing ratelimit".to_string(),
|
||||
)
|
||||
ARError::ReadWrite("Decrementing ratelimit".to_string())
|
||||
})?;
|
||||
let updated_value: usize = match res {
|
||||
ActorResponse::Update(c) => c.await?,
|
||||
@ -223,23 +205,15 @@ where
|
||||
// Safe unwraps, since usize is always convertible to string
|
||||
headers.insert(
|
||||
HeaderName::from_static("x-ratelimit-limit"),
|
||||
HeaderValue::from_str(
|
||||
max_requests.to_string().as_str(),
|
||||
)?,
|
||||
HeaderValue::from_str(max_requests.to_string().as_str())?,
|
||||
);
|
||||
headers.insert(
|
||||
HeaderName::from_static(
|
||||
"x-ratelimit-remaining",
|
||||
),
|
||||
HeaderValue::from_str(
|
||||
updated_value.to_string().as_str(),
|
||||
)?,
|
||||
HeaderName::from_static("x-ratelimit-remaining"),
|
||||
HeaderValue::from_str(updated_value.to_string().as_str())?,
|
||||
);
|
||||
headers.insert(
|
||||
HeaderName::from_static("x-ratelimit-reset"),
|
||||
HeaderValue::from_str(
|
||||
reset.as_secs().to_string().as_str(),
|
||||
)?,
|
||||
HeaderValue::from_str(reset.as_secs().to_string().as_str())?,
|
||||
);
|
||||
Ok(res)
|
||||
}
|
||||
@ -253,11 +227,7 @@ where
|
||||
expiry: interval,
|
||||
})
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ARError::ReadWrite(
|
||||
"Creating store entry".to_string(),
|
||||
)
|
||||
})?;
|
||||
.map_err(|_| ARError::ReadWrite("Creating store entry".to_string()))?;
|
||||
match res {
|
||||
ActorResponse::Set(c) => c.await?,
|
||||
_ => unreachable!(),
|
||||
@ -268,24 +238,15 @@ where
|
||||
// Safe unwraps, since usize is always convertible to string
|
||||
headers.insert(
|
||||
HeaderName::from_static("x-ratelimit-limit"),
|
||||
HeaderValue::from_str(
|
||||
max_requests.to_string().as_str(),
|
||||
)
|
||||
.unwrap(),
|
||||
HeaderValue::from_str(max_requests.to_string().as_str()).unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
HeaderName::from_static("x-ratelimit-remaining"),
|
||||
HeaderValue::from_str(
|
||||
current_value.to_string().as_str(),
|
||||
)
|
||||
.unwrap(),
|
||||
HeaderValue::from_str(current_value.to_string().as_str()).unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
HeaderName::from_static("x-ratelimit-reset"),
|
||||
HeaderValue::from_str(
|
||||
interval.as_secs().to_string().as_str(),
|
||||
)
|
||||
.unwrap(),
|
||||
HeaderValue::from_str(interval.as_secs().to_string().as_str()).unwrap(),
|
||||
);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@ -68,11 +68,8 @@ pub async fn maven_metadata(
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let project_id = params.into_inner().0;
|
||||
let project_data = database::models::Project::get_from_slug_or_project_id(
|
||||
&project_id,
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
let project_data =
|
||||
database::models::Project::get_from_slug_or_project_id(&project_id, &**pool).await?;
|
||||
|
||||
let data = if let Some(data) = project_data {
|
||||
data
|
||||
@ -150,9 +147,7 @@ fn find_file<'a>(
|
||||
version: &'a QueryVersion,
|
||||
file: &str,
|
||||
) -> Option<&'a QueryFile> {
|
||||
if let Some(selected_file) =
|
||||
version.files.iter().find(|x| x.filename == file)
|
||||
{
|
||||
if let Some(selected_file) = version.files.iter().find(|x| x.filename == file) {
|
||||
return Some(selected_file);
|
||||
}
|
||||
|
||||
@ -193,11 +188,7 @@ pub async fn version_file(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (project_id, vnum, file) = params.into_inner();
|
||||
let project_data =
|
||||
database::models::Project::get_full_from_slug_or_project_id(
|
||||
&project_id,
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
database::models::Project::get_full_from_slug_or_project_id(&project_id, &**pool).await?;
|
||||
|
||||
let project = if let Some(data) = project_data {
|
||||
data
|
||||
@ -229,11 +220,9 @@ pub async fn version_file(
|
||||
return Ok(HttpResponse::NotFound().body(""));
|
||||
};
|
||||
|
||||
let version = if let Some(version) = database::models::Version::get_full(
|
||||
database::models::ids::VersionId(vid.id),
|
||||
&**pool,
|
||||
)
|
||||
.await?
|
||||
let version = if let Some(version) =
|
||||
database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool)
|
||||
.await?
|
||||
{
|
||||
version
|
||||
} else {
|
||||
@ -263,9 +252,7 @@ pub async fn version_file(
|
||||
return Ok(HttpResponse::Ok()
|
||||
.content_type("text/xml")
|
||||
.body(yaserde::ser::to_string(&respdata).map_err(ApiError::Xml)?));
|
||||
} else if let Some(selected_file) =
|
||||
find_file(&project_id, &project, &version, &file)
|
||||
{
|
||||
} else if let Some(selected_file) = find_file(&project_id, &project, &version, &file) {
|
||||
return Ok(HttpResponse::TemporaryRedirect()
|
||||
.append_header(("location", &*selected_file.url))
|
||||
.body(""));
|
||||
@ -282,11 +269,7 @@ pub async fn version_file_sha1(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (project_id, vnum, file) = params.into_inner();
|
||||
let project_data =
|
||||
database::models::Project::get_full_from_slug_or_project_id(
|
||||
&project_id,
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
database::models::Project::get_full_from_slug_or_project_id(&project_id, &**pool).await?;
|
||||
|
||||
let project = if let Some(data) = project_data {
|
||||
data
|
||||
@ -318,11 +301,9 @@ pub async fn version_file_sha1(
|
||||
return Ok(HttpResponse::NotFound().body(""));
|
||||
};
|
||||
|
||||
let version = if let Some(version) = database::models::Version::get_full(
|
||||
database::models::ids::VersionId(vid.id),
|
||||
&**pool,
|
||||
)
|
||||
.await?
|
||||
let version = if let Some(version) =
|
||||
database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool)
|
||||
.await?
|
||||
{
|
||||
version
|
||||
} else {
|
||||
@ -343,11 +324,7 @@ pub async fn version_file_sha512(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (project_id, vnum, file) = params.into_inner();
|
||||
let project_data =
|
||||
database::models::Project::get_full_from_slug_or_project_id(
|
||||
&project_id,
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
database::models::Project::get_full_from_slug_or_project_id(&project_id, &**pool).await?;
|
||||
|
||||
let project = if let Some(data) = project_data {
|
||||
data
|
||||
@ -379,11 +356,9 @@ pub async fn version_file_sha512(
|
||||
return Ok(HttpResponse::NotFound().body(""));
|
||||
};
|
||||
|
||||
let version = if let Some(version) = database::models::Version::get_full(
|
||||
database::models::ids::VersionId(vid.id),
|
||||
&**pool,
|
||||
)
|
||||
.await?
|
||||
let version = if let Some(version) =
|
||||
database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool)
|
||||
.await?
|
||||
{
|
||||
version
|
||||
} else {
|
||||
|
||||
@ -97,30 +97,28 @@ impl actix_web::ResponseError for ApiError {
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::build(self.status_code()).json(
|
||||
crate::models::error::ApiError {
|
||||
error: match self {
|
||||
ApiError::Env(..) => "environment_error",
|
||||
ApiError::SqlxDatabase(..) => "database_error",
|
||||
ApiError::Database(..) => "database_error",
|
||||
ApiError::Authentication(..) => "unauthorized",
|
||||
ApiError::CustomAuthentication(..) => "unauthorized",
|
||||
ApiError::Xml(..) => "xml_error",
|
||||
ApiError::Json(..) => "json_error",
|
||||
ApiError::Search(..) => "search_error",
|
||||
ApiError::Indexing(..) => "indexing_error",
|
||||
ApiError::FileHosting(..) => "file_hosting_error",
|
||||
ApiError::InvalidInput(..) => "invalid_input",
|
||||
ApiError::Validation(..) => "invalid_input",
|
||||
ApiError::Analytics(..) => "analytics_error",
|
||||
ApiError::Crypto(..) => "crypto_error",
|
||||
ApiError::Payments(..) => "payments_error",
|
||||
ApiError::DiscordError(..) => "discord_error",
|
||||
ApiError::Decoding(..) => "decoding_error",
|
||||
ApiError::ImageError(..) => "invalid_image",
|
||||
},
|
||||
description: &self.to_string(),
|
||||
HttpResponse::build(self.status_code()).json(crate::models::error::ApiError {
|
||||
error: match self {
|
||||
ApiError::Env(..) => "environment_error",
|
||||
ApiError::SqlxDatabase(..) => "database_error",
|
||||
ApiError::Database(..) => "database_error",
|
||||
ApiError::Authentication(..) => "unauthorized",
|
||||
ApiError::CustomAuthentication(..) => "unauthorized",
|
||||
ApiError::Xml(..) => "xml_error",
|
||||
ApiError::Json(..) => "json_error",
|
||||
ApiError::Search(..) => "search_error",
|
||||
ApiError::Indexing(..) => "indexing_error",
|
||||
ApiError::FileHosting(..) => "file_hosting_error",
|
||||
ApiError::InvalidInput(..) => "invalid_input",
|
||||
ApiError::Validation(..) => "invalid_input",
|
||||
ApiError::Analytics(..) => "analytics_error",
|
||||
ApiError::Crypto(..) => "crypto_error",
|
||||
ApiError::Payments(..) => "payments_error",
|
||||
ApiError::DiscordError(..) => "discord_error",
|
||||
ApiError::Decoding(..) => "decoding_error",
|
||||
ApiError::ImageError(..) => "invalid_image",
|
||||
},
|
||||
)
|
||||
description: &self.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,9 +6,7 @@ use sqlx::PgPool;
|
||||
|
||||
use crate::database;
|
||||
use crate::models::projects::VersionType;
|
||||
use crate::util::auth::{
|
||||
filter_authorized_versions, get_user_from_headers, is_authorized,
|
||||
};
|
||||
use crate::util::auth::{filter_authorized_versions, get_user_from_headers, is_authorized};
|
||||
|
||||
use super::ApiError;
|
||||
|
||||
@ -26,10 +24,9 @@ pub async fn forge_updates(
|
||||
|
||||
let (id,) = info.into_inner();
|
||||
|
||||
let project =
|
||||
database::models::Project::get_from_slug_or_project_id(&id, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::InvalidInput(ERROR.to_string()))?;
|
||||
let project = database::models::Project::get_from_slug_or_project_id(&id, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::InvalidInput(ERROR.to_string()))?;
|
||||
|
||||
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
@ -48,11 +45,9 @@ pub async fn forge_updates(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let versions =
|
||||
database::models::Version::get_many_full(&version_ids, &**pool).await?;
|
||||
let versions = database::models::Version::get_many_full(&version_ids, &**pool).await?;
|
||||
|
||||
let mut versions =
|
||||
filter_authorized_versions(versions, &user_option, &pool).await?;
|
||||
let mut versions = filter_authorized_versions(versions, &user_option, &pool).await?;
|
||||
|
||||
versions.sort_by(|a, b| b.date_published.cmp(&a.date_published));
|
||||
|
||||
|
||||
@ -37,26 +37,22 @@ pub async fn count_download(
|
||||
download_body: web::Json<DownloadBody>,
|
||||
download_queue: web::Data<Arc<DownloadQueue>>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let project_id: crate::database::models::ids::ProjectId =
|
||||
download_body.project_id.into();
|
||||
let project_id: crate::database::models::ids::ProjectId = download_body.project_id.into();
|
||||
|
||||
let id_option = crate::models::ids::base62_impl::parse_base62(
|
||||
&download_body.version_name,
|
||||
)
|
||||
.ok()
|
||||
.map(|x| x as i64);
|
||||
let id_option = crate::models::ids::base62_impl::parse_base62(&download_body.version_name)
|
||||
.ok()
|
||||
.map(|x| x as i64);
|
||||
|
||||
let (version_id, project_id, file_type) = if let Some(version) =
|
||||
sqlx::query!(
|
||||
"
|
||||
let (version_id, project_id, file_type) = if let Some(version) = sqlx::query!(
|
||||
"
|
||||
SELECT v.id id, v.mod_id mod_id, file_type FROM files f
|
||||
INNER JOIN versions v ON v.id = f.version_id
|
||||
WHERE f.url = $1
|
||||
",
|
||||
download_body.url,
|
||||
)
|
||||
.fetch_optional(pool.as_ref())
|
||||
.await?
|
||||
download_body.url,
|
||||
)
|
||||
.fetch_optional(pool.as_ref())
|
||||
.await?
|
||||
{
|
||||
(version.id, version.mod_id, version.file_type)
|
||||
} else if let Some(version) = sqlx::query!(
|
||||
@ -143,17 +139,11 @@ pub async fn process_payout(
|
||||
)])
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ApiError::Analytics(
|
||||
"Error while fetching payout multipliers!".to_string(),
|
||||
)
|
||||
})?
|
||||
.map_err(|_| ApiError::Analytics("Error while fetching payout multipliers!".to_string()))?
|
||||
.json()
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ApiError::Analytics(
|
||||
"Error while deserializing payout multipliers!".to_string(),
|
||||
)
|
||||
ApiError::Analytics("Error while deserializing payout multipliers!".to_string())
|
||||
})?;
|
||||
|
||||
struct Project {
|
||||
@ -176,26 +166,33 @@ pub async fn process_payout(
|
||||
INNER JOIN project_types pt ON pt.id = m.project_type
|
||||
WHERE m.id = ANY($1) AND m.monetization_status = $2
|
||||
",
|
||||
&multipliers.values.keys().flat_map(|x| x.parse::<i64>().ok()).collect::<Vec<i64>>(),
|
||||
&multipliers
|
||||
.values
|
||||
.keys()
|
||||
.flat_map(|x| x.parse::<i64>().ok())
|
||||
.collect::<Vec<i64>>(),
|
||||
MonetizationStatus::Monetized.as_str(),
|
||||
)
|
||||
.fetch_many(&mut *transaction)
|
||||
.try_for_each(|e| {
|
||||
if let Some(row) = e.right() {
|
||||
if let Some(project) = projects_map.get_mut(&row.id) {
|
||||
project.team_members.push((row.user_id, row.payouts_split));
|
||||
} else {
|
||||
projects_map.insert(row.id, Project {
|
||||
.fetch_many(&mut *transaction)
|
||||
.try_for_each(|e| {
|
||||
if let Some(row) = e.right() {
|
||||
if let Some(project) = projects_map.get_mut(&row.id) {
|
||||
project.team_members.push((row.user_id, row.payouts_split));
|
||||
} else {
|
||||
projects_map.insert(
|
||||
row.id,
|
||||
Project {
|
||||
project_type: row.project_type,
|
||||
team_members: vec![(row.user_id, row.payouts_split)],
|
||||
split_team_members: Default::default()
|
||||
});
|
||||
}
|
||||
split_team_members: Default::default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
futures::future::ready(Ok(()))
|
||||
})
|
||||
.await?;
|
||||
futures::future::ready(Ok(()))
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Specific Payout Conditions (ex: modpack payout split)
|
||||
let mut projects_split_dependencies = Vec::new();
|
||||
@ -208,8 +205,7 @@ pub async fn process_payout(
|
||||
|
||||
if !projects_split_dependencies.is_empty() {
|
||||
// (dependent_id, (dependency_id, times_depended))
|
||||
let mut project_dependencies: HashMap<i64, Vec<(i64, i64)>> =
|
||||
HashMap::new();
|
||||
let mut project_dependencies: HashMap<i64, Vec<(i64, i64)>> = HashMap::new();
|
||||
// dependency_ids to fetch team members from
|
||||
let mut fetch_team_members: Vec<i64> = Vec::new();
|
||||
|
||||
@ -229,14 +225,11 @@ pub async fn process_payout(
|
||||
if let Some(row) = e.right() {
|
||||
fetch_team_members.push(row.id);
|
||||
|
||||
if let Some(project) = project_dependencies.get_mut(&row.mod_id)
|
||||
{
|
||||
if let Some(project) = project_dependencies.get_mut(&row.mod_id) {
|
||||
project.push((row.id, row.times_depended.unwrap_or(0)));
|
||||
} else {
|
||||
project_dependencies.insert(
|
||||
row.mod_id,
|
||||
vec![(row.id, row.times_depended.unwrap_or(0))],
|
||||
);
|
||||
project_dependencies
|
||||
.insert(row.mod_id, vec![(row.id, row.times_depended.unwrap_or(0))]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -245,8 +238,7 @@ pub async fn process_payout(
|
||||
.await?;
|
||||
|
||||
// (project_id, (user_id, payouts_split))
|
||||
let mut team_members: HashMap<i64, Vec<(i64, Decimal)>> =
|
||||
HashMap::new();
|
||||
let mut team_members: HashMap<i64, Vec<(i64, Decimal)>> = HashMap::new();
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
@ -263,8 +255,7 @@ pub async fn process_payout(
|
||||
if let Some(project) = team_members.get_mut(&row.id) {
|
||||
project.push((row.user_id, row.payouts_split));
|
||||
} else {
|
||||
team_members
|
||||
.insert(row.id, vec![(row.user_id, row.payouts_split)]);
|
||||
team_members.insert(row.id, vec![(row.user_id, row.payouts_split)]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -281,17 +272,14 @@ pub async fn process_payout(
|
||||
if dep_sum > 0 {
|
||||
for dependency in dependencies {
|
||||
let project_multiplier: Decimal =
|
||||
Decimal::from(dependency.1)
|
||||
/ Decimal::from(dep_sum);
|
||||
Decimal::from(dependency.1) / Decimal::from(dep_sum);
|
||||
|
||||
if let Some(members) = team_members.get(&dependency.0) {
|
||||
let members_sum: Decimal =
|
||||
members.iter().map(|x| x.1).sum();
|
||||
let members_sum: Decimal = members.iter().map(|x| x.1).sum();
|
||||
|
||||
if members_sum > Decimal::ZERO {
|
||||
for member in members {
|
||||
let member_multiplier: Decimal =
|
||||
member.1 / members_sum;
|
||||
let member_multiplier: Decimal = member.1 / members_sum;
|
||||
project.split_team_members.push((
|
||||
member.0,
|
||||
member_multiplier * project_multiplier,
|
||||
@ -315,10 +303,8 @@ pub async fn process_payout(
|
||||
let split_given = Decimal::ONE / Decimal::from(5);
|
||||
let split_retention = Decimal::from(4) / Decimal::from(5);
|
||||
|
||||
let sum_splits: Decimal =
|
||||
project.team_members.iter().map(|x| x.1).sum();
|
||||
let sum_tm_splits: Decimal =
|
||||
project.split_team_members.iter().map(|x| x.1).sum();
|
||||
let sum_splits: Decimal = project.team_members.iter().map(|x| x.1).sum();
|
||||
let sum_tm_splits: Decimal = project.split_team_members.iter().map(|x| x.1).sum();
|
||||
|
||||
if sum_splits > Decimal::ZERO {
|
||||
for (user_id, split) in project.team_members {
|
||||
@ -342,8 +328,8 @@ pub async fn process_payout(
|
||||
payout,
|
||||
start
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
@ -378,8 +364,8 @@ pub async fn process_payout(
|
||||
payout,
|
||||
start
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
|
||||
@ -58,12 +58,8 @@ impl actix_web::ResponseError for AuthorizationError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
AuthorizationError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
AuthorizationError::SqlxDatabase(..) => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
AuthorizationError::Database(..) => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
AuthorizationError::SqlxDatabase(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
AuthorizationError::Database(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
AuthorizationError::SerDe(..) => StatusCode::BAD_REQUEST,
|
||||
AuthorizationError::Github(..) => StatusCode::FAILED_DEPENDENCY,
|
||||
AuthorizationError::InvalidCredentials => StatusCode::UNAUTHORIZED,
|
||||
@ -84,9 +80,7 @@ impl actix_web::ResponseError for AuthorizationError {
|
||||
AuthorizationError::Github(..) => "github_error",
|
||||
AuthorizationError::InvalidCredentials => "invalid_credentials",
|
||||
AuthorizationError::Decoding(..) => "decoding_error",
|
||||
AuthorizationError::Authentication(..) => {
|
||||
"authentication_error"
|
||||
}
|
||||
AuthorizationError::Authentication(..) => "authentication_error",
|
||||
AuthorizationError::Url => "url_error",
|
||||
AuthorizationError::Banned => "user_banned",
|
||||
},
|
||||
@ -119,16 +113,12 @@ pub async fn init(
|
||||
Query(info): Query<AuthorizationInit>,
|
||||
client: Data<PgPool>,
|
||||
) -> Result<HttpResponse, AuthorizationError> {
|
||||
let url =
|
||||
url::Url::parse(&info.url).map_err(|_| AuthorizationError::Url)?;
|
||||
let url = url::Url::parse(&info.url).map_err(|_| AuthorizationError::Url)?;
|
||||
|
||||
let allowed_callback_urls =
|
||||
parse_strings_from_var("ALLOWED_CALLBACK_URLS").unwrap_or_default();
|
||||
let allowed_callback_urls = parse_strings_from_var("ALLOWED_CALLBACK_URLS").unwrap_or_default();
|
||||
|
||||
let domain = url.domain().ok_or(AuthorizationError::Url)?;
|
||||
if !allowed_callback_urls.iter().any(|x| domain.ends_with(x))
|
||||
&& domain != "modrinth.com"
|
||||
{
|
||||
if !allowed_callback_urls.iter().any(|x| domain.ends_with(x)) && domain != "modrinth.com" {
|
||||
return Err(AuthorizationError::Url);
|
||||
}
|
||||
|
||||
@ -215,8 +205,7 @@ pub async fn auth_callback(
|
||||
|
||||
let user = get_github_user_from_token(&token.access_token).await?;
|
||||
|
||||
let user_result =
|
||||
User::get_from_github_id(user.id, &mut *transaction).await?;
|
||||
let user_result = User::get_from_github_id(user.id, &mut *transaction).await?;
|
||||
match user_result {
|
||||
Some(_) => {}
|
||||
None => {
|
||||
@ -231,9 +220,7 @@ pub async fn auth_callback(
|
||||
return Err(AuthorizationError::Banned);
|
||||
}
|
||||
|
||||
let user_id =
|
||||
crate::database::models::generate_user_id(&mut transaction)
|
||||
.await?;
|
||||
let user_id = crate::database::models::generate_user_id(&mut transaction).await?;
|
||||
|
||||
let mut username_increment: i32 = 0;
|
||||
let mut username = None;
|
||||
|
||||
@ -54,17 +54,11 @@ pub async fn init_checkout(
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ApiError::Payments(
|
||||
"Error while creating checkout session!".to_string(),
|
||||
)
|
||||
})?
|
||||
.map_err(|_| ApiError::Payments("Error while creating checkout session!".to_string()))?
|
||||
.json::<Session>()
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ApiError::Payments(
|
||||
"Error while deserializing checkout response!".to_string(),
|
||||
)
|
||||
ApiError::Payments("Error while deserializing checkout response!".to_string())
|
||||
})?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(json!(
|
||||
@ -92,11 +86,7 @@ pub async fn init_customer_portal(
|
||||
.fetch_optional(&**pool)
|
||||
.await?
|
||||
.and_then(|x| x.stripe_customer_id)
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"User is not linked to stripe account!".to_string(),
|
||||
)
|
||||
})?;
|
||||
.ok_or_else(|| ApiError::InvalidInput("User is not linked to stripe account!".to_string()))?;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
@ -117,17 +107,11 @@ pub async fn init_customer_portal(
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ApiError::Payments(
|
||||
"Error while creating billing session!".to_string(),
|
||||
)
|
||||
})?
|
||||
.map_err(|_| ApiError::Payments("Error while creating billing session!".to_string()))?
|
||||
.json::<Session>()
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ApiError::Payments(
|
||||
"Error while deserializing billing response!".to_string(),
|
||||
)
|
||||
ApiError::Payments("Error while deserializing billing response!".to_string())
|
||||
})?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(json!(
|
||||
@ -166,27 +150,25 @@ pub async fn handle_stripe_webhook(
|
||||
if let Some(signature) = signature {
|
||||
type HmacSha256 = Hmac<sha2::Sha256>;
|
||||
|
||||
let mut key = HmacSha256::new_from_slice(dotenvy::var("STRIPE_WEBHOOK_SECRET")?.as_bytes()).map_err(|_| {
|
||||
ApiError::Crypto(
|
||||
"Unable to initialize HMAC instance due to invalid key length!".to_string(),
|
||||
)
|
||||
})?;
|
||||
let mut key =
|
||||
HmacSha256::new_from_slice(dotenvy::var("STRIPE_WEBHOOK_SECRET")?.as_bytes())
|
||||
.map_err(|_| {
|
||||
ApiError::Crypto(
|
||||
"Unable to initialize HMAC instance due to invalid key length!"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
key.update(format!("{timestamp}.{body}").as_bytes());
|
||||
|
||||
key.verify(&signature).map_err(|_| {
|
||||
ApiError::Crypto(
|
||||
"Unable to verify webhook signature!".to_string(),
|
||||
)
|
||||
ApiError::Crypto("Unable to verify webhook signature!".to_string())
|
||||
})?;
|
||||
|
||||
if timestamp < (Utc::now() - Duration::minutes(5)).timestamp()
|
||||
|| timestamp
|
||||
> (Utc::now() + Duration::minutes(5)).timestamp()
|
||||
|| timestamp > (Utc::now() + Duration::minutes(5)).timestamp()
|
||||
{
|
||||
return Err(ApiError::Crypto(
|
||||
"Webhook signature expired!".to_string(),
|
||||
));
|
||||
return Err(ApiError::Crypto("Webhook signature expired!".to_string()));
|
||||
}
|
||||
} else {
|
||||
return Err(ApiError::Crypto("Missing signature!".to_string()));
|
||||
@ -256,8 +238,7 @@ pub async fn handle_stripe_webhook(
|
||||
// TODO: Currently hardcoded to midas-only. When we add more stuff should include price IDs
|
||||
match &*webhook.type_ {
|
||||
"checkout.session.completed" => {
|
||||
let session: CheckoutSession =
|
||||
serde_json::from_value(webhook.data.object)?;
|
||||
let session: CheckoutSession = serde_json::from_value(webhook.data.object)?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
@ -276,8 +257,7 @@ pub async fn handle_stripe_webhook(
|
||||
|
||||
if let Some(item) = invoice.lines.data.first() {
|
||||
let expires: DateTime<Utc> = DateTime::from_utc(
|
||||
NaiveDateTime::from_timestamp_opt(item.period.end, 0)
|
||||
.unwrap_or_default(),
|
||||
NaiveDateTime::from_timestamp_opt(item.period.end, 0).unwrap_or_default(),
|
||||
Utc,
|
||||
) + Duration::days(1);
|
||||
|
||||
@ -323,8 +303,7 @@ pub async fn handle_stripe_webhook(
|
||||
}
|
||||
}
|
||||
"customer.subscription.deleted" => {
|
||||
let session: Subscription =
|
||||
serde_json::from_value(webhook.data.object)?;
|
||||
let session: Subscription = serde_json::from_value(webhook.data.object)?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
@ -334,8 +313,8 @@ pub async fn handle_stripe_webhook(
|
||||
",
|
||||
session.customer,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
@ -46,18 +46,15 @@ pub async fn get_projects(
|
||||
count.count as i64
|
||||
)
|
||||
.fetch_many(&**pool)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|m| database::models::ProjectId(m.id)))
|
||||
})
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|m| database::models::ProjectId(m.id))) })
|
||||
.try_collect::<Vec<database::models::ProjectId>>()
|
||||
.await?;
|
||||
|
||||
let projects: Vec<_> =
|
||||
database::Project::get_many_full(&project_ids, &**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(crate::models::projects::Project::from)
|
||||
.collect();
|
||||
let projects: Vec<_> = database::Project::get_many_full(&project_ids, &**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(crate::models::projects::Project::from)
|
||||
.collect();
|
||||
|
||||
Ok(HttpResponse::Ok().json(projects))
|
||||
}
|
||||
|
||||
@ -3,17 +3,19 @@ use crate::models::ids::NotificationId;
|
||||
use crate::models::notifications::Notification;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::auth::get_user_from_headers;
|
||||
use actix_web::{delete, get, web, HttpRequest, HttpResponse};
|
||||
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(notifications_get);
|
||||
cfg.service(notifications_delete);
|
||||
cfg.service(notifications_read);
|
||||
|
||||
cfg.service(
|
||||
web::scope("notification")
|
||||
.service(notification_get)
|
||||
.service(notifications_read)
|
||||
.service(notification_delete),
|
||||
);
|
||||
}
|
||||
@ -31,7 +33,6 @@ pub async fn notifications_get(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
// TODO: this is really confusingly named.
|
||||
use database::models::notification_item::Notification as DBNotification;
|
||||
use database::models::NotificationId as DBNotificationId;
|
||||
|
||||
@ -42,11 +43,8 @@ pub async fn notifications_get(
|
||||
.collect();
|
||||
|
||||
let notifications_data: Vec<DBNotification> =
|
||||
database::models::notification_item::Notification::get_many(
|
||||
¬ification_ids,
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
database::models::notification_item::Notification::get_many(¬ification_ids, &**pool)
|
||||
.await?;
|
||||
|
||||
let notifications: Vec<Notification> = notifications_data
|
||||
.into_iter()
|
||||
@ -68,11 +66,7 @@ pub async fn notification_get(
|
||||
let id = info.into_inner().0;
|
||||
|
||||
let notification_data =
|
||||
database::models::notification_item::Notification::get(
|
||||
id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
database::models::notification_item::Notification::get(id.into(), &**pool).await?;
|
||||
|
||||
if let Some(data) = notification_data {
|
||||
if user.id == data.user_id.into() || user.role.is_admin() {
|
||||
@ -85,6 +79,39 @@ pub async fn notification_get(
|
||||
}
|
||||
}
|
||||
|
||||
#[patch("{id}")]
|
||||
pub async fn notification_read(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(NotificationId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let id = info.into_inner().0;
|
||||
|
||||
let notification_data =
|
||||
database::models::notification_item::Notification::get(id.into(), &**pool).await?;
|
||||
|
||||
if let Some(data) = notification_data {
|
||||
if data.user_id == user.id.into() || user.role.is_admin() {
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
database::models::notification_item::Notification::read(id.into(), &mut transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to read this notification!".to_string(),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("{id}")]
|
||||
pub async fn notification_delete(
|
||||
req: HttpRequest,
|
||||
@ -96,29 +123,21 @@ pub async fn notification_delete(
|
||||
let id = info.into_inner().0;
|
||||
|
||||
let notification_data =
|
||||
database::models::notification_item::Notification::get(
|
||||
id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
database::models::notification_item::Notification::get(id.into(), &**pool).await?;
|
||||
|
||||
if let Some(data) = notification_data {
|
||||
if data.user_id == user.id.into() || user.role.is_admin() {
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
database::models::notification_item::Notification::remove(
|
||||
id.into(),
|
||||
&mut transaction,
|
||||
)
|
||||
.await?;
|
||||
database::models::notification_item::Notification::remove(id.into(), &mut transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to delete this notification!"
|
||||
.to_string(),
|
||||
"You are not authorized to delete this notification!".to_string(),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
@ -126,6 +145,41 @@ pub async fn notification_delete(
|
||||
}
|
||||
}
|
||||
|
||||
#[patch("notifications")]
|
||||
pub async fn notifications_read(
|
||||
req: HttpRequest,
|
||||
web::Query(ids): web::Query<NotificationIds>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let notification_ids = serde_json::from_str::<Vec<NotificationId>>(&ids.ids)?
|
||||
.into_iter()
|
||||
.map(|x| x.into())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let notifications_data =
|
||||
database::models::notification_item::Notification::get_many(¬ification_ids, &**pool)
|
||||
.await?;
|
||||
|
||||
let mut notifications: Vec<database::models::ids::NotificationId> = Vec::new();
|
||||
|
||||
for notification in notifications_data {
|
||||
if notification.user_id == user.id.into() || user.role.is_admin() {
|
||||
notifications.push(notification.id);
|
||||
}
|
||||
}
|
||||
|
||||
database::models::notification_item::Notification::read_many(¬ifications, &mut transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
#[delete("notifications")]
|
||||
pub async fn notifications_delete(
|
||||
req: HttpRequest,
|
||||
@ -134,23 +188,18 @@ pub async fn notifications_delete(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let notification_ids =
|
||||
serde_json::from_str::<Vec<NotificationId>>(&ids.ids)?
|
||||
.into_iter()
|
||||
.map(|x| x.into())
|
||||
.collect::<Vec<_>>();
|
||||
let notification_ids = serde_json::from_str::<Vec<NotificationId>>(&ids.ids)?
|
||||
.into_iter()
|
||||
.map(|x| x.into())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let notifications_data =
|
||||
database::models::notification_item::Notification::get_many(
|
||||
¬ification_ids,
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
database::models::notification_item::Notification::get_many(¬ification_ids, &**pool)
|
||||
.await?;
|
||||
|
||||
let mut notifications: Vec<database::models::ids::NotificationId> =
|
||||
Vec::new();
|
||||
let mut notifications: Vec<database::models::ids::NotificationId> = Vec::new();
|
||||
|
||||
for notification in notifications_data {
|
||||
if notification.user_id == user.id.into() || user.role.is_admin() {
|
||||
|
||||
@ -4,8 +4,8 @@ use crate::database::models::thread_item::ThreadBuilder;
|
||||
use crate::file_hosting::{FileHost, FileHostingError};
|
||||
use crate::models::error::ApiError;
|
||||
use crate::models::projects::{
|
||||
DonationLink, License, MonetizationStatus, ProjectId, ProjectStatus,
|
||||
SideType, VersionId, VersionStatus,
|
||||
DonationLink, License, MonetizationStatus, ProjectId, ProjectStatus, SideType, VersionId,
|
||||
VersionStatus,
|
||||
};
|
||||
use crate::models::threads::ThreadType;
|
||||
use crate::models::users::UserId;
|
||||
@ -79,14 +79,10 @@ impl actix_web::ResponseError for CreateError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
CreateError::EnvError(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
CreateError::SqlxDatabaseError(..) => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
CreateError::SqlxDatabaseError(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
CreateError::DatabaseError(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
CreateError::IndexingError(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
CreateError::FileHostingError(..) => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
CreateError::FileHostingError(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
CreateError::SerDeError(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::MultipartError(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::MissingValueError(..) => StatusCode::BAD_REQUEST,
|
||||
@ -97,9 +93,7 @@ impl actix_web::ResponseError for CreateError {
|
||||
CreateError::InvalidCategory(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::InvalidFileType(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::Unauthorized(..) => StatusCode::UNAUTHORIZED,
|
||||
CreateError::CustomAuthenticationError(..) => {
|
||||
StatusCode::UNAUTHORIZED
|
||||
}
|
||||
CreateError::CustomAuthenticationError(..) => StatusCode::UNAUTHORIZED,
|
||||
CreateError::SlugCollision => StatusCode::BAD_REQUEST,
|
||||
CreateError::ValidationError(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::FileValidationError(..) => StatusCode::BAD_REQUEST,
|
||||
@ -347,21 +341,17 @@ async fn project_create_inner(
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
|
||||
// The currently logged in user
|
||||
let current_user =
|
||||
get_user_from_headers(req.headers(), &mut *transaction).await?;
|
||||
let current_user = get_user_from_headers(req.headers(), &mut *transaction).await?;
|
||||
|
||||
let project_id: ProjectId =
|
||||
models::generate_project_id(transaction).await?.into();
|
||||
let project_id: ProjectId = models::generate_project_id(transaction).await?.into();
|
||||
|
||||
let project_create_data;
|
||||
let mut versions;
|
||||
let mut versions_map = std::collections::HashMap::new();
|
||||
let mut gallery_urls = Vec::new();
|
||||
|
||||
let all_game_versions =
|
||||
models::categories::GameVersion::list(&mut *transaction).await?;
|
||||
let all_loaders =
|
||||
models::categories::Loader::list(&mut *transaction).await?;
|
||||
let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?;
|
||||
let all_loaders = models::categories::Loader::list(&mut *transaction).await?;
|
||||
|
||||
{
|
||||
// The first multipart field must be named "data" and contain a
|
||||
@ -378,9 +368,9 @@ async fn project_create_inner(
|
||||
})?;
|
||||
|
||||
let content_disposition = field.content_disposition();
|
||||
let name = content_disposition.get_name().ok_or_else(|| {
|
||||
CreateError::MissingValueError(String::from("Missing content name"))
|
||||
})?;
|
||||
let name = content_disposition
|
||||
.get_name()
|
||||
.ok_or_else(|| CreateError::MissingValueError(String::from("Missing content name")))?;
|
||||
|
||||
if name != "data" {
|
||||
return Err(CreateError::InvalidInput(String::from(
|
||||
@ -390,22 +380,19 @@ async fn project_create_inner(
|
||||
|
||||
let mut data = Vec::new();
|
||||
while let Some(chunk) = field.next().await {
|
||||
data.extend_from_slice(
|
||||
&chunk.map_err(CreateError::MultipartError)?,
|
||||
);
|
||||
data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?);
|
||||
}
|
||||
let create_data: ProjectCreateData = serde_json::from_slice(&data)?;
|
||||
|
||||
create_data.validate().map_err(|err| {
|
||||
CreateError::InvalidInput(validation_errors_to_string(err, None))
|
||||
})?;
|
||||
create_data
|
||||
.validate()
|
||||
.map_err(|err| CreateError::InvalidInput(validation_errors_to_string(err, None)))?;
|
||||
|
||||
let slug_project_id_option: Option<ProjectId> =
|
||||
serde_json::from_str(&format!("\"{}\"", create_data.slug)).ok();
|
||||
|
||||
if let Some(slug_project_id) = slug_project_id_option {
|
||||
let slug_project_id: models::ids::ProjectId =
|
||||
slug_project_id.into();
|
||||
let slug_project_id: models::ids::ProjectId = slug_project_id.into();
|
||||
let results = sqlx::query!(
|
||||
"
|
||||
SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)
|
||||
@ -492,9 +479,7 @@ async fn project_create_inner(
|
||||
let content_disposition = field.content_disposition().clone();
|
||||
|
||||
let name = content_disposition.get_name().ok_or_else(|| {
|
||||
CreateError::MissingValueError(
|
||||
"Missing content name".to_string(),
|
||||
)
|
||||
CreateError::MissingValueError("Missing content name".to_string())
|
||||
})?;
|
||||
|
||||
let (file_name, file_extension) =
|
||||
@ -528,9 +513,7 @@ async fn project_create_inner(
|
||||
)));
|
||||
}
|
||||
|
||||
if let Some(item) =
|
||||
gallery_items.iter().find(|x| x.item == name)
|
||||
{
|
||||
if let Some(item) = gallery_items.iter().find(|x| x.item == name) {
|
||||
let data = read_from_field(
|
||||
&mut field,
|
||||
5 * (1 << 20),
|
||||
@ -540,22 +523,13 @@ async fn project_create_inner(
|
||||
|
||||
let hash = sha1::Sha1::from(&data).hexdigest();
|
||||
let (_, file_extension) =
|
||||
super::version_creation::get_name_ext(
|
||||
&content_disposition,
|
||||
)?;
|
||||
let content_type =
|
||||
crate::util::ext::get_image_content_type(
|
||||
file_extension,
|
||||
)
|
||||
super::version_creation::get_name_ext(&content_disposition)?;
|
||||
let content_type = crate::util::ext::get_image_content_type(file_extension)
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidIconFormat(
|
||||
file_extension.to_string(),
|
||||
)
|
||||
CreateError::InvalidIconFormat(file_extension.to_string())
|
||||
})?;
|
||||
|
||||
let url = format!(
|
||||
"data/{project_id}/images/{hash}.{file_extension}"
|
||||
);
|
||||
let url = format!("data/{project_id}/images/{hash}.{file_extension}");
|
||||
let upload_data = file_host
|
||||
.upload_file(content_type, &url, data.freeze())
|
||||
.await?;
|
||||
@ -588,8 +562,7 @@ async fn project_create_inner(
|
||||
|
||||
// `index` is always valid for these lists
|
||||
let created_version = versions.get_mut(index).unwrap();
|
||||
let version_data =
|
||||
project_create_data.initial_versions.get(index).unwrap();
|
||||
let version_data = project_create_data.initial_versions.get(index).unwrap();
|
||||
|
||||
// Upload the new jar file
|
||||
super::version_creation::upload_file(
|
||||
@ -642,8 +615,7 @@ async fn project_create_inner(
|
||||
}
|
||||
|
||||
// Convert the list of category names to actual categories
|
||||
let mut categories =
|
||||
Vec::with_capacity(project_create_data.categories.len());
|
||||
let mut categories = Vec::with_capacity(project_create_data.categories.len());
|
||||
for category in &project_create_data.categories {
|
||||
let id = models::categories::Category::get_id_project(
|
||||
category,
|
||||
@ -706,9 +678,7 @@ async fn project_create_inner(
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidInput(
|
||||
"Client side type specified does not exist.".to_string(),
|
||||
)
|
||||
CreateError::InvalidInput("Client side type specified does not exist.".to_string())
|
||||
})?;
|
||||
|
||||
let server_side_id = models::categories::SideType::get_id(
|
||||
@ -717,35 +687,27 @@ async fn project_create_inner(
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidInput(
|
||||
"Server side type specified does not exist.".to_string(),
|
||||
)
|
||||
CreateError::InvalidInput("Server side type specified does not exist.".to_string())
|
||||
})?;
|
||||
|
||||
let license_id = spdx::Expression::parse(
|
||||
&project_create_data.license_id,
|
||||
)
|
||||
.map_err(|err| {
|
||||
CreateError::InvalidInput(format!(
|
||||
"Invalid SPDX license identifier: {err}"
|
||||
))
|
||||
})?;
|
||||
let license_id =
|
||||
spdx::Expression::parse(&project_create_data.license_id).map_err(|err| {
|
||||
CreateError::InvalidInput(format!("Invalid SPDX license identifier: {err}"))
|
||||
})?;
|
||||
|
||||
let mut donation_urls = vec![];
|
||||
|
||||
if let Some(urls) = &project_create_data.donation_urls {
|
||||
for url in urls {
|
||||
let platform_id = models::categories::DonationPlatform::get_id(
|
||||
&url.id,
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidInput(format!(
|
||||
"Donation platform {} does not exist.",
|
||||
url.id.clone()
|
||||
))
|
||||
})?;
|
||||
let platform_id =
|
||||
models::categories::DonationPlatform::get_id(&url.id, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidInput(format!(
|
||||
"Donation platform {} does not exist.",
|
||||
url.id.clone()
|
||||
))
|
||||
})?;
|
||||
|
||||
donation_urls.push(models::project_item::DonationUrl {
|
||||
platform_id,
|
||||
@ -856,16 +818,10 @@ async fn project_create_inner(
|
||||
let _project_id = project_builder.insert(&mut *transaction).await?;
|
||||
|
||||
if status == ProjectStatus::Processing {
|
||||
if let Ok(webhook_url) = dotenvy::var("MODERATION_DISCORD_WEBHOOK")
|
||||
{
|
||||
crate::util::webhook::send_discord_webhook(
|
||||
response.id,
|
||||
pool,
|
||||
webhook_url,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
if let Ok(webhook_url) = dotenvy::var("MODERATION_DISCORD_WEBHOOK") {
|
||||
crate::util::webhook::send_discord_webhook(response.id, pool, webhook_url, None)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@ -888,13 +844,12 @@ async fn create_initial_version(
|
||||
)));
|
||||
}
|
||||
|
||||
version_data.validate().map_err(|err| {
|
||||
CreateError::ValidationError(validation_errors_to_string(err, None))
|
||||
})?;
|
||||
version_data
|
||||
.validate()
|
||||
.map_err(|err| CreateError::ValidationError(validation_errors_to_string(err, None)))?;
|
||||
|
||||
// Randomly generate a new id to be used for the version
|
||||
let version_id: VersionId =
|
||||
models::generate_version_id(transaction).await?.into();
|
||||
let version_id: VersionId = models::generate_version_id(transaction).await?.into();
|
||||
|
||||
let game_versions = version_data
|
||||
.game_versions
|
||||
@ -963,15 +918,8 @@ async fn process_icon_upload(
|
||||
mut field: Field,
|
||||
cdn_url: &str,
|
||||
) -> Result<(String, Option<u32>), CreateError> {
|
||||
if let Some(content_type) =
|
||||
crate::util::ext::get_image_content_type(file_extension)
|
||||
{
|
||||
let data = read_from_field(
|
||||
&mut field,
|
||||
262144,
|
||||
"Icons must be smaller than 256KiB",
|
||||
)
|
||||
.await?;
|
||||
if let Some(content_type) = crate::util::ext::get_image_content_type(file_extension) {
|
||||
let data = read_from_field(&mut field, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
|
||||
let color = crate::util::img::get_color_from_img(&data)?;
|
||||
|
||||
|
||||
@ -6,16 +6,13 @@ use crate::models;
|
||||
use crate::models::ids::base62_impl::parse_base62;
|
||||
use crate::models::notifications::NotificationBody;
|
||||
use crate::models::projects::{
|
||||
DonationLink, MonetizationStatus, Project, ProjectId, ProjectStatus,
|
||||
SearchRequest, SideType,
|
||||
DonationLink, MonetizationStatus, Project, ProjectId, ProjectStatus, SearchRequest, SideType,
|
||||
};
|
||||
use crate::models::teams::Permissions;
|
||||
use crate::models::threads::MessageBody;
|
||||
use crate::routes::ApiError;
|
||||
use crate::search::{search_for_project, SearchConfig, SearchError};
|
||||
use crate::util::auth::{
|
||||
filter_authorized_projects, get_user_from_headers, is_authorized,
|
||||
};
|
||||
use crate::util::auth::{filter_authorized_projects, get_user_from_headers, is_authorized};
|
||||
use crate::util::routes::read_from_payload;
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
|
||||
@ -78,30 +75,30 @@ pub async fn random_projects_get(
|
||||
web::Query(count): web::Query<RandomProjects>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
count.validate().map_err(|err| {
|
||||
ApiError::Validation(validation_errors_to_string(err, None))
|
||||
})?;
|
||||
count
|
||||
.validate()
|
||||
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
|
||||
|
||||
let project_ids = sqlx::query!(
|
||||
"
|
||||
"
|
||||
SELECT id FROM mods TABLESAMPLE SYSTEM_ROWS($1) WHERE status = ANY($2)
|
||||
",
|
||||
count.count as i32,
|
||||
&*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_searchable()).map(|x| x.to_string()).collect::<Vec<String>>(),
|
||||
)
|
||||
.fetch_many(&**pool)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|m| database::models::ids::ProjectId(m.id)))
|
||||
})
|
||||
.try_collect::<Vec<_>>()
|
||||
.await?;
|
||||
count.count as i32,
|
||||
&*crate::models::projects::ProjectStatus::iterator()
|
||||
.filter(|x| x.is_searchable())
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.fetch_many(&**pool)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|m| database::models::ids::ProjectId(m.id))) })
|
||||
.try_collect::<Vec<_>>()
|
||||
.await?;
|
||||
|
||||
let projects_data =
|
||||
database::models::Project::get_many_full(&project_ids, &**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Project::from)
|
||||
.collect::<Vec<_>>();
|
||||
let projects_data = database::models::Project::get_many_full(&project_ids, &**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Project::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(HttpResponse::Ok().json(projects_data))
|
||||
}
|
||||
@ -123,13 +120,11 @@ pub async fn projects_get(
|
||||
.map(|x| x.into())
|
||||
.collect();
|
||||
|
||||
let projects_data =
|
||||
database::models::Project::get_many_full(&project_ids, &**pool).await?;
|
||||
let projects_data = database::models::Project::get_many_full(&project_ids, &**pool).await?;
|
||||
|
||||
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
let projects =
|
||||
filter_authorized_projects(projects_data, &user_option, &pool).await?;
|
||||
let projects = filter_authorized_projects(projects_data, &user_option, &pool).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(projects))
|
||||
}
|
||||
@ -143,10 +138,7 @@ pub async fn project_get(
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let project_data =
|
||||
database::models::Project::get_full_from_slug_or_project_id(
|
||||
&string, &**pool,
|
||||
)
|
||||
.await?;
|
||||
database::models::Project::get_full_from_slug_or_project_id(&string, &**pool).await?;
|
||||
|
||||
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
@ -229,10 +221,7 @@ pub async fn dependency_list(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let result = database::models::Project::get_from_slug_or_project_id(
|
||||
&string, &**pool,
|
||||
)
|
||||
.await?;
|
||||
let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool).await?;
|
||||
|
||||
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
@ -259,12 +248,13 @@ pub async fn dependency_list(
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|x| {
|
||||
(
|
||||
x.dependency_id
|
||||
.map(database::models::VersionId),
|
||||
if x.mod_id == Some(0) { None } else { x.mod_id
|
||||
.map(database::models::ProjectId) },
|
||||
x.mod_dependency_id
|
||||
.map(database::models::ProjectId),
|
||||
x.dependency_id.map(database::models::VersionId),
|
||||
if x.mod_id == Some(0) {
|
||||
None
|
||||
} else {
|
||||
x.mod_id.map(database::models::ProjectId)
|
||||
},
|
||||
x.mod_dependency_id.map(database::models::ProjectId),
|
||||
)
|
||||
}))
|
||||
})
|
||||
@ -430,15 +420,13 @@ pub async fn project_edit(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
new_project.validate().map_err(|err| {
|
||||
ApiError::Validation(validation_errors_to_string(err, None))
|
||||
})?;
|
||||
new_project
|
||||
.validate()
|
||||
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
|
||||
|
||||
let string = info.into_inner().0;
|
||||
let result = database::models::Project::get_full_from_slug_or_project_id(
|
||||
&string, &**pool,
|
||||
)
|
||||
.await?;
|
||||
let result =
|
||||
database::models::Project::get_full_from_slug_or_project_id(&string, &**pool).await?;
|
||||
|
||||
if let Some(project_item) = result {
|
||||
let id = project_item.inner.id;
|
||||
@ -456,8 +444,7 @@ pub async fn project_edit(
|
||||
} else if let Some(ref member) = team_member {
|
||||
permissions = Some(member.permissions)
|
||||
} else if user.role.is_mod() {
|
||||
permissions =
|
||||
Some(Permissions::EDIT_DETAILS | Permissions::EDIT_BODY)
|
||||
permissions = Some(Permissions::EDIT_DETAILS | Permissions::EDIT_BODY)
|
||||
} else {
|
||||
permissions = None
|
||||
}
|
||||
@ -518,12 +505,10 @@ pub async fn project_edit(
|
||||
if !(user.role.is_mod()
|
||||
|| !project_item.inner.status.is_approved()
|
||||
&& status == &ProjectStatus::Processing
|
||||
|| project_item.inner.status.is_approved()
|
||||
&& status.can_be_requested())
|
||||
|| project_item.inner.status.is_approved() && status.can_be_requested())
|
||||
{
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to set this status!"
|
||||
.to_string(),
|
||||
"You don't have permission to set this status!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@ -545,9 +530,7 @@ pub async fn project_edit(
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
if let Ok(webhook_url) =
|
||||
dotenvy::var("MODERATION_DISCORD_WEBHOOK")
|
||||
{
|
||||
if let Ok(webhook_url) = dotenvy::var("MODERATION_DISCORD_WEBHOOK") {
|
||||
crate::util::webhook::send_discord_webhook(
|
||||
project_item.inner.id.into(),
|
||||
&pool,
|
||||
@ -559,9 +542,7 @@ pub async fn project_edit(
|
||||
}
|
||||
}
|
||||
|
||||
if status.is_approved()
|
||||
&& !project_item.inner.status.is_approved()
|
||||
{
|
||||
if status.is_approved() && !project_item.inner.status.is_approved() {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
@ -575,9 +556,7 @@ pub async fn project_edit(
|
||||
}
|
||||
|
||||
if status.is_searchable() && !project_item.inner.webhook_sent {
|
||||
if let Ok(webhook_url) =
|
||||
dotenvy::var("PUBLIC_DISCORD_WEBHOOK")
|
||||
{
|
||||
if let Ok(webhook_url) = dotenvy::var("PUBLIC_DISCORD_WEBHOOK") {
|
||||
crate::util::webhook::send_discord_webhook(
|
||||
project_item.inner.id.into(),
|
||||
&pool,
|
||||
@ -607,8 +586,7 @@ pub async fn project_edit(
|
||||
FROM team_members tm
|
||||
WHERE tm.team_id = $1 AND tm.accepted
|
||||
",
|
||||
project_item.inner.team_id
|
||||
as database::models::ids::TeamId
|
||||
project_item.inner.team_id as database::models::ids::TeamId
|
||||
)
|
||||
.fetch_many(&mut *transaction)
|
||||
.try_filter_map(|e| async {
|
||||
@ -653,9 +631,7 @@ pub async fn project_edit(
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
if project_item.inner.status.is_searchable()
|
||||
&& !status.is_searchable()
|
||||
{
|
||||
if project_item.inner.status.is_searchable() && !status.is_searchable() {
|
||||
delete_from_index(id.into(), config).await?;
|
||||
}
|
||||
}
|
||||
@ -726,17 +702,14 @@ pub async fn project_edit(
|
||||
|
||||
for category in categories {
|
||||
let category_id =
|
||||
database::models::categories::Category::get_id(
|
||||
category,
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(format!(
|
||||
"Category {} does not exist.",
|
||||
category.clone()
|
||||
))
|
||||
})?;
|
||||
database::models::categories::Category::get_id(category, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(format!(
|
||||
"Category {} does not exist.",
|
||||
category.clone()
|
||||
))
|
||||
})?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
@ -761,17 +734,14 @@ pub async fn project_edit(
|
||||
|
||||
for category in categories {
|
||||
let category_id =
|
||||
database::models::categories::Category::get_id(
|
||||
category,
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(format!(
|
||||
"Category {} does not exist.",
|
||||
category.clone()
|
||||
))
|
||||
})?;
|
||||
database::models::categories::Category::get_id(category, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(format!(
|
||||
"Category {} does not exist.",
|
||||
category.clone()
|
||||
))
|
||||
})?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
@ -899,8 +869,7 @@ pub async fn project_edit(
|
||||
));
|
||||
}
|
||||
|
||||
let slug_project_id_option: Option<u64> =
|
||||
parse_base62(slug).ok();
|
||||
let slug_project_id_option: Option<u64> = parse_base62(slug).ok();
|
||||
if let Some(slug_project_id) = slug_project_id_option {
|
||||
let results = sqlx::query!(
|
||||
"
|
||||
@ -913,8 +882,7 @@ pub async fn project_edit(
|
||||
|
||||
if results.exists.unwrap_or(true) {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Slug collides with other project's id!"
|
||||
.to_string(),
|
||||
"Slug collides with other project's id!".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -933,8 +901,7 @@ pub async fn project_edit(
|
||||
|
||||
if results.exists.unwrap_or(true) {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Slug collides with other project's id!"
|
||||
.to_string(),
|
||||
"Slug collides with other project's id!".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -960,13 +927,12 @@ pub async fn project_edit(
|
||||
));
|
||||
}
|
||||
|
||||
let side_type_id =
|
||||
database::models::categories::SideType::get_id(
|
||||
new_side.as_str(),
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?
|
||||
.expect("No database entry found for side type");
|
||||
let side_type_id = database::models::categories::SideType::get_id(
|
||||
new_side.as_str(),
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?
|
||||
.expect("No database entry found for side type");
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
@ -989,13 +955,12 @@ pub async fn project_edit(
|
||||
));
|
||||
}
|
||||
|
||||
let side_type_id =
|
||||
database::models::categories::SideType::get_id(
|
||||
new_side.as_str(),
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?
|
||||
.expect("No database entry found for side type");
|
||||
let side_type_id = database::models::categories::SideType::get_id(
|
||||
new_side.as_str(),
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?
|
||||
.expect("No database entry found for side type");
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
@ -1025,9 +990,7 @@ pub async fn project_edit(
|
||||
}
|
||||
|
||||
spdx::Expression::parse(&license).map_err(|err| {
|
||||
ApiError::InvalidInput(format!(
|
||||
"Invalid SPDX license identifier: {err}"
|
||||
))
|
||||
ApiError::InvalidInput(format!("Invalid SPDX license identifier: {err}"))
|
||||
})?;
|
||||
|
||||
sqlx::query!(
|
||||
@ -1062,18 +1025,17 @@ pub async fn project_edit(
|
||||
.await?;
|
||||
|
||||
for donation in donations {
|
||||
let platform_id =
|
||||
database::models::categories::DonationPlatform::get_id(
|
||||
&donation.id,
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(format!(
|
||||
"Platform {} does not exist.",
|
||||
donation.id.clone()
|
||||
))
|
||||
})?;
|
||||
let platform_id = database::models::categories::DonationPlatform::get_id(
|
||||
&donation.id,
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(format!(
|
||||
"Platform {} does not exist.",
|
||||
donation.id.clone()
|
||||
))
|
||||
})?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
@ -1090,9 +1052,7 @@ pub async fn project_edit(
|
||||
}
|
||||
|
||||
if let Some(moderation_message) = &new_project.moderation_message {
|
||||
if !user.role.is_mod()
|
||||
&& project_item.inner.status != ProjectStatus::Approved
|
||||
{
|
||||
if !user.role.is_mod() && project_item.inner.status != ProjectStatus::Approved {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the moderation message of this project!"
|
||||
.to_string(),
|
||||
@ -1112,12 +1072,8 @@ pub async fn project_edit(
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(moderation_message_body) =
|
||||
&new_project.moderation_message_body
|
||||
{
|
||||
if !user.role.is_mod()
|
||||
&& project_item.inner.status != ProjectStatus::Approved
|
||||
{
|
||||
if let Some(moderation_message_body) = &new_project.moderation_message_body {
|
||||
if !user.role.is_mod() && project_item.inner.status != ProjectStatus::Approved {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the moderation message body of this project!"
|
||||
.to_string(),
|
||||
@ -1158,8 +1114,7 @@ pub async fn project_edit(
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(monetization_status) = &new_project.monetization_status
|
||||
{
|
||||
if let Some(monetization_status) = &new_project.monetization_status {
|
||||
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the monetization status of this project!"
|
||||
@ -1167,8 +1122,7 @@ pub async fn project_edit(
|
||||
));
|
||||
}
|
||||
|
||||
if (*monetization_status
|
||||
== MonetizationStatus::ForceDemonetized
|
||||
if (*monetization_status == MonetizationStatus::ForceDemonetized
|
||||
|| project_item.inner.monetization_status
|
||||
== MonetizationStatus::ForceDemonetized)
|
||||
&& !user.role.is_mod()
|
||||
@ -1276,9 +1230,9 @@ pub async fn projects_edit(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
bulk_edit_project.validate().map_err(|err| {
|
||||
ApiError::Validation(validation_errors_to_string(err, None))
|
||||
})?;
|
||||
bulk_edit_project
|
||||
.validate()
|
||||
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
|
||||
|
||||
let project_ids: Vec<database::models::ids::ProjectId> =
|
||||
serde_json::from_str::<Vec<ProjectId>>(&ids.ids)?
|
||||
@ -1286,8 +1240,7 @@ pub async fn projects_edit(
|
||||
.map(|x| x.into())
|
||||
.collect();
|
||||
|
||||
let projects_data =
|
||||
database::models::Project::get_many_full(&project_ids, &**pool).await?;
|
||||
let projects_data = database::models::Project::get_many_full(&project_ids, &**pool).await?;
|
||||
|
||||
if let Some(id) = project_ids
|
||||
.iter()
|
||||
@ -1303,15 +1256,11 @@ pub async fn projects_edit(
|
||||
.iter()
|
||||
.map(|x| x.inner.team_id)
|
||||
.collect::<Vec<database::models::TeamId>>();
|
||||
let team_members = database::models::TeamMember::get_from_team_full_many(
|
||||
&team_ids, &**pool,
|
||||
)
|
||||
.await?;
|
||||
let team_members =
|
||||
database::models::TeamMember::get_from_team_full_many(&team_ids, &**pool).await?;
|
||||
|
||||
let categories =
|
||||
database::models::categories::Category::list(&**pool).await?;
|
||||
let donation_platforms =
|
||||
database::models::categories::DonationPlatform::list(&**pool).await?;
|
||||
let categories = database::models::categories::Category::list(&**pool).await?;
|
||||
let donation_platforms = database::models::categories::DonationPlatform::list(&**pool).await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
@ -1322,9 +1271,10 @@ pub async fn projects_edit(
|
||||
.find(|x| x.team_id == project.inner.team_id)
|
||||
{
|
||||
if !member.permissions.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
format!("You do not have the permissions to bulk edit project {}!", project.inner.title),
|
||||
));
|
||||
return Err(ApiError::CustomAuthentication(format!(
|
||||
"You do not have the permissions to bulk edit project {}!",
|
||||
project.inner.title
|
||||
)));
|
||||
}
|
||||
} else if project.inner.status.is_hidden() {
|
||||
return Ok(HttpResponse::NotFound().body(""));
|
||||
@ -1336,18 +1286,15 @@ pub async fn projects_edit(
|
||||
};
|
||||
}
|
||||
|
||||
let mut set_categories =
|
||||
if let Some(categories) = bulk_edit_project.categories.clone() {
|
||||
categories
|
||||
} else {
|
||||
project.categories.clone()
|
||||
};
|
||||
let mut set_categories = if let Some(categories) = bulk_edit_project.categories.clone() {
|
||||
categories
|
||||
} else {
|
||||
project.categories.clone()
|
||||
};
|
||||
|
||||
if let Some(delete_categories) = &bulk_edit_project.remove_categories {
|
||||
for category in delete_categories {
|
||||
if let Some(pos) =
|
||||
set_categories.iter().position(|x| x == category)
|
||||
{
|
||||
if let Some(pos) = set_categories.iter().position(|x| x == category) {
|
||||
set_categories.remove(pos);
|
||||
}
|
||||
}
|
||||
@ -1394,34 +1341,27 @@ pub async fn projects_edit(
|
||||
project.inner.id as database::models::ids::ProjectId,
|
||||
category_id as database::models::ids::CategoryId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut set_additional_categories = if let Some(categories) =
|
||||
bulk_edit_project.additional_categories.clone()
|
||||
{
|
||||
categories
|
||||
} else {
|
||||
project.additional_categories.clone()
|
||||
};
|
||||
let mut set_additional_categories =
|
||||
if let Some(categories) = bulk_edit_project.additional_categories.clone() {
|
||||
categories
|
||||
} else {
|
||||
project.additional_categories.clone()
|
||||
};
|
||||
|
||||
if let Some(delete_categories) =
|
||||
&bulk_edit_project.remove_additional_categories
|
||||
{
|
||||
if let Some(delete_categories) = &bulk_edit_project.remove_additional_categories {
|
||||
for category in delete_categories {
|
||||
if let Some(pos) =
|
||||
set_additional_categories.iter().position(|x| x == category)
|
||||
{
|
||||
if let Some(pos) = set_additional_categories.iter().position(|x| x == category) {
|
||||
set_additional_categories.remove(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(add_categories) =
|
||||
&bulk_edit_project.add_additional_categories
|
||||
{
|
||||
if let Some(add_categories) = &bulk_edit_project.add_additional_categories {
|
||||
for category in add_categories {
|
||||
if set_additional_categories.len() < 256 {
|
||||
set_additional_categories.push(category.clone());
|
||||
@ -1476,16 +1416,14 @@ pub async fn projects_edit(
|
||||
url: d.url,
|
||||
})
|
||||
.collect();
|
||||
let mut set_donation_links = if let Some(donation_links) =
|
||||
bulk_edit_project.donation_urls.clone()
|
||||
{
|
||||
donation_links
|
||||
} else {
|
||||
project_donations.clone()
|
||||
};
|
||||
let mut set_donation_links =
|
||||
if let Some(donation_links) = bulk_edit_project.donation_urls.clone() {
|
||||
donation_links
|
||||
} else {
|
||||
project_donations.clone()
|
||||
};
|
||||
|
||||
if let Some(delete_donations) = &bulk_edit_project.remove_donation_urls
|
||||
{
|
||||
if let Some(delete_donations) = &bulk_edit_project.remove_donation_urls {
|
||||
for donation in delete_donations {
|
||||
if let Some(pos) = set_donation_links
|
||||
.iter()
|
||||
@ -1532,8 +1470,8 @@ pub async fn projects_edit(
|
||||
platform_id as database::models::ids::DonationPlatformId,
|
||||
donation.url
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1616,8 +1554,7 @@ pub async fn project_schedule(
|
||||
|
||||
if scheduling_data.time < Utc::now() {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You cannot schedule a project to be released in the past!"
|
||||
.to_string(),
|
||||
"You cannot schedule a project to be released in the past!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@ -1628,10 +1565,7 @@ pub async fn project_schedule(
|
||||
}
|
||||
|
||||
let string = info.into_inner().0;
|
||||
let result = database::models::Project::get_from_slug_or_project_id(
|
||||
&string, &**pool,
|
||||
)
|
||||
.await?;
|
||||
let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool).await?;
|
||||
|
||||
if let Some(project_item) = result {
|
||||
let team_member = database::models::TeamMember::get_from_user_id(
|
||||
@ -1684,22 +1618,15 @@ pub async fn project_icon_edit(
|
||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
mut payload: web::Payload,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if let Some(content_type) =
|
||||
crate::util::ext::get_image_content_type(&ext.ext)
|
||||
{
|
||||
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let project_item =
|
||||
database::models::Project::get_from_slug_or_project_id(
|
||||
&string, &**pool,
|
||||
)
|
||||
let project_item = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified project does not exist!".to_string(),
|
||||
)
|
||||
ApiError::InvalidInput("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
if !user.role.is_mod() {
|
||||
@ -1711,15 +1638,12 @@ pub async fn project_icon_edit(
|
||||
.await
|
||||
.map_err(ApiError::Database)?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified project does not exist!".to_string(),
|
||||
)
|
||||
ApiError::InvalidInput("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this project's icon."
|
||||
.to_string(),
|
||||
"You don't have permission to edit this project's icon.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -1732,12 +1656,8 @@ pub async fn project_icon_edit(
|
||||
}
|
||||
}
|
||||
|
||||
let bytes = read_from_payload(
|
||||
&mut payload,
|
||||
262144,
|
||||
"Icons must be smaller than 256KiB",
|
||||
)
|
||||
.await?;
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
|
||||
let color = crate::util::img::get_color_from_img(&bytes)?;
|
||||
|
||||
@ -1787,15 +1707,11 @@ pub async fn delete_project_icon(
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let project_item = database::models::Project::get_from_slug_or_project_id(
|
||||
&string, &**pool,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified project does not exist!".to_string(),
|
||||
)
|
||||
})?;
|
||||
let project_item = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
if !user.role.is_mod() {
|
||||
let team_member = database::models::TeamMember::get_from_user_id(
|
||||
@ -1806,15 +1722,12 @@ pub async fn delete_project_icon(
|
||||
.await
|
||||
.map_err(ApiError::Database)?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified project does not exist!".to_string(),
|
||||
)
|
||||
ApiError::InvalidInput("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this project's icon."
|
||||
.to_string(),
|
||||
"You don't have permission to edit this project's icon.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -1866,32 +1779,24 @@ pub async fn add_gallery_item(
|
||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
mut payload: web::Payload,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if let Some(content_type) =
|
||||
crate::util::ext::get_image_content_type(&ext.ext)
|
||||
{
|
||||
item.validate().map_err(|err| {
|
||||
ApiError::Validation(validation_errors_to_string(err, None))
|
||||
})?;
|
||||
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
|
||||
item.validate()
|
||||
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
|
||||
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let project_item =
|
||||
database::models::Project::get_full_from_slug_or_project_id(
|
||||
&string, &**pool,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified project does not exist!".to_string(),
|
||||
)
|
||||
})?;
|
||||
database::models::Project::get_full_from_slug_or_project_id(&string, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
if project_item.gallery_items.len() > 64 {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You have reached the maximum of gallery images to upload."
|
||||
.to_string(),
|
||||
"You have reached the maximum of gallery images to upload.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@ -1904,15 +1809,12 @@ pub async fn add_gallery_item(
|
||||
.await
|
||||
.map_err(ApiError::Database)?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified project does not exist!".to_string(),
|
||||
)
|
||||
ApiError::InvalidInput("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this project's gallery."
|
||||
.to_string(),
|
||||
"You don't have permission to edit this project's gallery.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -2013,19 +1915,14 @@ pub async fn edit_gallery_item(
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let string = info.into_inner().0;
|
||||
|
||||
item.validate().map_err(|err| {
|
||||
ApiError::Validation(validation_errors_to_string(err, None))
|
||||
})?;
|
||||
item.validate()
|
||||
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
|
||||
|
||||
let project_item = database::models::Project::get_from_slug_or_project_id(
|
||||
&string, &**pool,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified project does not exist!".to_string(),
|
||||
)
|
||||
})?;
|
||||
let project_item = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
if !user.role.is_mod() {
|
||||
let team_member = database::models::TeamMember::get_from_user_id(
|
||||
@ -2036,15 +1933,12 @@ pub async fn edit_gallery_item(
|
||||
.await
|
||||
.map_err(ApiError::Database)?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified project does not exist!".to_string(),
|
||||
)
|
||||
ApiError::InvalidInput("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this project's gallery."
|
||||
.to_string(),
|
||||
"You don't have permission to edit this project's gallery.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -2157,15 +2051,11 @@ pub async fn delete_gallery_item(
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let project_item = database::models::Project::get_from_slug_or_project_id(
|
||||
&string, &**pool,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified project does not exist!".to_string(),
|
||||
)
|
||||
})?;
|
||||
let project_item = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
if !user.role.is_mod() {
|
||||
let team_member = database::models::TeamMember::get_from_user_id(
|
||||
@ -2176,15 +2066,12 @@ pub async fn delete_gallery_item(
|
||||
.await
|
||||
.map_err(ApiError::Database)?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified project does not exist!".to_string(),
|
||||
)
|
||||
ApiError::InvalidInput("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this project's gallery."
|
||||
.to_string(),
|
||||
"You don't have permission to edit this project's gallery.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -2241,30 +2128,23 @@ pub async fn project_delete(
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let project = database::models::Project::get_from_slug_or_project_id(
|
||||
&string, &**pool,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified project does not exist!".to_string(),
|
||||
)
|
||||
})?;
|
||||
let project = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
if !user.role.is_admin() {
|
||||
let team_member =
|
||||
database::models::TeamMember::get_from_user_id_project(
|
||||
project.id,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::Database)?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified project does not exist!".to_string(),
|
||||
)
|
||||
})?;
|
||||
let team_member = database::models::TeamMember::get_from_user_id_project(
|
||||
project.id,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::Database)?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
if !team_member
|
||||
.permissions
|
||||
@ -2278,9 +2158,7 @@ pub async fn project_delete(
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let result =
|
||||
database::models::Project::remove_full(project.id, &mut transaction)
|
||||
.await?;
|
||||
let result = database::models::Project::remove_full(project.id, &mut transaction).await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
@ -2302,15 +2180,11 @@ pub async fn project_follow(
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let result = database::models::Project::get_from_slug_or_project_id(
|
||||
&string, &**pool,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified project does not exist!".to_string(),
|
||||
)
|
||||
})?;
|
||||
let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
let user_id: database::models::ids::UserId = user.id.into();
|
||||
let project_id: database::models::ids::ProjectId = result.id;
|
||||
@ -2375,15 +2249,11 @@ pub async fn project_unfollow(
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let result = database::models::Project::get_from_slug_or_project_id(
|
||||
&string, &**pool,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified project does not exist!".to_string(),
|
||||
)
|
||||
})?;
|
||||
let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
let user_id: database::models::ids::UserId = user.id.into();
|
||||
let project_id = result.id;
|
||||
@ -2439,8 +2309,7 @@ pub async fn delete_from_index(
|
||||
id: ProjectId,
|
||||
config: web::Data<SearchConfig>,
|
||||
) -> Result<(), meilisearch_sdk::errors::Error> {
|
||||
let client =
|
||||
meilisearch_sdk::client::Client::new(&*config.address, &*config.key);
|
||||
let client = meilisearch_sdk::client::Client::new(&*config.address, &*config.key);
|
||||
|
||||
let indexes: IndexesResults = client.get_indexes().await?;
|
||||
|
||||
|
||||
@ -1,15 +1,9 @@
|
||||
use crate::database::models::thread_item::{
|
||||
ThreadBuilder, ThreadMessageBuilder,
|
||||
};
|
||||
use crate::models::ids::{
|
||||
base62_impl::parse_base62, ProjectId, UserId, VersionId,
|
||||
};
|
||||
use crate::database::models::thread_item::{ThreadBuilder, ThreadMessageBuilder};
|
||||
use crate::models::ids::{base62_impl::parse_base62, ProjectId, UserId, VersionId};
|
||||
use crate::models::reports::{ItemType, Report};
|
||||
use crate::models::threads::{MessageBody, ThreadType};
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::auth::{
|
||||
check_is_moderator_from_headers, get_user_from_headers,
|
||||
};
|
||||
use crate::util::auth::{check_is_moderator_from_headers, get_user_from_headers};
|
||||
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
|
||||
use chrono::Utc;
|
||||
use futures::StreamExt;
|
||||
@ -41,31 +35,24 @@ pub async fn report_create(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let current_user =
|
||||
get_user_from_headers(req.headers(), &mut *transaction).await?;
|
||||
let current_user = get_user_from_headers(req.headers(), &mut *transaction).await?;
|
||||
|
||||
let mut bytes = web::BytesMut::new();
|
||||
while let Some(item) = body.next().await {
|
||||
bytes.extend_from_slice(&item.map_err(|_| {
|
||||
ApiError::InvalidInput(
|
||||
"Error while parsing request payload!".to_string(),
|
||||
)
|
||||
ApiError::InvalidInput("Error while parsing request payload!".to_string())
|
||||
})?);
|
||||
}
|
||||
let new_report: CreateReport = serde_json::from_slice(bytes.as_ref())?;
|
||||
|
||||
let id =
|
||||
crate::database::models::generate_report_id(&mut transaction).await?;
|
||||
let id = crate::database::models::generate_report_id(&mut transaction).await?;
|
||||
let report_type = crate::database::models::categories::ReportType::get_id(
|
||||
&new_report.report_type,
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(format!(
|
||||
"Invalid report type: {}",
|
||||
new_report.report_type
|
||||
))
|
||||
ApiError::InvalidInput(format!("Invalid report type: {}", new_report.report_type))
|
||||
})?;
|
||||
|
||||
let thread_id = ThreadBuilder {
|
||||
@ -92,8 +79,7 @@ pub async fn report_create(
|
||||
|
||||
match new_report.item_type {
|
||||
ItemType::Project => {
|
||||
let project_id =
|
||||
ProjectId(parse_base62(new_report.item_id.as_str())?);
|
||||
let project_id = ProjectId(parse_base62(new_report.item_id.as_str())?);
|
||||
|
||||
let result = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM mods WHERE id = $1)",
|
||||
@ -112,8 +98,7 @@ pub async fn report_create(
|
||||
report.project_id = Some(project_id.into())
|
||||
}
|
||||
ItemType::Version => {
|
||||
let version_id =
|
||||
VersionId(parse_base62(new_report.item_id.as_str())?);
|
||||
let version_id = VersionId(parse_base62(new_report.item_id.as_str())?);
|
||||
|
||||
let result = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM versions WHERE id = $1)",
|
||||
@ -236,11 +221,8 @@ pub async fn reports(
|
||||
.await?
|
||||
};
|
||||
|
||||
let query_reports = crate::database::models::report_item::Report::get_many(
|
||||
&report_ids,
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
let query_reports =
|
||||
crate::database::models::report_item::Report::get_many(&report_ids, &**pool).await?;
|
||||
|
||||
let mut reports = Vec::new();
|
||||
|
||||
@ -260,8 +242,7 @@ pub async fn report_get(
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let id = info.into_inner().0.into();
|
||||
|
||||
let report =
|
||||
crate::database::models::report_item::Report::get(id, &**pool).await?;
|
||||
let report = crate::database::models::report_item::Report::get(id, &**pool).await?;
|
||||
|
||||
if let Some(report) = report {
|
||||
if !user.role.is_mod() && report.reporter != user.id.into() {
|
||||
@ -291,8 +272,7 @@ pub async fn report_edit(
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let id = info.into_inner().0.into();
|
||||
|
||||
let report =
|
||||
crate::database::models::report_item::Report::get(id, &**pool).await?;
|
||||
let report = crate::database::models::report_item::Report::get(id, &**pool).await?;
|
||||
|
||||
if let Some(report) = report {
|
||||
if !user.role.is_mod() && report.user_id != Some(user.id.into()) {
|
||||
@ -380,9 +360,7 @@ pub async fn report_delete(
|
||||
}
|
||||
}
|
||||
|
||||
fn to_report(
|
||||
x: crate::database::models::report_item::QueryReport,
|
||||
) -> Result<Report, ApiError> {
|
||||
fn to_report(x: crate::database::models::report_item::QueryReport) -> Result<Report, ApiError> {
|
||||
let mut item_id = "".to_string();
|
||||
let mut item_type = ItemType::Unknown;
|
||||
|
||||
|
||||
@ -8,9 +8,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
}
|
||||
|
||||
#[get("statistics")]
|
||||
pub async fn get_stats(
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
pub async fn get_stats(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
|
||||
let projects = sqlx::query!(
|
||||
"
|
||||
SELECT COUNT(id)
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
use super::ApiError;
|
||||
use crate::database::models;
|
||||
use crate::database::models::categories::{
|
||||
DonationPlatform, ProjectType, ReportType, SideType,
|
||||
};
|
||||
use crate::database::models::categories::{DonationPlatform, ProjectType, ReportType, SideType};
|
||||
use actix_web::{get, web, HttpResponse};
|
||||
use chrono::{DateTime, Utc};
|
||||
use models::categories::{Category, GameVersion, Loader};
|
||||
@ -34,9 +32,7 @@ pub struct CategoryData {
|
||||
// TODO: searching / filtering? Could be used to implement a live
|
||||
// searching category list
|
||||
#[get("category")]
|
||||
pub async fn category_list(
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
pub async fn category_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
|
||||
let results = Category::list(&**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
@ -59,9 +55,7 @@ pub struct LoaderData {
|
||||
}
|
||||
|
||||
#[get("loader")]
|
||||
pub async fn loader_list(
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
pub async fn loader_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
|
||||
let mut results = Loader::list(&**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
@ -97,11 +91,8 @@ pub async fn game_version_list(
|
||||
pool: web::Data<PgPool>,
|
||||
query: web::Query<GameVersionQuery>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let results: Vec<GameVersionQueryData> = if query.type_.is_some()
|
||||
|| query.major.is_some()
|
||||
{
|
||||
GameVersion::list_filter(query.type_.as_deref(), query.major, &**pool)
|
||||
.await?
|
||||
let results: Vec<GameVersionQueryData> = if query.type_.is_some() || query.major.is_some() {
|
||||
GameVersion::list_filter(query.type_.as_deref(), query.major, &**pool).await?
|
||||
} else {
|
||||
GameVersion::list(&**pool).await?
|
||||
}
|
||||
@ -145,9 +136,7 @@ pub struct LicenseText {
|
||||
}
|
||||
|
||||
#[get("license/{id}")]
|
||||
pub async fn license_text(
|
||||
params: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
pub async fn license_text(params: web::Path<(String,)>) -> Result<HttpResponse, ApiError> {
|
||||
let license_id = params.into_inner().0;
|
||||
|
||||
if license_id == *crate::models::projects::DEFAULT_LICENSE_ID {
|
||||
@ -176,41 +165,32 @@ pub struct DonationPlatformQueryData {
|
||||
}
|
||||
|
||||
#[get("donation_platform")]
|
||||
pub async fn donation_platform_list(
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let results: Vec<DonationPlatformQueryData> =
|
||||
DonationPlatform::list(&**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| DonationPlatformQueryData {
|
||||
short: x.short,
|
||||
name: x.name,
|
||||
})
|
||||
.collect();
|
||||
pub async fn donation_platform_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
|
||||
let results: Vec<DonationPlatformQueryData> = DonationPlatform::list(&**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| DonationPlatformQueryData {
|
||||
short: x.short,
|
||||
name: x.name,
|
||||
})
|
||||
.collect();
|
||||
Ok(HttpResponse::Ok().json(results))
|
||||
}
|
||||
|
||||
#[get("report_type")]
|
||||
pub async fn report_type_list(
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
pub async fn report_type_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
|
||||
let results = ReportType::list(&**pool).await?;
|
||||
Ok(HttpResponse::Ok().json(results))
|
||||
}
|
||||
|
||||
#[get("project_type")]
|
||||
pub async fn project_type_list(
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
pub async fn project_type_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
|
||||
let results = ProjectType::list(&**pool).await?;
|
||||
Ok(HttpResponse::Ok().json(results))
|
||||
}
|
||||
|
||||
#[get("side_type")]
|
||||
pub async fn side_type_list(
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
pub async fn side_type_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
|
||||
let results = SideType::list(&**pool).await?;
|
||||
Ok(HttpResponse::Ok().json(results))
|
||||
}
|
||||
|
||||
@ -33,33 +33,23 @@ pub async fn team_members_get_project(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let string = info.into_inner().0;
|
||||
let project_data =
|
||||
crate::database::models::Project::get_from_slug_or_project_id(
|
||||
&string, &**pool,
|
||||
)
|
||||
.await?;
|
||||
crate::database::models::Project::get_from_slug_or_project_id(&string, &**pool).await?;
|
||||
|
||||
if let Some(project) = project_data {
|
||||
let members_data =
|
||||
TeamMember::get_from_team_full(project.team_id, &**pool).await?;
|
||||
let members_data = TeamMember::get_from_team_full(project.team_id, &**pool).await?;
|
||||
|
||||
let current_user =
|
||||
get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
let current_user = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
if let Some(user) = current_user {
|
||||
let team_member = TeamMember::get_from_user_id(
|
||||
project.team_id,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::Database)?;
|
||||
let team_member =
|
||||
TeamMember::get_from_user_id(project.team_id, user.id.into(), &**pool)
|
||||
.await
|
||||
.map_err(ApiError::Database)?;
|
||||
|
||||
if team_member.is_some() {
|
||||
let team_members: Vec<_> = members_data
|
||||
.into_iter()
|
||||
.map(|data| {
|
||||
crate::models::teams::TeamMember::from(data, false)
|
||||
})
|
||||
.map(|data| crate::models::teams::TeamMember::from(data, false))
|
||||
.collect();
|
||||
|
||||
return Ok(HttpResponse::Ok().json(team_members));
|
||||
@ -85,16 +75,14 @@ pub async fn team_members_get(
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let id = info.into_inner().0;
|
||||
let members_data =
|
||||
TeamMember::get_from_team_full(id.into(), &**pool).await?;
|
||||
let members_data = TeamMember::get_from_team_full(id.into(), &**pool).await?;
|
||||
|
||||
let current_user = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
if let Some(user) = ¤t_user {
|
||||
let team_member =
|
||||
TeamMember::get_from_user_id(id.into(), user.id.into(), &**pool)
|
||||
.await
|
||||
.map_err(ApiError::Database)?;
|
||||
let team_member = TeamMember::get_from_user_id(id.into(), user.id.into(), &**pool)
|
||||
.await
|
||||
.map_err(ApiError::Database)?;
|
||||
|
||||
if team_member.is_some() {
|
||||
let team_members: Vec<_> = members_data
|
||||
@ -139,8 +127,7 @@ pub async fn teams_get(
|
||||
.map(|x| x.into())
|
||||
.collect::<Vec<crate::database::models::ids::TeamId>>();
|
||||
|
||||
let teams_data =
|
||||
TeamMember::get_from_team_full_many(&team_ids, &**pool).await?;
|
||||
let teams_data = TeamMember::get_from_team_full_many(&team_ids, &**pool).await?;
|
||||
|
||||
let current_user = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
let accepted = if let Some(user) = current_user {
|
||||
@ -159,9 +146,8 @@ pub async fn teams_get(
|
||||
|
||||
for (id, member_data) in &teams_groups {
|
||||
if accepted.contains(&id) {
|
||||
let team_members = member_data.map(|data| {
|
||||
crate::models::teams::TeamMember::from(data, false)
|
||||
});
|
||||
let team_members =
|
||||
member_data.map(|data| crate::models::teams::TeamMember::from(data, false));
|
||||
|
||||
teams.push(team_members.collect());
|
||||
|
||||
@ -187,12 +173,8 @@ pub async fn join_team(
|
||||
let team_id = info.into_inner().0.into();
|
||||
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let member = TeamMember::get_from_user_id_pending(
|
||||
team_id,
|
||||
current_user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
let member =
|
||||
TeamMember::get_from_user_id_pending(team_id, current_user.id.into(), &**pool).await?;
|
||||
|
||||
if let Some(member) = member {
|
||||
if member.accepted {
|
||||
@ -258,20 +240,17 @@ pub async fn add_team_member(
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let member =
|
||||
TeamMember::get_from_user_id(team_id, current_user.id.into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit members of this team"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
let member = TeamMember::get_from_user_id(team_id, current_user.id.into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit members of this team".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if !member.permissions.contains(Permissions::MANAGE_INVITES) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to invite users to this team"
|
||||
.to_string(),
|
||||
"You don't have permission to invite users to this team".to_string(),
|
||||
));
|
||||
}
|
||||
if !member.permissions.contains(new_member.permissions) {
|
||||
@ -286,9 +265,7 @@ pub async fn add_team_member(
|
||||
));
|
||||
}
|
||||
|
||||
if new_member.payouts_split < Decimal::ZERO
|
||||
|| new_member.payouts_split > Decimal::from(5000)
|
||||
{
|
||||
if new_member.payouts_split < Decimal::ZERO || new_member.payouts_split > Decimal::from(5000) {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Payouts split must be between 0 and 5000!".to_string(),
|
||||
));
|
||||
@ -308,21 +285,16 @@ pub async fn add_team_member(
|
||||
));
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"There is already a pending member request for this user"
|
||||
.to_string(),
|
||||
"There is already a pending member request for this user".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
crate::database::models::User::get(member.user_id, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("An invalid User ID specified".to_string())
|
||||
})?;
|
||||
.ok_or_else(|| ApiError::InvalidInput("An invalid User ID specified".to_string()))?;
|
||||
|
||||
let new_id =
|
||||
crate::database::models::ids::generate_team_member_id(&mut transaction)
|
||||
.await?;
|
||||
let new_id = crate::database::models::ids::generate_team_member_id(&mut transaction).await?;
|
||||
TeamMember {
|
||||
id: new_id,
|
||||
team_id,
|
||||
@ -383,24 +355,20 @@ pub async fn edit_team_member(
|
||||
let user_id = ids.1.into();
|
||||
|
||||
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let member =
|
||||
TeamMember::get_from_user_id(id, current_user.id.into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit members of this team"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
let edit_member_db =
|
||||
TeamMember::get_from_user_id_pending(id, user_id, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit members of this team"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit members of this team".to_string(),
|
||||
)
|
||||
})?;
|
||||
let edit_member_db = TeamMember::get_from_user_id_pending(id, user_id, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit members of this team".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
@ -408,30 +376,26 @@ pub async fn edit_team_member(
|
||||
&& (edit_member.role.is_some() || edit_member.permissions.is_some())
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
"The owner's permission and role of a team cannot be edited"
|
||||
.to_string(),
|
||||
"The owner's permission and role of a team cannot be edited".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if !member.permissions.contains(Permissions::EDIT_MEMBER) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit members of this team"
|
||||
.to_string(),
|
||||
"You don't have permission to edit members of this team".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(new_permissions) = edit_member.permissions {
|
||||
if !member.permissions.contains(new_permissions) {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"The new permissions have permissions that you don't have"
|
||||
.to_string(),
|
||||
"The new permissions have permissions that you don't have".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(payouts_split) = edit_member.payouts_split {
|
||||
if payouts_split < Decimal::ZERO || payouts_split > Decimal::from(5000)
|
||||
{
|
||||
if payouts_split < Decimal::ZERO || payouts_split > Decimal::from(5000) {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Payouts split must be between 0 and 5000!".to_string(),
|
||||
));
|
||||
@ -478,38 +442,26 @@ pub async fn transfer_ownership(
|
||||
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
if !current_user.role.is_admin() {
|
||||
let member = TeamMember::get_from_user_id(
|
||||
id.into(),
|
||||
current_user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit members of this team"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
let member = TeamMember::get_from_user_id(id.into(), current_user.id.into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit members of this team".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if member.role != crate::models::teams::OWNER_ROLE {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit the ownership of this team"
|
||||
.to_string(),
|
||||
"You don't have permission to edit the ownership of this team".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let new_member = TeamMember::get_from_user_id(
|
||||
id.into(),
|
||||
new_owner.user_id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The new owner specified does not exist".to_string(),
|
||||
)
|
||||
})?;
|
||||
let new_member = TeamMember::get_from_user_id(id.into(), new_owner.user_id.into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The new owner specified does not exist".to_string())
|
||||
})?;
|
||||
|
||||
if !new_member.accepted {
|
||||
return Err(ApiError::InvalidInput(
|
||||
@ -559,18 +511,15 @@ pub async fn remove_team_member(
|
||||
let user_id = ids.1.into();
|
||||
|
||||
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let member =
|
||||
TeamMember::get_from_user_id(id, current_user.id.into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit members of this team"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit members of this team".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let delete_member =
|
||||
TeamMember::get_from_user_id_pending(id, user_id, &**pool).await?;
|
||||
let delete_member = TeamMember::get_from_user_id_pending(id, user_id, &**pool).await?;
|
||||
|
||||
if let Some(delete_member) = delete_member {
|
||||
if delete_member.role == crate::models::teams::OWNER_ROLE {
|
||||
@ -586,8 +535,7 @@ pub async fn remove_team_member(
|
||||
// Members other than the owner can either leave the team, or be
|
||||
// removed by a member with the REMOVE_MEMBER permission.
|
||||
if delete_member.user_id == member.user_id
|
||||
|| (member.permissions.contains(Permissions::REMOVE_MEMBER)
|
||||
&& member.accepted)
|
||||
|| (member.permissions.contains(Permissions::REMOVE_MEMBER) && member.accepted)
|
||||
{
|
||||
TeamMember::delete(id, user_id, &mut transaction).await?;
|
||||
} else {
|
||||
@ -596,8 +544,7 @@ pub async fn remove_team_member(
|
||||
));
|
||||
}
|
||||
} else if delete_member.user_id == member.user_id
|
||||
|| (member.permissions.contains(Permissions::MANAGE_INVITES)
|
||||
&& member.accepted)
|
||||
|| (member.permissions.contains(Permissions::MANAGE_INVITES) && member.accepted)
|
||||
{
|
||||
// This is a pending invite rather than a member, so the
|
||||
// user being invited or team members with the MANAGE_INVITES
|
||||
@ -605,8 +552,7 @@ pub async fn remove_team_member(
|
||||
TeamMember::delete(id, user_id, &mut transaction).await?;
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to cancel a team invite"
|
||||
.to_string(),
|
||||
"You do not have permission to cancel a team invite".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@ -4,14 +4,10 @@ use crate::database::models::thread_item::ThreadMessageBuilder;
|
||||
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,
|
||||
};
|
||||
use crate::models::threads::{MessageBody, Thread, ThreadId, ThreadMessage, ThreadType};
|
||||
use crate::models::users::User;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::auth::{
|
||||
check_is_moderator_from_headers, get_user_from_headers,
|
||||
};
|
||||
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;
|
||||
@ -42,13 +38,13 @@ pub async fn is_authorized_thread(
|
||||
Ok(match thread.type_ {
|
||||
ThreadType::Report => {
|
||||
let report_exists = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM reports WHERE thread_id = $1 AND reporter = $2)",
|
||||
thread.id as database::models::ids::ThreadId,
|
||||
user_id as database::models::ids::UserId,
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await?
|
||||
.exists;
|
||||
"SELECT EXISTS(SELECT 1 FROM reports WHERE thread_id = $1 AND reporter = $2)",
|
||||
thread.id as database::models::ids::ThreadId,
|
||||
user_id as database::models::ids::UserId,
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await?
|
||||
.exists;
|
||||
|
||||
report_exists.unwrap_or(false)
|
||||
}
|
||||
@ -80,8 +76,7 @@ pub async fn filter_authorized_threads(
|
||||
|
||||
for thread in threads {
|
||||
if user.role.is_mod()
|
||||
|| (thread.type_ == ThreadType::DirectMessage
|
||||
&& thread.members.contains(&user_id))
|
||||
|| (thread.type_ == ThreadType::DirectMessage && thread.members.contains(&user_id))
|
||||
{
|
||||
return_threads.push(thread);
|
||||
} else {
|
||||
@ -106,23 +101,23 @@ pub async fn filter_authorized_threads(
|
||||
&*project_thread_ids,
|
||||
user_id as database::models::ids::UserId,
|
||||
)
|
||||
.fetch_many(&***pool)
|
||||
.try_for_each(|e| {
|
||||
if let Some(row) = e.right() {
|
||||
check_threads.retain(|x| {
|
||||
let bool = Some(x.id.0) == row.thread_id;
|
||||
.fetch_many(&***pool)
|
||||
.try_for_each(|e| {
|
||||
if let Some(row) = e.right() {
|
||||
check_threads.retain(|x| {
|
||||
let bool = Some(x.id.0) == row.thread_id;
|
||||
|
||||
if bool {
|
||||
return_threads.push(x.clone());
|
||||
}
|
||||
if bool {
|
||||
return_threads.push(x.clone());
|
||||
}
|
||||
|
||||
!bool
|
||||
});
|
||||
}
|
||||
!bool
|
||||
});
|
||||
}
|
||||
|
||||
futures::future::ready(Ok(()))
|
||||
})
|
||||
.await?;
|
||||
futures::future::ready(Ok(()))
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
let report_thread_ids = check_threads
|
||||
@ -176,12 +171,11 @@ pub async fn filter_authorized_threads(
|
||||
.collect::<Vec<database::models::UserId>>(),
|
||||
);
|
||||
|
||||
let users: Vec<User> =
|
||||
database::models::User::get_many(&user_ids, &***pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(From::from)
|
||||
.collect();
|
||||
let users: Vec<User> = database::models::User::get_many(&user_ids, &***pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(From::from)
|
||||
.collect();
|
||||
|
||||
let mut final_threads = Vec::new();
|
||||
|
||||
@ -210,11 +204,7 @@ pub async fn filter_authorized_threads(
|
||||
Ok(final_threads)
|
||||
}
|
||||
|
||||
fn convert_thread(
|
||||
data: database::models::Thread,
|
||||
users: Vec<User>,
|
||||
user: &User,
|
||||
) -> Thread {
|
||||
fn convert_thread(data: database::models::Thread, users: Vec<User>, user: &User) -> Thread {
|
||||
let thread_type = data.type_;
|
||||
|
||||
Thread {
|
||||
@ -279,16 +269,13 @@ pub async fn thread_get(
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
let users: Vec<User> =
|
||||
database::models::User::get_many(authors, &**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(From::from)
|
||||
.collect();
|
||||
let users: Vec<User> = database::models::User::get_many(authors, &**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(From::from)
|
||||
.collect();
|
||||
|
||||
return Ok(
|
||||
HttpResponse::Ok().json(convert_thread(data, users, &user))
|
||||
);
|
||||
return Ok(HttpResponse::Ok().json(convert_thread(data, users, &user)));
|
||||
}
|
||||
}
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
@ -313,8 +300,7 @@ pub async fn threads_get(
|
||||
.map(|x| x.into())
|
||||
.collect();
|
||||
|
||||
let threads_data =
|
||||
database::models::Thread::get_many(&thread_ids, &**pool).await?;
|
||||
let threads_data = database::models::Thread::get_many(&thread_ids, &**pool).await?;
|
||||
|
||||
let threads = filter_authorized_threads(threads_data, &user, &pool).await?;
|
||||
|
||||
@ -356,17 +342,13 @@ pub async fn thread_send_message(
|
||||
}
|
||||
|
||||
if let Some(replying_to) = replying_to {
|
||||
let thread_message = database::models::ThreadMessage::get(
|
||||
(*replying_to).into(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
let thread_message =
|
||||
database::models::ThreadMessage::get((*replying_to).into(), &**pool).await?;
|
||||
|
||||
if let Some(thread_message) = thread_message {
|
||||
if thread_message.thread_id != string {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Message replied to is from another thread!"
|
||||
.to_string(),
|
||||
"Message replied to is from another thread!".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
@ -394,17 +376,13 @@ pub async fn thread_send_message(
|
||||
None
|
||||
};
|
||||
|
||||
if report.as_ref().map(|x| x.closed).unwrap_or(false)
|
||||
&& !user.role.is_mod()
|
||||
{
|
||||
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 (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,
|
||||
@ -422,7 +400,17 @@ pub async fn thread_send_message(
|
||||
),
|
||||
)
|
||||
} else {
|
||||
(false, (thread.type_ == ThreadType::Report, None))
|
||||
(
|
||||
!user.role.is_mod(),
|
||||
(
|
||||
thread.type_ == ThreadType::Report
|
||||
&& !report
|
||||
.as_ref()
|
||||
.map(|x| x.reporter == user.id.into())
|
||||
.unwrap_or(false),
|
||||
None,
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
@ -498,14 +486,11 @@ pub async fn moderation_inbox(
|
||||
"
|
||||
)
|
||||
.fetch_many(&**pool)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|m| database::models::ThreadId(m.id)))
|
||||
})
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|m| database::models::ThreadId(m.id))) })
|
||||
.try_collect::<Vec<database::models::ThreadId>>()
|
||||
.await?;
|
||||
|
||||
let threads_data =
|
||||
database::models::Thread::get_many(&ids, &**pool).await?;
|
||||
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))
|
||||
@ -546,11 +531,7 @@ pub async fn message_delete(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let result = database::models::ThreadMessage::get(
|
||||
info.into_inner().0.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
let result = database::models::ThreadMessage::get(info.into_inner().0.into(), &**pool).await?;
|
||||
|
||||
if let Some(thread) = result {
|
||||
if !user.role.is_mod() && thread.author_id != Some(user.id.into()) {
|
||||
@ -560,11 +541,7 @@ pub async fn message_delete(
|
||||
}
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
database::models::ThreadMessage::remove_full(
|
||||
thread.id,
|
||||
&mut transaction,
|
||||
)
|
||||
.await?;
|
||||
database::models::ThreadMessage::remove_full(thread.id, &mut transaction).await?;
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
|
||||
@ -2,9 +2,7 @@ use crate::database::models::User;
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models::notifications::Notification;
|
||||
use crate::models::projects::Project;
|
||||
use crate::models::users::{
|
||||
Badges, RecipientType, RecipientWallet, Role, UserId,
|
||||
};
|
||||
use crate::models::users::{Badges, RecipientType, RecipientWallet, Role, UserId};
|
||||
use crate::queue::payouts::{PayoutAmount, PayoutItem, PayoutsQueue};
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::auth::get_user_from_headers;
|
||||
@ -24,6 +22,7 @@ use validator::Validate;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(user_auth_get);
|
||||
cfg.service(user_data_get);
|
||||
cfg.service(users_get);
|
||||
|
||||
cfg.service(
|
||||
@ -45,8 +44,44 @@ pub async fn user_auth_get(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
Ok(HttpResponse::Ok()
|
||||
.json(get_user_from_headers(req.headers(), &**pool).await?))
|
||||
Ok(HttpResponse::Ok().json(get_user_from_headers(req.headers(), &**pool).await?))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct UserData {
|
||||
pub notifs_count: u64,
|
||||
pub followed_projects: Vec<crate::models::ids::ProjectId>,
|
||||
}
|
||||
|
||||
#[get("user_data")]
|
||||
pub async fn user_data_get(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let data = sqlx::query!(
|
||||
"
|
||||
SELECT COUNT(DISTINCT n.id) notifs_count, ARRAY_AGG(mf.mod_id) followed_projects FROM notifications n
|
||||
LEFT OUTER JOIN mod_follows mf ON mf.follower_id = $1
|
||||
WHERE user_id = $1 AND read = FALSE
|
||||
",
|
||||
user.id.0 as i64
|
||||
).fetch_optional(&**pool).await?;
|
||||
|
||||
if let Some(data) = data {
|
||||
Ok(HttpResponse::Ok().json(UserData {
|
||||
notifs_count: data.notifs_count.map(|x| x as u64).unwrap_or(0),
|
||||
followed_projects: data
|
||||
.followed_projects
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|x| crate::models::ids::ProjectId(x as u64))
|
||||
.collect(),
|
||||
}))
|
||||
} else {
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@ -66,8 +101,7 @@ pub async fn users_get(
|
||||
|
||||
let users_data = User::get_many(&user_ids, &**pool).await?;
|
||||
|
||||
let users: Vec<crate::models::users::User> =
|
||||
users_data.into_iter().map(From::from).collect();
|
||||
let users: Vec<crate::models::users::User> = users_data.into_iter().map(From::from).collect();
|
||||
|
||||
Ok(HttpResponse::Ok().json(users))
|
||||
}
|
||||
@ -78,8 +112,7 @@ pub async fn user_get(
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let string = info.into_inner().0;
|
||||
let id_option: Option<UserId> =
|
||||
serde_json::from_str(&format!("\"{string}\"")).ok();
|
||||
let id_option: Option<UserId> = serde_json::from_str(&format!("\"{string}\"")).ok();
|
||||
|
||||
let mut user_data;
|
||||
|
||||
@ -109,8 +142,7 @@ pub async fn projects_list(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
let id_option =
|
||||
User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
|
||||
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
|
||||
|
||||
if let Some(id) = id_option {
|
||||
let user_id: UserId = id.into();
|
||||
@ -121,13 +153,12 @@ pub async fn projects_list(
|
||||
|
||||
let project_data = User::get_projects(id, &**pool).await?;
|
||||
|
||||
let response: Vec<_> =
|
||||
crate::database::Project::get_many_full(&project_data, &**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|x| can_view_private || x.inner.status.is_searchable())
|
||||
.map(Project::from)
|
||||
.collect();
|
||||
let response: Vec<_> = crate::database::Project::get_many_full(&project_data, &**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|x| can_view_private || x.inner.status.is_searchable())
|
||||
.map(Project::from)
|
||||
.collect();
|
||||
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
} else {
|
||||
@ -192,12 +223,11 @@ pub async fn user_edit(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
new_user.validate().map_err(|err| {
|
||||
ApiError::Validation(validation_errors_to_string(err, None))
|
||||
})?;
|
||||
new_user
|
||||
.validate()
|
||||
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
|
||||
|
||||
let id_option =
|
||||
User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
|
||||
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
|
||||
|
||||
if let Some(id) = id_option {
|
||||
let user_id: UserId = id.into();
|
||||
@ -320,23 +350,21 @@ pub async fn user_edit(
|
||||
|
||||
if let Some(payout_data) = &new_user.payout_data {
|
||||
if let Some(payout_data) = payout_data {
|
||||
if payout_data.payout_wallet_type
|
||||
== RecipientType::UserHandle
|
||||
if payout_data.payout_wallet_type == RecipientType::UserHandle
|
||||
&& payout_data.payout_wallet == RecipientWallet::Paypal
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You cannot use a paypal wallet with a user handle!"
|
||||
.to_string(),
|
||||
"You cannot use a paypal wallet with a user handle!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if !match payout_data.payout_wallet_type {
|
||||
RecipientType::Email => validator::validate_email(
|
||||
&payout_data.payout_address,
|
||||
),
|
||||
RecipientType::Phone => validator::validate_phone(
|
||||
&payout_data.payout_address,
|
||||
),
|
||||
RecipientType::Email => {
|
||||
validator::validate_email(&payout_data.payout_address)
|
||||
}
|
||||
RecipientType::Phone => {
|
||||
validator::validate_phone(&payout_data.payout_address)
|
||||
}
|
||||
RecipientType::UserHandle => true,
|
||||
} {
|
||||
return Err(ApiError::InvalidInput(
|
||||
@ -350,8 +378,8 @@ pub async fn user_edit(
|
||||
",
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.fetch_one(&mut *transaction)
|
||||
.await?;
|
||||
.fetch_one(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
if results.exists.unwrap_or(false) {
|
||||
return Err(ApiError::InvalidInput(
|
||||
@ -371,8 +399,8 @@ pub async fn user_edit(
|
||||
payout_data.payout_address,
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"
|
||||
@ -382,8 +410,8 @@ pub async fn user_edit(
|
||||
",
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
@ -413,20 +441,15 @@ pub async fn user_icon_edit(
|
||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
mut payload: web::Payload,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if let Some(content_type) =
|
||||
crate::util::ext::get_image_content_type(&ext.ext)
|
||||
{
|
||||
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let id_option =
|
||||
User::get_id_from_username_or_id(&info.into_inner().0, &**pool)
|
||||
.await?;
|
||||
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
|
||||
|
||||
if let Some(id) = id_option {
|
||||
if user.id != id.into() && !user.role.is_mod() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this user's icon."
|
||||
.to_string(),
|
||||
"You don't have permission to edit this user's icon.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@ -452,12 +475,8 @@ pub async fn user_icon_edit(
|
||||
}
|
||||
}
|
||||
|
||||
let bytes = read_from_payload(
|
||||
&mut payload,
|
||||
2097152,
|
||||
"Icons must be smaller than 2MiB",
|
||||
)
|
||||
.await?;
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 2097152, "Icons must be smaller than 2MiB").await?;
|
||||
|
||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||
let upload_data = file_host
|
||||
@ -509,8 +528,7 @@ pub async fn user_delete(
|
||||
removal_type: web::Query<RemovalType>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let id_option =
|
||||
User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
|
||||
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
|
||||
|
||||
if let Some(id) = id_option {
|
||||
if !user.role.is_admin() && user.id != id.into() {
|
||||
@ -546,11 +564,7 @@ pub async fn user_follows(
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let id_option = crate::database::models::User::get_id_from_username_or_id(
|
||||
&info.into_inner().0,
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
|
||||
|
||||
if let Some(id) = id_option {
|
||||
if !user.role.is_admin() && user.id != id.into() {
|
||||
@ -576,12 +590,11 @@ pub async fn user_follows(
|
||||
.try_collect::<Vec<crate::database::models::ProjectId>>()
|
||||
.await?;
|
||||
|
||||
let projects: Vec<_> =
|
||||
crate::database::Project::get_many_full(&project_ids, &**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Project::from)
|
||||
.collect();
|
||||
let projects: Vec<_> = crate::database::Project::get_many_full(&project_ids, &**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Project::from)
|
||||
.collect();
|
||||
|
||||
Ok(HttpResponse::Ok().json(projects))
|
||||
} else {
|
||||
@ -596,11 +609,7 @@ pub async fn user_notifications(
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let id_option = crate::database::models::User::get_id_from_username_or_id(
|
||||
&info.into_inner().0,
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
|
||||
|
||||
if let Some(id) = id_option {
|
||||
if !user.role.is_admin() && user.id != id.into() {
|
||||
@ -638,14 +647,12 @@ pub async fn user_payouts(
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let id_option =
|
||||
User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
|
||||
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
|
||||
|
||||
if let Some(id) = id_option {
|
||||
if !user.role.is_admin() && user.id != id.into() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to see the payouts of this user!"
|
||||
.to_string(),
|
||||
"You do not have permission to see the payouts of this user!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@ -717,22 +724,18 @@ pub async fn user_payouts_request(
|
||||
let mut payouts_queue = payouts_queue.lock().await;
|
||||
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let id_option =
|
||||
User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
|
||||
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
|
||||
|
||||
if let Some(id) = id_option {
|
||||
if !user.role.is_admin() && user.id != id.into() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to request payouts of this user!"
|
||||
.to_string(),
|
||||
"You do not have permission to request payouts of this user!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(payouts_data) = user.payout_data {
|
||||
if let Some(payout_address) = payouts_data.payout_address {
|
||||
if let Some(payout_wallet_type) =
|
||||
payouts_data.payout_wallet_type
|
||||
{
|
||||
if let Some(payout_wallet_type) = payouts_data.payout_wallet_type {
|
||||
if let Some(payout_wallet) = payouts_data.payout_wallet {
|
||||
return if data.amount < payouts_data.balance {
|
||||
let mut transaction = pool.begin().await?;
|
||||
@ -744,10 +747,15 @@ pub async fn user_payouts_request(
|
||||
value: data.amount,
|
||||
},
|
||||
receiver: payout_address,
|
||||
note: "Payment from Modrinth creator monetization program".to_string(),
|
||||
note: "Payment from Modrinth creator monetization program"
|
||||
.to_string(),
|
||||
recipient_type: payout_wallet_type.to_string().to_uppercase(),
|
||||
recipient_wallet: payout_wallet.as_str_api().to_string(),
|
||||
sender_item_id: format!("{}-{}", UserId::from(id), Utc::now().timestamp()),
|
||||
sender_item_id: format!(
|
||||
"{}-{}",
|
||||
UserId::from(id),
|
||||
Utc::now().timestamp()
|
||||
),
|
||||
})
|
||||
.await?;
|
||||
|
||||
@ -760,8 +768,8 @@ pub async fn user_payouts_request(
|
||||
data.amount,
|
||||
"success"
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
@ -780,8 +788,7 @@ pub async fn user_payouts_request(
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::InvalidInput(
|
||||
"You do not have enough funds to make this payout!"
|
||||
.to_string(),
|
||||
"You do not have enough funds to make this payout!".to_string(),
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
@ -8,8 +8,8 @@ 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,
|
||||
Version, VersionFile, VersionId, VersionStatus, VersionType,
|
||||
Dependency, DependencyType, FileType, GameVersion, Loader, ProjectId, Version, VersionFile,
|
||||
VersionId, VersionStatus, VersionType,
|
||||
};
|
||||
use crate::models::teams::Permissions;
|
||||
use crate::util::auth::get_user_from_headers;
|
||||
@ -97,11 +97,8 @@ pub async fn version_create(
|
||||
.await;
|
||||
|
||||
if result.is_err() {
|
||||
let undo_result = super::project_creation::undo_uploads(
|
||||
&***file_host,
|
||||
&uploaded_files,
|
||||
)
|
||||
.await;
|
||||
let undo_result =
|
||||
super::project_creation::undo_uploads(&***file_host, &uploaded_files).await;
|
||||
let rollback_result = transaction.rollback().await;
|
||||
|
||||
undo_result?;
|
||||
@ -127,10 +124,8 @@ async fn version_create_inner(
|
||||
let mut initial_version_data = None;
|
||||
let mut version_builder = None;
|
||||
|
||||
let all_game_versions =
|
||||
models::categories::GameVersion::list(&mut *transaction).await?;
|
||||
let all_loaders =
|
||||
models::categories::Loader::list(&mut *transaction).await?;
|
||||
let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?;
|
||||
let all_loaders = models::categories::Loader::list(&mut *transaction).await?;
|
||||
|
||||
let user = get_user_from_headers(req.headers(), &mut *transaction).await?;
|
||||
|
||||
@ -145,9 +140,7 @@ async fn version_create_inner(
|
||||
let result = async {
|
||||
let content_disposition = field.content_disposition().clone();
|
||||
let name = content_disposition.get_name().ok_or_else(|| {
|
||||
CreateError::MissingValueError(
|
||||
"Missing content name".to_string(),
|
||||
)
|
||||
CreateError::MissingValueError("Missing content name".to_string())
|
||||
})?;
|
||||
|
||||
if name == "data" {
|
||||
@ -156,11 +149,9 @@ async fn version_create_inner(
|
||||
data.extend_from_slice(&chunk?);
|
||||
}
|
||||
|
||||
let version_create_data: InitialVersionData =
|
||||
serde_json::from_slice(&data)?;
|
||||
let version_create_data: InitialVersionData = serde_json::from_slice(&data)?;
|
||||
initial_version_data = Some(version_create_data);
|
||||
let version_create_data =
|
||||
initial_version_data.as_ref().unwrap();
|
||||
let version_create_data = initial_version_data.as_ref().unwrap();
|
||||
if version_create_data.project_id.is_none() {
|
||||
return Err(CreateError::MissingValueError(
|
||||
"Missing project id".to_string(),
|
||||
@ -168,9 +159,7 @@ async fn version_create_inner(
|
||||
}
|
||||
|
||||
version_create_data.validate().map_err(|err| {
|
||||
CreateError::ValidationError(validation_errors_to_string(
|
||||
err, None,
|
||||
))
|
||||
CreateError::ValidationError(validation_errors_to_string(err, None))
|
||||
})?;
|
||||
|
||||
if !version_create_data.status.can_be_requested() {
|
||||
@ -179,8 +168,7 @@ async fn version_create_inner(
|
||||
));
|
||||
}
|
||||
|
||||
let project_id: models::ProjectId =
|
||||
version_create_data.project_id.unwrap().into();
|
||||
let project_id: models::ProjectId = version_create_data.project_id.unwrap().into();
|
||||
|
||||
// Ensure that the project this version is being added to exists
|
||||
let results = sqlx::query!(
|
||||
@ -206,8 +194,7 @@ async fn version_create_inner(
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CreateError::CustomAuthenticationError(
|
||||
"You don't have permission to upload this version!"
|
||||
.to_string(),
|
||||
"You don't have permission to upload this version!".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
@ -216,13 +203,11 @@ async fn version_create_inner(
|
||||
.contains(Permissions::UPLOAD_VERSION)
|
||||
{
|
||||
return Err(CreateError::CustomAuthenticationError(
|
||||
"You don't have permission to upload this version!"
|
||||
.to_string(),
|
||||
"You don't have permission to upload this version!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let version_id: VersionId =
|
||||
models::generate_version_id(transaction).await?.into();
|
||||
let version_id: VersionId = models::generate_version_id(transaction).await?.into();
|
||||
|
||||
let project_type = sqlx::query!(
|
||||
"
|
||||
@ -243,13 +228,10 @@ async fn version_create_inner(
|
||||
all_game_versions
|
||||
.iter()
|
||||
.find(|y| y.version == x.0)
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidGameVersion(x.0.clone())
|
||||
})
|
||||
.ok_or_else(|| CreateError::InvalidGameVersion(x.0.clone()))
|
||||
.map(|y| y.id)
|
||||
})
|
||||
.collect::<Result<Vec<models::GameVersionId>, CreateError>>(
|
||||
)?;
|
||||
.collect::<Result<Vec<models::GameVersionId>, CreateError>>()?;
|
||||
|
||||
let loaders = version_create_data
|
||||
.loaders
|
||||
@ -258,13 +240,9 @@ async fn version_create_inner(
|
||||
all_loaders
|
||||
.iter()
|
||||
.find(|y| {
|
||||
y.loader == x.0
|
||||
&& y.supported_project_types
|
||||
.contains(&project_type)
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidLoader(x.0.clone())
|
||||
y.loader == x.0 && y.supported_project_types.contains(&project_type)
|
||||
})
|
||||
.ok_or_else(|| CreateError::InvalidLoader(x.0.clone()))
|
||||
.map(|y| y.id)
|
||||
})
|
||||
.collect::<Result<Vec<models::LoaderId>, CreateError>>()?;
|
||||
@ -286,17 +264,12 @@ async fn version_create_inner(
|
||||
author_id: user.id.into(),
|
||||
name: version_create_data.version_title.clone(),
|
||||
version_number: version_create_data.version_number.clone(),
|
||||
changelog: version_create_data
|
||||
.version_body
|
||||
.clone()
|
||||
.unwrap_or_default(),
|
||||
changelog: version_create_data.version_body.clone().unwrap_or_default(),
|
||||
files: Vec::new(),
|
||||
dependencies,
|
||||
game_versions,
|
||||
loaders,
|
||||
version_type: version_create_data
|
||||
.release_channel
|
||||
.to_string(),
|
||||
version_type: version_create_data.release_channel.to_string(),
|
||||
featured: version_create_data.featured,
|
||||
status: version_create_data.status,
|
||||
requested_status: None,
|
||||
@ -306,9 +279,7 @@ async fn version_create_inner(
|
||||
}
|
||||
|
||||
let version = version_builder.as_mut().ok_or_else(|| {
|
||||
CreateError::InvalidInput(String::from(
|
||||
"`data` field must come before file fields",
|
||||
))
|
||||
CreateError::InvalidInput(String::from("`data` field must come before file fields"))
|
||||
})?;
|
||||
|
||||
let project_type = sqlx::query!(
|
||||
@ -323,12 +294,9 @@ async fn version_create_inner(
|
||||
.await?
|
||||
.name;
|
||||
|
||||
let version_data =
|
||||
initial_version_data.clone().ok_or_else(|| {
|
||||
CreateError::InvalidInput(
|
||||
"`data` field is required".to_string(),
|
||||
)
|
||||
})?;
|
||||
let version_data = initial_version_data
|
||||
.clone()
|
||||
.ok_or_else(|| CreateError::InvalidInput("`data` field is required".to_string()))?;
|
||||
|
||||
upload_file(
|
||||
&mut field,
|
||||
@ -365,12 +333,10 @@ async fn version_create_inner(
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let version_data = initial_version_data.ok_or_else(|| {
|
||||
CreateError::InvalidInput("`data` field is required".to_string())
|
||||
})?;
|
||||
let builder = version_builder.ok_or_else(|| {
|
||||
CreateError::InvalidInput("`data` field is required".to_string())
|
||||
})?;
|
||||
let version_data = initial_version_data
|
||||
.ok_or_else(|| CreateError::InvalidInput("`data` field is required".to_string()))?;
|
||||
let builder = version_builder
|
||||
.ok_or_else(|| CreateError::InvalidInput("`data` field is required".to_string()))?;
|
||||
|
||||
if builder.files.is_empty() {
|
||||
return Err(CreateError::InvalidInput(
|
||||
@ -388,9 +354,7 @@ async fn version_create_inner(
|
||||
builder.project_id as crate::database::models::ids::ProjectId
|
||||
)
|
||||
.fetch_many(&mut *transaction)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|m| models::ids::UserId(m.follower_id)))
|
||||
})
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|m| models::ids::UserId(m.follower_id))) })
|
||||
.try_collect::<Vec<models::ids::UserId>>()
|
||||
.await?;
|
||||
|
||||
@ -453,8 +417,7 @@ async fn version_create_inner(
|
||||
let project_id = builder.project_id;
|
||||
builder.insert(transaction).await?;
|
||||
|
||||
models::Project::update_game_versions(project_id, &mut *transaction)
|
||||
.await?;
|
||||
models::Project::update_game_versions(project_id, &mut *transaction).await?;
|
||||
models::Project::update_loaders(project_id, &mut *transaction).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
@ -486,11 +449,8 @@ pub async fn upload_file_to_version(
|
||||
.await;
|
||||
|
||||
if result.is_err() {
|
||||
let undo_result = super::project_creation::undo_uploads(
|
||||
&***file_host,
|
||||
&uploaded_files,
|
||||
)
|
||||
.await;
|
||||
let undo_result =
|
||||
super::project_creation::undo_uploads(&***file_host, &uploaded_files).await;
|
||||
let rollback_result = transaction.rollback().await;
|
||||
|
||||
undo_result?;
|
||||
@ -541,8 +501,7 @@ async fn upload_file_to_version_inner(
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CreateError::CustomAuthenticationError(
|
||||
"You don't have permission to upload files to this version!"
|
||||
.to_string(),
|
||||
"You don't have permission to upload files to this version!".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
@ -551,8 +510,7 @@ async fn upload_file_to_version_inner(
|
||||
.contains(Permissions::UPLOAD_VERSION)
|
||||
{
|
||||
return Err(CreateError::CustomAuthenticationError(
|
||||
"You don't have permission to upload files to this version!"
|
||||
.to_string(),
|
||||
"You don't have permission to upload files to this version!".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -571,8 +529,7 @@ async fn upload_file_to_version_inner(
|
||||
.await?
|
||||
.name;
|
||||
|
||||
let all_game_versions =
|
||||
models::categories::GameVersion::list(&mut *transaction).await?;
|
||||
let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?;
|
||||
|
||||
let mut error = None;
|
||||
while let Some(item) = payload.next().await {
|
||||
@ -585,9 +542,7 @@ async fn upload_file_to_version_inner(
|
||||
let result = async {
|
||||
let content_disposition = field.content_disposition().clone();
|
||||
let name = content_disposition.get_name().ok_or_else(|| {
|
||||
CreateError::MissingValueError(
|
||||
"Missing content name".to_string(),
|
||||
)
|
||||
CreateError::MissingValueError("Missing content name".to_string())
|
||||
})?;
|
||||
|
||||
if name == "data" {
|
||||
@ -602,9 +557,7 @@ async fn upload_file_to_version_inner(
|
||||
}
|
||||
|
||||
let file_data = initial_file_data.as_ref().ok_or_else(|| {
|
||||
CreateError::InvalidInput(String::from(
|
||||
"`data` field must come before file fields",
|
||||
))
|
||||
CreateError::InvalidInput(String::from("`data` field must come before file fields"))
|
||||
})?;
|
||||
|
||||
let mut dependencies = version
|
||||
@ -703,9 +656,7 @@ pub async fn upload_file(
|
||||
}
|
||||
|
||||
let content_type = crate::util::ext::project_file_type(file_extension)
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidFileType(file_extension.to_string())
|
||||
})?;
|
||||
.ok_or_else(|| CreateError::InvalidFileType(file_extension.to_string()))?;
|
||||
|
||||
let data = read_from_field(
|
||||
field, 500 * (1 << 20),
|
||||
@ -731,8 +682,7 @@ pub async fn upload_file(
|
||||
|
||||
if exists {
|
||||
return Err(CreateError::InvalidInput(
|
||||
"Duplicate files are not allowed to be uploaded to Modrinth!"
|
||||
.to_string(),
|
||||
"Duplicate files are not allowed to be uploaded to Modrinth!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@ -761,23 +711,20 @@ pub async fn upload_file(
|
||||
.collect();
|
||||
|
||||
let res = sqlx::query!(
|
||||
"
|
||||
"
|
||||
SELECT v.id version_id, v.mod_id project_id, h.hash hash FROM hashes h
|
||||
INNER JOIN files f on h.file_id = f.id
|
||||
INNER JOIN versions v on f.version_id = v.id
|
||||
WHERE h.algorithm = 'sha1' AND h.hash = ANY($1)
|
||||
",
|
||||
&*hashes
|
||||
)
|
||||
.fetch_all(&mut *transaction).await?;
|
||||
&*hashes
|
||||
)
|
||||
.fetch_all(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
for file in &format.files {
|
||||
if let Some(dep) = res.iter().find(|x| {
|
||||
Some(&*x.hash)
|
||||
== file
|
||||
.hashes
|
||||
.get(&PackFileHash::Sha1)
|
||||
.map(|x| x.as_bytes())
|
||||
Some(&*x.hash) == file.hashes.get(&PackFileHash::Sha1).map(|x| x.as_bytes())
|
||||
}) {
|
||||
dependencies.push(DependencyBuilder {
|
||||
project_id: Some(models::ProjectId(dep.project_id)),
|
||||
@ -828,8 +775,7 @@ pub async fn upload_file(
|
||||
version_id,
|
||||
urlencoding::encode(file_name)
|
||||
);
|
||||
let file_path =
|
||||
format!("data/{}/versions/{}/{}", project_id, version_id, &file_name);
|
||||
let file_path = format!("data/{}/versions/{}/{}", project_id, version_id, &file_name);
|
||||
|
||||
let upload_data = file_host
|
||||
.upload_file(content_type, &file_path, data)
|
||||
@ -849,8 +795,7 @@ pub async fn upload_file(
|
||||
.any(|y| y.hash == sha1_bytes || y.hash == sha512_bytes)
|
||||
}) {
|
||||
return Err(CreateError::InvalidInput(
|
||||
"Duplicate files are not allowed to be uploaded to Modrinth!"
|
||||
.to_string(),
|
||||
"Duplicate files are not allowed to be uploaded to Modrinth!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@ -888,9 +833,9 @@ pub async fn upload_file(
|
||||
pub fn get_name_ext(
|
||||
content_disposition: &actix_web::http::header::ContentDisposition,
|
||||
) -> Result<(&str, &str), CreateError> {
|
||||
let file_name = content_disposition.get_filename().ok_or_else(|| {
|
||||
CreateError::MissingValueError("Missing content file name".to_string())
|
||||
})?;
|
||||
let file_name = content_disposition
|
||||
.get_filename()
|
||||
.ok_or_else(|| CreateError::MissingValueError("Missing content file name".to_string()))?;
|
||||
let file_extension = if let Some(last_period) = file_name.rfind('.') {
|
||||
file_name.get((last_period + 1)..).unwrap_or("")
|
||||
} else {
|
||||
|
||||
@ -84,8 +84,7 @@ pub async fn get_version_from_hash(
|
||||
.iter()
|
||||
.map(|x| database::models::VersionId(x.version_id))
|
||||
.collect::<Vec<_>>();
|
||||
let versions_data =
|
||||
database::models::Version::get_many_full(&version_ids, &**pool).await?;
|
||||
let versions_data = database::models::Version::get_many_full(&version_ids, &**pool).await?;
|
||||
|
||||
if let Some(first) = versions_data.first() {
|
||||
if hash_query.multiple {
|
||||
@ -96,8 +95,7 @@ pub async fn get_version_from_hash(
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
} else {
|
||||
Ok(HttpResponse::Ok()
|
||||
.json(models::projects::Version::from(first.clone())))
|
||||
Ok(HttpResponse::Ok().json(models::projects::Version::from(first.clone())))
|
||||
}
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
@ -128,10 +126,16 @@ pub async fn download_version(
|
||||
WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ANY($4)
|
||||
ORDER BY v.date_published ASC
|
||||
",
|
||||
&*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
|
||||
&*crate::models::projects::VersionStatus::iterator()
|
||||
.filter(|x| x.is_hidden())
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
hash.as_bytes(),
|
||||
hash_query.algorithm,
|
||||
&*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
|
||||
&*crate::models::projects::ProjectStatus::iterator()
|
||||
.filter(|x| x.is_hidden())
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.fetch_optional(&mut *transaction)
|
||||
.await?;
|
||||
@ -178,28 +182,25 @@ pub async fn delete_file(
|
||||
|| Some(x.version_id) == hash_query.version_id.map(|x| x.0 as i64)
|
||||
}) {
|
||||
if !user.role.is_admin() {
|
||||
let team_member =
|
||||
database::models::TeamMember::get_from_user_id_version(
|
||||
database::models::ids::VersionId(row.version_id),
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
let team_member = database::models::TeamMember::get_from_user_id_version(
|
||||
database::models::ids::VersionId(row.version_id),
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::Database)?
|
||||
.ok_or_else(|| {
|
||||
ApiError::CustomAuthentication(
|
||||
"You don't have permission to delete this file!".to_string(),
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::Database)?
|
||||
.ok_or_else(|| {
|
||||
ApiError::CustomAuthentication(
|
||||
"You don't have permission to delete this file!"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
})?;
|
||||
|
||||
if !team_member
|
||||
.permissions
|
||||
.contains(Permissions::DELETE_VERSION)
|
||||
{
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to delete this file!"
|
||||
.to_string(),
|
||||
"You don't have permission to delete this file!".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -220,8 +221,7 @@ pub async fn delete_file(
|
||||
|
||||
if files.len() < 2 {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Versions must have at least one file uploaded to them"
|
||||
.to_string(),
|
||||
"Versions must have at least one file uploaded to them".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@ -324,9 +324,7 @@ pub async fn get_update_from_hash(
|
||||
.await?;
|
||||
|
||||
if let Some(version_id) = version_ids.first() {
|
||||
let version_data =
|
||||
database::models::Version::get_full(*version_id, &**pool)
|
||||
.await?;
|
||||
let version_data = database::models::Version::get_full(*version_id, &**pool).await?;
|
||||
|
||||
ok_or_not_found::<QueryVersion, Version>(version_data)
|
||||
} else {
|
||||
@ -364,10 +362,16 @@ pub async fn get_versions_from_hashes(
|
||||
INNER JOIN mods m on v.mod_id = m.id
|
||||
WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ANY($4)
|
||||
",
|
||||
&*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
|
||||
&*crate::models::projects::VersionStatus::iterator()
|
||||
.filter(|x| x.is_hidden())
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
hashes_parsed.as_slice(),
|
||||
file_data.algorithm,
|
||||
&*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
|
||||
&*crate::models::projects::ProjectStatus::iterator()
|
||||
.filter(|x| x.is_hidden())
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.fetch_all(&**pool)
|
||||
.await?;
|
||||
@ -376,8 +380,7 @@ pub async fn get_versions_from_hashes(
|
||||
.iter()
|
||||
.map(|x| database::models::VersionId(x.version_id))
|
||||
.collect::<Vec<_>>();
|
||||
let versions_data =
|
||||
database::models::Version::get_many_full(&version_ids, &**pool).await?;
|
||||
let versions_data = database::models::Version::get_many_full(&version_ids, &**pool).await?;
|
||||
|
||||
let response: Result<HashMap<String, Version>, ApiError> = result
|
||||
.into_iter()
|
||||
@ -388,10 +391,7 @@ pub async fn get_versions_from_hashes(
|
||||
.find(|x| x.inner.id.0 == row.version_id)
|
||||
.map(|v| {
|
||||
if let Ok(parsed_hash) = String::from_utf8(row.hash) {
|
||||
Ok((
|
||||
parsed_hash,
|
||||
crate::models::projects::Version::from(v),
|
||||
))
|
||||
Ok((parsed_hash, crate::models::projects::Version::from(v)))
|
||||
} else {
|
||||
Err(ApiError::Database(DatabaseError::Other(format!(
|
||||
"Could not parse hash for version {}",
|
||||
@ -423,20 +423,25 @@ pub async fn get_projects_from_hashes(
|
||||
INNER JOIN mods m on v.mod_id = m.id
|
||||
WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ANY($4)
|
||||
",
|
||||
&*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
|
||||
&*crate::models::projects::VersionStatus::iterator()
|
||||
.filter(|x| x.is_hidden())
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
hashes_parsed.as_slice(),
|
||||
file_data.algorithm,
|
||||
&*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
|
||||
&*crate::models::projects::ProjectStatus::iterator()
|
||||
.filter(|x| x.is_hidden())
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.fetch_all(&**pool)
|
||||
.await?;
|
||||
.fetch_all(&**pool)
|
||||
.await?;
|
||||
|
||||
let project_ids = result
|
||||
.iter()
|
||||
.map(|x| database::models::ProjectId(x.project_id))
|
||||
.collect::<Vec<_>>();
|
||||
let versions_data =
|
||||
database::models::Project::get_many_full(&project_ids, &**pool).await?;
|
||||
let versions_data = database::models::Project::get_many_full(&project_ids, &**pool).await?;
|
||||
|
||||
let response: Result<HashMap<String, Project>, ApiError> = result
|
||||
.into_iter()
|
||||
@ -447,10 +452,7 @@ pub async fn get_projects_from_hashes(
|
||||
.find(|x| x.inner.id.0 == row.project_id)
|
||||
.map(|v| {
|
||||
if let Ok(parsed_hash) = String::from_utf8(row.hash) {
|
||||
Ok((
|
||||
parsed_hash,
|
||||
crate::models::projects::Project::from(v),
|
||||
))
|
||||
Ok((parsed_hash, crate::models::projects::Project::from(v)))
|
||||
} else {
|
||||
Err(ApiError::Database(DatabaseError::Other(format!(
|
||||
"Could not parse hash for version {}",
|
||||
@ -538,20 +540,26 @@ pub async fn update_files(
|
||||
INNER JOIN mods m on v.mod_id = m.id
|
||||
WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ANY($4)
|
||||
",
|
||||
&*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
|
||||
&*crate::models::projects::VersionStatus::iterator()
|
||||
.filter(|x| x.is_hidden())
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
hashes_parsed.as_slice(),
|
||||
update_data.algorithm,
|
||||
&*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
|
||||
&*crate::models::projects::ProjectStatus::iterator()
|
||||
.filter(|x| x.is_hidden())
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.fetch_many(&mut *transaction)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|m| (m.hash, database::models::ids::ProjectId(m.mod_id))))
|
||||
})
|
||||
.try_collect::<Vec<_>>()
|
||||
.await?;
|
||||
.fetch_many(&mut *transaction)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right()
|
||||
.map(|m| (m.hash, database::models::ids::ProjectId(m.mod_id))))
|
||||
})
|
||||
.try_collect::<Vec<_>>()
|
||||
.await?;
|
||||
|
||||
let mut version_ids: HashMap<database::models::VersionId, Vec<u8>> =
|
||||
HashMap::new();
|
||||
let mut version_ids: HashMap<database::models::VersionId, Vec<u8>> = HashMap::new();
|
||||
|
||||
let updated_versions = database::models::Version::get_projects_versions(
|
||||
result
|
||||
@ -583,17 +591,13 @@ pub async fn update_files(
|
||||
.await?;
|
||||
|
||||
for (hash, id) in result {
|
||||
if let Some(latest_version) =
|
||||
updated_versions.get(&id).and_then(|x| x.last())
|
||||
{
|
||||
if let Some(latest_version) = updated_versions.get(&id).and_then(|x| x.last()) {
|
||||
version_ids.insert(*latest_version, hash);
|
||||
}
|
||||
}
|
||||
|
||||
let query_version_ids = version_ids.keys().copied().collect::<Vec<_>>();
|
||||
let versions =
|
||||
database::models::Version::get_many_full(&query_version_ids, &**pool)
|
||||
.await?;
|
||||
let versions = database::models::Version::get_many_full(&query_version_ids, &**pool).await?;
|
||||
|
||||
let mut response = HashMap::new();
|
||||
|
||||
@ -602,10 +606,7 @@ pub async fn update_files(
|
||||
|
||||
if let Some(hash) = hash {
|
||||
if let Ok(parsed_hash) = String::from_utf8(hash.clone()) {
|
||||
response.insert(
|
||||
parsed_hash,
|
||||
models::projects::Version::from(version),
|
||||
);
|
||||
response.insert(parsed_hash, models::projects::Version::from(version));
|
||||
} else {
|
||||
let version_id: VersionId = version.inner.id.into();
|
||||
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
use super::ApiError;
|
||||
use crate::database;
|
||||
use crate::models;
|
||||
use crate::models::projects::{
|
||||
Dependency, FileType, VersionStatus, VersionType,
|
||||
};
|
||||
use crate::models::projects::{Dependency, FileType, VersionStatus, VersionType};
|
||||
use crate::models::teams::Permissions;
|
||||
use crate::util::auth::{
|
||||
filter_authorized_versions, get_user_from_headers, is_authorized,
|
||||
is_authorized_version,
|
||||
filter_authorized_versions, get_user_from_headers, is_authorized, is_authorized_version,
|
||||
};
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
|
||||
@ -49,10 +46,7 @@ pub async fn version_list(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let result = database::models::Project::get_from_slug_or_project_id(
|
||||
&string, &**pool,
|
||||
)
|
||||
.await?;
|
||||
let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool).await?;
|
||||
|
||||
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
@ -80,9 +74,7 @@ pub async fn version_list(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut versions =
|
||||
database::models::Version::get_many_full(&version_ids, &**pool)
|
||||
.await?;
|
||||
let mut versions = database::models::Version::get_many_full(&version_ids, &**pool).await?;
|
||||
|
||||
let mut response = versions
|
||||
.iter()
|
||||
@ -95,22 +87,13 @@ pub async fn version_list(
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
versions.sort_by(|a, b| {
|
||||
b.inner.date_published.cmp(&a.inner.date_published)
|
||||
});
|
||||
versions.sort_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published));
|
||||
|
||||
// Attempt to populate versions with "auto featured" versions
|
||||
if response.is_empty()
|
||||
&& !versions.is_empty()
|
||||
&& filters.featured.unwrap_or(false)
|
||||
{
|
||||
if response.is_empty() && !versions.is_empty() && filters.featured.unwrap_or(false) {
|
||||
let (loaders, game_versions) = futures::future::try_join(
|
||||
database::models::categories::Loader::list(&**pool),
|
||||
database::models::categories::GameVersion::list_filter(
|
||||
None,
|
||||
Some(true),
|
||||
&**pool,
|
||||
),
|
||||
database::models::categories::GameVersion::list_filter(None, Some(true), &**pool),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@ -139,13 +122,10 @@ pub async fn version_list(
|
||||
}
|
||||
}
|
||||
|
||||
response.sort_by(|a, b| {
|
||||
b.inner.date_published.cmp(&a.inner.date_published)
|
||||
});
|
||||
response.sort_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published));
|
||||
response.dedup_by(|a, b| a.inner.id == b.inner.id);
|
||||
|
||||
let response =
|
||||
filter_authorized_versions(response, &user_option, &pool).await?;
|
||||
let response = filter_authorized_versions(response, &user_option, &pool).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
} else {
|
||||
@ -162,16 +142,13 @@ pub async fn version_project_get(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let id = info.into_inner();
|
||||
let version_data =
|
||||
database::models::Version::get_full_from_id_slug(&id.0, &id.1, &**pool)
|
||||
.await?;
|
||||
database::models::Version::get_full_from_id_slug(&id.0, &id.1, &**pool).await?;
|
||||
|
||||
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
if let Some(data) = version_data {
|
||||
if is_authorized_version(&data.inner, &user_option, &pool).await? {
|
||||
return Ok(
|
||||
HttpResponse::Ok().json(models::projects::Version::from(data))
|
||||
);
|
||||
return Ok(HttpResponse::Ok().json(models::projects::Version::from(data)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,18 +166,15 @@ pub async fn versions_get(
|
||||
web::Query(ids): web::Query<VersionIds>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let version_ids =
|
||||
serde_json::from_str::<Vec<models::ids::VersionId>>(&ids.ids)?
|
||||
.into_iter()
|
||||
.map(|x| x.into())
|
||||
.collect::<Vec<database::models::VersionId>>();
|
||||
let versions_data =
|
||||
database::models::Version::get_many_full(&version_ids, &**pool).await?;
|
||||
let version_ids = serde_json::from_str::<Vec<models::ids::VersionId>>(&ids.ids)?
|
||||
.into_iter()
|
||||
.map(|x| x.into())
|
||||
.collect::<Vec<database::models::VersionId>>();
|
||||
let versions_data = database::models::Version::get_many_full(&version_ids, &**pool).await?;
|
||||
|
||||
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
let versions =
|
||||
filter_authorized_versions(versions_data, &user_option, &pool).await?;
|
||||
let versions = filter_authorized_versions(versions_data, &user_option, &pool).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(versions))
|
||||
}
|
||||
@ -212,16 +186,13 @@ pub async fn version_get(
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let id = info.into_inner().0;
|
||||
let version_data =
|
||||
database::models::Version::get_full(id.into(), &**pool).await?;
|
||||
let version_data = database::models::Version::get_full(id.into(), &**pool).await?;
|
||||
|
||||
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
if let Some(data) = version_data {
|
||||
if is_authorized_version(&data.inner, &user_option, &pool).await? {
|
||||
return Ok(
|
||||
HttpResponse::Ok().json(models::projects::Version::from(data))
|
||||
);
|
||||
return Ok(HttpResponse::Ok().json(models::projects::Version::from(data)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,9 +244,9 @@ pub async fn version_edit(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
new_version.validate().map_err(|err| {
|
||||
ApiError::Validation(validation_errors_to_string(err, None))
|
||||
})?;
|
||||
new_version
|
||||
.validate()
|
||||
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
|
||||
|
||||
let version_id = info.into_inner().0;
|
||||
let id = version_id.into();
|
||||
@ -283,19 +254,15 @@ pub async fn version_edit(
|
||||
let result = database::models::Version::get_full(id, &**pool).await?;
|
||||
|
||||
if let Some(version_item) = result {
|
||||
let project_item = database::models::Project::get_full(
|
||||
version_item.inner.project_id,
|
||||
let project_item =
|
||||
database::models::Project::get_full(version_item.inner.project_id, &**pool).await?;
|
||||
|
||||
let team_member = database::models::TeamMember::get_from_user_id_version(
|
||||
version_item.inner.id,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let team_member =
|
||||
database::models::TeamMember::get_from_user_id_version(
|
||||
version_item.inner.id,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
let permissions;
|
||||
|
||||
if user.role.is_admin() {
|
||||
@ -303,8 +270,7 @@ pub async fn version_edit(
|
||||
} else if let Some(member) = team_member {
|
||||
permissions = Some(member.permissions)
|
||||
} else if user.role.is_mod() {
|
||||
permissions =
|
||||
Some(Permissions::EDIT_DETAILS | Permissions::EDIT_BODY)
|
||||
permissions = Some(Permissions::EDIT_DETAILS | Permissions::EDIT_BODY)
|
||||
} else {
|
||||
permissions = None
|
||||
}
|
||||
@ -312,8 +278,7 @@ pub async fn version_edit(
|
||||
if let Some(perms) = permissions {
|
||||
if !perms.contains(Permissions::UPLOAD_VERSION) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit this version!"
|
||||
.to_string(),
|
||||
"You do not have the permissions to edit this version!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@ -403,18 +368,16 @@ pub async fn version_edit(
|
||||
.await?;
|
||||
|
||||
for game_version in game_versions {
|
||||
let game_version_id =
|
||||
database::models::categories::GameVersion::get_id(
|
||||
&game_version.0,
|
||||
&mut *transaction,
|
||||
let game_version_id = database::models::categories::GameVersion::get_id(
|
||||
&game_version.0,
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"No database entry for game version provided.".to_string(),
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"No database entry for game version provided."
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
})?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
@ -447,17 +410,13 @@ pub async fn version_edit(
|
||||
|
||||
for loader in loaders {
|
||||
let loader_id =
|
||||
database::models::categories::Loader::get_id(
|
||||
&loader.0,
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"No database entry for loader provided."
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
database::models::categories::Loader::get_id(&loader.0, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"No database entry for loader provided.".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
@ -551,8 +510,7 @@ pub async fn version_edit(
|
||||
if let Some(downloads) = &new_version.downloads {
|
||||
if !user.role.is_mod() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to set the downloads of this mod"
|
||||
.to_string(),
|
||||
"You don't have permission to set the downloads of this mod".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@ -577,8 +535,7 @@ pub async fn version_edit(
|
||||
WHERE (id = $2)
|
||||
",
|
||||
diff as i32,
|
||||
version_item.inner.project_id
|
||||
as database::models::ids::ProjectId,
|
||||
version_item.inner.project_id as database::models::ids::ProjectId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
@ -667,8 +624,7 @@ pub async fn version_schedule(
|
||||
|
||||
if scheduling_data.time < Utc::now() {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You cannot schedule a version to be released in the past!"
|
||||
.to_string(),
|
||||
"You cannot schedule a version to be released in the past!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@ -679,17 +635,15 @@ pub async fn version_schedule(
|
||||
}
|
||||
|
||||
let string = info.into_inner().0;
|
||||
let result =
|
||||
database::models::Version::get_full(string.into(), &**pool).await?;
|
||||
let result = database::models::Version::get_full(string.into(), &**pool).await?;
|
||||
|
||||
if let Some(version_item) = result {
|
||||
let team_member =
|
||||
database::models::TeamMember::get_from_user_id_version(
|
||||
version_item.inner.id,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
let team_member = database::models::TeamMember::get_from_user_id_version(
|
||||
version_item.inner.id,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !user.role.is_mod()
|
||||
&& !team_member
|
||||
@ -748,17 +702,14 @@ pub async fn version_delete(
|
||||
.contains(Permissions::DELETE_VERSION)
|
||||
{
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to delete versions in this team"
|
||||
.to_string(),
|
||||
"You do not have permission to delete versions in this team".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let result =
|
||||
database::models::Version::remove_full(id.into(), &mut transaction)
|
||||
.await?;
|
||||
let result = database::models::Version::remove_full(id.into(), &mut transaction).await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
|
||||
@ -32,13 +32,9 @@ impl Drop for Scheduler {
|
||||
|
||||
use log::{info, warn};
|
||||
|
||||
pub fn schedule_versions(
|
||||
scheduler: &mut Scheduler,
|
||||
pool: sqlx::Pool<sqlx::Postgres>,
|
||||
) {
|
||||
let version_index_interval = std::time::Duration::from_secs(
|
||||
parse_var("VERSION_INDEX_INTERVAL").unwrap_or(1800),
|
||||
);
|
||||
pub fn schedule_versions(scheduler: &mut Scheduler, pool: sqlx::Pool<sqlx::Postgres>) {
|
||||
let version_index_interval =
|
||||
std::time::Duration::from_secs(parse_var("VERSION_INDEX_INTERVAL").unwrap_or(1800));
|
||||
|
||||
scheduler.run(version_index_interval, move || {
|
||||
let pool_ref = pool.clone();
|
||||
@ -82,15 +78,11 @@ struct VersionFormat<'a> {
|
||||
release_time: DateTime<Utc>,
|
||||
}
|
||||
|
||||
async fn update_versions(
|
||||
pool: &sqlx::Pool<sqlx::Postgres>,
|
||||
) -> Result<(), VersionIndexingError> {
|
||||
let input = reqwest::get(
|
||||
"https://piston-meta.mojang.com/mc/game/version_manifest_v2.json",
|
||||
)
|
||||
.await?
|
||||
.json::<InputFormat>()
|
||||
.await?;
|
||||
async fn update_versions(pool: &sqlx::Pool<sqlx::Postgres>) -> Result<(), VersionIndexingError> {
|
||||
let input = reqwest::get("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json")
|
||||
.await?
|
||||
.json::<InputFormat>()
|
||||
.await?;
|
||||
|
||||
let mut skipped_versions_count = 0u32;
|
||||
|
||||
@ -152,8 +144,7 @@ async fn update_versions(
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c))
|
||||
{
|
||||
if let Some((_, alternate)) =
|
||||
HALL_OF_SHAME.iter().find(|(version, _)| name == *version)
|
||||
if let Some((_, alternate)) = HALL_OF_SHAME.iter().find(|(version, _)| name == *version)
|
||||
{
|
||||
name = String::from(*alternate);
|
||||
} else {
|
||||
|
||||
@ -4,11 +4,10 @@ use log::info;
|
||||
use super::IndexingError;
|
||||
use crate::database::models::ProjectId;
|
||||
use crate::search::UploadSearchProject;
|
||||
use serde::Deserialize;
|
||||
use sqlx::postgres::PgPool;
|
||||
|
||||
pub async fn index_local(
|
||||
pool: PgPool,
|
||||
) -> Result<Vec<UploadSearchProject>, IndexingError> {
|
||||
pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchProject>, IndexingError> {
|
||||
info!("Indexing local projects!");
|
||||
|
||||
Ok(
|
||||
@ -23,7 +22,8 @@ pub async fn index_local(
|
||||
ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,
|
||||
ARRAY_AGG(DISTINCT gv.version) filter (where gv.version is not null) versions,
|
||||
ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery,
|
||||
ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery
|
||||
ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery,
|
||||
JSONB_AGG(DISTINCT jsonb_build_object('id', mdep.id, 'dep_type', d.dependency_type)) filter (where mdep.id is not null) dependencies
|
||||
FROM mods m
|
||||
LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id
|
||||
LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id
|
||||
@ -33,6 +33,8 @@ pub async fn index_local(
|
||||
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 dependencies d ON d.dependent_id = v.id
|
||||
LEFT OUTER JOIN mods mdep ON mdep.id = d.mod_dependency_id
|
||||
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
|
||||
@ -70,6 +72,21 @@ pub async fn index_local(
|
||||
_ => false,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TempDependency {
|
||||
id: ProjectId,
|
||||
dep_type: String
|
||||
}
|
||||
|
||||
let dependencies = serde_json::from_value::<Vec<TempDependency>>(
|
||||
m.dependencies.unwrap_or_default(),
|
||||
)
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|x| format!("{}-{}", crate::models::ids::ProjectId::from(x.id), x.dep_type))
|
||||
.collect();
|
||||
|
||||
UploadSearchProject {
|
||||
project_id: project_id.to_string(),
|
||||
title: m.title,
|
||||
@ -95,6 +112,7 @@ pub async fn index_local(
|
||||
open_source,
|
||||
color: m.color.map(|x| x as u32),
|
||||
featured_gallery: m.featured_gallery.unwrap_or_default().first().cloned(),
|
||||
dependencies,
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
@ -203,10 +203,10 @@ const DEFAULT_DISPLAYED_ATTRIBUTES: &[&str] = &[
|
||||
"gallery",
|
||||
"featured_gallery",
|
||||
"color",
|
||||
"dependencies",
|
||||
];
|
||||
|
||||
const DEFAULT_SEARCHABLE_ATTRIBUTES: &[&str] =
|
||||
&["title", "description", "author", "slug"];
|
||||
const DEFAULT_SEARCHABLE_ATTRIBUTES: &[&str] = &["title", "description", "author", "slug"];
|
||||
|
||||
const DEFAULT_ATTRIBUTES_FOR_FACETING: &[&str] = &[
|
||||
"categories",
|
||||
@ -224,6 +224,7 @@ const DEFAULT_ATTRIBUTES_FOR_FACETING: &[&str] = &[
|
||||
"project_id",
|
||||
"open_source",
|
||||
"color",
|
||||
"dependencies",
|
||||
];
|
||||
|
||||
const DEFAULT_SORTABLE_ATTRIBUTES: &[&str] =
|
||||
|
||||
@ -99,6 +99,8 @@ pub struct UploadSearchProject {
|
||||
pub modified_timestamp: i64,
|
||||
pub open_source: bool,
|
||||
pub color: Option<u32>,
|
||||
/// format: {project_id}-{dep_type}
|
||||
pub dependencies: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@ -134,6 +136,8 @@ pub struct ResultSearchProject {
|
||||
pub gallery: Vec<String>,
|
||||
pub featured_gallery: Option<String>,
|
||||
pub color: Option<u32>,
|
||||
/// format: {project_id}-{dep_type}
|
||||
pub dependencies: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn search_for_project(
|
||||
@ -177,13 +181,12 @@ pub async fn search_for_project(
|
||||
None
|
||||
};
|
||||
|
||||
let filters: Cow<_> =
|
||||
match (info.filters.as_deref(), info.version.as_deref()) {
|
||||
(Some(f), Some(v)) => format!("({f}) AND ({v})").into(),
|
||||
(Some(f), None) => f.into(),
|
||||
(None, Some(v)) => v.into(),
|
||||
(None, None) => "".into(),
|
||||
};
|
||||
let filters: Cow<_> = match (info.filters.as_deref(), info.version.as_deref()) {
|
||||
(Some(f), Some(v)) => format!("({f}) AND ({v})").into(),
|
||||
(Some(f), None) => f.into(),
|
||||
(None, Some(v)) => v.into(),
|
||||
(None, None) => "".into(),
|
||||
};
|
||||
|
||||
if let Some(facets) = facets {
|
||||
filter_string.push('(');
|
||||
|
||||
@ -59,8 +59,7 @@ where
|
||||
{
|
||||
let github_user = get_github_user_from_token(access_token).await?;
|
||||
|
||||
let res =
|
||||
models::User::get_from_github_id(github_user.id, executor).await?;
|
||||
let res = models::User::get_from_github_id(github_user.id, executor).await?;
|
||||
|
||||
match res {
|
||||
Some(result) => Ok(User {
|
||||
@ -190,8 +189,7 @@ pub async fn filter_authorized_projects(
|
||||
.try_for_each(|e| {
|
||||
if let Some(row) = e.right() {
|
||||
check_projects.retain(|x| {
|
||||
let bool = x.inner.id.0 == row.id
|
||||
&& x.inner.team_id.0 == row.team_id;
|
||||
let bool = x.inner.id.0 == row.id && x.inner.team_id.0 == row.team_id;
|
||||
|
||||
if bool {
|
||||
return_projects.push(x.clone().into());
|
||||
@ -274,25 +272,29 @@ pub async fn filter_authorized_versions(
|
||||
INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2
|
||||
WHERE m.id = ANY($1)
|
||||
",
|
||||
&check_versions.iter().map(|x| x.inner.project_id.0).collect::<Vec<_>>(),
|
||||
&check_versions
|
||||
.iter()
|
||||
.map(|x| x.inner.project_id.0)
|
||||
.collect::<Vec<_>>(),
|
||||
user_id as database::models::ids::UserId,
|
||||
)
|
||||
.fetch_many(&***pool)
|
||||
.try_for_each(|e| {
|
||||
if let Some(row) = e.right() {
|
||||
check_versions.retain(|x| {
|
||||
let bool = x.inner.project_id.0 == row.id;
|
||||
.fetch_many(&***pool)
|
||||
.try_for_each(|e| {
|
||||
if let Some(row) = e.right() {
|
||||
check_versions.retain(|x| {
|
||||
let bool = x.inner.project_id.0 == row.id;
|
||||
|
||||
if bool {
|
||||
return_versions.push(x.clone().into());
|
||||
}
|
||||
if bool {
|
||||
return_versions.push(x.clone().into());
|
||||
}
|
||||
|
||||
!bool
|
||||
});
|
||||
}
|
||||
!bool
|
||||
});
|
||||
}
|
||||
|
||||
futures::future::ready(Ok(()))
|
||||
}).await?;
|
||||
futures::future::ready(Ok(()))
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,9 +2,8 @@ use actix_web::guard::GuardContext;
|
||||
|
||||
pub const ADMIN_KEY_HEADER: &str = "Modrinth-Admin";
|
||||
pub fn admin_key_guard(ctx: &GuardContext) -> bool {
|
||||
let admin_key = std::env::var("LABRINTH_ADMIN_KEY").expect(
|
||||
"No admin key provided, this should have been caught by check_env_vars",
|
||||
);
|
||||
let admin_key = std::env::var("LABRINTH_ADMIN_KEY")
|
||||
.expect("No admin key provided, this should have been caught by check_env_vars");
|
||||
|
||||
ctx.head()
|
||||
.headers()
|
||||
|
||||
@ -6,15 +6,10 @@ pub fn get_color_from_img(data: &[u8]) -> Result<Option<u32>, ImageError> {
|
||||
let image = image::load_from_memory(data)?
|
||||
.resize(256, 256, FilterType::Nearest)
|
||||
.crop_imm(128, 128, 64, 64);
|
||||
let color = color_thief::get_palette(
|
||||
image.to_rgb8().as_bytes(),
|
||||
ColorFormat::Rgb,
|
||||
10,
|
||||
2,
|
||||
)
|
||||
.ok()
|
||||
.and_then(|x| x.get(0).copied())
|
||||
.map(|x| (x.r as u32) << 16 | (x.g as u32) << 8 | (x.b as u32));
|
||||
let color = color_thief::get_palette(image.to_rgb8().as_bytes(), ColorFormat::Rgb, 10, 2)
|
||||
.ok()
|
||||
.and_then(|x| x.get(0).copied())
|
||||
.map(|x| (x.r as u32) << 16 | (x.g as u32) << 8 | (x.b as u32));
|
||||
|
||||
Ok(color)
|
||||
}
|
||||
|
||||
@ -18,9 +18,7 @@ pub async fn read_from_payload(
|
||||
return Err(ApiError::InvalidInput(String::from(err_msg)));
|
||||
} else {
|
||||
bytes.extend_from_slice(&item.map_err(|_| {
|
||||
ApiError::InvalidInput(
|
||||
"Unable to parse bytes in payload sent!".to_string(),
|
||||
)
|
||||
ApiError::InvalidInput("Unable to parse bytes in payload sent!".to_string())
|
||||
})?);
|
||||
}
|
||||
}
|
||||
@ -43,9 +41,7 @@ pub async fn read_from_field(
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub(crate) fn ok_or_not_found<T, U>(
|
||||
version_data: Option<T>,
|
||||
) -> Result<HttpResponse, ApiError>
|
||||
pub(crate) fn ok_or_not_found<T, U>(version_data: Option<T>) -> Result<HttpResponse, ApiError>
|
||||
where
|
||||
U: From<T> + Serialize,
|
||||
{
|
||||
|
||||
@ -4,15 +4,11 @@ use regex::Regex;
|
||||
use validator::{ValidationErrors, ValidationErrorsKind};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref RE_URL_SAFE: Regex =
|
||||
Regex::new(r#"^[a-zA-Z0-9!@$()`.+,_"-]*$"#).unwrap();
|
||||
pub static ref RE_URL_SAFE: Regex = Regex::new(r#"^[a-zA-Z0-9!@$()`.+,_"-]*$"#).unwrap();
|
||||
}
|
||||
|
||||
//TODO: In order to ensure readability, only the first error is printed, this may need to be expanded on in the future!
|
||||
pub fn validation_errors_to_string(
|
||||
errors: ValidationErrors,
|
||||
adder: Option<String>,
|
||||
) -> String {
|
||||
pub fn validation_errors_to_string(errors: ValidationErrors, adder: Option<String>) -> String {
|
||||
let mut output = String::new();
|
||||
|
||||
let map = errors.into_errors();
|
||||
@ -23,10 +19,7 @@ pub fn validation_errors_to_string(
|
||||
if let Some(error) = map.get(field) {
|
||||
return match error {
|
||||
ValidationErrorsKind::Struct(errors) => {
|
||||
validation_errors_to_string(
|
||||
*errors.clone(),
|
||||
Some(format!("of item {field}")),
|
||||
)
|
||||
validation_errors_to_string(*errors.clone(), Some(format!("of item {field}")))
|
||||
}
|
||||
ValidationErrorsKind::List(list) => {
|
||||
if let Some((index, errors)) = list.iter().next() {
|
||||
|
||||
@ -180,8 +180,7 @@ pub async fn send_discord_webhook(
|
||||
}
|
||||
|
||||
if !versions.is_empty() {
|
||||
let formatted_game_versions: String =
|
||||
get_gv_range(versions, all_game_versions);
|
||||
let formatted_game_versions: String = get_gv_range(versions, all_game_versions);
|
||||
|
||||
fields.push(DiscordEmbedField {
|
||||
name: "Versions",
|
||||
@ -229,9 +228,7 @@ pub async fn send_discord_webhook(
|
||||
thumbnail: DiscordEmbedThumbnail {
|
||||
url: project.icon_url,
|
||||
},
|
||||
image: if let Some(first) =
|
||||
project.featured_gallery.unwrap_or_default().first()
|
||||
{
|
||||
image: if let Some(first) = project.featured_gallery.unwrap_or_default().first() {
|
||||
Some(first.clone())
|
||||
} else {
|
||||
project.gallery.unwrap_or_default().first().cloned()
|
||||
@ -242,9 +239,7 @@ pub async fn send_discord_webhook(
|
||||
"{}{display_project_type} on Modrinth",
|
||||
display_project_type.remove(0).to_uppercase()
|
||||
),
|
||||
icon_url: Some(
|
||||
"https://cdn-raw.modrinth.com/modrinth-new.png".to_string(),
|
||||
),
|
||||
icon_url: Some("https://cdn-raw.modrinth.com/modrinth-new.png".to_string()),
|
||||
}),
|
||||
};
|
||||
|
||||
@ -253,10 +248,7 @@ pub async fn send_discord_webhook(
|
||||
client
|
||||
.post(&webhook_url)
|
||||
.json(&DiscordWebhook {
|
||||
avatar_url: Some(
|
||||
"https://cdn.modrinth.com/Modrinth_Dark_Logo.png"
|
||||
.to_string(),
|
||||
),
|
||||
avatar_url: Some("https://cdn.modrinth.com/Modrinth_Dark_Logo.png".to_string()),
|
||||
username: Some("Modrinth Release".to_string()),
|
||||
embeds: vec![embed],
|
||||
content: message,
|
||||
@ -264,9 +256,7 @@ pub async fn send_discord_webhook(
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ApiError::DiscordError(
|
||||
"Error while sending projects webhook".to_string(),
|
||||
)
|
||||
ApiError::DiscordError("Error while sending projects webhook".to_string())
|
||||
})?;
|
||||
}
|
||||
|
||||
@ -310,21 +300,15 @@ fn get_gv_range(
|
||||
} 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)
|
||||
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];
|
||||
intervals[current_interval][1] = vec![i, index, release_index];
|
||||
} else {
|
||||
intervals[current_interval]
|
||||
.insert(1, vec![i, index, release_index]);
|
||||
intervals[current_interval].insert(1, vec![i, index, release_index]);
|
||||
}
|
||||
} else {
|
||||
current_interval += 1;
|
||||
@ -336,10 +320,7 @@ fn get_gv_range(
|
||||
let mut new_intervals = Vec::new();
|
||||
|
||||
for interval in intervals {
|
||||
if interval.len() == 2
|
||||
&& interval[0][2] != MAX_VALUE
|
||||
&& interval[1][2] == MAX_VALUE
|
||||
{
|
||||
if interval.len() == 2 && interval[0][2] != MAX_VALUE && interval[1][2] == MAX_VALUE {
|
||||
let mut last_snapshot: Option<usize> = None;
|
||||
|
||||
for j in ((interval[0][1] + 1)..=interval[1][1]).rev() {
|
||||
@ -349,16 +330,12 @@ fn get_gv_range(
|
||||
vec![
|
||||
game_versions
|
||||
.iter()
|
||||
.position(|x| {
|
||||
x.version == all_game_versions[j].version
|
||||
})
|
||||
.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
|
||||
})
|
||||
.position(|x| x.version == all_game_versions[j].version)
|
||||
.unwrap_or(MAX_VALUE),
|
||||
],
|
||||
]);
|
||||
@ -370,10 +347,7 @@ fn get_gv_range(
|
||||
game_versions
|
||||
.iter()
|
||||
.position(|x| {
|
||||
x.version
|
||||
== all_game_versions
|
||||
[last_snapshot]
|
||||
.version
|
||||
x.version == all_game_versions[last_snapshot].version
|
||||
})
|
||||
.unwrap_or(MAX_VALUE),
|
||||
last_snapshot,
|
||||
@ -402,8 +376,7 @@ fn get_gv_range(
|
||||
if interval.len() == 2 {
|
||||
output.push(format!(
|
||||
"{}—{}",
|
||||
&game_versions[interval[0][0]].version,
|
||||
&game_versions[interval[1][0]].version
|
||||
&game_versions[interval[0][0]].version, &game_versions[interval[1][0]].version
|
||||
))
|
||||
} else {
|
||||
output.push(game_versions[interval[0][0]].version.clone())
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
use crate::validate::{
|
||||
SupportedGameVersions, ValidationError, ValidationResult,
|
||||
};
|
||||
use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult};
|
||||
use std::io::Cursor;
|
||||
use zip::ZipArchive;
|
||||
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
use crate::validate::{
|
||||
SupportedGameVersions, ValidationError, ValidationResult,
|
||||
};
|
||||
use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult};
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use std::io::Cursor;
|
||||
use zip::ZipArchive;
|
||||
@ -38,9 +36,10 @@ impl super::Validator for FabricValidator {
|
||||
));
|
||||
}
|
||||
|
||||
if !archive.file_names().any(|name| {
|
||||
name.ends_with("refmap.json") || name.ends_with(".class")
|
||||
}) {
|
||||
if !archive
|
||||
.file_names()
|
||||
.any(|name| name.ends_with("refmap.json") || name.ends_with(".class"))
|
||||
{
|
||||
return Ok(ValidationResult::Warning(
|
||||
"Fabric mod file is a source file!",
|
||||
));
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
use crate::validate::{
|
||||
SupportedGameVersions, ValidationError, ValidationResult,
|
||||
};
|
||||
use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult};
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use std::io::Cursor;
|
||||
use zip::ZipArchive;
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
use crate::validate::{
|
||||
SupportedGameVersions, ValidationError, ValidationResult,
|
||||
};
|
||||
use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult};
|
||||
use std::io::Cursor;
|
||||
use zip::ZipArchive;
|
||||
|
||||
|
||||
@ -8,9 +8,7 @@ use crate::validate::modpack::ModpackValidator;
|
||||
use crate::validate::plugin::*;
|
||||
use crate::validate::quilt::QuiltValidator;
|
||||
use crate::validate::resourcepack::{PackValidator, TexturePackValidator};
|
||||
use crate::validate::shader::{
|
||||
CanvasShaderValidator, CoreShaderValidator, ShaderValidator,
|
||||
};
|
||||
use crate::validate::shader::{CanvasShaderValidator, CoreShaderValidator, ShaderValidator};
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::io::Cursor;
|
||||
use thiserror::Error;
|
||||
@ -119,8 +117,7 @@ pub async fn validate_file(
|
||||
|
||||
if let Some(file_type) = file_type {
|
||||
match file_type {
|
||||
FileType::RequiredResourcePack
|
||||
| FileType::OptionalResourcePack => {
|
||||
FileType::RequiredResourcePack | FileType::OptionalResourcePack => {
|
||||
project_type = "resourcepack".to_string();
|
||||
loaders = vec![Loader("minecraft".to_string())];
|
||||
}
|
||||
@ -150,13 +147,12 @@ pub async fn validate_file(
|
||||
|
||||
if visited {
|
||||
if ALWAYS_ALLOWED_EXT.contains(&&*file_extension) {
|
||||
Ok(ValidationResult::Warning("File extension is invalid for input file"))
|
||||
Ok(ValidationResult::Warning(
|
||||
"File extension is invalid for input file",
|
||||
))
|
||||
} else {
|
||||
Err(ValidationError::InvalidInput(
|
||||
format!(
|
||||
"File extension {file_extension} is invalid for input file"
|
||||
)
|
||||
.into(),
|
||||
format!("File extension {file_extension} is invalid for input file").into(),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
@ -173,24 +169,20 @@ fn game_version_supported(
|
||||
) -> bool {
|
||||
match supported_game_versions {
|
||||
SupportedGameVersions::All => true,
|
||||
SupportedGameVersions::PastDate(date) => {
|
||||
game_versions.iter().any(|x| {
|
||||
all_game_versions
|
||||
.iter()
|
||||
.find(|y| y.version == x.0)
|
||||
.map(|x| x.created > date)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
SupportedGameVersions::Range(before, after) => {
|
||||
game_versions.iter().any(|x| {
|
||||
all_game_versions
|
||||
.iter()
|
||||
.find(|y| y.version == x.0)
|
||||
.map(|x| x.created > before && x.created < after)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
SupportedGameVersions::PastDate(date) => game_versions.iter().any(|x| {
|
||||
all_game_versions
|
||||
.iter()
|
||||
.find(|y| y.version == x.0)
|
||||
.map(|x| x.created > date)
|
||||
.unwrap_or(false)
|
||||
}),
|
||||
SupportedGameVersions::Range(before, after) => game_versions.iter().any(|x| {
|
||||
all_game_versions
|
||||
.iter()
|
||||
.find(|y| y.version == x.0)
|
||||
.map(|x| x.created > before && x.created < after)
|
||||
.unwrap_or(false)
|
||||
}),
|
||||
SupportedGameVersions::Custom(versions) => {
|
||||
versions.iter().any(|x| game_versions.contains(x))
|
||||
}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
use crate::models::pack::{PackFileHash, PackFormat};
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
use crate::validate::{
|
||||
SupportedGameVersions, ValidationError, ValidationResult,
|
||||
};
|
||||
use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult};
|
||||
use std::io::{Cursor, Read};
|
||||
use std::path::Component;
|
||||
use validator::Validate;
|
||||
@ -32,14 +30,11 @@ impl super::Validator for ModpackValidator {
|
||||
archive: &mut ZipArchive<Cursor<bytes::Bytes>>,
|
||||
) -> Result<ValidationResult, ValidationError> {
|
||||
let pack: PackFormat = {
|
||||
let mut file =
|
||||
if let Ok(file) = archive.by_name("modrinth.index.json") {
|
||||
file
|
||||
} else {
|
||||
return Ok(ValidationResult::Warning(
|
||||
"Pack manifest is missing.",
|
||||
));
|
||||
};
|
||||
let mut file = if let Ok(file) = archive.by_name("modrinth.index.json") {
|
||||
file
|
||||
} else {
|
||||
return Ok(ValidationResult::Warning("Pack manifest is missing."));
|
||||
};
|
||||
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
@ -48,9 +43,7 @@ impl super::Validator for ModpackValidator {
|
||||
};
|
||||
|
||||
pack.validate().map_err(|err| {
|
||||
ValidationError::InvalidInput(
|
||||
validation_errors_to_string(err, None).into(),
|
||||
)
|
||||
ValidationError::InvalidInput(validation_errors_to_string(err, None).into())
|
||||
})?;
|
||||
|
||||
if pack.game != "minecraft" {
|
||||
@ -75,11 +68,7 @@ impl super::Validator for ModpackValidator {
|
||||
let path = std::path::Path::new(&file.path)
|
||||
.components()
|
||||
.next()
|
||||
.ok_or_else(|| {
|
||||
ValidationError::InvalidInput(
|
||||
"Invalid pack file path!".into(),
|
||||
)
|
||||
})?;
|
||||
.ok_or_else(|| ValidationError::InvalidInput("Invalid pack file path!".into()))?;
|
||||
|
||||
match path {
|
||||
Component::CurDir | Component::Normal(_) => {}
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
use crate::validate::{
|
||||
SupportedGameVersions, ValidationError, ValidationResult,
|
||||
};
|
||||
use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult};
|
||||
use std::io::Cursor;
|
||||
use zip::ZipArchive;
|
||||
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
use crate::validate::{
|
||||
SupportedGameVersions, ValidationError, ValidationResult,
|
||||
};
|
||||
use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult};
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use std::io::Cursor;
|
||||
use zip::ZipArchive;
|
||||
@ -37,9 +35,10 @@ impl super::Validator for QuiltValidator {
|
||||
));
|
||||
}
|
||||
|
||||
if !archive.file_names().any(|name| {
|
||||
name.ends_with("refmap.json") || name.ends_with(".class")
|
||||
}) {
|
||||
if !archive
|
||||
.file_names()
|
||||
.any(|name| name.ends_with("refmap.json") || name.ends_with(".class"))
|
||||
{
|
||||
return Ok(ValidationResult::Warning(
|
||||
"Quilt mod file is a source file!",
|
||||
));
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
use crate::validate::{
|
||||
SupportedGameVersions, ValidationError, ValidationResult,
|
||||
};
|
||||
use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult};
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use std::io::Cursor;
|
||||
use zip::ZipArchive;
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
use crate::validate::{
|
||||
SupportedGameVersions, ValidationError, ValidationResult,
|
||||
};
|
||||
use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult};
|
||||
use std::io::Cursor;
|
||||
use zip::ZipArchive;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user