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:
Geometrically
2023-04-20 16:38:30 -07:00
committed by GitHub
parent 5c559af936
commit 59f24df294
65 changed files with 1518 additions and 2218 deletions

View File

@@ -1,2 +0,0 @@
edition = "2018"
max_width = 80

View File

@@ -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
];
}

View File

@@ -525,26 +525,6 @@
}, },
"query": "\n SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)\n " "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": { "0fb1cca8a2a37107104244953371fe2f8a5e6edd57f4b325c5842c6571eb16b4": {
"describe": { "describe": {
"columns": [ "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 " "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": { "1209ffc1ffbea89f7060573275dc7325ac4d7b4885b6c1d1ec92998e6012e455": {
"describe": { "describe": {
"columns": [], "columns": [],
@@ -611,6 +763,18 @@
}, },
"query": "\n UPDATE mods\n SET webhook_sent = TRUE\n WHERE id = $1\n " "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": { "1411c9ae3af067679aa21d7f45937cd94d457e4eb17a108566776a9bd1ee77e2": {
"describe": { "describe": {
"columns": [], "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 " "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": { "3af747b5543a5a9b10dcce0a1eb9c2a1926dd5a507fe0d8b7f52d8ccc7fcd0af": {
"describe": { "describe": {
"columns": [], "columns": [],
@@ -2673,6 +2824,26 @@
}, },
"query": "\n UPDATE versions\n SET version_number = $1\n WHERE (id = $2)\n " "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": { "57743e20646dab2bcc02fe555d6b8ddb999697b7e95ec732d1a1a9e2bfdb8181": {
"describe": { "describe": {
"columns": [ "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 " "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": { "aa59f79136ef87dd4121d5f367f5dbdbca80e936c1b986ec99c09c3e95daa756": {
"describe": { "describe": {
"columns": [], "columns": [],
@@ -4752,172 +4949,6 @@
}, },
"query": "\n DELETE FROM game_versions_versions gvv\n WHERE gvv.joining_version_id = $1\n " "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": { "bf7f721664f5e0ed41adc41b5483037256635f28ff6c4e5d3cbcec4387f9c8ef": {
"describe": { "describe": {
"columns": [ "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 " "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": { "d12bc07adb4dc8147d0ddccd72a4f23ed38cd31d7db3d36ebbe2c9b627130f0b": {
"describe": { "describe": {
"columns": [], "columns": [],

View File

@@ -52,10 +52,7 @@ pub struct DonationPlatform {
} }
impl Category { impl Category {
pub async fn get_id<'a, E>( pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<CategoryId>, DatabaseError>
name: &str,
exec: E,
) -> Result<Option<CategoryId>, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -124,10 +121,7 @@ impl Category {
} }
impl Loader { impl Loader {
pub async fn get_id<'a, E>( pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<LoaderId>, DatabaseError>
name: &str,
exec: E,
) -> Result<Option<LoaderId>, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -315,10 +309,7 @@ impl GameVersion {
impl<'a> GameVersionBuilder<'a> { impl<'a> GameVersionBuilder<'a> {
/// The game version. Spaces must be replaced with '_' for it to be valid /// The game version. Spaces must be replaced with '_' for it to be valid
pub fn version( pub fn version(self, version: &'a str) -> Result<GameVersionBuilder<'a>, DatabaseError> {
self,
version: &'a str,
) -> Result<GameVersionBuilder<'a>, DatabaseError> {
Ok(Self { Ok(Self {
version: Some(version), version: Some(version),
..self ..self
@@ -342,10 +333,7 @@ impl<'a> GameVersionBuilder<'a> {
} }
} }
pub async fn insert<'b, E>( pub async fn insert<'b, E>(self, exec: E) -> Result<GameVersionId, DatabaseError>
self,
exec: E,
) -> Result<GameVersionId, DatabaseError>
where where
E: sqlx::Executor<'b, Database = sqlx::Postgres>, E: sqlx::Executor<'b, Database = sqlx::Postgres>,
{ {
@@ -393,9 +381,7 @@ impl DonationPlatform {
Ok(result.map(|r| DonationPlatformId(r.id))) Ok(result.map(|r| DonationPlatformId(r.id)))
} }
pub async fn list<'a, E>( pub async fn list<'a, E>(exec: E) -> Result<Vec<DonationPlatform>, DatabaseError>
exec: E,
) -> Result<Vec<DonationPlatform>, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -420,10 +406,7 @@ impl DonationPlatform {
} }
impl ReportType { impl ReportType {
pub async fn get_id<'a, E>( pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<ReportTypeId>, DatabaseError>
name: &str,
exec: E,
) -> Result<Option<ReportTypeId>, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -459,10 +442,7 @@ impl ReportType {
} }
impl ProjectType { impl ProjectType {
pub async fn get_id<'a, E>( pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<ProjectTypeId>, DatabaseError>
name: &str,
exec: E,
) -> Result<Option<ProjectTypeId>, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -498,10 +478,7 @@ impl ProjectType {
} }
impl SideType { impl SideType {
pub async fn get_id<'a, E>( pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<SideTypeId>, DatabaseError>
name: &str,
exec: E,
) -> Result<Option<SideTypeId>, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {

View File

@@ -102,8 +102,7 @@ impl Notification {
{ {
use futures::stream::TryStreamExt; use futures::stream::TryStreamExt;
let notification_ids_parsed: Vec<i64> = let notification_ids_parsed: Vec<i64> = notification_ids.iter().map(|x| x.0).collect();
notification_ids.iter().map(|x| x.0).collect();
sqlx::query!( sqlx::query!(
" "
SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type, n.body, 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 .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)
",
&notification_ids_parsed
)
.execute(&mut *transaction)
.await?;
Ok(Some(()))
}
pub async fn remove( pub async fn remove(
id: NotificationId, id: NotificationId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
@@ -215,8 +241,7 @@ impl Notification {
notification_ids: &[NotificationId], notification_ids: &[NotificationId],
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Option<()>, sqlx::error::Error> { ) -> Result<Option<()>, sqlx::error::Error> {
let notification_ids_parsed: Vec<i64> = let notification_ids_parsed: Vec<i64> = notification_ids.iter().map(|x| x.0).collect();
notification_ids.iter().map(|x| x.0).collect();
sqlx::query!( sqlx::query!(
" "

View File

@@ -186,12 +186,11 @@ impl ProjectBuilder {
self.project_id as ProjectId, self.project_id as ProjectId,
category as CategoryId, category as CategoryId,
) )
.execute(&mut *transaction) .execute(&mut *transaction)
.await?; .await?;
} }
Project::update_game_versions(self.project_id, &mut *transaction) Project::update_game_versions(self.project_id, &mut *transaction).await?;
.await?;
Project::update_loaders(self.project_id, &mut *transaction).await?; Project::update_loaders(self.project_id, &mut *transaction).await?;
Ok(self.project_id) Ok(self.project_id)
@@ -307,8 +306,7 @@ impl Project {
{ {
use futures::stream::TryStreamExt; use futures::stream::TryStreamExt;
let project_ids_parsed: Vec<i64> = let project_ids_parsed: Vec<i64> = project_ids.iter().map(|x| x.0).collect();
project_ids.iter().map(|x| x.0).collect();
let projects = sqlx::query!( let projects = sqlx::query!(
" "
SELECT id, project_type, title, description, downloads, follows, SELECT id, project_type, title, description, downloads, follows,
@@ -342,12 +340,8 @@ impl Project {
license_url: m.license_url, license_url: m.license_url,
discord_url: m.discord_url, discord_url: m.discord_url,
client_side: SideTypeId(m.client_side), client_side: SideTypeId(m.client_side),
status: ProjectStatus::from_str( status: ProjectStatus::from_str(&m.status),
&m.status, requested_status: m.requested_status.map(|x| ProjectStatus::from_str(&x)),
),
requested_status: m.requested_status.map(|x| ProjectStatus::from_str(
&x,
)),
server_side: SideTypeId(m.server_side), server_side: SideTypeId(m.server_side),
license: m.license, license: m.license,
slug: m.slug, slug: m.slug,
@@ -402,11 +396,7 @@ impl Project {
if let Some(thread_id) = thread_id { if let Some(thread_id) = thread_id {
if let Some(id) = thread_id.thread_id { if let Some(id) = thread_id.thread_id {
crate::database::models::Thread::remove_full( crate::database::models::Thread::remove_full(ThreadId(id), transaction).await?;
ThreadId(id),
transaction,
)
.await?;
} }
} }
@@ -595,23 +585,18 @@ impl Project {
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{ {
let id_option = let id_option = crate::models::ids::base62_impl::parse_base62(slug_or_project_id).ok();
crate::models::ids::base62_impl::parse_base62(slug_or_project_id)
.ok();
if let Some(id) = id_option { if let Some(id) = id_option {
let mut project = let mut project = Project::get(ProjectId(id as i64), executor).await?;
Project::get(ProjectId(id as i64), executor).await?;
if project.is_none() { if project.is_none() {
project = Project::get_from_slug(slug_or_project_id, executor) project = Project::get_from_slug(slug_or_project_id, executor).await?;
.await?;
} }
Ok(project) Ok(project)
} else { } else {
let project = let project = Project::get_from_slug(slug_or_project_id, executor).await?;
Project::get_from_slug(slug_or_project_id, executor).await?;
Ok(project) Ok(project)
} }
@@ -624,25 +609,18 @@ impl Project {
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{ {
let id_option = let id_option = crate::models::ids::base62_impl::parse_base62(slug_or_project_id).ok();
crate::models::ids::base62_impl::parse_base62(slug_or_project_id)
.ok();
if let Some(id) = id_option { if let Some(id) = id_option {
let mut project = let mut project = Project::get_full(ProjectId(id as i64), executor).await?;
Project::get_full(ProjectId(id as i64), executor).await?;
if project.is_none() { if project.is_none() {
project = project = Project::get_full_from_slug(slug_or_project_id, executor).await?;
Project::get_full_from_slug(slug_or_project_id, executor)
.await?;
} }
Ok(project) Ok(project)
} else { } else {
let project = let project = Project::get_full_from_slug(slug_or_project_id, executor).await?;
Project::get_full_from_slug(slug_or_project_id, executor)
.await?;
Ok(project) Ok(project)
} }
} }
@@ -668,8 +646,7 @@ impl Project {
{ {
use futures::TryStreamExt; use futures::TryStreamExt;
let project_ids_parsed: Vec<i64> = let project_ids_parsed: Vec<i64> = project_ids.iter().map(|x| x.0).collect();
project_ids.iter().map(|x| x.0).collect();
sqlx::query!( sqlx::query!(
" "
SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows, SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,

View File

@@ -58,10 +58,7 @@ impl Report {
Ok(()) Ok(())
} }
pub async fn get<'a, E>( pub async fn get<'a, E>(id: ReportId, exec: E) -> Result<Option<QueryReport>, sqlx::Error>
id: ReportId,
exec: E,
) -> Result<Option<QueryReport>, sqlx::Error>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{ {
@@ -79,8 +76,7 @@ impl Report {
{ {
use futures::stream::TryStreamExt; use futures::stream::TryStreamExt;
let report_ids_parsed: Vec<i64> = let report_ids_parsed: Vec<i64> = report_ids.iter().map(|x| x.0).collect();
report_ids.iter().map(|x| x.0).collect();
let reports = sqlx::query!( 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 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(thread_id) = thread_id {
if let Some(id) = thread_id.thread_id { if let Some(id) = thread_id.thread_id {
crate::database::models::Thread::remove_full( crate::database::models::Thread::remove_full(ThreadId(id), transaction).await?;
ThreadId(id),
transaction,
)
.await?;
} }
} }

View File

@@ -36,8 +36,7 @@ impl TeamBuilder {
.await?; .await?;
for member in self.members { for member in self.members {
let team_member_id = let team_member_id = generate_team_member_id(&mut *transaction).await?;
generate_team_member_id(&mut *transaction).await?;
let team_member = TeamMember { let team_member = TeamMember {
id: team_member_id, id: team_member_id,
team_id, team_id,
@@ -224,16 +223,16 @@ impl TeamMember {
.fetch_many(executor) .fetch_many(executor)
.try_filter_map(|e| async { .try_filter_map(|e| async {
if let Some(m) = e.right() { if let Some(m) = e.right() {
Ok(Some(Ok(TeamMember { Ok(Some(Ok(TeamMember {
id: TeamMemberId(m.id), id: TeamMemberId(m.id),
team_id: TeamId(m.team_id), team_id: TeamId(m.team_id),
user_id, user_id,
role: m.role, 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, accepted: m.accepted,
payouts_split: m.payouts_split, payouts_split: m.payouts_split,
ordering: m.ordering, ordering: m.ordering,
}))) })))
} else { } else {
Ok(None) Ok(None)
} }
@@ -275,8 +274,7 @@ impl TeamMember {
team_id: id, team_id: id,
user_id, user_id,
role: m.role, role: m.role,
permissions: Permissions::from_bits(m.permissions as u64) permissions: Permissions::from_bits(m.permissions as u64).unwrap_or_default(),
.unwrap_or_default(),
accepted: m.accepted, accepted: m.accepted,
payouts_split: m.payouts_split, payouts_split: m.payouts_split,
ordering: m.ordering, ordering: m.ordering,
@@ -448,8 +446,7 @@ impl TeamMember {
team_id: TeamId(m.team_id), team_id: TeamId(m.team_id),
user_id, user_id,
role: m.role, role: m.role,
permissions: Permissions::from_bits(m.permissions as u64) permissions: Permissions::from_bits(m.permissions as u64).unwrap_or_default(),
.unwrap_or_default(),
accepted: m.accepted, accepted: m.accepted,
payouts_split: m.payouts_split, payouts_split: m.payouts_split,
ordering: m.ordering, ordering: m.ordering,
@@ -486,8 +483,7 @@ impl TeamMember {
team_id: TeamId(m.team_id), team_id: TeamId(m.team_id),
user_id, user_id,
role: m.role, role: m.role,
permissions: Permissions::from_bits(m.permissions as u64) permissions: Permissions::from_bits(m.permissions as u64).unwrap_or_default(),
.unwrap_or_default(),
accepted: m.accepted, accepted: m.accepted,
payouts_split: m.payouts_split, payouts_split: m.payouts_split,
ordering: m.ordering, ordering: m.ordering,

View File

@@ -42,8 +42,7 @@ impl ThreadMessageBuilder {
&self, &self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<ThreadMessageId, DatabaseError> { ) -> Result<ThreadMessageId, DatabaseError> {
let thread_message_id = let thread_message_id = generate_thread_message_id(&mut *transaction).await?;
generate_thread_message_id(&mut *transaction).await?;
sqlx::query!( sqlx::query!(
" "
@@ -76,14 +75,16 @@ impl ThreadBuilder {
sqlx::query!( sqlx::query!(
" "
INSERT INTO threads ( INSERT INTO threads (
id, thread_type id, thread_type, report_id, project_id
) )
VALUES ( VALUES (
$1, $2 $1, $2, $3, $4
) )
", ",
thread_id as ThreadId, thread_id as ThreadId,
self.type_.as_str(), self.type_.as_str(),
self.report_id.map(|x| x.0),
self.project_id.map(|x| x.0),
) )
.execute(&mut *transaction) .execute(&mut *transaction)
.await?; .await?;
@@ -110,10 +111,7 @@ impl ThreadBuilder {
} }
impl Thread { impl Thread {
pub async fn get<'a, E>( pub async fn get<'a, E>(id: ThreadId, exec: E) -> Result<Option<Thread>, sqlx::Error>
id: ThreadId,
exec: E,
) -> Result<Option<Thread>, sqlx::Error>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{ {
@@ -131,8 +129,7 @@ impl Thread {
{ {
use futures::stream::TryStreamExt; use futures::stream::TryStreamExt;
let thread_ids_parsed: Vec<i64> = let thread_ids_parsed: Vec<i64> = thread_ids.iter().map(|x| x.0).collect();
thread_ids.iter().map(|x| x.0).collect();
let threads = sqlx::query!( let threads = sqlx::query!(
" "
SELECT t.id, t.thread_type, t.show_in_mod_inbox, t.project_id, t.report_id, 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; use futures::stream::TryStreamExt;
let message_ids_parsed: Vec<i64> = let message_ids_parsed: Vec<i64> = message_ids.iter().map(|x| x.0).collect();
message_ids.iter().map(|x| x.0).collect();
let messages = sqlx::query!( let messages = sqlx::query!(
" "
SELECT tm.id, tm.author_id, tm.thread_id, tm.body, tm.created SELECT tm.id, tm.author_id, tm.thread_id, tm.body, tm.created
@@ -246,8 +242,7 @@ impl ThreadMessage {
id: ThreadMessageId(x.id), id: ThreadMessageId(x.id),
thread_id: ThreadId(x.thread_id), thread_id: ThreadId(x.thread_id),
author_id: x.author_id.map(UserId), author_id: x.author_id.map(UserId),
body: serde_json::from_value(x.body) body: serde_json::from_value(x.body).unwrap_or(MessageBody::Deleted),
.unwrap_or(MessageBody::Deleted),
created: x.created, created: x.created,
})) }))
}) })
@@ -268,8 +263,7 @@ impl ThreadMessage {
WHERE id = $1 WHERE id = $1
", ",
id as ThreadMessageId, id as ThreadMessageId,
serde_json::to_value(MessageBody::Deleted) serde_json::to_value(MessageBody::Deleted).unwrap_or(serde_json::json!({}))
.unwrap_or(serde_json::json!({}))
) )
.execute(&mut *transaction) .execute(&mut *transaction)
.await?; .await?;

View File

@@ -50,10 +50,7 @@ impl User {
Ok(()) Ok(())
} }
pub async fn get<'a, 'b, E>( pub async fn get<'a, 'b, E>(id: UserId, executor: E) -> Result<Option<Self>, sqlx::error::Error>
id: UserId,
executor: E,
) -> Result<Option<Self>, sqlx::error::Error>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{ {
@@ -95,12 +92,9 @@ impl User {
bio: row.bio, bio: row.bio,
created: row.created, created: row.created,
role: row.role, role: row.role,
badges: Badges::from_bits(row.badges as u64) badges: Badges::from_bits(row.badges as u64).unwrap_or_default(),
.unwrap_or_default(),
balance: row.balance, balance: row.balance,
payout_wallet: row payout_wallet: row.payout_wallet.map(|x| RecipientWallet::from_string(&x)),
.payout_wallet
.map(|x| RecipientWallet::from_string(&x)),
payout_wallet_type: row payout_wallet_type: row
.payout_wallet_type .payout_wallet_type
.map(|x| RecipientType::from_string(&x)), .map(|x| RecipientType::from_string(&x)),
@@ -144,12 +138,9 @@ impl User {
bio: row.bio, bio: row.bio,
created: row.created, created: row.created,
role: row.role, role: row.role,
badges: Badges::from_bits(row.badges as u64) badges: Badges::from_bits(row.badges as u64).unwrap_or_default(),
.unwrap_or_default(),
balance: row.balance, balance: row.balance,
payout_wallet: row payout_wallet: row.payout_wallet.map(|x| RecipientWallet::from_string(&x)),
.payout_wallet
.map(|x| RecipientWallet::from_string(&x)),
payout_wallet_type: row payout_wallet_type: row
.payout_wallet_type .payout_wallet_type
.map(|x| RecipientType::from_string(&x)), .map(|x| RecipientType::from_string(&x)),
@@ -160,10 +151,7 @@ impl User {
} }
} }
pub async fn get_many<'a, E>( pub async fn get_many<'a, E>(user_ids: &[UserId], exec: E) -> Result<Vec<User>, sqlx::Error>
user_ids: &[UserId],
exec: E,
) -> Result<Vec<User>, sqlx::Error>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{ {
@@ -196,12 +184,8 @@ impl User {
role: u.role, role: u.role,
badges: Badges::from_bits(u.badges as u64).unwrap_or_default(), badges: Badges::from_bits(u.badges as u64).unwrap_or_default(),
balance: u.balance, balance: u.balance,
payout_wallet: u payout_wallet: u.payout_wallet.map(|x| RecipientWallet::from_string(&x)),
.payout_wallet payout_wallet_type: u.payout_wallet_type.map(|x| RecipientType::from_string(&x)),
.map(|x| RecipientWallet::from_string(&x)),
payout_wallet_type: u
.payout_wallet_type
.map(|x| RecipientType::from_string(&x)),
payout_address: u.payout_address, payout_address: u.payout_address,
})) }))
}) })
@@ -384,11 +368,8 @@ impl User {
.await?; .await?;
for project_id in projects { for project_id in projects {
let _result = super::project_item::Project::remove_full( let _result =
project_id, super::project_item::Project::remove_full(project_id, transaction).await?;
transaction,
)
.await?;
} }
let notifications: Vec<i64> = sqlx::query!( let notifications: Vec<i64> = sqlx::query!(
@@ -489,8 +470,7 @@ impl User {
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{ {
let id_option = let id_option = crate::models::ids::base62_impl::parse_base62(username_or_id).ok();
crate::models::ids::base62_impl::parse_base62(username_or_id).ok();
if let Some(id) = id_option { if let Some(id) = id_option {
let id = UserId(id as i64); let id = UserId(id as i64);

View File

@@ -499,8 +499,7 @@ impl Version {
{ {
use futures::stream::TryStreamExt; use futures::stream::TryStreamExt;
let version_ids_parsed: Vec<i64> = let version_ids_parsed: Vec<i64> = version_ids.iter().map(|x| x.0).collect();
version_ids.iter().map(|x| x.0).collect();
sqlx::query!( 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, 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 where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{ {
let project_id_opt = let project_id_opt = parse_base62(project_id_or_slug).ok().map(|x| x as i64);
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_opt = parse_base62(slug).ok().map(|x| x as i64);
let id = sqlx::query!( let id = sqlx::query!(
" "

View File

@@ -6,8 +6,7 @@ use std::time::Duration;
pub async fn connect() -> Result<PgPool, sqlx::Error> { pub async fn connect() -> Result<PgPool, sqlx::Error> {
info!("Initializing database connection"); info!("Initializing database connection");
let database_url = let database_url = dotenvy::var("DATABASE_URL").expect("`DATABASE_URL` not in .env");
dotenvy::var("DATABASE_URL").expect("`DATABASE_URL` not in .env");
let pool = PgPoolOptions::new() let pool = PgPoolOptions::new()
.min_connections( .min_connections(
dotenvy::var("DATABASE_MIN_CONNECTIONS") dotenvy::var("DATABASE_MIN_CONNECTIONS")

View File

@@ -16,12 +16,10 @@ pub struct BackblazeHost {
impl BackblazeHost { impl BackblazeHost {
pub async fn new(key_id: &str, key: &str, bucket_id: &str) -> Self { pub async fn new(key_id: &str, key: &str, bucket_id: &str) -> Self {
let authorization_data = let authorization_data = authorization::authorize_account(key_id, key).await.unwrap();
authorization::authorize_account(key_id, key).await.unwrap(); let upload_url_data = authorization::get_upload_url(&authorization_data, bucket_id)
let upload_url_data = .await
authorization::get_upload_url(&authorization_data, bucket_id) .unwrap();
.await
.unwrap();
BackblazeHost { BackblazeHost {
upload_url_data, upload_url_data,
@@ -40,13 +38,8 @@ impl FileHost for BackblazeHost {
) -> Result<UploadFileData, FileHostingError> { ) -> Result<UploadFileData, FileHostingError> {
let content_sha512 = format!("{:x}", sha2::Sha512::digest(&file_bytes)); let content_sha512 = format!("{:x}", sha2::Sha512::digest(&file_bytes));
let upload_data = upload::upload_file( let upload_data =
&self.upload_url_data, upload::upload_file(&self.upload_url_data, content_type, file_name, file_bytes).await?;
content_type,
file_name,
file_bytes,
)
.await?;
Ok(UploadFileData { Ok(UploadFileData {
file_id: upload_data.file_id, file_id: upload_data.file_id,
file_name: upload_data.file_name, file_name: upload_data.file_name,
@@ -81,12 +74,8 @@ impl FileHost for BackblazeHost {
file_id: &str, file_id: &str,
file_name: &str, file_name: &str,
) -> Result<DeleteFileData, FileHostingError> { ) -> Result<DeleteFileData, FileHostingError> {
let delete_data = delete::delete_file_version( let delete_data =
&self.authorization_data, delete::delete_file_version(&self.authorization_data, file_id, file_name).await?;
file_id,
file_name,
)
.await?;
Ok(DeleteFileData { Ok(DeleteFileData {
file_id: delete_data.file_id, file_id: delete_data.file_id,
file_name: delete_data.file_name, file_name: delete_data.file_name,
@@ -94,9 +83,7 @@ impl FileHost for BackblazeHost {
} }
} }
pub async fn process_response<T>( pub async fn process_response<T>(response: Response) -> Result<T, FileHostingError>
response: Response,
) -> Result<T, FileHostingError>
where where
T: for<'de> Deserialize<'de>, T: for<'de> Deserialize<'de>,
{ {

View File

@@ -56,13 +56,7 @@ pub async fn get_upload_url(
bucket_id: &str, bucket_id: &str,
) -> Result<UploadUrlData, FileHostingError> { ) -> Result<UploadUrlData, FileHostingError> {
let response = reqwest::Client::new() let response = reqwest::Client::new()
.post( .post(&format!("{}/b2api/v2/b2_get_upload_url", authorization_data.api_url).to_string())
&format!(
"{}/b2api/v2/b2_get_upload_url",
authorization_data.api_url
)
.to_string(),
)
.header(reqwest::header::CONTENT_TYPE, "application/json") .header(reqwest::header::CONTENT_TYPE, "application/json")
.header( .header(
reqwest::header::AUTHORIZATION, reqwest::header::AUTHORIZATION,

View File

@@ -20,12 +20,9 @@ impl FileHost for MockHost {
file_name: &str, file_name: &str,
file_bytes: Bytes, file_bytes: Bytes,
) -> Result<UploadFileData, FileHostingError> { ) -> Result<UploadFileData, FileHostingError> {
let path = let path = std::path::Path::new(&dotenvy::var("MOCK_FILE_PATH").unwrap())
std::path::Path::new(&dotenvy::var("MOCK_FILE_PATH").unwrap()) .join(file_name.replace("../", ""));
.join(file_name.replace("../", "")); std::fs::create_dir_all(path.parent().ok_or(FileHostingError::InvalidFilename)?)?;
std::fs::create_dir_all(
path.parent().ok_or(FileHostingError::InvalidFilename)?,
)?;
let content_sha1 = sha1::Sha1::from(&file_bytes).hexdigest(); let content_sha1 = sha1::Sha1::from(&file_bytes).hexdigest();
let content_sha512 = format!("{:x}", sha2::Sha512::digest(&file_bytes)); let content_sha512 = format!("{:x}", sha2::Sha512::digest(&file_bytes));
@@ -47,9 +44,8 @@ impl FileHost for MockHost {
file_id: &str, file_id: &str,
file_name: &str, file_name: &str,
) -> Result<DeleteFileData, FileHostingError> { ) -> Result<DeleteFileData, FileHostingError> {
let path = let path = std::path::Path::new(&dotenvy::var("MOCK_FILE_PATH").unwrap())
std::path::Path::new(&dotenvy::var("MOCK_FILE_PATH").unwrap()) .join(file_name.replace("../", ""));
.join(file_name.replace("../", ""));
std::fs::remove_file(path)?; std::fs::remove_file(path)?;
Ok(DeleteFileData { Ok(DeleteFileData {

View File

@@ -1,6 +1,4 @@
use crate::file_hosting::{ use crate::file_hosting::{DeleteFileData, FileHost, FileHostingError, UploadFileData};
DeleteFileData, FileHost, FileHostingError, UploadFileData,
};
use async_trait::async_trait; use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use chrono::Utc; use chrono::Utc;
@@ -33,23 +31,12 @@ impl S3Host {
endpoint: url.to_string(), endpoint: url.to_string(),
} }
}, },
Credentials::new( Credentials::new(Some(access_token), Some(secret), None, None, None).map_err(|_| {
Some(access_token), FileHostingError::S3Error("Error while creating credentials".to_string())
Some(secret),
None,
None,
None,
)
.map_err(|_| {
FileHostingError::S3Error(
"Error while creating credentials".to_string(),
)
})?, })?,
) )
.map_err(|_| { .map_err(|_| {
FileHostingError::S3Error( FileHostingError::S3Error("Error while creating Bucket instance".to_string())
"Error while creating Bucket instance".to_string(),
)
})?; })?;
Ok(S3Host { bucket }) Ok(S3Host { bucket })
@@ -68,16 +55,10 @@ impl FileHost for S3Host {
let content_sha512 = format!("{:x}", sha2::Sha512::digest(&file_bytes)); let content_sha512 = format!("{:x}", sha2::Sha512::digest(&file_bytes));
self.bucket self.bucket
.put_object_with_content_type( .put_object_with_content_type(format!("/{file_name}"), &file_bytes, content_type)
format!("/{file_name}"),
&file_bytes,
content_type,
)
.await .await
.map_err(|_| { .map_err(|_| {
FileHostingError::S3Error( FileHostingError::S3Error("Error while uploading file to S3".to_string())
"Error while uploading file to S3".to_string(),
)
})?; })?;
Ok(UploadFileData { Ok(UploadFileData {
@@ -101,9 +82,7 @@ impl FileHost for S3Host {
.delete_object(format!("/{file_name}")) .delete_object(format!("/{file_name}"))
.await .await
.map_err(|_| { .map_err(|_| {
FileHostingError::S3Error( FileHostingError::S3Error("Error while deleting file from S3".to_string())
"Error while deleting file from S3".to_string(),
)
})?; })?;
Ok(DeleteFileData { Ok(DeleteFileData {

View File

@@ -1,9 +1,7 @@
use actix_web::web; use actix_web::web;
use sqlx::PgPool; use sqlx::PgPool;
pub async fn test_database( pub async fn test_database(postgres: web::Data<PgPool>) -> Result<(), sqlx::Error> {
postgres: web::Data<PgPool>,
) -> Result<(), sqlx::Error> {
let mut transaction = postgres.acquire().await?; let mut transaction = postgres.acquire().await?;
sqlx::query( sqlx::query(
" "

View File

@@ -35,8 +35,7 @@ pub struct Pepper {
#[actix_rt::main] #[actix_rt::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
env_logger::Builder::from_env(Env::default().default_filter_or("info")) env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
.init();
if check_env_vars() { if check_env_vars() {
error!("Some environment variables are missing!"); error!("Some environment variables are missing!");
@@ -75,40 +74,37 @@ async fn main() -> std::io::Result<()> {
.await .await
.expect("Database connection failed"); .expect("Database connection failed");
let storage_backend = let storage_backend = dotenvy::var("STORAGE_BACKEND").unwrap_or_else(|_| "local".to_string());
dotenvy::var("STORAGE_BACKEND").unwrap_or_else(|_| "local".to_string());
let file_host: Arc<dyn file_hosting::FileHost + Send + Sync> = let file_host: Arc<dyn file_hosting::FileHost + Send + Sync> = match storage_backend.as_str() {
match storage_backend.as_str() { "backblaze" => Arc::new(
"backblaze" => Arc::new( file_hosting::BackblazeHost::new(
file_hosting::BackblazeHost::new( &dotenvy::var("BACKBLAZE_KEY_ID").unwrap(),
&dotenvy::var("BACKBLAZE_KEY_ID").unwrap(), &dotenvy::var("BACKBLAZE_KEY").unwrap(),
&dotenvy::var("BACKBLAZE_KEY").unwrap(), &dotenvy::var("BACKBLAZE_BUCKET_ID").unwrap(),
&dotenvy::var("BACKBLAZE_BUCKET_ID").unwrap(), )
) .await,
.await, ),
), "s3" => Arc::new(
"s3" => Arc::new( S3Host::new(
S3Host::new( &dotenvy::var("S3_BUCKET_NAME").unwrap(),
&dotenvy::var("S3_BUCKET_NAME").unwrap(), &dotenvy::var("S3_REGION").unwrap(),
&dotenvy::var("S3_REGION").unwrap(), &dotenvy::var("S3_URL").unwrap(),
&dotenvy::var("S3_URL").unwrap(), &dotenvy::var("S3_ACCESS_TOKEN").unwrap(),
&dotenvy::var("S3_ACCESS_TOKEN").unwrap(), &dotenvy::var("S3_SECRET").unwrap(),
&dotenvy::var("S3_SECRET").unwrap(), )
) .unwrap(),
.unwrap(), ),
), "local" => Arc::new(file_hosting::MockHost::new()),
"local" => Arc::new(file_hosting::MockHost::new()), _ => panic!("Invalid storage backend specified. Aborting startup!"),
_ => panic!("Invalid storage backend specified. Aborting startup!"), };
};
let mut scheduler = scheduler::Scheduler::new(); let mut scheduler = scheduler::Scheduler::new();
// The interval in seconds at which the local database is indexed // The interval in seconds at which the local database is indexed
// for searching. Defaults to 1 hour if unset. // for searching. Defaults to 1 hour if unset.
let local_index_interval = std::time::Duration::from_secs( let local_index_interval =
parse_var("LOCAL_INDEX_INTERVAL").unwrap_or(3600), std::time::Duration::from_secs(parse_var("LOCAL_INDEX_INTERVAL").unwrap_or(3600));
);
let pool_ref = pool.clone(); let pool_ref = pool.clone();
let search_config_ref = search_config.clone(); let search_config_ref = search_config.clone();
@@ -118,8 +114,7 @@ async fn main() -> std::io::Result<()> {
async move { async move {
info!("Indexing local database"); info!("Indexing local database");
let settings = IndexingSettings { index_local: true }; let settings = IndexingSettings { index_local: true };
let result = let result = index_projects(pool_ref, settings, &search_config_ref).await;
index_projects(pool_ref, settings, &search_config_ref).await;
if let Err(e) = result { if let Err(e) = result {
warn!("Local project indexing failed: {:?}", e); warn!("Local project indexing failed: {:?}", e);
} }
@@ -170,14 +165,11 @@ async fn main() -> std::io::Result<()> {
", ",
crate::models::projects::ProjectStatus::Scheduled.as_str(), crate::models::projects::ProjectStatus::Scheduled.as_str(),
) )
.execute(&pool_ref) .execute(&pool_ref)
.await; .await;
if let Err(e) = projects_results { if let Err(e) = projects_results {
warn!( warn!("Syncing scheduled releases for projects failed: {:?}", e);
"Syncing scheduled releases for projects failed: {:?}",
e
);
} }
let versions_results = sqlx::query!( let versions_results = sqlx::query!(
@@ -188,21 +180,18 @@ async fn main() -> std::io::Result<()> {
", ",
crate::models::projects::VersionStatus::Scheduled.as_str(), crate::models::projects::VersionStatus::Scheduled.as_str(),
) )
.execute(&pool_ref) .execute(&pool_ref)
.await; .await;
if let Err(e) = versions_results { if let Err(e) = versions_results {
warn!( warn!("Syncing scheduled releases for versions failed: {:?}", e);
"Syncing scheduled releases for versions failed: {:?}",
e
);
} }
info!("Finished releasing scheduled versions/projects"); 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 pool_ref = pool.clone();
let webhook_message_sent = Arc::new(Mutex::new(Vec::<( let webhook_message_sent = Arc::new(Mutex::new(Vec::<(
database::models::ProjectId, database::models::ProjectId,
@@ -212,7 +201,7 @@ async fn main() -> std::io::Result<()> {
scheduler.run(std::time::Duration::from_secs(10 * 60), move || { scheduler.run(std::time::Duration::from_secs(10 * 60), move || {
let pool_ref = pool_ref.clone(); let pool_ref = pool_ref.clone();
let webhook_message_sent_ref = webhook_message_sent.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 { async move {
let do_steps = async { let do_steps = async {
@@ -221,7 +210,7 @@ async fn main() -> std::io::Result<()> {
let project_ids = sqlx::query!( let project_ids = sqlx::query!(
" "
SELECT id FROM mods 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 ORDER BY updated ASC
", ",
crate::models::projects::ProjectStatus::Processing.as_str(), crate::models::projects::ProjectStatus::Processing.as_str(),
@@ -247,7 +236,7 @@ async fn main() -> std::io::Result<()> {
project.into(), project.into(),
&pool_ref, &pool_ref,
webhook_url, 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 .await
.ok(); .ok();
@@ -261,12 +250,12 @@ async fn main() -> std::io::Result<()> {
if let Err(e) = do_steps.await { if let Err(e) = do_steps.await {
warn!( warn!(
"Checking reviewed projects submitted more than 24hrs ago failed: {:?}", "Checking reviewed projects submitted more than 40hrs ago failed: {:?}",
e 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 { let ip_salt = Pepper {
pepper: models::ids::Base62Id(models::ids::random_base62(11)) pepper: models::ids::Base62Id(models::ids::random_base62(11)).to_string(),
.to_string(),
}; };
let payouts_queue = Arc::new(Mutex::new(PayoutsQueue::new())); 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()) RateLimiter::new(MemoryStoreActor::from(store.clone()).start())
.with_identifier(|req| { .with_identifier(|req| {
let connection_info = req.connection_info(); let connection_info = req.connection_info();
let ip = String::from( let ip =
if parse_var("CLOUDFLARE_INTEGRATION") String::from(if parse_var("CLOUDFLARE_INTEGRATION").unwrap_or(false) {
.unwrap_or(false) if let Some(header) = req.headers().get("CF-Connecting-IP") {
{ header.to_str().map_err(|_| ARError::Identification)?
if let Some(header) =
req.headers().get("CF-Connecting-IP")
{
header
.to_str()
.map_err(|_| ARError::Identification)?
} else { } else {
connection_info connection_info.peer_addr().ok_or(ARError::Identification)?
.peer_addr()
.ok_or(ARError::Identification)?
} }
} else { } else {
connection_info connection_info.peer_addr().ok_or(ARError::Identification)?
.peer_addr() });
.ok_or(ARError::Identification)?
},
);
Ok(ip) Ok(ip)
}) })
.with_interval(std::time::Duration::from_secs(60)) .with_interval(std::time::Duration::from_secs(60))
.with_max_requests(300) .with_max_requests(300)
.with_ignore_key( .with_ignore_key(dotenvy::var("RATE_LIMIT_IGNORE_KEY").ok()),
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(pool.clone()))
.app_data(web::Data::new(file_host.clone())) .app_data(web::Data::new(file_host.clone()))
.app_data(web::Data::new(search_config.clone())) .app_data(web::Data::new(search_config.clone()))

View File

@@ -131,10 +131,7 @@ pub mod base62_impl {
impl<'de> Visitor<'de> for Base62Visitor { impl<'de> Visitor<'de> for Base62Visitor {
type Value = Base62Id; type Value = Base62Id;
fn expecting( fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
formatter.write_str("a base62 string id") 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 // We don't want this panicking or wrapping on integer overflow
if let Some(n) = if let Some(n) = num.checked_mul(62).and_then(|n| n.checked_add(next_digit)) {
num.checked_mul(62).and_then(|n| n.checked_add(next_digit))
{
num = n; num = n;
} else { } else {
return Err(DecodingError::Overflow); return Err(DecodingError::Overflow);

View File

@@ -2,9 +2,7 @@ use super::ids::Base62Id;
use super::users::UserId; use super::users::UserId;
use crate::database::models::notification_item::Notification as DBNotification; use crate::database::models::notification_item::Notification as DBNotification;
use crate::database::models::notification_item::NotificationAction as DBNotificationAction; use crate::database::models::notification_item::NotificationAction as DBNotificationAction;
use crate::models::ids::{ use crate::models::ids::{ProjectId, ReportId, TeamId, ThreadId, ThreadMessageId, VersionId};
ProjectId, ReportId, TeamId, ThreadId, ThreadMessageId, VersionId,
};
use crate::models::projects::ProjectStatus; use crate::models::projects::ProjectStatus;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -91,27 +89,18 @@ impl From<DBNotification> for Notification {
} => ( } => (
Some("team_invite".to_string()), Some("team_invite".to_string()),
"You have been invited to join a team!".to_string(), "You have been invited to join a team!".to_string(),
format!( format!("An invite has been sent for you to be {} of a team", role),
"An invite has been sent for you to be {} of a team",
role
),
format!("/project/{}", project_id), format!("/project/{}", project_id),
vec![ vec![
NotificationAction { NotificationAction {
title: "Accept".to_string(), title: "Accept".to_string(),
action_route: ( action_route: ("POST".to_string(), format!("team/{team_id}/join")),
"POST".to_string(),
format!("team/{team_id}/join"),
),
}, },
NotificationAction { NotificationAction {
title: "Deny".to_string(), title: "Deny".to_string(),
action_route: ( action_route: (
"DELETE".to_string(), "DELETE".to_string(),
format!( format!("team/{team_id}/members/{}", UserId::from(notif.user_id)),
"team/{team_id}/members/{}",
UserId::from(notif.user_id)
),
), ),
}, },
], ],

View File

@@ -30,9 +30,7 @@ pub struct PackFile {
pub file_size: u32, pub file_size: u32,
} }
fn validate_download_url( fn validate_download_url(values: &[String]) -> Result<(), validator::ValidationError> {
values: &[String],
) -> Result<(), validator::ValidationError> {
for value in values { for value in values {
let url = url::Url::parse(value) let url = url::Url::parse(value)
.ok() .ok()
@@ -42,8 +40,7 @@ fn validate_download_url(
return Err(validator::ValidationError::new("invalid URL")); return Err(validator::ValidationError::new("invalid URL"));
} }
let domains = parse_strings_from_var("WHITELISTED_MODPACK_DOMAINS") let domains = parse_strings_from_var("WHITELISTED_MODPACK_DOMAINS").unwrap_or_default();
.unwrap_or_default();
if !domains.contains( if !domains.contains(
&url.domain() &url.domain()
.ok_or_else(|| validator::ValidationError::new("invalid URL"))? .ok_or_else(|| validator::ValidationError::new("invalid URL"))?

View File

@@ -534,16 +534,10 @@ impl From<QueryVersion> for Version {
version_id: d.version_id.map(|i| VersionId(i.0 as u64)), version_id: d.version_id.map(|i| VersionId(i.0 as u64)),
project_id: d.project_id.map(|i| ProjectId(i.0 as u64)), project_id: d.project_id.map(|i| ProjectId(i.0 as u64)),
file_name: d.file_name, file_name: d.file_name,
dependency_type: DependencyType::from_str( dependency_type: DependencyType::from_str(d.dependency_type.as_str()),
d.dependency_type.as_str(),
),
}) })
.collect(), .collect(),
game_versions: data game_versions: data.game_versions.into_iter().map(GameVersion).collect(),
.game_versions
.into_iter()
.map(GameVersion)
.collect(),
loaders: data.loaders.into_iter().map(Loader).collect(), loaders: data.loaders.into_iter().map(Loader).collect(),
} }
} }

View File

@@ -68,36 +68,25 @@ impl PayoutsQueue {
.form(&form) .form(&form)
.send() .send()
.await .await
.map_err(|_| { .map_err(|_| ApiError::Payments("Error while authenticating with PayPal".to_string()))?
ApiError::Payments(
"Error while authenticating with PayPal".to_string(),
)
})?
.json() .json()
.await .await
.map_err(|_| { .map_err(|_| {
ApiError::Payments( ApiError::Payments(
"Error while authenticating with PayPal (deser error)" "Error while authenticating with PayPal (deser error)".to_string(),
.to_string(),
) )
})?; })?;
self.credential_expires = self.credential_expires = Utc::now() + Duration::seconds(credential.expires_in);
Utc::now() + Duration::seconds(credential.expires_in);
self.credential = credential; self.credential = credential;
Ok(()) Ok(())
} }
pub async fn send_payout( pub async fn send_payout(&mut self, mut payout: PayoutItem) -> Result<Decimal, ApiError> {
&mut self,
mut payout: PayoutItem,
) -> Result<Decimal, ApiError> {
if self.credential_expires < Utc::now() { if self.credential_expires < Utc::now() {
self.refresh_token().await.map_err(|_| { self.refresh_token().await.map_err(|_| {
ApiError::Payments( ApiError::Payments("Error while authenticating with PayPal".to_string())
"Error while authenticating with PayPal".to_string(),
)
})?; })?;
} }
@@ -109,8 +98,7 @@ impl PayoutsQueue {
std::cmp::min( std::cmp::min(
std::cmp::max( std::cmp::max(
Decimal::ONE / Decimal::from(4), Decimal::ONE / Decimal::from(4),
(Decimal::from(2) / Decimal::ONE_HUNDRED) (Decimal::from(2) / Decimal::ONE_HUNDRED) * payout.amount.value,
* payout.amount.value,
), ),
Decimal::from(20), Decimal::from(20),
) )
@@ -151,9 +139,7 @@ impl PayoutsQueue {
} }
let body: PayPalError = res.json().await.map_err(|_| { let body: PayPalError = res.json().await.map_err(|_| {
ApiError::Payments( ApiError::Payments("Error while registering payment in PayPal!".to_string())
"Error while registering payment in PayPal!".to_string(),
)
})?; })?;
return Err(ApiError::Payments(format!( return Err(ApiError::Payments(format!(
@@ -190,8 +176,7 @@ impl PayoutsQueue {
"Authorization", "Authorization",
format!( format!(
"{} {}", "{} {}",
self.credential.token_type, self.credential.token_type, self.credential.access_token
self.credential.access_token
), ),
) )
.send() .send()
@@ -199,9 +184,7 @@ impl PayoutsQueue {
{ {
if let Ok(res) = res.json::<PayoutData>().await { if let Ok(res) = res.json::<PayoutData>().await {
if let Some(data) = res.items.first() { if let Some(data) = res.items.first() {
if (fee - data.payout_item_fee.value) if (fee - data.payout_item_fee.value) > Decimal::ZERO {
> Decimal::ZERO
{
return Ok(fee - data.payout_item_fee.value); return Ok(fee - data.payout_item_fee.value);
} }
} }

View File

@@ -35,27 +35,18 @@ impl ResponseError for ARError {
reset, reset,
} => { } => {
let mut response = actix_web::HttpResponse::TooManyRequests(); let mut response = actix_web::HttpResponse::TooManyRequests();
response.insert_header(( response.insert_header(("x-ratelimit-limit", max_requests.to_string()));
"x-ratelimit-limit", response.insert_header(("x-ratelimit-remaining", remaining.to_string()));
max_requests.to_string(), response.insert_header(("x-ratelimit-reset", reset.to_string()));
));
response.insert_header((
"x-ratelimit-remaining",
remaining.to_string(),
));
response
.insert_header(("x-ratelimit-reset", reset.to_string()));
response.json(ApiError { response.json(ApiError {
error: "ratelimit_error", error: "ratelimit_error",
description: &self.to_string(), description: &self.to_string(),
}) })
} }
_ => actix_web::HttpResponse::build(self.status_code()).json( _ => actix_web::HttpResponse::build(self.status_code()).json(ApiError {
ApiError { error: "ratelimit_error",
error: "ratelimit_error", description: &self.to_string(),
description: &self.to_string(), }),
},
),
} }
} }
} }

View File

@@ -36,9 +36,9 @@ impl MemoryStore {
pub fn with_capacity(capacity: usize) -> Self { pub fn with_capacity(capacity: usize) -> Self {
debug!("Creating new MemoryStore"); debug!("Creating new MemoryStore");
MemoryStore { MemoryStore {
inner: Arc::new( inner: Arc::new(DashMap::<String, (usize, Duration)>::with_capacity(
DashMap::<String, (usize, Duration)>::with_capacity(capacity), capacity,
), )),
} }
} }
} }
@@ -74,18 +74,10 @@ impl Supervised for MemoryStoreActor {
impl Handler<ActorMessage> for MemoryStoreActor { impl Handler<ActorMessage> for MemoryStoreActor {
type Result = ActorResponse; type Result = ActorResponse;
fn handle( fn handle(&mut self, msg: ActorMessage, ctx: &mut Self::Context) -> Self::Result {
&mut self,
msg: ActorMessage,
ctx: &mut Self::Context,
) -> Self::Result {
match msg { match msg {
ActorMessage::Set { key, value, expiry } => { ActorMessage::Set { key, value, expiry } => {
debug!( debug!("Inserting key {} with expiry {}", &key, &expiry.as_secs());
"Inserting key {} with expiry {}",
&key,
&expiry.as_secs()
);
let future_key = String::from(&key); let future_key = String::from(&key);
let now = SystemTime::now(); let now = SystemTime::now();
let now = now.duration_since(UNIX_EPOCH).unwrap(); 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); ctx.notify_later(ActorMessage::Remove(future_key), expiry);
ActorResponse::Set(Box::pin(future::ready(Ok(())))) ActorResponse::Set(Box::pin(future::ready(Ok(()))))
} }
ActorMessage::Update { key, value } => match self ActorMessage::Update { key, value } => match self.inner.get_mut(&key) {
.inner
.get_mut(&key)
{
Some(mut c) => { Some(mut c) => {
let val_mut: &mut (usize, Duration) = c.value_mut(); let val_mut: &mut (usize, Duration) = c.value_mut();
if val_mut.0 > value { if val_mut.0 > value {
@@ -107,22 +96,18 @@ impl Handler<ActorMessage> for MemoryStoreActor {
let new_val = val_mut.0; let new_val = val_mut.0;
ActorResponse::Update(Box::pin(future::ready(Ok(new_val)))) ActorResponse::Update(Box::pin(future::ready(Ok(new_val))))
} }
None => ActorResponse::Update(Box::pin(future::ready(Err( None => ActorResponse::Update(Box::pin(future::ready(Err(ARError::ReadWrite(
ARError::ReadWrite( "memory store: read failed!".to_string(),
"memory store: read failed!".to_string(), ))))),
),
)))),
}, },
ActorMessage::Get(key) => { ActorMessage::Get(key) => {
if self.inner.contains_key(&key) { if self.inner.contains_key(&key) {
let val = match self.inner.get(&key) { let val = match self.inner.get(&key) {
Some(c) => c, Some(c) => c,
None => { None => {
return ActorResponse::Get(Box::pin(future::ready( return ActorResponse::Get(Box::pin(future::ready(Err(
Err(ARError::ReadWrite( ARError::ReadWrite("memory store: read failed!".to_string()),
"memory store: read failed!".to_string(), ))))
)),
)))
} }
}; };
let val = val.value().0; let val = val.value().0;
@@ -135,17 +120,14 @@ impl Handler<ActorMessage> for MemoryStoreActor {
let c = match self.inner.get(&key) { let c = match self.inner.get(&key) {
Some(d) => d, Some(d) => d,
None => { None => {
return ActorResponse::Expire(Box::pin(future::ready( return ActorResponse::Expire(Box::pin(future::ready(Err(
Err(ARError::ReadWrite( ARError::ReadWrite("memory store: read failed!".to_string()),
"memory store: read failed!".to_string(), ))))
)),
)))
} }
}; };
let dur = c.value().1; let dur = c.value().1;
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
let res = let res = dur.checked_sub(now).unwrap_or_else(|| Duration::new(0, 0));
dur.checked_sub(now).unwrap_or_else(|| Duration::new(0, 0));
ActorResponse::Expire(Box::pin(future::ready(Ok(res)))) ActorResponse::Expire(Box::pin(future::ready(Ok(res))))
} }
ActorMessage::Remove(key) => { ActorMessage::Remove(key) => {
@@ -153,11 +135,9 @@ impl Handler<ActorMessage> for MemoryStoreActor {
let val = match self.inner.remove::<String>(&key) { let val = match self.inner.remove::<String>(&key) {
Some(c) => c, Some(c) => c,
None => { None => {
return ActorResponse::Remove(Box::pin(future::ready( return ActorResponse::Remove(Box::pin(future::ready(Err(
Err(ARError::ReadWrite( ARError::ReadWrite("memory store: remove failed!".to_string()),
"memory store: remove failed!".to_string(), ))))
)),
)))
} }
}; };
let val = val.1; let val = val.1;

View File

@@ -18,8 +18,7 @@ use std::{
time::Duration, time::Duration,
}; };
type RateLimiterIdentifier = type RateLimiterIdentifier = Rc<Box<dyn Fn(&ServiceRequest) -> Result<String, ARError> + 'static>>;
Rc<Box<dyn Fn(&ServiceRequest) -> Result<String, ARError> + 'static>>;
pub struct RateLimiter<T> pub struct RateLimiter<T>
where where
@@ -42,8 +41,7 @@ where
pub fn new(store: Addr<T>) -> Self { pub fn new(store: Addr<T>) -> Self {
let identifier = |req: &ServiceRequest| { let identifier = |req: &ServiceRequest| {
let connection_info = req.connection_info(); let connection_info = req.connection_info();
let ip = let ip = connection_info.peer_addr().ok_or(ARError::Identification)?;
connection_info.peer_addr().ok_or(ARError::Identification)?;
Ok(String::from(ip)) Ok(String::from(ip))
}; };
RateLimiter { RateLimiter {
@@ -74,9 +72,7 @@ where
} }
/// Function to get the identifier for the client request /// Function to get the identifier for the client request
pub fn with_identifier< pub fn with_identifier<F: Fn(&ServiceRequest) -> Result<String, ARError> + 'static>(
F: Fn(&ServiceRequest) -> Result<String, ARError> + 'static,
>(
mut self, mut self,
identifier: F, identifier: F,
) -> Self { ) -> Self {
@@ -89,8 +85,7 @@ impl<T, S, B> Transform<S, ServiceRequest> for RateLimiter<T>
where where
T: Handler<ActorMessage> + Send + Sync + 'static, T: Handler<ActorMessage> + Send + Sync + 'static,
T::Context: ToEnvelope<T, ActorMessage>, T::Context: ToEnvelope<T, ActorMessage>,
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = AWError> S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = AWError> + 'static,
+ 'static,
S::Future: 'static, S::Future: 'static,
B: 'static, B: 'static,
{ {
@@ -130,21 +125,16 @@ where
impl<T, S, B> Service<ServiceRequest> for RateLimitMiddleware<S, T> impl<T, S, B> Service<ServiceRequest> for RateLimitMiddleware<S, T>
where where
T: Handler<ActorMessage> + 'static, T: Handler<ActorMessage> + 'static,
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = AWError> S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = AWError> + 'static,
+ 'static,
S::Future: 'static, S::Future: 'static,
B: 'static, B: 'static,
T::Context: ToEnvelope<T, ActorMessage>, T::Context: ToEnvelope<T, ActorMessage>,
{ {
type Response = ServiceResponse<B>; type Response = ServiceResponse<B>;
type Error = S::Error; type Error = S::Error;
type Future = type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
fn poll_ready( fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
&self,
cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>> {
self.service.borrow_mut().poll_ready(cx) self.service.borrow_mut().poll_ready(cx)
} }
@@ -178,15 +168,9 @@ where
if let Some(c) = opt { if let Some(c) = opt {
// Existing entry in store // Existing entry in store
let expiry = store let expiry = store
.send(ActorMessage::Expire(String::from( .send(ActorMessage::Expire(String::from(&identifier)))
&identifier,
)))
.await .await
.map_err(|_| { .map_err(|_| ARError::ReadWrite("Setting timeout".to_string()))?;
ARError::ReadWrite(
"Setting timeout".to_string(),
)
})?;
let reset: Duration = match expiry { let reset: Duration = match expiry {
ActorResponse::Expire(dur) => dur.await?, ActorResponse::Expire(dur) => dur.await?,
_ => unreachable!(), _ => unreachable!(),
@@ -208,9 +192,7 @@ where
}) })
.await .await
.map_err(|_| { .map_err(|_| {
ARError::ReadWrite( ARError::ReadWrite("Decrementing ratelimit".to_string())
"Decrementing ratelimit".to_string(),
)
})?; })?;
let updated_value: usize = match res { let updated_value: usize = match res {
ActorResponse::Update(c) => c.await?, ActorResponse::Update(c) => c.await?,
@@ -223,23 +205,15 @@ where
// Safe unwraps, since usize is always convertible to string // Safe unwraps, since usize is always convertible to string
headers.insert( headers.insert(
HeaderName::from_static("x-ratelimit-limit"), HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_str( HeaderValue::from_str(max_requests.to_string().as_str())?,
max_requests.to_string().as_str(),
)?,
); );
headers.insert( headers.insert(
HeaderName::from_static( HeaderName::from_static("x-ratelimit-remaining"),
"x-ratelimit-remaining", HeaderValue::from_str(updated_value.to_string().as_str())?,
),
HeaderValue::from_str(
updated_value.to_string().as_str(),
)?,
); );
headers.insert( headers.insert(
HeaderName::from_static("x-ratelimit-reset"), HeaderName::from_static("x-ratelimit-reset"),
HeaderValue::from_str( HeaderValue::from_str(reset.as_secs().to_string().as_str())?,
reset.as_secs().to_string().as_str(),
)?,
); );
Ok(res) Ok(res)
} }
@@ -253,11 +227,7 @@ where
expiry: interval, expiry: interval,
}) })
.await .await
.map_err(|_| { .map_err(|_| ARError::ReadWrite("Creating store entry".to_string()))?;
ARError::ReadWrite(
"Creating store entry".to_string(),
)
})?;
match res { match res {
ActorResponse::Set(c) => c.await?, ActorResponse::Set(c) => c.await?,
_ => unreachable!(), _ => unreachable!(),
@@ -268,24 +238,15 @@ where
// Safe unwraps, since usize is always convertible to string // Safe unwraps, since usize is always convertible to string
headers.insert( headers.insert(
HeaderName::from_static("x-ratelimit-limit"), HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_str( HeaderValue::from_str(max_requests.to_string().as_str()).unwrap(),
max_requests.to_string().as_str(),
)
.unwrap(),
); );
headers.insert( headers.insert(
HeaderName::from_static("x-ratelimit-remaining"), HeaderName::from_static("x-ratelimit-remaining"),
HeaderValue::from_str( HeaderValue::from_str(current_value.to_string().as_str()).unwrap(),
current_value.to_string().as_str(),
)
.unwrap(),
); );
headers.insert( headers.insert(
HeaderName::from_static("x-ratelimit-reset"), HeaderName::from_static("x-ratelimit-reset"),
HeaderValue::from_str( HeaderValue::from_str(interval.as_secs().to_string().as_str()).unwrap(),
interval.as_secs().to_string().as_str(),
)
.unwrap(),
); );
Ok(res) Ok(res)
} }

View File

@@ -68,11 +68,8 @@ pub async fn maven_metadata(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let project_id = params.into_inner().0; let project_id = params.into_inner().0;
let project_data = database::models::Project::get_from_slug_or_project_id( let project_data =
&project_id, database::models::Project::get_from_slug_or_project_id(&project_id, &**pool).await?;
&**pool,
)
.await?;
let data = if let Some(data) = project_data { let data = if let Some(data) = project_data {
data data
@@ -150,9 +147,7 @@ fn find_file<'a>(
version: &'a QueryVersion, version: &'a QueryVersion,
file: &str, file: &str,
) -> Option<&'a QueryFile> { ) -> Option<&'a QueryFile> {
if let Some(selected_file) = if let Some(selected_file) = version.files.iter().find(|x| x.filename == file) {
version.files.iter().find(|x| x.filename == file)
{
return Some(selected_file); return Some(selected_file);
} }
@@ -193,11 +188,7 @@ pub async fn version_file(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let (project_id, vnum, file) = params.into_inner(); let (project_id, vnum, file) = params.into_inner();
let project_data = let project_data =
database::models::Project::get_full_from_slug_or_project_id( database::models::Project::get_full_from_slug_or_project_id(&project_id, &**pool).await?;
&project_id,
&**pool,
)
.await?;
let project = if let Some(data) = project_data { let project = if let Some(data) = project_data {
data data
@@ -229,11 +220,9 @@ pub async fn version_file(
return Ok(HttpResponse::NotFound().body("")); return Ok(HttpResponse::NotFound().body(""));
}; };
let version = if let Some(version) = database::models::Version::get_full( let version = if let Some(version) =
database::models::ids::VersionId(vid.id), database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool)
&**pool, .await?
)
.await?
{ {
version version
} else { } else {
@@ -263,9 +252,7 @@ pub async fn version_file(
return Ok(HttpResponse::Ok() return Ok(HttpResponse::Ok()
.content_type("text/xml") .content_type("text/xml")
.body(yaserde::ser::to_string(&respdata).map_err(ApiError::Xml)?)); .body(yaserde::ser::to_string(&respdata).map_err(ApiError::Xml)?));
} else if let Some(selected_file) = } else if let Some(selected_file) = find_file(&project_id, &project, &version, &file) {
find_file(&project_id, &project, &version, &file)
{
return Ok(HttpResponse::TemporaryRedirect() return Ok(HttpResponse::TemporaryRedirect()
.append_header(("location", &*selected_file.url)) .append_header(("location", &*selected_file.url))
.body("")); .body(""));
@@ -282,11 +269,7 @@ pub async fn version_file_sha1(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let (project_id, vnum, file) = params.into_inner(); let (project_id, vnum, file) = params.into_inner();
let project_data = let project_data =
database::models::Project::get_full_from_slug_or_project_id( database::models::Project::get_full_from_slug_or_project_id(&project_id, &**pool).await?;
&project_id,
&**pool,
)
.await?;
let project = if let Some(data) = project_data { let project = if let Some(data) = project_data {
data data
@@ -318,11 +301,9 @@ pub async fn version_file_sha1(
return Ok(HttpResponse::NotFound().body("")); return Ok(HttpResponse::NotFound().body(""));
}; };
let version = if let Some(version) = database::models::Version::get_full( let version = if let Some(version) =
database::models::ids::VersionId(vid.id), database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool)
&**pool, .await?
)
.await?
{ {
version version
} else { } else {
@@ -343,11 +324,7 @@ pub async fn version_file_sha512(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let (project_id, vnum, file) = params.into_inner(); let (project_id, vnum, file) = params.into_inner();
let project_data = let project_data =
database::models::Project::get_full_from_slug_or_project_id( database::models::Project::get_full_from_slug_or_project_id(&project_id, &**pool).await?;
&project_id,
&**pool,
)
.await?;
let project = if let Some(data) = project_data { let project = if let Some(data) = project_data {
data data
@@ -379,11 +356,9 @@ pub async fn version_file_sha512(
return Ok(HttpResponse::NotFound().body("")); return Ok(HttpResponse::NotFound().body(""));
}; };
let version = if let Some(version) = database::models::Version::get_full( let version = if let Some(version) =
database::models::ids::VersionId(vid.id), database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool)
&**pool, .await?
)
.await?
{ {
version version
} else { } else {

View File

@@ -97,30 +97,28 @@ impl actix_web::ResponseError for ApiError {
} }
fn error_response(&self) -> HttpResponse { fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code()).json( HttpResponse::build(self.status_code()).json(crate::models::error::ApiError {
crate::models::error::ApiError { error: match self {
error: match self { ApiError::Env(..) => "environment_error",
ApiError::Env(..) => "environment_error", ApiError::SqlxDatabase(..) => "database_error",
ApiError::SqlxDatabase(..) => "database_error", ApiError::Database(..) => "database_error",
ApiError::Database(..) => "database_error", ApiError::Authentication(..) => "unauthorized",
ApiError::Authentication(..) => "unauthorized", ApiError::CustomAuthentication(..) => "unauthorized",
ApiError::CustomAuthentication(..) => "unauthorized", ApiError::Xml(..) => "xml_error",
ApiError::Xml(..) => "xml_error", ApiError::Json(..) => "json_error",
ApiError::Json(..) => "json_error", ApiError::Search(..) => "search_error",
ApiError::Search(..) => "search_error", ApiError::Indexing(..) => "indexing_error",
ApiError::Indexing(..) => "indexing_error", ApiError::FileHosting(..) => "file_hosting_error",
ApiError::FileHosting(..) => "file_hosting_error", ApiError::InvalidInput(..) => "invalid_input",
ApiError::InvalidInput(..) => "invalid_input", ApiError::Validation(..) => "invalid_input",
ApiError::Validation(..) => "invalid_input", ApiError::Analytics(..) => "analytics_error",
ApiError::Analytics(..) => "analytics_error", ApiError::Crypto(..) => "crypto_error",
ApiError::Crypto(..) => "crypto_error", ApiError::Payments(..) => "payments_error",
ApiError::Payments(..) => "payments_error", ApiError::DiscordError(..) => "discord_error",
ApiError::DiscordError(..) => "discord_error", ApiError::Decoding(..) => "decoding_error",
ApiError::Decoding(..) => "decoding_error", ApiError::ImageError(..) => "invalid_image",
ApiError::ImageError(..) => "invalid_image",
},
description: &self.to_string(),
}, },
) description: &self.to_string(),
})
} }
} }

View File

@@ -6,9 +6,7 @@ use sqlx::PgPool;
use crate::database; use crate::database;
use crate::models::projects::VersionType; use crate::models::projects::VersionType;
use crate::util::auth::{ use crate::util::auth::{filter_authorized_versions, get_user_from_headers, is_authorized};
filter_authorized_versions, get_user_from_headers, is_authorized,
};
use super::ApiError; use super::ApiError;
@@ -26,10 +24,9 @@ pub async fn forge_updates(
let (id,) = info.into_inner(); let (id,) = info.into_inner();
let project = let project = database::models::Project::get_from_slug_or_project_id(&id, &**pool)
database::models::Project::get_from_slug_or_project_id(&id, &**pool) .await?
.await? .ok_or_else(|| ApiError::InvalidInput(ERROR.to_string()))?;
.ok_or_else(|| ApiError::InvalidInput(ERROR.to_string()))?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
@@ -48,11 +45,9 @@ pub async fn forge_updates(
) )
.await?; .await?;
let versions = let versions = database::models::Version::get_many_full(&version_ids, &**pool).await?;
database::models::Version::get_many_full(&version_ids, &**pool).await?;
let mut versions = let mut versions = filter_authorized_versions(versions, &user_option, &pool).await?;
filter_authorized_versions(versions, &user_option, &pool).await?;
versions.sort_by(|a, b| b.date_published.cmp(&a.date_published)); versions.sort_by(|a, b| b.date_published.cmp(&a.date_published));

View File

@@ -37,26 +37,22 @@ pub async fn count_download(
download_body: web::Json<DownloadBody>, download_body: web::Json<DownloadBody>,
download_queue: web::Data<Arc<DownloadQueue>>, download_queue: web::Data<Arc<DownloadQueue>>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let project_id: crate::database::models::ids::ProjectId = let project_id: crate::database::models::ids::ProjectId = download_body.project_id.into();
download_body.project_id.into();
let id_option = crate::models::ids::base62_impl::parse_base62( let id_option = crate::models::ids::base62_impl::parse_base62(&download_body.version_name)
&download_body.version_name, .ok()
) .map(|x| x as i64);
.ok()
.map(|x| x as i64);
let (version_id, project_id, file_type) = if let Some(version) = let (version_id, project_id, file_type) = if let Some(version) = sqlx::query!(
sqlx::query!( "
"
SELECT v.id id, v.mod_id mod_id, file_type FROM files f SELECT v.id id, v.mod_id mod_id, file_type FROM files f
INNER JOIN versions v ON v.id = f.version_id INNER JOIN versions v ON v.id = f.version_id
WHERE f.url = $1 WHERE f.url = $1
", ",
download_body.url, download_body.url,
) )
.fetch_optional(pool.as_ref()) .fetch_optional(pool.as_ref())
.await? .await?
{ {
(version.id, version.mod_id, version.file_type) (version.id, version.mod_id, version.file_type)
} else if let Some(version) = sqlx::query!( } else if let Some(version) = sqlx::query!(
@@ -143,17 +139,11 @@ pub async fn process_payout(
)]) )])
.send() .send()
.await .await
.map_err(|_| { .map_err(|_| ApiError::Analytics("Error while fetching payout multipliers!".to_string()))?
ApiError::Analytics(
"Error while fetching payout multipliers!".to_string(),
)
})?
.json() .json()
.await .await
.map_err(|_| { .map_err(|_| {
ApiError::Analytics( ApiError::Analytics("Error while deserializing payout multipliers!".to_string())
"Error while deserializing payout multipliers!".to_string(),
)
})?; })?;
struct Project { struct Project {
@@ -176,26 +166,33 @@ pub async fn process_payout(
INNER JOIN project_types pt ON pt.id = m.project_type INNER JOIN project_types pt ON pt.id = m.project_type
WHERE m.id = ANY($1) AND m.monetization_status = $2 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(), MonetizationStatus::Monetized.as_str(),
) )
.fetch_many(&mut *transaction) .fetch_many(&mut *transaction)
.try_for_each(|e| { .try_for_each(|e| {
if let Some(row) = e.right() { if let Some(row) = e.right() {
if let Some(project) = projects_map.get_mut(&row.id) { if let Some(project) = projects_map.get_mut(&row.id) {
project.team_members.push((row.user_id, row.payouts_split)); project.team_members.push((row.user_id, row.payouts_split));
} else { } else {
projects_map.insert(row.id, Project { projects_map.insert(
row.id,
Project {
project_type: row.project_type, project_type: row.project_type,
team_members: vec![(row.user_id, row.payouts_split)], team_members: vec![(row.user_id, row.payouts_split)],
split_team_members: Default::default() split_team_members: Default::default(),
}); },
} );
} }
}
futures::future::ready(Ok(())) futures::future::ready(Ok(()))
}) })
.await?; .await?;
// Specific Payout Conditions (ex: modpack payout split) // Specific Payout Conditions (ex: modpack payout split)
let mut projects_split_dependencies = Vec::new(); let mut projects_split_dependencies = Vec::new();
@@ -208,8 +205,7 @@ pub async fn process_payout(
if !projects_split_dependencies.is_empty() { if !projects_split_dependencies.is_empty() {
// (dependent_id, (dependency_id, times_depended)) // (dependent_id, (dependency_id, times_depended))
let mut project_dependencies: HashMap<i64, Vec<(i64, i64)>> = let mut project_dependencies: HashMap<i64, Vec<(i64, i64)>> = HashMap::new();
HashMap::new();
// dependency_ids to fetch team members from // dependency_ids to fetch team members from
let mut fetch_team_members: Vec<i64> = Vec::new(); let mut fetch_team_members: Vec<i64> = Vec::new();
@@ -229,14 +225,11 @@ pub async fn process_payout(
if let Some(row) = e.right() { if let Some(row) = e.right() {
fetch_team_members.push(row.id); 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))); project.push((row.id, row.times_depended.unwrap_or(0)));
} else { } else {
project_dependencies.insert( project_dependencies
row.mod_id, .insert(row.mod_id, vec![(row.id, row.times_depended.unwrap_or(0))]);
vec![(row.id, row.times_depended.unwrap_or(0))],
);
} }
} }
@@ -245,8 +238,7 @@ pub async fn process_payout(
.await?; .await?;
// (project_id, (user_id, payouts_split)) // (project_id, (user_id, payouts_split))
let mut team_members: HashMap<i64, Vec<(i64, Decimal)>> = let mut team_members: HashMap<i64, Vec<(i64, Decimal)>> = HashMap::new();
HashMap::new();
sqlx::query!( sqlx::query!(
" "
@@ -263,8 +255,7 @@ pub async fn process_payout(
if let Some(project) = team_members.get_mut(&row.id) { if let Some(project) = team_members.get_mut(&row.id) {
project.push((row.user_id, row.payouts_split)); project.push((row.user_id, row.payouts_split));
} else { } else {
team_members team_members.insert(row.id, vec![(row.user_id, row.payouts_split)]);
.insert(row.id, vec![(row.user_id, row.payouts_split)]);
} }
} }
@@ -281,17 +272,14 @@ pub async fn process_payout(
if dep_sum > 0 { if dep_sum > 0 {
for dependency in dependencies { for dependency in dependencies {
let project_multiplier: Decimal = let project_multiplier: Decimal =
Decimal::from(dependency.1) Decimal::from(dependency.1) / Decimal::from(dep_sum);
/ Decimal::from(dep_sum);
if let Some(members) = team_members.get(&dependency.0) { if let Some(members) = team_members.get(&dependency.0) {
let members_sum: Decimal = let members_sum: Decimal = members.iter().map(|x| x.1).sum();
members.iter().map(|x| x.1).sum();
if members_sum > Decimal::ZERO { if members_sum > Decimal::ZERO {
for member in members { for member in members {
let member_multiplier: Decimal = let member_multiplier: Decimal = member.1 / members_sum;
member.1 / members_sum;
project.split_team_members.push(( project.split_team_members.push((
member.0, member.0,
member_multiplier * project_multiplier, member_multiplier * project_multiplier,
@@ -315,10 +303,8 @@ pub async fn process_payout(
let split_given = Decimal::ONE / Decimal::from(5); let split_given = Decimal::ONE / Decimal::from(5);
let split_retention = Decimal::from(4) / Decimal::from(5); let split_retention = Decimal::from(4) / Decimal::from(5);
let sum_splits: Decimal = let sum_splits: Decimal = project.team_members.iter().map(|x| x.1).sum();
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_tm_splits: Decimal =
project.split_team_members.iter().map(|x| x.1).sum();
if sum_splits > Decimal::ZERO { if sum_splits > Decimal::ZERO {
for (user_id, split) in project.team_members { for (user_id, split) in project.team_members {
@@ -342,8 +328,8 @@ pub async fn process_payout(
payout, payout,
start start
) )
.execute(&mut *transaction) .execute(&mut *transaction)
.await?; .await?;
sqlx::query!( sqlx::query!(
" "
@@ -378,8 +364,8 @@ pub async fn process_payout(
payout, payout,
start start
) )
.execute(&mut *transaction) .execute(&mut *transaction)
.await?; .await?;
sqlx::query!( sqlx::query!(
" "

View File

@@ -58,12 +58,8 @@ impl actix_web::ResponseError for AuthorizationError {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
match self { match self {
AuthorizationError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR, AuthorizationError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR,
AuthorizationError::SqlxDatabase(..) => { AuthorizationError::SqlxDatabase(..) => StatusCode::INTERNAL_SERVER_ERROR,
StatusCode::INTERNAL_SERVER_ERROR AuthorizationError::Database(..) => StatusCode::INTERNAL_SERVER_ERROR,
}
AuthorizationError::Database(..) => {
StatusCode::INTERNAL_SERVER_ERROR
}
AuthorizationError::SerDe(..) => StatusCode::BAD_REQUEST, AuthorizationError::SerDe(..) => StatusCode::BAD_REQUEST,
AuthorizationError::Github(..) => StatusCode::FAILED_DEPENDENCY, AuthorizationError::Github(..) => StatusCode::FAILED_DEPENDENCY,
AuthorizationError::InvalidCredentials => StatusCode::UNAUTHORIZED, AuthorizationError::InvalidCredentials => StatusCode::UNAUTHORIZED,
@@ -84,9 +80,7 @@ impl actix_web::ResponseError for AuthorizationError {
AuthorizationError::Github(..) => "github_error", AuthorizationError::Github(..) => "github_error",
AuthorizationError::InvalidCredentials => "invalid_credentials", AuthorizationError::InvalidCredentials => "invalid_credentials",
AuthorizationError::Decoding(..) => "decoding_error", AuthorizationError::Decoding(..) => "decoding_error",
AuthorizationError::Authentication(..) => { AuthorizationError::Authentication(..) => "authentication_error",
"authentication_error"
}
AuthorizationError::Url => "url_error", AuthorizationError::Url => "url_error",
AuthorizationError::Banned => "user_banned", AuthorizationError::Banned => "user_banned",
}, },
@@ -119,16 +113,12 @@ pub async fn init(
Query(info): Query<AuthorizationInit>, Query(info): Query<AuthorizationInit>,
client: Data<PgPool>, client: Data<PgPool>,
) -> Result<HttpResponse, AuthorizationError> { ) -> Result<HttpResponse, AuthorizationError> {
let url = let url = url::Url::parse(&info.url).map_err(|_| AuthorizationError::Url)?;
url::Url::parse(&info.url).map_err(|_| AuthorizationError::Url)?;
let allowed_callback_urls = let allowed_callback_urls = parse_strings_from_var("ALLOWED_CALLBACK_URLS").unwrap_or_default();
parse_strings_from_var("ALLOWED_CALLBACK_URLS").unwrap_or_default();
let domain = url.domain().ok_or(AuthorizationError::Url)?; let domain = url.domain().ok_or(AuthorizationError::Url)?;
if !allowed_callback_urls.iter().any(|x| domain.ends_with(x)) if !allowed_callback_urls.iter().any(|x| domain.ends_with(x)) && domain != "modrinth.com" {
&& domain != "modrinth.com"
{
return Err(AuthorizationError::Url); 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 = get_github_user_from_token(&token.access_token).await?;
let user_result = let user_result = User::get_from_github_id(user.id, &mut *transaction).await?;
User::get_from_github_id(user.id, &mut *transaction).await?;
match user_result { match user_result {
Some(_) => {} Some(_) => {}
None => { None => {
@@ -231,9 +220,7 @@ pub async fn auth_callback(
return Err(AuthorizationError::Banned); return Err(AuthorizationError::Banned);
} }
let user_id = let user_id = crate::database::models::generate_user_id(&mut transaction).await?;
crate::database::models::generate_user_id(&mut transaction)
.await?;
let mut username_increment: i32 = 0; let mut username_increment: i32 = 0;
let mut username = None; let mut username = None;

View File

@@ -54,17 +54,11 @@ pub async fn init_checkout(
]) ])
.send() .send()
.await .await
.map_err(|_| { .map_err(|_| ApiError::Payments("Error while creating checkout session!".to_string()))?
ApiError::Payments(
"Error while creating checkout session!".to_string(),
)
})?
.json::<Session>() .json::<Session>()
.await .await
.map_err(|_| { .map_err(|_| {
ApiError::Payments( ApiError::Payments("Error while deserializing checkout response!".to_string())
"Error while deserializing checkout response!".to_string(),
)
})?; })?;
Ok(HttpResponse::Ok().json(json!( Ok(HttpResponse::Ok().json(json!(
@@ -92,11 +86,7 @@ pub async fn init_customer_portal(
.fetch_optional(&**pool) .fetch_optional(&**pool)
.await? .await?
.and_then(|x| x.stripe_customer_id) .and_then(|x| x.stripe_customer_id)
.ok_or_else(|| { .ok_or_else(|| ApiError::InvalidInput("User is not linked to stripe account!".to_string()))?;
ApiError::InvalidInput(
"User is not linked to stripe account!".to_string(),
)
})?;
let client = reqwest::Client::new(); let client = reqwest::Client::new();
@@ -117,17 +107,11 @@ pub async fn init_customer_portal(
]) ])
.send() .send()
.await .await
.map_err(|_| { .map_err(|_| ApiError::Payments("Error while creating billing session!".to_string()))?
ApiError::Payments(
"Error while creating billing session!".to_string(),
)
})?
.json::<Session>() .json::<Session>()
.await .await
.map_err(|_| { .map_err(|_| {
ApiError::Payments( ApiError::Payments("Error while deserializing billing response!".to_string())
"Error while deserializing billing response!".to_string(),
)
})?; })?;
Ok(HttpResponse::Ok().json(json!( Ok(HttpResponse::Ok().json(json!(
@@ -166,27 +150,25 @@ pub async fn handle_stripe_webhook(
if let Some(signature) = signature { if let Some(signature) = signature {
type HmacSha256 = Hmac<sha2::Sha256>; type HmacSha256 = Hmac<sha2::Sha256>;
let mut key = HmacSha256::new_from_slice(dotenvy::var("STRIPE_WEBHOOK_SECRET")?.as_bytes()).map_err(|_| { let mut key =
ApiError::Crypto( HmacSha256::new_from_slice(dotenvy::var("STRIPE_WEBHOOK_SECRET")?.as_bytes())
"Unable to initialize HMAC instance due to invalid key length!".to_string(), .map_err(|_| {
) ApiError::Crypto(
})?; "Unable to initialize HMAC instance due to invalid key length!"
.to_string(),
)
})?;
key.update(format!("{timestamp}.{body}").as_bytes()); key.update(format!("{timestamp}.{body}").as_bytes());
key.verify(&signature).map_err(|_| { key.verify(&signature).map_err(|_| {
ApiError::Crypto( ApiError::Crypto("Unable to verify webhook signature!".to_string())
"Unable to verify webhook signature!".to_string(),
)
})?; })?;
if timestamp < (Utc::now() - Duration::minutes(5)).timestamp() if timestamp < (Utc::now() - Duration::minutes(5)).timestamp()
|| timestamp || timestamp > (Utc::now() + Duration::minutes(5)).timestamp()
> (Utc::now() + Duration::minutes(5)).timestamp()
{ {
return Err(ApiError::Crypto( return Err(ApiError::Crypto("Webhook signature expired!".to_string()));
"Webhook signature expired!".to_string(),
));
} }
} else { } else {
return Err(ApiError::Crypto("Missing signature!".to_string())); 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 // TODO: Currently hardcoded to midas-only. When we add more stuff should include price IDs
match &*webhook.type_ { match &*webhook.type_ {
"checkout.session.completed" => { "checkout.session.completed" => {
let session: CheckoutSession = let session: CheckoutSession = serde_json::from_value(webhook.data.object)?;
serde_json::from_value(webhook.data.object)?;
sqlx::query!( sqlx::query!(
" "
@@ -276,8 +257,7 @@ pub async fn handle_stripe_webhook(
if let Some(item) = invoice.lines.data.first() { if let Some(item) = invoice.lines.data.first() {
let expires: DateTime<Utc> = DateTime::from_utc( let expires: DateTime<Utc> = DateTime::from_utc(
NaiveDateTime::from_timestamp_opt(item.period.end, 0) NaiveDateTime::from_timestamp_opt(item.period.end, 0).unwrap_or_default(),
.unwrap_or_default(),
Utc, Utc,
) + Duration::days(1); ) + Duration::days(1);
@@ -323,8 +303,7 @@ pub async fn handle_stripe_webhook(
} }
} }
"customer.subscription.deleted" => { "customer.subscription.deleted" => {
let session: Subscription = let session: Subscription = serde_json::from_value(webhook.data.object)?;
serde_json::from_value(webhook.data.object)?;
sqlx::query!( sqlx::query!(
" "
@@ -334,8 +313,8 @@ pub async fn handle_stripe_webhook(
", ",
session.customer, session.customer,
) )
.execute(&mut *transaction) .execute(&mut *transaction)
.await?; .await?;
} }
_ => {} _ => {}
}; };

View File

@@ -46,18 +46,15 @@ pub async fn get_projects(
count.count as i64 count.count as i64
) )
.fetch_many(&**pool) .fetch_many(&**pool)
.try_filter_map(|e| async { .try_filter_map(|e| async { Ok(e.right().map(|m| database::models::ProjectId(m.id))) })
Ok(e.right().map(|m| database::models::ProjectId(m.id)))
})
.try_collect::<Vec<database::models::ProjectId>>() .try_collect::<Vec<database::models::ProjectId>>()
.await?; .await?;
let projects: Vec<_> = let projects: Vec<_> = database::Project::get_many_full(&project_ids, &**pool)
database::Project::get_many_full(&project_ids, &**pool) .await?
.await? .into_iter()
.into_iter() .map(crate::models::projects::Project::from)
.map(crate::models::projects::Project::from) .collect();
.collect();
Ok(HttpResponse::Ok().json(projects)) Ok(HttpResponse::Ok().json(projects))
} }

View File

@@ -3,17 +3,19 @@ use crate::models::ids::NotificationId;
use crate::models::notifications::Notification; use crate::models::notifications::Notification;
use crate::routes::ApiError; use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers; 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 serde::{Deserialize, Serialize};
use sqlx::PgPool; use sqlx::PgPool;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(notifications_get); cfg.service(notifications_get);
cfg.service(notifications_delete); cfg.service(notifications_delete);
cfg.service(notifications_read);
cfg.service( cfg.service(
web::scope("notification") web::scope("notification")
.service(notification_get) .service(notification_get)
.service(notifications_read)
.service(notification_delete), .service(notification_delete),
); );
} }
@@ -31,7 +33,6 @@ pub async fn notifications_get(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?; 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::notification_item::Notification as DBNotification;
use database::models::NotificationId as DBNotificationId; use database::models::NotificationId as DBNotificationId;
@@ -42,11 +43,8 @@ pub async fn notifications_get(
.collect(); .collect();
let notifications_data: Vec<DBNotification> = let notifications_data: Vec<DBNotification> =
database::models::notification_item::Notification::get_many( database::models::notification_item::Notification::get_many(&notification_ids, &**pool)
&notification_ids, .await?;
&**pool,
)
.await?;
let notifications: Vec<Notification> = notifications_data let notifications: Vec<Notification> = notifications_data
.into_iter() .into_iter()
@@ -68,11 +66,7 @@ pub async fn notification_get(
let id = info.into_inner().0; let id = info.into_inner().0;
let notification_data = let notification_data =
database::models::notification_item::Notification::get( database::models::notification_item::Notification::get(id.into(), &**pool).await?;
id.into(),
&**pool,
)
.await?;
if let Some(data) = notification_data { if let Some(data) = notification_data {
if user.id == data.user_id.into() || user.role.is_admin() { 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}")] #[delete("{id}")]
pub async fn notification_delete( pub async fn notification_delete(
req: HttpRequest, req: HttpRequest,
@@ -96,29 +123,21 @@ pub async fn notification_delete(
let id = info.into_inner().0; let id = info.into_inner().0;
let notification_data = let notification_data =
database::models::notification_item::Notification::get( database::models::notification_item::Notification::get(id.into(), &**pool).await?;
id.into(),
&**pool,
)
.await?;
if let Some(data) = notification_data { if let Some(data) = notification_data {
if data.user_id == user.id.into() || user.role.is_admin() { if data.user_id == user.id.into() || user.role.is_admin() {
let mut transaction = pool.begin().await?; let mut transaction = pool.begin().await?;
database::models::notification_item::Notification::remove( database::models::notification_item::Notification::remove(id.into(), &mut transaction)
id.into(), .await?;
&mut transaction,
)
.await?;
transaction.commit().await?; transaction.commit().await?;
Ok(HttpResponse::NoContent().body("")) Ok(HttpResponse::NoContent().body(""))
} else { } else {
Err(ApiError::CustomAuthentication( Err(ApiError::CustomAuthentication(
"You are not authorized to delete this notification!" "You are not authorized to delete this notification!".to_string(),
.to_string(),
)) ))
} }
} else { } 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(&notification_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(&notifications, &mut transaction)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
}
#[delete("notifications")] #[delete("notifications")]
pub async fn notifications_delete( pub async fn notifications_delete(
req: HttpRequest, req: HttpRequest,
@@ -134,23 +188,18 @@ pub async fn notifications_delete(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let notification_ids = let notification_ids = serde_json::from_str::<Vec<NotificationId>>(&ids.ids)?
serde_json::from_str::<Vec<NotificationId>>(&ids.ids)? .into_iter()
.into_iter() .map(|x| x.into())
.map(|x| x.into()) .collect::<Vec<_>>();
.collect::<Vec<_>>();
let mut transaction = pool.begin().await?; let mut transaction = pool.begin().await?;
let notifications_data = let notifications_data =
database::models::notification_item::Notification::get_many( database::models::notification_item::Notification::get_many(&notification_ids, &**pool)
&notification_ids, .await?;
&**pool,
)
.await?;
let mut notifications: Vec<database::models::ids::NotificationId> = let mut notifications: Vec<database::models::ids::NotificationId> = Vec::new();
Vec::new();
for notification in notifications_data { for notification in notifications_data {
if notification.user_id == user.id.into() || user.role.is_admin() { if notification.user_id == user.id.into() || user.role.is_admin() {

View File

@@ -4,8 +4,8 @@ use crate::database::models::thread_item::ThreadBuilder;
use crate::file_hosting::{FileHost, FileHostingError}; use crate::file_hosting::{FileHost, FileHostingError};
use crate::models::error::ApiError; use crate::models::error::ApiError;
use crate::models::projects::{ use crate::models::projects::{
DonationLink, License, MonetizationStatus, ProjectId, ProjectStatus, DonationLink, License, MonetizationStatus, ProjectId, ProjectStatus, SideType, VersionId,
SideType, VersionId, VersionStatus, VersionStatus,
}; };
use crate::models::threads::ThreadType; use crate::models::threads::ThreadType;
use crate::models::users::UserId; use crate::models::users::UserId;
@@ -79,14 +79,10 @@ impl actix_web::ResponseError for CreateError {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
match self { match self {
CreateError::EnvError(..) => StatusCode::INTERNAL_SERVER_ERROR, CreateError::EnvError(..) => StatusCode::INTERNAL_SERVER_ERROR,
CreateError::SqlxDatabaseError(..) => { CreateError::SqlxDatabaseError(..) => StatusCode::INTERNAL_SERVER_ERROR,
StatusCode::INTERNAL_SERVER_ERROR
}
CreateError::DatabaseError(..) => StatusCode::INTERNAL_SERVER_ERROR, CreateError::DatabaseError(..) => StatusCode::INTERNAL_SERVER_ERROR,
CreateError::IndexingError(..) => StatusCode::INTERNAL_SERVER_ERROR, CreateError::IndexingError(..) => StatusCode::INTERNAL_SERVER_ERROR,
CreateError::FileHostingError(..) => { CreateError::FileHostingError(..) => StatusCode::INTERNAL_SERVER_ERROR,
StatusCode::INTERNAL_SERVER_ERROR
}
CreateError::SerDeError(..) => StatusCode::BAD_REQUEST, CreateError::SerDeError(..) => StatusCode::BAD_REQUEST,
CreateError::MultipartError(..) => StatusCode::BAD_REQUEST, CreateError::MultipartError(..) => StatusCode::BAD_REQUEST,
CreateError::MissingValueError(..) => StatusCode::BAD_REQUEST, CreateError::MissingValueError(..) => StatusCode::BAD_REQUEST,
@@ -97,9 +93,7 @@ impl actix_web::ResponseError for CreateError {
CreateError::InvalidCategory(..) => StatusCode::BAD_REQUEST, CreateError::InvalidCategory(..) => StatusCode::BAD_REQUEST,
CreateError::InvalidFileType(..) => StatusCode::BAD_REQUEST, CreateError::InvalidFileType(..) => StatusCode::BAD_REQUEST,
CreateError::Unauthorized(..) => StatusCode::UNAUTHORIZED, CreateError::Unauthorized(..) => StatusCode::UNAUTHORIZED,
CreateError::CustomAuthenticationError(..) => { CreateError::CustomAuthenticationError(..) => StatusCode::UNAUTHORIZED,
StatusCode::UNAUTHORIZED
}
CreateError::SlugCollision => StatusCode::BAD_REQUEST, CreateError::SlugCollision => StatusCode::BAD_REQUEST,
CreateError::ValidationError(..) => StatusCode::BAD_REQUEST, CreateError::ValidationError(..) => StatusCode::BAD_REQUEST,
CreateError::FileValidationError(..) => StatusCode::BAD_REQUEST, CreateError::FileValidationError(..) => StatusCode::BAD_REQUEST,
@@ -347,21 +341,17 @@ async fn project_create_inner(
let cdn_url = dotenvy::var("CDN_URL")?; let cdn_url = dotenvy::var("CDN_URL")?;
// The currently logged in user // The currently logged in user
let current_user = let current_user = get_user_from_headers(req.headers(), &mut *transaction).await?;
get_user_from_headers(req.headers(), &mut *transaction).await?;
let project_id: ProjectId = let project_id: ProjectId = models::generate_project_id(transaction).await?.into();
models::generate_project_id(transaction).await?.into();
let project_create_data; let project_create_data;
let mut versions; let mut versions;
let mut versions_map = std::collections::HashMap::new(); let mut versions_map = std::collections::HashMap::new();
let mut gallery_urls = Vec::new(); let mut gallery_urls = Vec::new();
let all_game_versions = let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?;
models::categories::GameVersion::list(&mut *transaction).await?; let all_loaders = models::categories::Loader::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 // 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 content_disposition = field.content_disposition();
let name = content_disposition.get_name().ok_or_else(|| { let name = content_disposition
CreateError::MissingValueError(String::from("Missing content name")) .get_name()
})?; .ok_or_else(|| CreateError::MissingValueError(String::from("Missing content name")))?;
if name != "data" { if name != "data" {
return Err(CreateError::InvalidInput(String::from( return Err(CreateError::InvalidInput(String::from(
@@ -390,22 +380,19 @@ async fn project_create_inner(
let mut data = Vec::new(); let mut data = Vec::new();
while let Some(chunk) = field.next().await { while let Some(chunk) = field.next().await {
data.extend_from_slice( data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?);
&chunk.map_err(CreateError::MultipartError)?,
);
} }
let create_data: ProjectCreateData = serde_json::from_slice(&data)?; let create_data: ProjectCreateData = serde_json::from_slice(&data)?;
create_data.validate().map_err(|err| { create_data
CreateError::InvalidInput(validation_errors_to_string(err, None)) .validate()
})?; .map_err(|err| CreateError::InvalidInput(validation_errors_to_string(err, None)))?;
let slug_project_id_option: Option<ProjectId> = let slug_project_id_option: Option<ProjectId> =
serde_json::from_str(&format!("\"{}\"", create_data.slug)).ok(); serde_json::from_str(&format!("\"{}\"", create_data.slug)).ok();
if let Some(slug_project_id) = slug_project_id_option { if let Some(slug_project_id) = slug_project_id_option {
let slug_project_id: models::ids::ProjectId = let slug_project_id: models::ids::ProjectId = slug_project_id.into();
slug_project_id.into();
let results = sqlx::query!( let results = sqlx::query!(
" "
SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1) 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 content_disposition = field.content_disposition().clone();
let name = content_disposition.get_name().ok_or_else(|| { let name = content_disposition.get_name().ok_or_else(|| {
CreateError::MissingValueError( CreateError::MissingValueError("Missing content name".to_string())
"Missing content name".to_string(),
)
})?; })?;
let (file_name, file_extension) = let (file_name, file_extension) =
@@ -528,9 +513,7 @@ async fn project_create_inner(
))); )));
} }
if let Some(item) = if let Some(item) = gallery_items.iter().find(|x| x.item == name) {
gallery_items.iter().find(|x| x.item == name)
{
let data = read_from_field( let data = read_from_field(
&mut field, &mut field,
5 * (1 << 20), 5 * (1 << 20),
@@ -540,22 +523,13 @@ async fn project_create_inner(
let hash = sha1::Sha1::from(&data).hexdigest(); let hash = sha1::Sha1::from(&data).hexdigest();
let (_, file_extension) = let (_, file_extension) =
super::version_creation::get_name_ext( super::version_creation::get_name_ext(&content_disposition)?;
&content_disposition, let content_type = crate::util::ext::get_image_content_type(file_extension)
)?;
let content_type =
crate::util::ext::get_image_content_type(
file_extension,
)
.ok_or_else(|| { .ok_or_else(|| {
CreateError::InvalidIconFormat( CreateError::InvalidIconFormat(file_extension.to_string())
file_extension.to_string(),
)
})?; })?;
let url = format!( let url = format!("data/{project_id}/images/{hash}.{file_extension}");
"data/{project_id}/images/{hash}.{file_extension}"
);
let upload_data = file_host let upload_data = file_host
.upload_file(content_type, &url, data.freeze()) .upload_file(content_type, &url, data.freeze())
.await?; .await?;
@@ -588,8 +562,7 @@ async fn project_create_inner(
// `index` is always valid for these lists // `index` is always valid for these lists
let created_version = versions.get_mut(index).unwrap(); let created_version = versions.get_mut(index).unwrap();
let version_data = let version_data = project_create_data.initial_versions.get(index).unwrap();
project_create_data.initial_versions.get(index).unwrap();
// Upload the new jar file // Upload the new jar file
super::version_creation::upload_file( super::version_creation::upload_file(
@@ -642,8 +615,7 @@ async fn project_create_inner(
} }
// Convert the list of category names to actual categories // Convert the list of category names to actual categories
let mut categories = let mut categories = Vec::with_capacity(project_create_data.categories.len());
Vec::with_capacity(project_create_data.categories.len());
for category in &project_create_data.categories { for category in &project_create_data.categories {
let id = models::categories::Category::get_id_project( let id = models::categories::Category::get_id_project(
category, category,
@@ -706,9 +678,7 @@ async fn project_create_inner(
) )
.await? .await?
.ok_or_else(|| { .ok_or_else(|| {
CreateError::InvalidInput( CreateError::InvalidInput("Client side type specified does not exist.".to_string())
"Client side type specified does not exist.".to_string(),
)
})?; })?;
let server_side_id = models::categories::SideType::get_id( let server_side_id = models::categories::SideType::get_id(
@@ -717,35 +687,27 @@ async fn project_create_inner(
) )
.await? .await?
.ok_or_else(|| { .ok_or_else(|| {
CreateError::InvalidInput( CreateError::InvalidInput("Server side type specified does not exist.".to_string())
"Server side type specified does not exist.".to_string(),
)
})?; })?;
let license_id = spdx::Expression::parse( let license_id =
&project_create_data.license_id, spdx::Expression::parse(&project_create_data.license_id).map_err(|err| {
) CreateError::InvalidInput(format!("Invalid SPDX license identifier: {err}"))
.map_err(|err| { })?;
CreateError::InvalidInput(format!(
"Invalid SPDX license identifier: {err}"
))
})?;
let mut donation_urls = vec![]; let mut donation_urls = vec![];
if let Some(urls) = &project_create_data.donation_urls { if let Some(urls) = &project_create_data.donation_urls {
for url in urls { for url in urls {
let platform_id = models::categories::DonationPlatform::get_id( let platform_id =
&url.id, models::categories::DonationPlatform::get_id(&url.id, &mut *transaction)
&mut *transaction, .await?
) .ok_or_else(|| {
.await? CreateError::InvalidInput(format!(
.ok_or_else(|| { "Donation platform {} does not exist.",
CreateError::InvalidInput(format!( url.id.clone()
"Donation platform {} does not exist.", ))
url.id.clone() })?;
))
})?;
donation_urls.push(models::project_item::DonationUrl { donation_urls.push(models::project_item::DonationUrl {
platform_id, platform_id,
@@ -856,16 +818,10 @@ async fn project_create_inner(
let _project_id = project_builder.insert(&mut *transaction).await?; let _project_id = project_builder.insert(&mut *transaction).await?;
if status == ProjectStatus::Processing { if status == ProjectStatus::Processing {
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(response.id, pool, webhook_url, None)
crate::util::webhook::send_discord_webhook( .await
response.id, .ok();
pool,
webhook_url,
None,
)
.await
.ok();
} }
} }
@@ -888,13 +844,12 @@ async fn create_initial_version(
))); )));
} }
version_data.validate().map_err(|err| { version_data
CreateError::ValidationError(validation_errors_to_string(err, None)) .validate()
})?; .map_err(|err| CreateError::ValidationError(validation_errors_to_string(err, None)))?;
// Randomly generate a new id to be used for the version // Randomly generate a new id to be used for the version
let version_id: VersionId = let version_id: VersionId = models::generate_version_id(transaction).await?.into();
models::generate_version_id(transaction).await?.into();
let game_versions = version_data let game_versions = version_data
.game_versions .game_versions
@@ -963,15 +918,8 @@ async fn process_icon_upload(
mut field: Field, mut field: Field,
cdn_url: &str, cdn_url: &str,
) -> Result<(String, Option<u32>), CreateError> { ) -> Result<(String, Option<u32>), CreateError> {
if let Some(content_type) = if let Some(content_type) = crate::util::ext::get_image_content_type(file_extension) {
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 data = read_from_field(
&mut field,
262144,
"Icons must be smaller than 256KiB",
)
.await?;
let color = crate::util::img::get_color_from_img(&data)?; let color = crate::util::img::get_color_from_img(&data)?;

View File

@@ -6,16 +6,13 @@ use crate::models;
use crate::models::ids::base62_impl::parse_base62; use crate::models::ids::base62_impl::parse_base62;
use crate::models::notifications::NotificationBody; use crate::models::notifications::NotificationBody;
use crate::models::projects::{ use crate::models::projects::{
DonationLink, MonetizationStatus, Project, ProjectId, ProjectStatus, DonationLink, MonetizationStatus, Project, ProjectId, ProjectStatus, SearchRequest, SideType,
SearchRequest, SideType,
}; };
use crate::models::teams::Permissions; use crate::models::teams::Permissions;
use crate::models::threads::MessageBody; use crate::models::threads::MessageBody;
use crate::routes::ApiError; use crate::routes::ApiError;
use crate::search::{search_for_project, SearchConfig, SearchError}; use crate::search::{search_for_project, SearchConfig, SearchError};
use crate::util::auth::{ use crate::util::auth::{filter_authorized_projects, get_user_from_headers, is_authorized};
filter_authorized_projects, get_user_from_headers, is_authorized,
};
use crate::util::routes::read_from_payload; use crate::util::routes::read_from_payload;
use crate::util::validate::validation_errors_to_string; use crate::util::validate::validation_errors_to_string;
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; 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>, web::Query(count): web::Query<RandomProjects>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
count.validate().map_err(|err| { count
ApiError::Validation(validation_errors_to_string(err, None)) .validate()
})?; .map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
let project_ids = sqlx::query!( let project_ids = sqlx::query!(
" "
SELECT id FROM mods TABLESAMPLE SYSTEM_ROWS($1) WHERE status = ANY($2) SELECT id FROM mods TABLESAMPLE SYSTEM_ROWS($1) WHERE status = ANY($2)
", ",
count.count as i32, count.count as i32,
&*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_searchable()).map(|x| x.to_string()).collect::<Vec<String>>(), &*crate::models::projects::ProjectStatus::iterator()
) .filter(|x| x.is_searchable())
.fetch_many(&**pool) .map(|x| x.to_string())
.try_filter_map(|e| async { .collect::<Vec<String>>(),
Ok(e.right().map(|m| database::models::ids::ProjectId(m.id))) )
}) .fetch_many(&**pool)
.try_collect::<Vec<_>>() .try_filter_map(|e| async { Ok(e.right().map(|m| database::models::ids::ProjectId(m.id))) })
.await?; .try_collect::<Vec<_>>()
.await?;
let projects_data = let projects_data = database::models::Project::get_many_full(&project_ids, &**pool)
database::models::Project::get_many_full(&project_ids, &**pool) .await?
.await? .into_iter()
.into_iter() .map(Project::from)
.map(Project::from) .collect::<Vec<_>>();
.collect::<Vec<_>>();
Ok(HttpResponse::Ok().json(projects_data)) Ok(HttpResponse::Ok().json(projects_data))
} }
@@ -123,13 +120,11 @@ pub async fn projects_get(
.map(|x| x.into()) .map(|x| x.into())
.collect(); .collect();
let projects_data = let projects_data = database::models::Project::get_many_full(&project_ids, &**pool).await?;
database::models::Project::get_many_full(&project_ids, &**pool).await?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
let projects = let projects = filter_authorized_projects(projects_data, &user_option, &pool).await?;
filter_authorized_projects(projects_data, &user_option, &pool).await?;
Ok(HttpResponse::Ok().json(projects)) Ok(HttpResponse::Ok().json(projects))
} }
@@ -143,10 +138,7 @@ pub async fn project_get(
let string = info.into_inner().0; let string = info.into_inner().0;
let project_data = let project_data =
database::models::Project::get_full_from_slug_or_project_id( database::models::Project::get_full_from_slug_or_project_id(&string, &**pool).await?;
&string, &**pool,
)
.await?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
@@ -229,10 +221,7 @@ pub async fn dependency_list(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0; let string = info.into_inner().0;
let result = database::models::Project::get_from_slug_or_project_id( let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool).await?;
&string, &**pool,
)
.await?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); 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 { .try_filter_map(|e| async {
Ok(e.right().map(|x| { Ok(e.right().map(|x| {
( (
x.dependency_id x.dependency_id.map(database::models::VersionId),
.map(database::models::VersionId), if x.mod_id == Some(0) {
if x.mod_id == Some(0) { None } else { x.mod_id None
.map(database::models::ProjectId) }, } else {
x.mod_dependency_id x.mod_id.map(database::models::ProjectId)
.map(database::models::ProjectId), },
x.mod_dependency_id.map(database::models::ProjectId),
) )
})) }))
}) })
@@ -430,15 +420,13 @@ pub async fn project_edit(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
new_project.validate().map_err(|err| { new_project
ApiError::Validation(validation_errors_to_string(err, None)) .validate()
})?; .map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
let string = info.into_inner().0; let string = info.into_inner().0;
let result = database::models::Project::get_full_from_slug_or_project_id( let result =
&string, &**pool, database::models::Project::get_full_from_slug_or_project_id(&string, &**pool).await?;
)
.await?;
if let Some(project_item) = result { if let Some(project_item) = result {
let id = project_item.inner.id; let id = project_item.inner.id;
@@ -456,8 +444,7 @@ pub async fn project_edit(
} else if let Some(ref member) = team_member { } else if let Some(ref member) = team_member {
permissions = Some(member.permissions) permissions = Some(member.permissions)
} else if user.role.is_mod() { } else if user.role.is_mod() {
permissions = permissions = Some(Permissions::EDIT_DETAILS | Permissions::EDIT_BODY)
Some(Permissions::EDIT_DETAILS | Permissions::EDIT_BODY)
} else { } else {
permissions = None permissions = None
} }
@@ -518,12 +505,10 @@ pub async fn project_edit(
if !(user.role.is_mod() if !(user.role.is_mod()
|| !project_item.inner.status.is_approved() || !project_item.inner.status.is_approved()
&& status == &ProjectStatus::Processing && status == &ProjectStatus::Processing
|| project_item.inner.status.is_approved() || project_item.inner.status.is_approved() && status.can_be_requested())
&& status.can_be_requested())
{ {
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(
"You don't have permission to set this status!" "You don't have permission to set this status!".to_string(),
.to_string(),
)); ));
} }
@@ -545,9 +530,7 @@ pub async fn project_edit(
.execute(&mut *transaction) .execute(&mut *transaction)
.await?; .await?;
if let Ok(webhook_url) = if let Ok(webhook_url) = dotenvy::var("MODERATION_DISCORD_WEBHOOK") {
dotenvy::var("MODERATION_DISCORD_WEBHOOK")
{
crate::util::webhook::send_discord_webhook( crate::util::webhook::send_discord_webhook(
project_item.inner.id.into(), project_item.inner.id.into(),
&pool, &pool,
@@ -559,9 +542,7 @@ pub async fn project_edit(
} }
} }
if status.is_approved() if status.is_approved() && !project_item.inner.status.is_approved() {
&& !project_item.inner.status.is_approved()
{
sqlx::query!( sqlx::query!(
" "
UPDATE mods UPDATE mods
@@ -575,9 +556,7 @@ pub async fn project_edit(
} }
if status.is_searchable() && !project_item.inner.webhook_sent { if status.is_searchable() && !project_item.inner.webhook_sent {
if let Ok(webhook_url) = if let Ok(webhook_url) = dotenvy::var("PUBLIC_DISCORD_WEBHOOK") {
dotenvy::var("PUBLIC_DISCORD_WEBHOOK")
{
crate::util::webhook::send_discord_webhook( crate::util::webhook::send_discord_webhook(
project_item.inner.id.into(), project_item.inner.id.into(),
&pool, &pool,
@@ -607,8 +586,7 @@ pub async fn project_edit(
FROM team_members tm FROM team_members tm
WHERE tm.team_id = $1 AND tm.accepted WHERE tm.team_id = $1 AND tm.accepted
", ",
project_item.inner.team_id project_item.inner.team_id as database::models::ids::TeamId
as database::models::ids::TeamId
) )
.fetch_many(&mut *transaction) .fetch_many(&mut *transaction)
.try_filter_map(|e| async { .try_filter_map(|e| async {
@@ -653,9 +631,7 @@ pub async fn project_edit(
.execute(&mut *transaction) .execute(&mut *transaction)
.await?; .await?;
if project_item.inner.status.is_searchable() if project_item.inner.status.is_searchable() && !status.is_searchable() {
&& !status.is_searchable()
{
delete_from_index(id.into(), config).await?; delete_from_index(id.into(), config).await?;
} }
} }
@@ -726,17 +702,14 @@ pub async fn project_edit(
for category in categories { for category in categories {
let category_id = let category_id =
database::models::categories::Category::get_id( database::models::categories::Category::get_id(category, &mut *transaction)
category, .await?
&mut *transaction, .ok_or_else(|| {
) ApiError::InvalidInput(format!(
.await? "Category {} does not exist.",
.ok_or_else(|| { category.clone()
ApiError::InvalidInput(format!( ))
"Category {} does not exist.", })?;
category.clone()
))
})?;
sqlx::query!( sqlx::query!(
" "
@@ -761,17 +734,14 @@ pub async fn project_edit(
for category in categories { for category in categories {
let category_id = let category_id =
database::models::categories::Category::get_id( database::models::categories::Category::get_id(category, &mut *transaction)
category, .await?
&mut *transaction, .ok_or_else(|| {
) ApiError::InvalidInput(format!(
.await? "Category {} does not exist.",
.ok_or_else(|| { category.clone()
ApiError::InvalidInput(format!( ))
"Category {} does not exist.", })?;
category.clone()
))
})?;
sqlx::query!( sqlx::query!(
" "
@@ -899,8 +869,7 @@ pub async fn project_edit(
)); ));
} }
let slug_project_id_option: Option<u64> = let slug_project_id_option: Option<u64> = parse_base62(slug).ok();
parse_base62(slug).ok();
if let Some(slug_project_id) = slug_project_id_option { if let Some(slug_project_id) = slug_project_id_option {
let results = sqlx::query!( let results = sqlx::query!(
" "
@@ -913,8 +882,7 @@ pub async fn project_edit(
if results.exists.unwrap_or(true) { if results.exists.unwrap_or(true) {
return Err(ApiError::InvalidInput( return Err(ApiError::InvalidInput(
"Slug collides with other project's id!" "Slug collides with other project's id!".to_string(),
.to_string(),
)); ));
} }
} }
@@ -933,8 +901,7 @@ pub async fn project_edit(
if results.exists.unwrap_or(true) { if results.exists.unwrap_or(true) {
return Err(ApiError::InvalidInput( return Err(ApiError::InvalidInput(
"Slug collides with other project's id!" "Slug collides with other project's id!".to_string(),
.to_string(),
)); ));
} }
} }
@@ -960,13 +927,12 @@ pub async fn project_edit(
)); ));
} }
let side_type_id = let side_type_id = database::models::categories::SideType::get_id(
database::models::categories::SideType::get_id( new_side.as_str(),
new_side.as_str(), &mut *transaction,
&mut *transaction, )
) .await?
.await? .expect("No database entry found for side type");
.expect("No database entry found for side type");
sqlx::query!( sqlx::query!(
" "
@@ -989,13 +955,12 @@ pub async fn project_edit(
)); ));
} }
let side_type_id = let side_type_id = database::models::categories::SideType::get_id(
database::models::categories::SideType::get_id( new_side.as_str(),
new_side.as_str(), &mut *transaction,
&mut *transaction, )
) .await?
.await? .expect("No database entry found for side type");
.expect("No database entry found for side type");
sqlx::query!( sqlx::query!(
" "
@@ -1025,9 +990,7 @@ pub async fn project_edit(
} }
spdx::Expression::parse(&license).map_err(|err| { spdx::Expression::parse(&license).map_err(|err| {
ApiError::InvalidInput(format!( ApiError::InvalidInput(format!("Invalid SPDX license identifier: {err}"))
"Invalid SPDX license identifier: {err}"
))
})?; })?;
sqlx::query!( sqlx::query!(
@@ -1062,18 +1025,17 @@ pub async fn project_edit(
.await?; .await?;
for donation in donations { for donation in donations {
let platform_id = let platform_id = database::models::categories::DonationPlatform::get_id(
database::models::categories::DonationPlatform::get_id( &donation.id,
&donation.id, &mut *transaction,
&mut *transaction, )
) .await?
.await? .ok_or_else(|| {
.ok_or_else(|| { ApiError::InvalidInput(format!(
ApiError::InvalidInput(format!( "Platform {} does not exist.",
"Platform {} does not exist.", donation.id.clone()
donation.id.clone() ))
)) })?;
})?;
sqlx::query!( sqlx::query!(
" "
@@ -1090,9 +1052,7 @@ pub async fn project_edit(
} }
if let Some(moderation_message) = &new_project.moderation_message { if let Some(moderation_message) = &new_project.moderation_message {
if !user.role.is_mod() if !user.role.is_mod() && project_item.inner.status != ProjectStatus::Approved {
&& project_item.inner.status != ProjectStatus::Approved
{
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the moderation message of this project!" "You do not have the permissions to edit the moderation message of this project!"
.to_string(), .to_string(),
@@ -1112,12 +1072,8 @@ pub async fn project_edit(
.await?; .await?;
} }
if let Some(moderation_message_body) = if let Some(moderation_message_body) = &new_project.moderation_message_body {
&new_project.moderation_message_body 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( return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the moderation message body of this project!" "You do not have the permissions to edit the moderation message body of this project!"
.to_string(), .to_string(),
@@ -1158,8 +1114,7 @@ pub async fn project_edit(
.await?; .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) { if !perms.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the monetization status of this project!" "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 if (*monetization_status == MonetizationStatus::ForceDemonetized
== MonetizationStatus::ForceDemonetized
|| project_item.inner.monetization_status || project_item.inner.monetization_status
== MonetizationStatus::ForceDemonetized) == MonetizationStatus::ForceDemonetized)
&& !user.role.is_mod() && !user.role.is_mod()
@@ -1276,9 +1230,9 @@ pub async fn projects_edit(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
bulk_edit_project.validate().map_err(|err| { bulk_edit_project
ApiError::Validation(validation_errors_to_string(err, None)) .validate()
})?; .map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
let project_ids: Vec<database::models::ids::ProjectId> = let project_ids: Vec<database::models::ids::ProjectId> =
serde_json::from_str::<Vec<ProjectId>>(&ids.ids)? serde_json::from_str::<Vec<ProjectId>>(&ids.ids)?
@@ -1286,8 +1240,7 @@ pub async fn projects_edit(
.map(|x| x.into()) .map(|x| x.into())
.collect(); .collect();
let projects_data = let projects_data = database::models::Project::get_many_full(&project_ids, &**pool).await?;
database::models::Project::get_many_full(&project_ids, &**pool).await?;
if let Some(id) = project_ids if let Some(id) = project_ids
.iter() .iter()
@@ -1303,15 +1256,11 @@ pub async fn projects_edit(
.iter() .iter()
.map(|x| x.inner.team_id) .map(|x| x.inner.team_id)
.collect::<Vec<database::models::TeamId>>(); .collect::<Vec<database::models::TeamId>>();
let team_members = database::models::TeamMember::get_from_team_full_many( let team_members =
&team_ids, &**pool, database::models::TeamMember::get_from_team_full_many(&team_ids, &**pool).await?;
)
.await?;
let categories = let categories = database::models::categories::Category::list(&**pool).await?;
database::models::categories::Category::list(&**pool).await?; let donation_platforms = database::models::categories::DonationPlatform::list(&**pool).await?;
let donation_platforms =
database::models::categories::DonationPlatform::list(&**pool).await?;
let mut transaction = pool.begin().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) .find(|x| x.team_id == project.inner.team_id)
{ {
if !member.permissions.contains(Permissions::EDIT_DETAILS) { if !member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(format!(
format!("You do not have the permissions to bulk edit project {}!", project.inner.title), "You do not have the permissions to bulk edit project {}!",
)); project.inner.title
)));
} }
} else if project.inner.status.is_hidden() { } else if project.inner.status.is_hidden() {
return Ok(HttpResponse::NotFound().body("")); return Ok(HttpResponse::NotFound().body(""));
@@ -1336,18 +1286,15 @@ pub async fn projects_edit(
}; };
} }
let mut set_categories = let mut set_categories = if let Some(categories) = bulk_edit_project.categories.clone() {
if let Some(categories) = bulk_edit_project.categories.clone() { categories
categories } else {
} else { project.categories.clone()
project.categories.clone() };
};
if let Some(delete_categories) = &bulk_edit_project.remove_categories { if let Some(delete_categories) = &bulk_edit_project.remove_categories {
for category in delete_categories { for category in delete_categories {
if let Some(pos) = if let Some(pos) = set_categories.iter().position(|x| x == category) {
set_categories.iter().position(|x| x == category)
{
set_categories.remove(pos); set_categories.remove(pos);
} }
} }
@@ -1394,34 +1341,27 @@ pub async fn projects_edit(
project.inner.id as database::models::ids::ProjectId, project.inner.id as database::models::ids::ProjectId,
category_id as database::models::ids::CategoryId, category_id as database::models::ids::CategoryId,
) )
.execute(&mut *transaction) .execute(&mut *transaction)
.await?; .await?;
} }
} }
let mut set_additional_categories = if let Some(categories) = let mut set_additional_categories =
bulk_edit_project.additional_categories.clone() if let Some(categories) = bulk_edit_project.additional_categories.clone() {
{ categories
categories } else {
} else { project.additional_categories.clone()
project.additional_categories.clone() };
};
if let Some(delete_categories) = if let Some(delete_categories) = &bulk_edit_project.remove_additional_categories {
&bulk_edit_project.remove_additional_categories
{
for category in delete_categories { for category in delete_categories {
if let Some(pos) = if let Some(pos) = set_additional_categories.iter().position(|x| x == category) {
set_additional_categories.iter().position(|x| x == category)
{
set_additional_categories.remove(pos); set_additional_categories.remove(pos);
} }
} }
} }
if let Some(add_categories) = if let Some(add_categories) = &bulk_edit_project.add_additional_categories {
&bulk_edit_project.add_additional_categories
{
for category in add_categories { for category in add_categories {
if set_additional_categories.len() < 256 { if set_additional_categories.len() < 256 {
set_additional_categories.push(category.clone()); set_additional_categories.push(category.clone());
@@ -1476,16 +1416,14 @@ pub async fn projects_edit(
url: d.url, url: d.url,
}) })
.collect(); .collect();
let mut set_donation_links = if let Some(donation_links) = let mut set_donation_links =
bulk_edit_project.donation_urls.clone() if let Some(donation_links) = bulk_edit_project.donation_urls.clone() {
{ donation_links
donation_links } else {
} else { project_donations.clone()
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 { for donation in delete_donations {
if let Some(pos) = set_donation_links if let Some(pos) = set_donation_links
.iter() .iter()
@@ -1532,8 +1470,8 @@ pub async fn projects_edit(
platform_id as database::models::ids::DonationPlatformId, platform_id as database::models::ids::DonationPlatformId,
donation.url donation.url
) )
.execute(&mut *transaction) .execute(&mut *transaction)
.await?; .await?;
} }
} }
@@ -1616,8 +1554,7 @@ pub async fn project_schedule(
if scheduling_data.time < Utc::now() { if scheduling_data.time < Utc::now() {
return Err(ApiError::InvalidInput( return Err(ApiError::InvalidInput(
"You cannot schedule a project to be released in the past!" "You cannot schedule a project to be released in the past!".to_string(),
.to_string(),
)); ));
} }
@@ -1628,10 +1565,7 @@ pub async fn project_schedule(
} }
let string = info.into_inner().0; let string = info.into_inner().0;
let result = database::models::Project::get_from_slug_or_project_id( let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool).await?;
&string, &**pool,
)
.await?;
if let Some(project_item) = result { if let Some(project_item) = result {
let team_member = database::models::TeamMember::get_from_user_id( 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>>, file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
mut payload: web::Payload, mut payload: web::Payload,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
if let Some(content_type) = if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
crate::util::ext::get_image_content_type(&ext.ext)
{
let cdn_url = dotenvy::var("CDN_URL")?; let cdn_url = dotenvy::var("CDN_URL")?;
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0; let string = info.into_inner().0;
let project_item = let project_item = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
database::models::Project::get_from_slug_or_project_id(
&string, &**pool,
)
.await? .await?
.ok_or_else(|| { .ok_or_else(|| {
ApiError::InvalidInput( ApiError::InvalidInput("The specified project does not exist!".to_string())
"The specified project does not exist!".to_string(),
)
})?; })?;
if !user.role.is_mod() { if !user.role.is_mod() {
@@ -1711,15 +1638,12 @@ pub async fn project_icon_edit(
.await .await
.map_err(ApiError::Database)? .map_err(ApiError::Database)?
.ok_or_else(|| { .ok_or_else(|| {
ApiError::InvalidInput( ApiError::InvalidInput("The specified project does not exist!".to_string())
"The specified project does not exist!".to_string(),
)
})?; })?;
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) { if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this project's icon." "You don't have permission to edit this project's icon.".to_string(),
.to_string(),
)); ));
} }
} }
@@ -1732,12 +1656,8 @@ pub async fn project_icon_edit(
} }
} }
let bytes = read_from_payload( let bytes =
&mut payload, read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
262144,
"Icons must be smaller than 256KiB",
)
.await?;
let color = crate::util::img::get_color_from_img(&bytes)?; 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 user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0; let string = info.into_inner().0;
let project_item = database::models::Project::get_from_slug_or_project_id( let project_item = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
&string, &**pool, .await?
) .ok_or_else(|| {
.await? ApiError::InvalidInput("The specified project does not exist!".to_string())
.ok_or_else(|| { })?;
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
if !user.role.is_mod() { if !user.role.is_mod() {
let team_member = database::models::TeamMember::get_from_user_id( let team_member = database::models::TeamMember::get_from_user_id(
@@ -1806,15 +1722,12 @@ pub async fn delete_project_icon(
.await .await
.map_err(ApiError::Database)? .map_err(ApiError::Database)?
.ok_or_else(|| { .ok_or_else(|| {
ApiError::InvalidInput( ApiError::InvalidInput("The specified project does not exist!".to_string())
"The specified project does not exist!".to_string(),
)
})?; })?;
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) { if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this project's icon." "You don't have permission to edit this project's icon.".to_string(),
.to_string(),
)); ));
} }
} }
@@ -1866,32 +1779,24 @@ pub async fn add_gallery_item(
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>, file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
mut payload: web::Payload, mut payload: web::Payload,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
if let Some(content_type) = if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
crate::util::ext::get_image_content_type(&ext.ext) 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 cdn_url = dotenvy::var("CDN_URL")?; let cdn_url = dotenvy::var("CDN_URL")?;
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0; let string = info.into_inner().0;
let project_item = let project_item =
database::models::Project::get_full_from_slug_or_project_id( database::models::Project::get_full_from_slug_or_project_id(&string, &**pool)
&string, &**pool, .await?
) .ok_or_else(|| {
.await? ApiError::InvalidInput("The specified project does not exist!".to_string())
.ok_or_else(|| { })?;
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
if project_item.gallery_items.len() > 64 { if project_item.gallery_items.len() > 64 {
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(
"You have reached the maximum of gallery images to upload." "You have reached the maximum of gallery images to upload.".to_string(),
.to_string(),
)); ));
} }
@@ -1904,15 +1809,12 @@ pub async fn add_gallery_item(
.await .await
.map_err(ApiError::Database)? .map_err(ApiError::Database)?
.ok_or_else(|| { .ok_or_else(|| {
ApiError::InvalidInput( ApiError::InvalidInput("The specified project does not exist!".to_string())
"The specified project does not exist!".to_string(),
)
})?; })?;
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) { if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this project's gallery." "You don't have permission to edit this project's gallery.".to_string(),
.to_string(),
)); ));
} }
} }
@@ -2013,19 +1915,14 @@ pub async fn edit_gallery_item(
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0; let string = info.into_inner().0;
item.validate().map_err(|err| { item.validate()
ApiError::Validation(validation_errors_to_string(err, None)) .map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
})?;
let project_item = database::models::Project::get_from_slug_or_project_id( let project_item = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
&string, &**pool, .await?
) .ok_or_else(|| {
.await? ApiError::InvalidInput("The specified project does not exist!".to_string())
.ok_or_else(|| { })?;
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
if !user.role.is_mod() { if !user.role.is_mod() {
let team_member = database::models::TeamMember::get_from_user_id( let team_member = database::models::TeamMember::get_from_user_id(
@@ -2036,15 +1933,12 @@ pub async fn edit_gallery_item(
.await .await
.map_err(ApiError::Database)? .map_err(ApiError::Database)?
.ok_or_else(|| { .ok_or_else(|| {
ApiError::InvalidInput( ApiError::InvalidInput("The specified project does not exist!".to_string())
"The specified project does not exist!".to_string(),
)
})?; })?;
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) { if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this project's gallery." "You don't have permission to edit this project's gallery.".to_string(),
.to_string(),
)); ));
} }
} }
@@ -2157,15 +2051,11 @@ pub async fn delete_gallery_item(
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0; let string = info.into_inner().0;
let project_item = database::models::Project::get_from_slug_or_project_id( let project_item = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
&string, &**pool, .await?
) .ok_or_else(|| {
.await? ApiError::InvalidInput("The specified project does not exist!".to_string())
.ok_or_else(|| { })?;
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
if !user.role.is_mod() { if !user.role.is_mod() {
let team_member = database::models::TeamMember::get_from_user_id( let team_member = database::models::TeamMember::get_from_user_id(
@@ -2176,15 +2066,12 @@ pub async fn delete_gallery_item(
.await .await
.map_err(ApiError::Database)? .map_err(ApiError::Database)?
.ok_or_else(|| { .ok_or_else(|| {
ApiError::InvalidInput( ApiError::InvalidInput("The specified project does not exist!".to_string())
"The specified project does not exist!".to_string(),
)
})?; })?;
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) { if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this project's gallery." "You don't have permission to edit this project's gallery.".to_string(),
.to_string(),
)); ));
} }
} }
@@ -2241,30 +2128,23 @@ pub async fn project_delete(
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0; let string = info.into_inner().0;
let project = database::models::Project::get_from_slug_or_project_id( let project = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
&string, &**pool, .await?
) .ok_or_else(|| {
.await? ApiError::InvalidInput("The specified project does not exist!".to_string())
.ok_or_else(|| { })?;
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
if !user.role.is_admin() { if !user.role.is_admin() {
let team_member = let team_member = database::models::TeamMember::get_from_user_id_project(
database::models::TeamMember::get_from_user_id_project( project.id,
project.id, user.id.into(),
user.id.into(), &**pool,
&**pool, )
) .await
.await .map_err(ApiError::Database)?
.map_err(ApiError::Database)? .ok_or_else(|| {
.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 if !team_member
.permissions .permissions
@@ -2278,9 +2158,7 @@ pub async fn project_delete(
let mut transaction = pool.begin().await?; let mut transaction = pool.begin().await?;
let result = let result = database::models::Project::remove_full(project.id, &mut transaction).await?;
database::models::Project::remove_full(project.id, &mut transaction)
.await?;
transaction.commit().await?; transaction.commit().await?;
@@ -2302,15 +2180,11 @@ pub async fn project_follow(
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0; let string = info.into_inner().0;
let result = database::models::Project::get_from_slug_or_project_id( let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
&string, &**pool, .await?
) .ok_or_else(|| {
.await? ApiError::InvalidInput("The specified project does not exist!".to_string())
.ok_or_else(|| { })?;
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
let user_id: database::models::ids::UserId = user.id.into(); let user_id: database::models::ids::UserId = user.id.into();
let project_id: database::models::ids::ProjectId = result.id; 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 user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0; let string = info.into_inner().0;
let result = database::models::Project::get_from_slug_or_project_id( let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
&string, &**pool, .await?
) .ok_or_else(|| {
.await? ApiError::InvalidInput("The specified project does not exist!".to_string())
.ok_or_else(|| { })?;
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
let user_id: database::models::ids::UserId = user.id.into(); let user_id: database::models::ids::UserId = user.id.into();
let project_id = result.id; let project_id = result.id;
@@ -2439,8 +2309,7 @@ pub async fn delete_from_index(
id: ProjectId, id: ProjectId,
config: web::Data<SearchConfig>, config: web::Data<SearchConfig>,
) -> Result<(), meilisearch_sdk::errors::Error> { ) -> Result<(), meilisearch_sdk::errors::Error> {
let client = let client = meilisearch_sdk::client::Client::new(&*config.address, &*config.key);
meilisearch_sdk::client::Client::new(&*config.address, &*config.key);
let indexes: IndexesResults = client.get_indexes().await?; let indexes: IndexesResults = client.get_indexes().await?;

View File

@@ -1,15 +1,9 @@
use crate::database::models::thread_item::{ use crate::database::models::thread_item::{ThreadBuilder, ThreadMessageBuilder};
ThreadBuilder, ThreadMessageBuilder, use crate::models::ids::{base62_impl::parse_base62, ProjectId, UserId, VersionId};
};
use crate::models::ids::{
base62_impl::parse_base62, ProjectId, UserId, VersionId,
};
use crate::models::reports::{ItemType, Report}; use crate::models::reports::{ItemType, Report};
use crate::models::threads::{MessageBody, ThreadType}; use crate::models::threads::{MessageBody, ThreadType};
use crate::routes::ApiError; use crate::routes::ApiError;
use crate::util::auth::{ use crate::util::auth::{check_is_moderator_from_headers, get_user_from_headers};
check_is_moderator_from_headers, get_user_from_headers,
};
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use chrono::Utc; use chrono::Utc;
use futures::StreamExt; use futures::StreamExt;
@@ -41,31 +35,24 @@ pub async fn report_create(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let mut transaction = pool.begin().await?; let mut transaction = pool.begin().await?;
let current_user = let current_user = get_user_from_headers(req.headers(), &mut *transaction).await?;
get_user_from_headers(req.headers(), &mut *transaction).await?;
let mut bytes = web::BytesMut::new(); let mut bytes = web::BytesMut::new();
while let Some(item) = body.next().await { while let Some(item) = body.next().await {
bytes.extend_from_slice(&item.map_err(|_| { bytes.extend_from_slice(&item.map_err(|_| {
ApiError::InvalidInput( ApiError::InvalidInput("Error while parsing request payload!".to_string())
"Error while parsing request payload!".to_string(),
)
})?); })?);
} }
let new_report: CreateReport = serde_json::from_slice(bytes.as_ref())?; let new_report: CreateReport = serde_json::from_slice(bytes.as_ref())?;
let id = let id = crate::database::models::generate_report_id(&mut transaction).await?;
crate::database::models::generate_report_id(&mut transaction).await?;
let report_type = crate::database::models::categories::ReportType::get_id( let report_type = crate::database::models::categories::ReportType::get_id(
&new_report.report_type, &new_report.report_type,
&mut *transaction, &mut *transaction,
) )
.await? .await?
.ok_or_else(|| { .ok_or_else(|| {
ApiError::InvalidInput(format!( ApiError::InvalidInput(format!("Invalid report type: {}", new_report.report_type))
"Invalid report type: {}",
new_report.report_type
))
})?; })?;
let thread_id = ThreadBuilder { let thread_id = ThreadBuilder {
@@ -92,8 +79,7 @@ pub async fn report_create(
match new_report.item_type { match new_report.item_type {
ItemType::Project => { ItemType::Project => {
let project_id = let project_id = ProjectId(parse_base62(new_report.item_id.as_str())?);
ProjectId(parse_base62(new_report.item_id.as_str())?);
let result = sqlx::query!( let result = sqlx::query!(
"SELECT EXISTS(SELECT 1 FROM mods WHERE id = $1)", "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()) report.project_id = Some(project_id.into())
} }
ItemType::Version => { ItemType::Version => {
let version_id = let version_id = VersionId(parse_base62(new_report.item_id.as_str())?);
VersionId(parse_base62(new_report.item_id.as_str())?);
let result = sqlx::query!( let result = sqlx::query!(
"SELECT EXISTS(SELECT 1 FROM versions WHERE id = $1)", "SELECT EXISTS(SELECT 1 FROM versions WHERE id = $1)",
@@ -236,11 +221,8 @@ pub async fn reports(
.await? .await?
}; };
let query_reports = crate::database::models::report_item::Report::get_many( let query_reports =
&report_ids, crate::database::models::report_item::Report::get_many(&report_ids, &**pool).await?;
&**pool,
)
.await?;
let mut reports = Vec::new(); 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 user = get_user_from_headers(req.headers(), &**pool).await?;
let id = info.into_inner().0.into(); let id = info.into_inner().0.into();
let report = let report = crate::database::models::report_item::Report::get(id, &**pool).await?;
crate::database::models::report_item::Report::get(id, &**pool).await?;
if let Some(report) = report { if let Some(report) = report {
if !user.role.is_mod() && report.reporter != user.id.into() { 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 user = get_user_from_headers(req.headers(), &**pool).await?;
let id = info.into_inner().0.into(); let id = info.into_inner().0.into();
let report = let report = crate::database::models::report_item::Report::get(id, &**pool).await?;
crate::database::models::report_item::Report::get(id, &**pool).await?;
if let Some(report) = report { if let Some(report) = report {
if !user.role.is_mod() && report.user_id != Some(user.id.into()) { if !user.role.is_mod() && report.user_id != Some(user.id.into()) {
@@ -380,9 +360,7 @@ pub async fn report_delete(
} }
} }
fn to_report( fn to_report(x: crate::database::models::report_item::QueryReport) -> Result<Report, ApiError> {
x: crate::database::models::report_item::QueryReport,
) -> Result<Report, ApiError> {
let mut item_id = "".to_string(); let mut item_id = "".to_string();
let mut item_type = ItemType::Unknown; let mut item_type = ItemType::Unknown;

View File

@@ -8,9 +8,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
} }
#[get("statistics")] #[get("statistics")]
pub async fn get_stats( pub async fn get_stats(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let projects = sqlx::query!( let projects = sqlx::query!(
" "
SELECT COUNT(id) SELECT COUNT(id)

View File

@@ -1,8 +1,6 @@
use super::ApiError; use super::ApiError;
use crate::database::models; use crate::database::models;
use crate::database::models::categories::{ use crate::database::models::categories::{DonationPlatform, ProjectType, ReportType, SideType};
DonationPlatform, ProjectType, ReportType, SideType,
};
use actix_web::{get, web, HttpResponse}; use actix_web::{get, web, HttpResponse};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use models::categories::{Category, GameVersion, Loader}; use models::categories::{Category, GameVersion, Loader};
@@ -34,9 +32,7 @@ pub struct CategoryData {
// TODO: searching / filtering? Could be used to implement a live // TODO: searching / filtering? Could be used to implement a live
// searching category list // searching category list
#[get("category")] #[get("category")]
pub async fn category_list( pub async fn category_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let results = Category::list(&**pool) let results = Category::list(&**pool)
.await? .await?
.into_iter() .into_iter()
@@ -59,9 +55,7 @@ pub struct LoaderData {
} }
#[get("loader")] #[get("loader")]
pub async fn loader_list( pub async fn loader_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let mut results = Loader::list(&**pool) let mut results = Loader::list(&**pool)
.await? .await?
.into_iter() .into_iter()
@@ -97,11 +91,8 @@ pub async fn game_version_list(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
query: web::Query<GameVersionQuery>, query: web::Query<GameVersionQuery>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let results: Vec<GameVersionQueryData> = if query.type_.is_some() let results: Vec<GameVersionQueryData> = if query.type_.is_some() || query.major.is_some() {
|| query.major.is_some() GameVersion::list_filter(query.type_.as_deref(), query.major, &**pool).await?
{
GameVersion::list_filter(query.type_.as_deref(), query.major, &**pool)
.await?
} else { } else {
GameVersion::list(&**pool).await? GameVersion::list(&**pool).await?
} }
@@ -145,9 +136,7 @@ pub struct LicenseText {
} }
#[get("license/{id}")] #[get("license/{id}")]
pub async fn license_text( pub async fn license_text(params: web::Path<(String,)>) -> Result<HttpResponse, ApiError> {
params: web::Path<(String,)>,
) -> Result<HttpResponse, ApiError> {
let license_id = params.into_inner().0; let license_id = params.into_inner().0;
if license_id == *crate::models::projects::DEFAULT_LICENSE_ID { if license_id == *crate::models::projects::DEFAULT_LICENSE_ID {
@@ -176,41 +165,32 @@ pub struct DonationPlatformQueryData {
} }
#[get("donation_platform")] #[get("donation_platform")]
pub async fn donation_platform_list( pub async fn donation_platform_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
pool: web::Data<PgPool>, let results: Vec<DonationPlatformQueryData> = DonationPlatform::list(&**pool)
) -> Result<HttpResponse, ApiError> { .await?
let results: Vec<DonationPlatformQueryData> = .into_iter()
DonationPlatform::list(&**pool) .map(|x| DonationPlatformQueryData {
.await? short: x.short,
.into_iter() name: x.name,
.map(|x| DonationPlatformQueryData { })
short: x.short, .collect();
name: x.name,
})
.collect();
Ok(HttpResponse::Ok().json(results)) Ok(HttpResponse::Ok().json(results))
} }
#[get("report_type")] #[get("report_type")]
pub async fn report_type_list( pub async fn report_type_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let results = ReportType::list(&**pool).await?; let results = ReportType::list(&**pool).await?;
Ok(HttpResponse::Ok().json(results)) Ok(HttpResponse::Ok().json(results))
} }
#[get("project_type")] #[get("project_type")]
pub async fn project_type_list( pub async fn project_type_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let results = ProjectType::list(&**pool).await?; let results = ProjectType::list(&**pool).await?;
Ok(HttpResponse::Ok().json(results)) Ok(HttpResponse::Ok().json(results))
} }
#[get("side_type")] #[get("side_type")]
pub async fn side_type_list( pub async fn side_type_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let results = SideType::list(&**pool).await?; let results = SideType::list(&**pool).await?;
Ok(HttpResponse::Ok().json(results)) Ok(HttpResponse::Ok().json(results))
} }

View File

@@ -33,33 +33,23 @@ pub async fn team_members_get_project(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0; let string = info.into_inner().0;
let project_data = let project_data =
crate::database::models::Project::get_from_slug_or_project_id( crate::database::models::Project::get_from_slug_or_project_id(&string, &**pool).await?;
&string, &**pool,
)
.await?;
if let Some(project) = project_data { if let Some(project) = project_data {
let members_data = let members_data = TeamMember::get_from_team_full(project.team_id, &**pool).await?;
TeamMember::get_from_team_full(project.team_id, &**pool).await?;
let current_user = let current_user = get_user_from_headers(req.headers(), &**pool).await.ok();
get_user_from_headers(req.headers(), &**pool).await.ok();
if let Some(user) = current_user { if let Some(user) = current_user {
let team_member = TeamMember::get_from_user_id( let team_member =
project.team_id, TeamMember::get_from_user_id(project.team_id, user.id.into(), &**pool)
user.id.into(), .await
&**pool, .map_err(ApiError::Database)?;
)
.await
.map_err(ApiError::Database)?;
if team_member.is_some() { if team_member.is_some() {
let team_members: Vec<_> = members_data let team_members: Vec<_> = members_data
.into_iter() .into_iter()
.map(|data| { .map(|data| crate::models::teams::TeamMember::from(data, false))
crate::models::teams::TeamMember::from(data, false)
})
.collect(); .collect();
return Ok(HttpResponse::Ok().json(team_members)); return Ok(HttpResponse::Ok().json(team_members));
@@ -85,16 +75,14 @@ pub async fn team_members_get(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let id = info.into_inner().0; let id = info.into_inner().0;
let members_data = let members_data = TeamMember::get_from_team_full(id.into(), &**pool).await?;
TeamMember::get_from_team_full(id.into(), &**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 { if let Some(user) = &current_user {
let team_member = let team_member = TeamMember::get_from_user_id(id.into(), user.id.into(), &**pool)
TeamMember::get_from_user_id(id.into(), user.id.into(), &**pool) .await
.await .map_err(ApiError::Database)?;
.map_err(ApiError::Database)?;
if team_member.is_some() { if team_member.is_some() {
let team_members: Vec<_> = members_data let team_members: Vec<_> = members_data
@@ -139,8 +127,7 @@ pub async fn teams_get(
.map(|x| x.into()) .map(|x| x.into())
.collect::<Vec<crate::database::models::ids::TeamId>>(); .collect::<Vec<crate::database::models::ids::TeamId>>();
let teams_data = let teams_data = TeamMember::get_from_team_full_many(&team_ids, &**pool).await?;
TeamMember::get_from_team_full_many(&team_ids, &**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();
let accepted = if let Some(user) = current_user { let accepted = if let Some(user) = current_user {
@@ -159,9 +146,8 @@ pub async fn teams_get(
for (id, member_data) in &teams_groups { for (id, member_data) in &teams_groups {
if accepted.contains(&id) { if accepted.contains(&id) {
let team_members = member_data.map(|data| { let team_members =
crate::models::teams::TeamMember::from(data, false) member_data.map(|data| crate::models::teams::TeamMember::from(data, false));
});
teams.push(team_members.collect()); teams.push(team_members.collect());
@@ -187,12 +173,8 @@ pub async fn join_team(
let team_id = info.into_inner().0.into(); let team_id = info.into_inner().0.into();
let current_user = get_user_from_headers(req.headers(), &**pool).await?; let current_user = get_user_from_headers(req.headers(), &**pool).await?;
let member = TeamMember::get_from_user_id_pending( let member =
team_id, TeamMember::get_from_user_id_pending(team_id, current_user.id.into(), &**pool).await?;
current_user.id.into(),
&**pool,
)
.await?;
if let Some(member) = member { if let Some(member) = member {
if member.accepted { if member.accepted {
@@ -258,20 +240,17 @@ pub async fn add_team_member(
let mut transaction = pool.begin().await?; let mut transaction = pool.begin().await?;
let current_user = get_user_from_headers(req.headers(), &**pool).await?; let current_user = get_user_from_headers(req.headers(), &**pool).await?;
let member = let member = TeamMember::get_from_user_id(team_id, current_user.id.into(), &**pool)
TeamMember::get_from_user_id(team_id, current_user.id.into(), &**pool) .await?
.await? .ok_or_else(|| {
.ok_or_else(|| { ApiError::CustomAuthentication(
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 !member.permissions.contains(Permissions::MANAGE_INVITES) { if !member.permissions.contains(Permissions::MANAGE_INVITES) {
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(
"You don't have permission to invite users to this team" "You don't have permission to invite users to this team".to_string(),
.to_string(),
)); ));
} }
if !member.permissions.contains(new_member.permissions) { if !member.permissions.contains(new_member.permissions) {
@@ -286,9 +265,7 @@ pub async fn add_team_member(
)); ));
} }
if new_member.payouts_split < Decimal::ZERO if new_member.payouts_split < Decimal::ZERO || new_member.payouts_split > Decimal::from(5000) {
|| new_member.payouts_split > Decimal::from(5000)
{
return Err(ApiError::InvalidInput( return Err(ApiError::InvalidInput(
"Payouts split must be between 0 and 5000!".to_string(), "Payouts split must be between 0 and 5000!".to_string(),
)); ));
@@ -308,21 +285,16 @@ pub async fn add_team_member(
)); ));
} else { } else {
return Err(ApiError::InvalidInput( return Err(ApiError::InvalidInput(
"There is already a pending member request for this user" "There is already a pending member request for this user".to_string(),
.to_string(),
)); ));
} }
} }
crate::database::models::User::get(member.user_id, &**pool) crate::database::models::User::get(member.user_id, &**pool)
.await? .await?
.ok_or_else(|| { .ok_or_else(|| ApiError::InvalidInput("An invalid User ID specified".to_string()))?;
ApiError::InvalidInput("An invalid User ID specified".to_string())
})?;
let new_id = let new_id = crate::database::models::ids::generate_team_member_id(&mut transaction).await?;
crate::database::models::ids::generate_team_member_id(&mut transaction)
.await?;
TeamMember { TeamMember {
id: new_id, id: new_id,
team_id, team_id,
@@ -383,24 +355,20 @@ pub async fn edit_team_member(
let user_id = ids.1.into(); let user_id = ids.1.into();
let current_user = get_user_from_headers(req.headers(), &**pool).await?; let current_user = get_user_from_headers(req.headers(), &**pool).await?;
let member = let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool)
TeamMember::get_from_user_id(id, current_user.id.into(), &**pool) .await?
.await? .ok_or_else(|| {
.ok_or_else(|| { ApiError::CustomAuthentication(
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(), })?;
) let edit_member_db = TeamMember::get_from_user_id_pending(id, user_id, &**pool)
})?; .await?
let edit_member_db = .ok_or_else(|| {
TeamMember::get_from_user_id_pending(id, user_id, &**pool) ApiError::CustomAuthentication(
.await? "You don't have permission to edit members of this team".to_string(),
.ok_or_else(|| { )
ApiError::CustomAuthentication( })?;
"You don't have permission to edit members of this team"
.to_string(),
)
})?;
let mut transaction = pool.begin().await?; 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()) && (edit_member.role.is_some() || edit_member.permissions.is_some())
{ {
return Err(ApiError::InvalidInput( return Err(ApiError::InvalidInput(
"The owner's permission and role of a team cannot be edited" "The owner's permission and role of a team cannot be edited".to_string(),
.to_string(),
)); ));
} }
if !member.permissions.contains(Permissions::EDIT_MEMBER) { if !member.permissions.contains(Permissions::EDIT_MEMBER) {
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(
"You don't have permission to edit members of this team" "You don't have permission to edit members of this team".to_string(),
.to_string(),
)); ));
} }
if let Some(new_permissions) = edit_member.permissions { if let Some(new_permissions) = edit_member.permissions {
if !member.permissions.contains(new_permissions) { if !member.permissions.contains(new_permissions) {
return Err(ApiError::InvalidInput( return Err(ApiError::InvalidInput(
"The new permissions have permissions that you don't have" "The new permissions have permissions that you don't have".to_string(),
.to_string(),
)); ));
} }
} }
if let Some(payouts_split) = edit_member.payouts_split { 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( return Err(ApiError::InvalidInput(
"Payouts split must be between 0 and 5000!".to_string(), "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?; let current_user = get_user_from_headers(req.headers(), &**pool).await?;
if !current_user.role.is_admin() { if !current_user.role.is_admin() {
let member = TeamMember::get_from_user_id( let member = TeamMember::get_from_user_id(id.into(), current_user.id.into(), &**pool)
id.into(), .await?
current_user.id.into(), .ok_or_else(|| {
&**pool, ApiError::CustomAuthentication(
) "You don't have permission to edit members of this team".to_string(),
.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 { if member.role != crate::models::teams::OWNER_ROLE {
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(
"You don't have permission to edit the ownership of this team" "You don't have permission to edit the ownership of this team".to_string(),
.to_string(),
)); ));
} }
} }
let new_member = TeamMember::get_from_user_id( let new_member = TeamMember::get_from_user_id(id.into(), new_owner.user_id.into(), &**pool)
id.into(), .await?
new_owner.user_id.into(), .ok_or_else(|| {
&**pool, ApiError::InvalidInput("The new owner specified does not exist".to_string())
) })?;
.await?
.ok_or_else(|| {
ApiError::InvalidInput(
"The new owner specified does not exist".to_string(),
)
})?;
if !new_member.accepted { if !new_member.accepted {
return Err(ApiError::InvalidInput( return Err(ApiError::InvalidInput(
@@ -559,18 +511,15 @@ pub async fn remove_team_member(
let user_id = ids.1.into(); let user_id = ids.1.into();
let current_user = get_user_from_headers(req.headers(), &**pool).await?; let current_user = get_user_from_headers(req.headers(), &**pool).await?;
let member = let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool)
TeamMember::get_from_user_id(id, current_user.id.into(), &**pool) .await?
.await? .ok_or_else(|| {
.ok_or_else(|| { ApiError::CustomAuthentication(
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(), })?;
)
})?;
let delete_member = let delete_member = TeamMember::get_from_user_id_pending(id, user_id, &**pool).await?;
TeamMember::get_from_user_id_pending(id, user_id, &**pool).await?;
if let Some(delete_member) = delete_member { if let Some(delete_member) = delete_member {
if delete_member.role == crate::models::teams::OWNER_ROLE { 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 // Members other than the owner can either leave the team, or be
// removed by a member with the REMOVE_MEMBER permission. // removed by a member with the REMOVE_MEMBER permission.
if delete_member.user_id == member.user_id if delete_member.user_id == member.user_id
|| (member.permissions.contains(Permissions::REMOVE_MEMBER) || (member.permissions.contains(Permissions::REMOVE_MEMBER) && member.accepted)
&& member.accepted)
{ {
TeamMember::delete(id, user_id, &mut transaction).await?; TeamMember::delete(id, user_id, &mut transaction).await?;
} else { } else {
@@ -596,8 +544,7 @@ pub async fn remove_team_member(
)); ));
} }
} else if delete_member.user_id == member.user_id } else if delete_member.user_id == member.user_id
|| (member.permissions.contains(Permissions::MANAGE_INVITES) || (member.permissions.contains(Permissions::MANAGE_INVITES) && member.accepted)
&& member.accepted)
{ {
// This is a pending invite rather than a member, so the // This is a pending invite rather than a member, so the
// user being invited or team members with the MANAGE_INVITES // 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?; TeamMember::delete(id, user_id, &mut transaction).await?;
} else { } else {
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(
"You do not have permission to cancel a team invite" "You do not have permission to cancel a team invite".to_string(),
.to_string(),
)); ));
} }

View File

@@ -4,14 +4,10 @@ use crate::database::models::thread_item::ThreadMessageBuilder;
use crate::models::ids::{ReportId, ThreadMessageId}; use crate::models::ids::{ReportId, ThreadMessageId};
use crate::models::notifications::NotificationBody; use crate::models::notifications::NotificationBody;
use crate::models::projects::{ProjectId, ProjectStatus}; use crate::models::projects::{ProjectId, ProjectStatus};
use crate::models::threads::{ use crate::models::threads::{MessageBody, Thread, ThreadId, ThreadMessage, ThreadType};
MessageBody, Thread, ThreadId, ThreadMessage, ThreadType,
};
use crate::models::users::User; use crate::models::users::User;
use crate::routes::ApiError; use crate::routes::ApiError;
use crate::util::auth::{ use crate::util::auth::{check_is_moderator_from_headers, get_user_from_headers};
check_is_moderator_from_headers, get_user_from_headers,
};
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse}; use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
use futures::TryStreamExt; use futures::TryStreamExt;
use serde::Deserialize; use serde::Deserialize;
@@ -42,13 +38,13 @@ pub async fn is_authorized_thread(
Ok(match thread.type_ { Ok(match thread.type_ {
ThreadType::Report => { ThreadType::Report => {
let report_exists = sqlx::query!( let report_exists = sqlx::query!(
"SELECT EXISTS(SELECT 1 FROM reports WHERE thread_id = $1 AND reporter = $2)", "SELECT EXISTS(SELECT 1 FROM reports WHERE thread_id = $1 AND reporter = $2)",
thread.id as database::models::ids::ThreadId, thread.id as database::models::ids::ThreadId,
user_id as database::models::ids::UserId, user_id as database::models::ids::UserId,
) )
.fetch_one(pool) .fetch_one(pool)
.await? .await?
.exists; .exists;
report_exists.unwrap_or(false) report_exists.unwrap_or(false)
} }
@@ -80,8 +76,7 @@ pub async fn filter_authorized_threads(
for thread in threads { for thread in threads {
if user.role.is_mod() if user.role.is_mod()
|| (thread.type_ == ThreadType::DirectMessage || (thread.type_ == ThreadType::DirectMessage && thread.members.contains(&user_id))
&& thread.members.contains(&user_id))
{ {
return_threads.push(thread); return_threads.push(thread);
} else { } else {
@@ -106,23 +101,23 @@ pub async fn filter_authorized_threads(
&*project_thread_ids, &*project_thread_ids,
user_id as database::models::ids::UserId, user_id as database::models::ids::UserId,
) )
.fetch_many(&***pool) .fetch_many(&***pool)
.try_for_each(|e| { .try_for_each(|e| {
if let Some(row) = e.right() { if let Some(row) = e.right() {
check_threads.retain(|x| { check_threads.retain(|x| {
let bool = Some(x.id.0) == row.thread_id; let bool = Some(x.id.0) == row.thread_id;
if bool { if bool {
return_threads.push(x.clone()); return_threads.push(x.clone());
} }
!bool !bool
}); });
} }
futures::future::ready(Ok(())) futures::future::ready(Ok(()))
}) })
.await?; .await?;
} }
let report_thread_ids = check_threads let report_thread_ids = check_threads
@@ -176,12 +171,11 @@ pub async fn filter_authorized_threads(
.collect::<Vec<database::models::UserId>>(), .collect::<Vec<database::models::UserId>>(),
); );
let users: Vec<User> = let users: Vec<User> = database::models::User::get_many(&user_ids, &***pool)
database::models::User::get_many(&user_ids, &***pool) .await?
.await? .into_iter()
.into_iter() .map(From::from)
.map(From::from) .collect();
.collect();
let mut final_threads = Vec::new(); let mut final_threads = Vec::new();
@@ -210,11 +204,7 @@ pub async fn filter_authorized_threads(
Ok(final_threads) Ok(final_threads)
} }
fn convert_thread( fn convert_thread(data: database::models::Thread, users: Vec<User>, user: &User) -> Thread {
data: database::models::Thread,
users: Vec<User>,
user: &User,
) -> Thread {
let thread_type = data.type_; let thread_type = data.type_;
Thread { Thread {
@@ -279,16 +269,13 @@ pub async fn thread_get(
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
); );
let users: Vec<User> = let users: Vec<User> = database::models::User::get_many(authors, &**pool)
database::models::User::get_many(authors, &**pool) .await?
.await? .into_iter()
.into_iter() .map(From::from)
.map(From::from) .collect();
.collect();
return Ok( return Ok(HttpResponse::Ok().json(convert_thread(data, users, &user)));
HttpResponse::Ok().json(convert_thread(data, users, &user))
);
} }
} }
Ok(HttpResponse::NotFound().body("")) Ok(HttpResponse::NotFound().body(""))
@@ -313,8 +300,7 @@ pub async fn threads_get(
.map(|x| x.into()) .map(|x| x.into())
.collect(); .collect();
let threads_data = let threads_data = database::models::Thread::get_many(&thread_ids, &**pool).await?;
database::models::Thread::get_many(&thread_ids, &**pool).await?;
let threads = filter_authorized_threads(threads_data, &user, &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 { if let Some(replying_to) = replying_to {
let thread_message = database::models::ThreadMessage::get( let thread_message =
(*replying_to).into(), database::models::ThreadMessage::get((*replying_to).into(), &**pool).await?;
&**pool,
)
.await?;
if let Some(thread_message) = thread_message { if let Some(thread_message) = thread_message {
if thread_message.thread_id != string { if thread_message.thread_id != string {
return Err(ApiError::InvalidInput( return Err(ApiError::InvalidInput(
"Message replied to is from another thread!" "Message replied to is from another thread!".to_string(),
.to_string(),
)); ));
} }
} else { } else {
@@ -394,17 +376,13 @@ pub async fn thread_send_message(
None None
}; };
if report.as_ref().map(|x| x.closed).unwrap_or(false) if report.as_ref().map(|x| x.closed).unwrap_or(false) && !user.role.is_mod() {
&& !user.role.is_mod()
{
return Err(ApiError::InvalidInput( return Err(ApiError::InvalidInput(
"You may not reply to a closed report".to_string(), "You may not reply to a closed report".to_string(),
)); ));
} }
let (mod_notif, (user_notif, team_id)) = if thread.type_ let (mod_notif, (user_notif, team_id)) = if thread.type_ == ThreadType::Project {
== ThreadType::Project
{
let record = sqlx::query!( let record = sqlx::query!(
"SELECT m.status, m.team_id FROM mods m WHERE thread_id = $1", "SELECT m.status, m.team_id FROM mods m WHERE thread_id = $1",
thread.id as database::models::ids::ThreadId, thread.id as database::models::ids::ThreadId,
@@ -422,7 +400,17 @@ pub async fn thread_send_message(
), ),
) )
} else { } 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?; let mut transaction = pool.begin().await?;
@@ -498,14 +486,11 @@ pub async fn moderation_inbox(
" "
) )
.fetch_many(&**pool) .fetch_many(&**pool)
.try_filter_map(|e| async { .try_filter_map(|e| async { Ok(e.right().map(|m| database::models::ThreadId(m.id))) })
Ok(e.right().map(|m| database::models::ThreadId(m.id)))
})
.try_collect::<Vec<database::models::ThreadId>>() .try_collect::<Vec<database::models::ThreadId>>()
.await?; .await?;
let threads_data = let threads_data = database::models::Thread::get_many(&ids, &**pool).await?;
database::models::Thread::get_many(&ids, &**pool).await?;
let threads = filter_authorized_threads(threads_data, &user, &pool).await?; let threads = filter_authorized_threads(threads_data, &user, &pool).await?;
Ok(HttpResponse::Ok().json(threads)) Ok(HttpResponse::Ok().json(threads))
@@ -546,11 +531,7 @@ pub async fn message_delete(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let result = database::models::ThreadMessage::get( let result = database::models::ThreadMessage::get(info.into_inner().0.into(), &**pool).await?;
info.into_inner().0.into(),
&**pool,
)
.await?;
if let Some(thread) = result { if let Some(thread) = result {
if !user.role.is_mod() && thread.author_id != Some(user.id.into()) { 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?; let mut transaction = pool.begin().await?;
database::models::ThreadMessage::remove_full( database::models::ThreadMessage::remove_full(thread.id, &mut transaction).await?;
thread.id,
&mut transaction,
)
.await?;
transaction.commit().await?; transaction.commit().await?;
Ok(HttpResponse::NoContent().body("")) Ok(HttpResponse::NoContent().body(""))

View File

@@ -2,9 +2,7 @@ use crate::database::models::User;
use crate::file_hosting::FileHost; use crate::file_hosting::FileHost;
use crate::models::notifications::Notification; use crate::models::notifications::Notification;
use crate::models::projects::Project; use crate::models::projects::Project;
use crate::models::users::{ use crate::models::users::{Badges, RecipientType, RecipientWallet, Role, UserId};
Badges, RecipientType, RecipientWallet, Role, UserId,
};
use crate::queue::payouts::{PayoutAmount, PayoutItem, PayoutsQueue}; use crate::queue::payouts::{PayoutAmount, PayoutItem, PayoutsQueue};
use crate::routes::ApiError; use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers; use crate::util::auth::get_user_from_headers;
@@ -24,6 +22,7 @@ use validator::Validate;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(user_auth_get); cfg.service(user_auth_get);
cfg.service(user_data_get);
cfg.service(users_get); cfg.service(users_get);
cfg.service( cfg.service(
@@ -45,8 +44,44 @@ pub async fn user_auth_get(
req: HttpRequest, req: HttpRequest,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok().json(get_user_from_headers(req.headers(), &**pool).await?))
.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)] #[derive(Serialize, Deserialize)]
@@ -66,8 +101,7 @@ pub async fn users_get(
let users_data = User::get_many(&user_ids, &**pool).await?; let users_data = User::get_many(&user_ids, &**pool).await?;
let users: Vec<crate::models::users::User> = let users: Vec<crate::models::users::User> = users_data.into_iter().map(From::from).collect();
users_data.into_iter().map(From::from).collect();
Ok(HttpResponse::Ok().json(users)) Ok(HttpResponse::Ok().json(users))
} }
@@ -78,8 +112,7 @@ pub async fn user_get(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0; let string = info.into_inner().0;
let id_option: Option<UserId> = let id_option: Option<UserId> = serde_json::from_str(&format!("\"{string}\"")).ok();
serde_json::from_str(&format!("\"{string}\"")).ok();
let mut user_data; let mut user_data;
@@ -109,8 +142,7 @@ pub async fn projects_list(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await.ok(); let user = get_user_from_headers(req.headers(), &**pool).await.ok();
let id_option = let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
if let Some(id) = id_option { if let Some(id) = id_option {
let user_id: UserId = id.into(); 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 project_data = User::get_projects(id, &**pool).await?;
let response: Vec<_> = let response: Vec<_> = crate::database::Project::get_many_full(&project_data, &**pool)
crate::database::Project::get_many_full(&project_data, &**pool) .await?
.await? .into_iter()
.into_iter() .filter(|x| can_view_private || x.inner.status.is_searchable())
.filter(|x| can_view_private || x.inner.status.is_searchable()) .map(Project::from)
.map(Project::from) .collect();
.collect();
Ok(HttpResponse::Ok().json(response)) Ok(HttpResponse::Ok().json(response))
} else { } else {
@@ -192,12 +223,11 @@ pub async fn user_edit(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
new_user.validate().map_err(|err| { new_user
ApiError::Validation(validation_errors_to_string(err, None)) .validate()
})?; .map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
let id_option = let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
if let Some(id) = id_option { if let Some(id) = id_option {
let user_id: UserId = id.into(); 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) = &new_user.payout_data {
if let Some(payout_data) = payout_data { if let Some(payout_data) = payout_data {
if payout_data.payout_wallet_type if payout_data.payout_wallet_type == RecipientType::UserHandle
== RecipientType::UserHandle
&& payout_data.payout_wallet == RecipientWallet::Paypal && payout_data.payout_wallet == RecipientWallet::Paypal
{ {
return Err(ApiError::InvalidInput( return Err(ApiError::InvalidInput(
"You cannot use a paypal wallet with a user handle!" "You cannot use a paypal wallet with a user handle!".to_string(),
.to_string(),
)); ));
} }
if !match payout_data.payout_wallet_type { if !match payout_data.payout_wallet_type {
RecipientType::Email => validator::validate_email( RecipientType::Email => {
&payout_data.payout_address, validator::validate_email(&payout_data.payout_address)
), }
RecipientType::Phone => validator::validate_phone( RecipientType::Phone => {
&payout_data.payout_address, validator::validate_phone(&payout_data.payout_address)
), }
RecipientType::UserHandle => true, RecipientType::UserHandle => true,
} { } {
return Err(ApiError::InvalidInput( return Err(ApiError::InvalidInput(
@@ -350,8 +378,8 @@ pub async fn user_edit(
", ",
id as crate::database::models::ids::UserId, id as crate::database::models::ids::UserId,
) )
.fetch_one(&mut *transaction) .fetch_one(&mut *transaction)
.await?; .await?;
if results.exists.unwrap_or(false) { if results.exists.unwrap_or(false) {
return Err(ApiError::InvalidInput( return Err(ApiError::InvalidInput(
@@ -371,8 +399,8 @@ pub async fn user_edit(
payout_data.payout_address, payout_data.payout_address,
id as crate::database::models::ids::UserId, id as crate::database::models::ids::UserId,
) )
.execute(&mut *transaction) .execute(&mut *transaction)
.await?; .await?;
} else { } else {
sqlx::query!( sqlx::query!(
" "
@@ -382,8 +410,8 @@ pub async fn user_edit(
", ",
id as crate::database::models::ids::UserId, id as crate::database::models::ids::UserId,
) )
.execute(&mut *transaction) .execute(&mut *transaction)
.await?; .await?;
} }
} }
@@ -413,20 +441,15 @@ pub async fn user_icon_edit(
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>, file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
mut payload: web::Payload, mut payload: web::Payload,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
if let Some(content_type) = if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
crate::util::ext::get_image_content_type(&ext.ext)
{
let cdn_url = dotenvy::var("CDN_URL")?; let cdn_url = dotenvy::var("CDN_URL")?;
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option = let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
User::get_id_from_username_or_id(&info.into_inner().0, &**pool)
.await?;
if let Some(id) = id_option { if let Some(id) = id_option {
if user.id != id.into() && !user.role.is_mod() { if user.id != id.into() && !user.role.is_mod() {
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this user's icon." "You don't have permission to edit this user's icon.".to_string(),
.to_string(),
)); ));
} }
@@ -452,12 +475,8 @@ pub async fn user_icon_edit(
} }
} }
let bytes = read_from_payload( let bytes =
&mut payload, read_from_payload(&mut payload, 2097152, "Icons must be smaller than 2MiB").await?;
2097152,
"Icons must be smaller than 2MiB",
)
.await?;
let hash = sha1::Sha1::from(&bytes).hexdigest(); let hash = sha1::Sha1::from(&bytes).hexdigest();
let upload_data = file_host let upload_data = file_host
@@ -509,8 +528,7 @@ pub async fn user_delete(
removal_type: web::Query<RemovalType>, removal_type: web::Query<RemovalType>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option = let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
if let Some(id) = id_option { if let Some(id) = id_option {
if !user.role.is_admin() && user.id != id.into() { if !user.role.is_admin() && user.id != id.into() {
@@ -546,11 +564,7 @@ pub async fn user_follows(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option = crate::database::models::User::get_id_from_username_or_id( let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
&info.into_inner().0,
&**pool,
)
.await?;
if let Some(id) = id_option { if let Some(id) = id_option {
if !user.role.is_admin() && user.id != id.into() { 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>>() .try_collect::<Vec<crate::database::models::ProjectId>>()
.await?; .await?;
let projects: Vec<_> = let projects: Vec<_> = crate::database::Project::get_many_full(&project_ids, &**pool)
crate::database::Project::get_many_full(&project_ids, &**pool) .await?
.await? .into_iter()
.into_iter() .map(Project::from)
.map(Project::from) .collect();
.collect();
Ok(HttpResponse::Ok().json(projects)) Ok(HttpResponse::Ok().json(projects))
} else { } else {
@@ -596,11 +609,7 @@ pub async fn user_notifications(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option = crate::database::models::User::get_id_from_username_or_id( let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
&info.into_inner().0,
&**pool,
)
.await?;
if let Some(id) = id_option { if let Some(id) = id_option {
if !user.role.is_admin() && user.id != id.into() { if !user.role.is_admin() && user.id != id.into() {
@@ -638,14 +647,12 @@ pub async fn user_payouts(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option = let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
if let Some(id) = id_option { if let Some(id) = id_option {
if !user.role.is_admin() && user.id != id.into() { if !user.role.is_admin() && user.id != id.into() {
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(
"You do not have permission to see the payouts of this user!" "You do not have permission to see the payouts of this user!".to_string(),
.to_string(),
)); ));
} }
@@ -717,22 +724,18 @@ pub async fn user_payouts_request(
let mut payouts_queue = payouts_queue.lock().await; let mut payouts_queue = payouts_queue.lock().await;
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option = let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
if let Some(id) = id_option { if let Some(id) = id_option {
if !user.role.is_admin() && user.id != id.into() { if !user.role.is_admin() && user.id != id.into() {
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(
"You do not have permission to request payouts of this user!" "You do not have permission to request payouts of this user!".to_string(),
.to_string(),
)); ));
} }
if let Some(payouts_data) = user.payout_data { if let Some(payouts_data) = user.payout_data {
if let Some(payout_address) = payouts_data.payout_address { if let Some(payout_address) = payouts_data.payout_address {
if let Some(payout_wallet_type) = if let Some(payout_wallet_type) = payouts_data.payout_wallet_type {
payouts_data.payout_wallet_type
{
if let Some(payout_wallet) = payouts_data.payout_wallet { if let Some(payout_wallet) = payouts_data.payout_wallet {
return if data.amount < payouts_data.balance { return if data.amount < payouts_data.balance {
let mut transaction = pool.begin().await?; let mut transaction = pool.begin().await?;
@@ -744,10 +747,15 @@ pub async fn user_payouts_request(
value: data.amount, value: data.amount,
}, },
receiver: payout_address, 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_type: payout_wallet_type.to_string().to_uppercase(),
recipient_wallet: payout_wallet.as_str_api().to_string(), 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?; .await?;
@@ -760,8 +768,8 @@ pub async fn user_payouts_request(
data.amount, data.amount,
"success" "success"
) )
.execute(&mut *transaction) .execute(&mut *transaction)
.await?; .await?;
sqlx::query!( sqlx::query!(
" "
@@ -780,8 +788,7 @@ pub async fn user_payouts_request(
Ok(HttpResponse::NoContent().body("")) Ok(HttpResponse::NoContent().body(""))
} else { } else {
Err(ApiError::InvalidInput( Err(ApiError::InvalidInput(
"You do not have enough funds to make this payout!" "You do not have enough funds to make this payout!".to_string(),
.to_string(),
)) ))
}; };
} }

View File

@@ -8,8 +8,8 @@ use crate::file_hosting::FileHost;
use crate::models::notifications::NotificationBody; use crate::models::notifications::NotificationBody;
use crate::models::pack::PackFileHash; use crate::models::pack::PackFileHash;
use crate::models::projects::{ use crate::models::projects::{
Dependency, DependencyType, FileType, GameVersion, Loader, ProjectId, Dependency, DependencyType, FileType, GameVersion, Loader, ProjectId, Version, VersionFile,
Version, VersionFile, VersionId, VersionStatus, VersionType, VersionId, VersionStatus, VersionType,
}; };
use crate::models::teams::Permissions; use crate::models::teams::Permissions;
use crate::util::auth::get_user_from_headers; use crate::util::auth::get_user_from_headers;
@@ -97,11 +97,8 @@ pub async fn version_create(
.await; .await;
if result.is_err() { if result.is_err() {
let undo_result = super::project_creation::undo_uploads( let undo_result =
&***file_host, super::project_creation::undo_uploads(&***file_host, &uploaded_files).await;
&uploaded_files,
)
.await;
let rollback_result = transaction.rollback().await; let rollback_result = transaction.rollback().await;
undo_result?; undo_result?;
@@ -127,10 +124,8 @@ async fn version_create_inner(
let mut initial_version_data = None; let mut initial_version_data = None;
let mut version_builder = None; let mut version_builder = None;
let all_game_versions = let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?;
models::categories::GameVersion::list(&mut *transaction).await?; let all_loaders = models::categories::Loader::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?; let user = get_user_from_headers(req.headers(), &mut *transaction).await?;
@@ -145,9 +140,7 @@ async fn version_create_inner(
let result = async { let result = async {
let content_disposition = field.content_disposition().clone(); let content_disposition = field.content_disposition().clone();
let name = content_disposition.get_name().ok_or_else(|| { let name = content_disposition.get_name().ok_or_else(|| {
CreateError::MissingValueError( CreateError::MissingValueError("Missing content name".to_string())
"Missing content name".to_string(),
)
})?; })?;
if name == "data" { if name == "data" {
@@ -156,11 +149,9 @@ async fn version_create_inner(
data.extend_from_slice(&chunk?); data.extend_from_slice(&chunk?);
} }
let version_create_data: InitialVersionData = let version_create_data: InitialVersionData = serde_json::from_slice(&data)?;
serde_json::from_slice(&data)?;
initial_version_data = Some(version_create_data); initial_version_data = Some(version_create_data);
let version_create_data = let version_create_data = initial_version_data.as_ref().unwrap();
initial_version_data.as_ref().unwrap();
if version_create_data.project_id.is_none() { if version_create_data.project_id.is_none() {
return Err(CreateError::MissingValueError( return Err(CreateError::MissingValueError(
"Missing project id".to_string(), "Missing project id".to_string(),
@@ -168,9 +159,7 @@ async fn version_create_inner(
} }
version_create_data.validate().map_err(|err| { version_create_data.validate().map_err(|err| {
CreateError::ValidationError(validation_errors_to_string( CreateError::ValidationError(validation_errors_to_string(err, None))
err, None,
))
})?; })?;
if !version_create_data.status.can_be_requested() { if !version_create_data.status.can_be_requested() {
@@ -179,8 +168,7 @@ async fn version_create_inner(
)); ));
} }
let project_id: models::ProjectId = let project_id: models::ProjectId = version_create_data.project_id.unwrap().into();
version_create_data.project_id.unwrap().into();
// Ensure that the project this version is being added to exists // Ensure that the project this version is being added to exists
let results = sqlx::query!( let results = sqlx::query!(
@@ -206,8 +194,7 @@ async fn version_create_inner(
.await? .await?
.ok_or_else(|| { .ok_or_else(|| {
CreateError::CustomAuthenticationError( CreateError::CustomAuthenticationError(
"You don't have permission to upload this version!" "You don't have permission to upload this version!".to_string(),
.to_string(),
) )
})?; })?;
@@ -216,13 +203,11 @@ async fn version_create_inner(
.contains(Permissions::UPLOAD_VERSION) .contains(Permissions::UPLOAD_VERSION)
{ {
return Err(CreateError::CustomAuthenticationError( return Err(CreateError::CustomAuthenticationError(
"You don't have permission to upload this version!" "You don't have permission to upload this version!".to_string(),
.to_string(),
)); ));
} }
let version_id: VersionId = let version_id: VersionId = models::generate_version_id(transaction).await?.into();
models::generate_version_id(transaction).await?.into();
let project_type = sqlx::query!( let project_type = sqlx::query!(
" "
@@ -243,13 +228,10 @@ async fn version_create_inner(
all_game_versions all_game_versions
.iter() .iter()
.find(|y| y.version == x.0) .find(|y| y.version == x.0)
.ok_or_else(|| { .ok_or_else(|| CreateError::InvalidGameVersion(x.0.clone()))
CreateError::InvalidGameVersion(x.0.clone())
})
.map(|y| y.id) .map(|y| y.id)
}) })
.collect::<Result<Vec<models::GameVersionId>, CreateError>>( .collect::<Result<Vec<models::GameVersionId>, CreateError>>()?;
)?;
let loaders = version_create_data let loaders = version_create_data
.loaders .loaders
@@ -258,13 +240,9 @@ async fn version_create_inner(
all_loaders all_loaders
.iter() .iter()
.find(|y| { .find(|y| {
y.loader == x.0 y.loader == x.0 && y.supported_project_types.contains(&project_type)
&& y.supported_project_types
.contains(&project_type)
})
.ok_or_else(|| {
CreateError::InvalidLoader(x.0.clone())
}) })
.ok_or_else(|| CreateError::InvalidLoader(x.0.clone()))
.map(|y| y.id) .map(|y| y.id)
}) })
.collect::<Result<Vec<models::LoaderId>, CreateError>>()?; .collect::<Result<Vec<models::LoaderId>, CreateError>>()?;
@@ -286,17 +264,12 @@ async fn version_create_inner(
author_id: user.id.into(), author_id: user.id.into(),
name: version_create_data.version_title.clone(), name: version_create_data.version_title.clone(),
version_number: version_create_data.version_number.clone(), version_number: version_create_data.version_number.clone(),
changelog: version_create_data changelog: version_create_data.version_body.clone().unwrap_or_default(),
.version_body
.clone()
.unwrap_or_default(),
files: Vec::new(), files: Vec::new(),
dependencies, dependencies,
game_versions, game_versions,
loaders, loaders,
version_type: version_create_data version_type: version_create_data.release_channel.to_string(),
.release_channel
.to_string(),
featured: version_create_data.featured, featured: version_create_data.featured,
status: version_create_data.status, status: version_create_data.status,
requested_status: None, requested_status: None,
@@ -306,9 +279,7 @@ async fn version_create_inner(
} }
let version = version_builder.as_mut().ok_or_else(|| { let version = version_builder.as_mut().ok_or_else(|| {
CreateError::InvalidInput(String::from( CreateError::InvalidInput(String::from("`data` field must come before file fields"))
"`data` field must come before file fields",
))
})?; })?;
let project_type = sqlx::query!( let project_type = sqlx::query!(
@@ -323,12 +294,9 @@ async fn version_create_inner(
.await? .await?
.name; .name;
let version_data = let version_data = initial_version_data
initial_version_data.clone().ok_or_else(|| { .clone()
CreateError::InvalidInput( .ok_or_else(|| CreateError::InvalidInput("`data` field is required".to_string()))?;
"`data` field is required".to_string(),
)
})?;
upload_file( upload_file(
&mut field, &mut field,
@@ -365,12 +333,10 @@ async fn version_create_inner(
return Err(error); return Err(error);
} }
let version_data = initial_version_data.ok_or_else(|| { let version_data = initial_version_data
CreateError::InvalidInput("`data` field is required".to_string()) .ok_or_else(|| CreateError::InvalidInput("`data` field is required".to_string()))?;
})?; let builder = version_builder
let builder = version_builder.ok_or_else(|| { .ok_or_else(|| CreateError::InvalidInput("`data` field is required".to_string()))?;
CreateError::InvalidInput("`data` field is required".to_string())
})?;
if builder.files.is_empty() { if builder.files.is_empty() {
return Err(CreateError::InvalidInput( return Err(CreateError::InvalidInput(
@@ -388,9 +354,7 @@ async fn version_create_inner(
builder.project_id as crate::database::models::ids::ProjectId builder.project_id as crate::database::models::ids::ProjectId
) )
.fetch_many(&mut *transaction) .fetch_many(&mut *transaction)
.try_filter_map(|e| async { .try_filter_map(|e| async { Ok(e.right().map(|m| models::ids::UserId(m.follower_id))) })
Ok(e.right().map(|m| models::ids::UserId(m.follower_id)))
})
.try_collect::<Vec<models::ids::UserId>>() .try_collect::<Vec<models::ids::UserId>>()
.await?; .await?;
@@ -453,8 +417,7 @@ async fn version_create_inner(
let project_id = builder.project_id; let project_id = builder.project_id;
builder.insert(transaction).await?; builder.insert(transaction).await?;
models::Project::update_game_versions(project_id, &mut *transaction) models::Project::update_game_versions(project_id, &mut *transaction).await?;
.await?;
models::Project::update_loaders(project_id, &mut *transaction).await?; models::Project::update_loaders(project_id, &mut *transaction).await?;
Ok(HttpResponse::Ok().json(response)) Ok(HttpResponse::Ok().json(response))
@@ -486,11 +449,8 @@ pub async fn upload_file_to_version(
.await; .await;
if result.is_err() { if result.is_err() {
let undo_result = super::project_creation::undo_uploads( let undo_result =
&***file_host, super::project_creation::undo_uploads(&***file_host, &uploaded_files).await;
&uploaded_files,
)
.await;
let rollback_result = transaction.rollback().await; let rollback_result = transaction.rollback().await;
undo_result?; undo_result?;
@@ -541,8 +501,7 @@ async fn upload_file_to_version_inner(
.await? .await?
.ok_or_else(|| { .ok_or_else(|| {
CreateError::CustomAuthenticationError( CreateError::CustomAuthenticationError(
"You don't have permission to upload files to this version!" "You don't have permission to upload files to this version!".to_string(),
.to_string(),
) )
})?; })?;
@@ -551,8 +510,7 @@ async fn upload_file_to_version_inner(
.contains(Permissions::UPLOAD_VERSION) .contains(Permissions::UPLOAD_VERSION)
{ {
return Err(CreateError::CustomAuthenticationError( return Err(CreateError::CustomAuthenticationError(
"You don't have permission to upload files to this version!" "You don't have permission to upload files to this version!".to_string(),
.to_string(),
)); ));
} }
} }
@@ -571,8 +529,7 @@ async fn upload_file_to_version_inner(
.await? .await?
.name; .name;
let all_game_versions = let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?;
models::categories::GameVersion::list(&mut *transaction).await?;
let mut error = None; let mut error = None;
while let Some(item) = payload.next().await { while let Some(item) = payload.next().await {
@@ -585,9 +542,7 @@ async fn upload_file_to_version_inner(
let result = async { let result = async {
let content_disposition = field.content_disposition().clone(); let content_disposition = field.content_disposition().clone();
let name = content_disposition.get_name().ok_or_else(|| { let name = content_disposition.get_name().ok_or_else(|| {
CreateError::MissingValueError( CreateError::MissingValueError("Missing content name".to_string())
"Missing content name".to_string(),
)
})?; })?;
if name == "data" { 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(|| { let file_data = initial_file_data.as_ref().ok_or_else(|| {
CreateError::InvalidInput(String::from( CreateError::InvalidInput(String::from("`data` field must come before file fields"))
"`data` field must come before file fields",
))
})?; })?;
let mut dependencies = version let mut dependencies = version
@@ -703,9 +656,7 @@ pub async fn upload_file(
} }
let content_type = crate::util::ext::project_file_type(file_extension) let content_type = crate::util::ext::project_file_type(file_extension)
.ok_or_else(|| { .ok_or_else(|| CreateError::InvalidFileType(file_extension.to_string()))?;
CreateError::InvalidFileType(file_extension.to_string())
})?;
let data = read_from_field( let data = read_from_field(
field, 500 * (1 << 20), field, 500 * (1 << 20),
@@ -731,8 +682,7 @@ pub async fn upload_file(
if exists { if exists {
return Err(CreateError::InvalidInput( return Err(CreateError::InvalidInput(
"Duplicate files are not allowed to be uploaded to Modrinth!" "Duplicate files are not allowed to be uploaded to Modrinth!".to_string(),
.to_string(),
)); ));
} }
@@ -761,23 +711,20 @@ pub async fn upload_file(
.collect(); .collect();
let res = sqlx::query!( let res = sqlx::query!(
" "
SELECT v.id version_id, v.mod_id project_id, h.hash hash FROM hashes h 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 files f on h.file_id = f.id
INNER JOIN versions v on f.version_id = v.id INNER JOIN versions v on f.version_id = v.id
WHERE h.algorithm = 'sha1' AND h.hash = ANY($1) WHERE h.algorithm = 'sha1' AND h.hash = ANY($1)
", ",
&*hashes &*hashes
) )
.fetch_all(&mut *transaction).await?; .fetch_all(&mut *transaction)
.await?;
for file in &format.files { for file in &format.files {
if let Some(dep) = res.iter().find(|x| { if let Some(dep) = res.iter().find(|x| {
Some(&*x.hash) Some(&*x.hash) == file.hashes.get(&PackFileHash::Sha1).map(|x| x.as_bytes())
== file
.hashes
.get(&PackFileHash::Sha1)
.map(|x| x.as_bytes())
}) { }) {
dependencies.push(DependencyBuilder { dependencies.push(DependencyBuilder {
project_id: Some(models::ProjectId(dep.project_id)), project_id: Some(models::ProjectId(dep.project_id)),
@@ -828,8 +775,7 @@ pub async fn upload_file(
version_id, version_id,
urlencoding::encode(file_name) urlencoding::encode(file_name)
); );
let file_path = let file_path = format!("data/{}/versions/{}/{}", project_id, version_id, &file_name);
format!("data/{}/versions/{}/{}", project_id, version_id, &file_name);
let upload_data = file_host let upload_data = file_host
.upload_file(content_type, &file_path, data) .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) .any(|y| y.hash == sha1_bytes || y.hash == sha512_bytes)
}) { }) {
return Err(CreateError::InvalidInput( return Err(CreateError::InvalidInput(
"Duplicate files are not allowed to be uploaded to Modrinth!" "Duplicate files are not allowed to be uploaded to Modrinth!".to_string(),
.to_string(),
)); ));
} }
@@ -888,9 +833,9 @@ pub async fn upload_file(
pub fn get_name_ext( pub fn get_name_ext(
content_disposition: &actix_web::http::header::ContentDisposition, content_disposition: &actix_web::http::header::ContentDisposition,
) -> Result<(&str, &str), CreateError> { ) -> Result<(&str, &str), CreateError> {
let file_name = content_disposition.get_filename().ok_or_else(|| { let file_name = content_disposition
CreateError::MissingValueError("Missing content file name".to_string()) .get_filename()
})?; .ok_or_else(|| CreateError::MissingValueError("Missing content file name".to_string()))?;
let file_extension = if let Some(last_period) = file_name.rfind('.') { let file_extension = if let Some(last_period) = file_name.rfind('.') {
file_name.get((last_period + 1)..).unwrap_or("") file_name.get((last_period + 1)..).unwrap_or("")
} else { } else {

View File

@@ -84,8 +84,7 @@ pub async fn get_version_from_hash(
.iter() .iter()
.map(|x| database::models::VersionId(x.version_id)) .map(|x| database::models::VersionId(x.version_id))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let versions_data = let versions_data = database::models::Version::get_many_full(&version_ids, &**pool).await?;
database::models::Version::get_many_full(&version_ids, &**pool).await?;
if let Some(first) = versions_data.first() { if let Some(first) = versions_data.first() {
if hash_query.multiple { if hash_query.multiple {
@@ -96,8 +95,7 @@ pub async fn get_version_from_hash(
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
)) ))
} else { } else {
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok().json(models::projects::Version::from(first.clone())))
.json(models::projects::Version::from(first.clone())))
} }
} else { } else {
Ok(HttpResponse::NotFound().body("")) 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) WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ANY($4)
ORDER BY v.date_published ASC 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.as_bytes(),
hash_query.algorithm, 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) .fetch_optional(&mut *transaction)
.await?; .await?;
@@ -178,28 +182,25 @@ pub async fn delete_file(
|| Some(x.version_id) == hash_query.version_id.map(|x| x.0 as i64) || Some(x.version_id) == hash_query.version_id.map(|x| x.0 as i64)
}) { }) {
if !user.role.is_admin() { if !user.role.is_admin() {
let team_member = let team_member = database::models::TeamMember::get_from_user_id_version(
database::models::TeamMember::get_from_user_id_version( database::models::ids::VersionId(row.version_id),
database::models::ids::VersionId(row.version_id), user.id.into(),
user.id.into(), &**pool,
&**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 if !team_member
.permissions .permissions
.contains(Permissions::DELETE_VERSION) .contains(Permissions::DELETE_VERSION)
{ {
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(
"You don't have permission to delete this file!" "You don't have permission to delete this file!".to_string(),
.to_string(),
)); ));
} }
} }
@@ -220,8 +221,7 @@ pub async fn delete_file(
if files.len() < 2 { if files.len() < 2 {
return Err(ApiError::InvalidInput( return Err(ApiError::InvalidInput(
"Versions must have at least one file uploaded to them" "Versions must have at least one file uploaded to them".to_string(),
.to_string(),
)); ));
} }
@@ -324,9 +324,7 @@ pub async fn get_update_from_hash(
.await?; .await?;
if let Some(version_id) = version_ids.first() { if let Some(version_id) = version_ids.first() {
let version_data = let version_data = database::models::Version::get_full(*version_id, &**pool).await?;
database::models::Version::get_full(*version_id, &**pool)
.await?;
ok_or_not_found::<QueryVersion, Version>(version_data) ok_or_not_found::<QueryVersion, Version>(version_data)
} else { } else {
@@ -364,10 +362,16 @@ pub async fn get_versions_from_hashes(
INNER JOIN mods m on v.mod_id = m.id 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) 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(), hashes_parsed.as_slice(),
file_data.algorithm, 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) .fetch_all(&**pool)
.await?; .await?;
@@ -376,8 +380,7 @@ pub async fn get_versions_from_hashes(
.iter() .iter()
.map(|x| database::models::VersionId(x.version_id)) .map(|x| database::models::VersionId(x.version_id))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let versions_data = let versions_data = database::models::Version::get_many_full(&version_ids, &**pool).await?;
database::models::Version::get_many_full(&version_ids, &**pool).await?;
let response: Result<HashMap<String, Version>, ApiError> = result let response: Result<HashMap<String, Version>, ApiError> = result
.into_iter() .into_iter()
@@ -388,10 +391,7 @@ pub async fn get_versions_from_hashes(
.find(|x| x.inner.id.0 == row.version_id) .find(|x| x.inner.id.0 == row.version_id)
.map(|v| { .map(|v| {
if let Ok(parsed_hash) = String::from_utf8(row.hash) { if let Ok(parsed_hash) = String::from_utf8(row.hash) {
Ok(( Ok((parsed_hash, crate::models::projects::Version::from(v)))
parsed_hash,
crate::models::projects::Version::from(v),
))
} else { } else {
Err(ApiError::Database(DatabaseError::Other(format!( Err(ApiError::Database(DatabaseError::Other(format!(
"Could not parse hash for version {}", "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 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) 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(), hashes_parsed.as_slice(),
file_data.algorithm, 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) .fetch_all(&**pool)
.await?; .await?;
let project_ids = result let project_ids = result
.iter() .iter()
.map(|x| database::models::ProjectId(x.project_id)) .map(|x| database::models::ProjectId(x.project_id))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let versions_data = let versions_data = database::models::Project::get_many_full(&project_ids, &**pool).await?;
database::models::Project::get_many_full(&project_ids, &**pool).await?;
let response: Result<HashMap<String, Project>, ApiError> = result let response: Result<HashMap<String, Project>, ApiError> = result
.into_iter() .into_iter()
@@ -447,10 +452,7 @@ pub async fn get_projects_from_hashes(
.find(|x| x.inner.id.0 == row.project_id) .find(|x| x.inner.id.0 == row.project_id)
.map(|v| { .map(|v| {
if let Ok(parsed_hash) = String::from_utf8(row.hash) { if let Ok(parsed_hash) = String::from_utf8(row.hash) {
Ok(( Ok((parsed_hash, crate::models::projects::Project::from(v)))
parsed_hash,
crate::models::projects::Project::from(v),
))
} else { } else {
Err(ApiError::Database(DatabaseError::Other(format!( Err(ApiError::Database(DatabaseError::Other(format!(
"Could not parse hash for version {}", "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 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) 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(), hashes_parsed.as_slice(),
update_data.algorithm, 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) .fetch_many(&mut *transaction)
.try_filter_map(|e| async { .try_filter_map(|e| async {
Ok(e.right().map(|m| (m.hash, database::models::ids::ProjectId(m.mod_id)))) Ok(e.right()
}) .map(|m| (m.hash, database::models::ids::ProjectId(m.mod_id))))
.try_collect::<Vec<_>>() })
.await?; .try_collect::<Vec<_>>()
.await?;
let mut version_ids: HashMap<database::models::VersionId, Vec<u8>> = let mut version_ids: HashMap<database::models::VersionId, Vec<u8>> = HashMap::new();
HashMap::new();
let updated_versions = database::models::Version::get_projects_versions( let updated_versions = database::models::Version::get_projects_versions(
result result
@@ -583,17 +591,13 @@ pub async fn update_files(
.await?; .await?;
for (hash, id) in result { for (hash, id) in result {
if let Some(latest_version) = if let Some(latest_version) = updated_versions.get(&id).and_then(|x| x.last()) {
updated_versions.get(&id).and_then(|x| x.last())
{
version_ids.insert(*latest_version, hash); version_ids.insert(*latest_version, hash);
} }
} }
let query_version_ids = version_ids.keys().copied().collect::<Vec<_>>(); let query_version_ids = version_ids.keys().copied().collect::<Vec<_>>();
let versions = let versions = database::models::Version::get_many_full(&query_version_ids, &**pool).await?;
database::models::Version::get_many_full(&query_version_ids, &**pool)
.await?;
let mut response = HashMap::new(); let mut response = HashMap::new();
@@ -602,10 +606,7 @@ pub async fn update_files(
if let Some(hash) = hash { if let Some(hash) = hash {
if let Ok(parsed_hash) = String::from_utf8(hash.clone()) { if let Ok(parsed_hash) = String::from_utf8(hash.clone()) {
response.insert( response.insert(parsed_hash, models::projects::Version::from(version));
parsed_hash,
models::projects::Version::from(version),
);
} else { } else {
let version_id: VersionId = version.inner.id.into(); let version_id: VersionId = version.inner.id.into();

View File

@@ -1,13 +1,10 @@
use super::ApiError; use super::ApiError;
use crate::database; use crate::database;
use crate::models; use crate::models;
use crate::models::projects::{ use crate::models::projects::{Dependency, FileType, VersionStatus, VersionType};
Dependency, FileType, VersionStatus, VersionType,
};
use crate::models::teams::Permissions; use crate::models::teams::Permissions;
use crate::util::auth::{ use crate::util::auth::{
filter_authorized_versions, get_user_from_headers, is_authorized, filter_authorized_versions, get_user_from_headers, is_authorized, is_authorized_version,
is_authorized_version,
}; };
use crate::util::validate::validation_errors_to_string; use crate::util::validate::validation_errors_to_string;
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
@@ -49,10 +46,7 @@ pub async fn version_list(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0; let string = info.into_inner().0;
let result = database::models::Project::get_from_slug_or_project_id( let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool).await?;
&string, &**pool,
)
.await?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
@@ -80,9 +74,7 @@ pub async fn version_list(
) )
.await?; .await?;
let mut versions = let mut versions = database::models::Version::get_many_full(&version_ids, &**pool).await?;
database::models::Version::get_many_full(&version_ids, &**pool)
.await?;
let mut response = versions let mut response = versions
.iter() .iter()
@@ -95,22 +87,13 @@ pub async fn version_list(
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
versions.sort_by(|a, b| { versions.sort_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published));
b.inner.date_published.cmp(&a.inner.date_published)
});
// Attempt to populate versions with "auto featured" versions // Attempt to populate versions with "auto featured" versions
if response.is_empty() if response.is_empty() && !versions.is_empty() && filters.featured.unwrap_or(false) {
&& !versions.is_empty()
&& filters.featured.unwrap_or(false)
{
let (loaders, game_versions) = futures::future::try_join( let (loaders, game_versions) = futures::future::try_join(
database::models::categories::Loader::list(&**pool), database::models::categories::Loader::list(&**pool),
database::models::categories::GameVersion::list_filter( database::models::categories::GameVersion::list_filter(None, Some(true), &**pool),
None,
Some(true),
&**pool,
),
) )
.await?; .await?;
@@ -139,13 +122,10 @@ pub async fn version_list(
} }
} }
response.sort_by(|a, b| { response.sort_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published));
b.inner.date_published.cmp(&a.inner.date_published)
});
response.dedup_by(|a, b| a.inner.id == b.inner.id); response.dedup_by(|a, b| a.inner.id == b.inner.id);
let response = let response = filter_authorized_versions(response, &user_option, &pool).await?;
filter_authorized_versions(response, &user_option, &pool).await?;
Ok(HttpResponse::Ok().json(response)) Ok(HttpResponse::Ok().json(response))
} else { } else {
@@ -162,16 +142,13 @@ pub async fn version_project_get(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let id = info.into_inner(); let id = info.into_inner();
let version_data = let version_data =
database::models::Version::get_full_from_id_slug(&id.0, &id.1, &**pool) database::models::Version::get_full_from_id_slug(&id.0, &id.1, &**pool).await?;
.await?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
if let Some(data) = version_data { if let Some(data) = version_data {
if is_authorized_version(&data.inner, &user_option, &pool).await? { if is_authorized_version(&data.inner, &user_option, &pool).await? {
return Ok( return Ok(HttpResponse::Ok().json(models::projects::Version::from(data)));
HttpResponse::Ok().json(models::projects::Version::from(data))
);
} }
} }
@@ -189,18 +166,15 @@ pub async fn versions_get(
web::Query(ids): web::Query<VersionIds>, web::Query(ids): web::Query<VersionIds>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let version_ids = let version_ids = serde_json::from_str::<Vec<models::ids::VersionId>>(&ids.ids)?
serde_json::from_str::<Vec<models::ids::VersionId>>(&ids.ids)? .into_iter()
.into_iter() .map(|x| x.into())
.map(|x| x.into()) .collect::<Vec<database::models::VersionId>>();
.collect::<Vec<database::models::VersionId>>(); 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 user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
let versions = let versions = filter_authorized_versions(versions_data, &user_option, &pool).await?;
filter_authorized_versions(versions_data, &user_option, &pool).await?;
Ok(HttpResponse::Ok().json(versions)) Ok(HttpResponse::Ok().json(versions))
} }
@@ -212,16 +186,13 @@ pub async fn version_get(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let id = info.into_inner().0; let id = info.into_inner().0;
let version_data = let version_data = database::models::Version::get_full(id.into(), &**pool).await?;
database::models::Version::get_full(id.into(), &**pool).await?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
if let Some(data) = version_data { if let Some(data) = version_data {
if is_authorized_version(&data.inner, &user_option, &pool).await? { if is_authorized_version(&data.inner, &user_option, &pool).await? {
return Ok( return Ok(HttpResponse::Ok().json(models::projects::Version::from(data)));
HttpResponse::Ok().json(models::projects::Version::from(data))
);
} }
} }
@@ -273,9 +244,9 @@ pub async fn version_edit(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
new_version.validate().map_err(|err| { new_version
ApiError::Validation(validation_errors_to_string(err, None)) .validate()
})?; .map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
let version_id = info.into_inner().0; let version_id = info.into_inner().0;
let id = version_id.into(); let id = version_id.into();
@@ -283,19 +254,15 @@ pub async fn version_edit(
let result = database::models::Version::get_full(id, &**pool).await?; let result = database::models::Version::get_full(id, &**pool).await?;
if let Some(version_item) = result { if let Some(version_item) = result {
let project_item = database::models::Project::get_full( let project_item =
version_item.inner.project_id, 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, &**pool,
) )
.await?; .await?;
let team_member =
database::models::TeamMember::get_from_user_id_version(
version_item.inner.id,
user.id.into(),
&**pool,
)
.await?;
let permissions; let permissions;
if user.role.is_admin() { if user.role.is_admin() {
@@ -303,8 +270,7 @@ pub async fn version_edit(
} else if let Some(member) = team_member { } else if let Some(member) = team_member {
permissions = Some(member.permissions) permissions = Some(member.permissions)
} else if user.role.is_mod() { } else if user.role.is_mod() {
permissions = permissions = Some(Permissions::EDIT_DETAILS | Permissions::EDIT_BODY)
Some(Permissions::EDIT_DETAILS | Permissions::EDIT_BODY)
} else { } else {
permissions = None permissions = None
} }
@@ -312,8 +278,7 @@ pub async fn version_edit(
if let Some(perms) = permissions { if let Some(perms) = permissions {
if !perms.contains(Permissions::UPLOAD_VERSION) { if !perms.contains(Permissions::UPLOAD_VERSION) {
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit this version!" "You do not have the permissions to edit this version!".to_string(),
.to_string(),
)); ));
} }
@@ -403,18 +368,16 @@ pub async fn version_edit(
.await?; .await?;
for game_version in game_versions { for game_version in game_versions {
let game_version_id = let game_version_id = database::models::categories::GameVersion::get_id(
database::models::categories::GameVersion::get_id( &game_version.0,
&game_version.0, &mut *transaction,
&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!( sqlx::query!(
" "
@@ -447,17 +410,13 @@ pub async fn version_edit(
for loader in loaders { for loader in loaders {
let loader_id = let loader_id =
database::models::categories::Loader::get_id( database::models::categories::Loader::get_id(&loader.0, &mut *transaction)
&loader.0, .await?
&mut *transaction, .ok_or_else(|| {
) ApiError::InvalidInput(
.await? "No database entry for loader provided.".to_string(),
.ok_or_else(|| { )
ApiError::InvalidInput( })?;
"No database entry for loader provided."
.to_string(),
)
})?;
sqlx::query!( sqlx::query!(
" "
@@ -551,8 +510,7 @@ pub async fn version_edit(
if let Some(downloads) = &new_version.downloads { if let Some(downloads) = &new_version.downloads {
if !user.role.is_mod() { if !user.role.is_mod() {
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(
"You don't have permission to set the downloads of this mod" "You don't have permission to set the downloads of this mod".to_string(),
.to_string(),
)); ));
} }
@@ -577,8 +535,7 @@ pub async fn version_edit(
WHERE (id = $2) WHERE (id = $2)
", ",
diff as i32, diff as i32,
version_item.inner.project_id version_item.inner.project_id as database::models::ids::ProjectId,
as database::models::ids::ProjectId,
) )
.execute(&mut *transaction) .execute(&mut *transaction)
.await?; .await?;
@@ -667,8 +624,7 @@ pub async fn version_schedule(
if scheduling_data.time < Utc::now() { if scheduling_data.time < Utc::now() {
return Err(ApiError::InvalidInput( return Err(ApiError::InvalidInput(
"You cannot schedule a version to be released in the past!" "You cannot schedule a version to be released in the past!".to_string(),
.to_string(),
)); ));
} }
@@ -679,17 +635,15 @@ pub async fn version_schedule(
} }
let string = info.into_inner().0; let string = info.into_inner().0;
let result = let result = database::models::Version::get_full(string.into(), &**pool).await?;
database::models::Version::get_full(string.into(), &**pool).await?;
if let Some(version_item) = result { if let Some(version_item) = result {
let team_member = let team_member = database::models::TeamMember::get_from_user_id_version(
database::models::TeamMember::get_from_user_id_version( version_item.inner.id,
version_item.inner.id, user.id.into(),
user.id.into(), &**pool,
&**pool, )
) .await?;
.await?;
if !user.role.is_mod() if !user.role.is_mod()
&& !team_member && !team_member
@@ -748,17 +702,14 @@ pub async fn version_delete(
.contains(Permissions::DELETE_VERSION) .contains(Permissions::DELETE_VERSION)
{ {
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(
"You do not have permission to delete versions in this team" "You do not have permission to delete versions in this team".to_string(),
.to_string(),
)); ));
} }
} }
let mut transaction = pool.begin().await?; let mut transaction = pool.begin().await?;
let result = let result = database::models::Version::remove_full(id.into(), &mut transaction).await?;
database::models::Version::remove_full(id.into(), &mut transaction)
.await?;
transaction.commit().await?; transaction.commit().await?;

View File

@@ -32,13 +32,9 @@ impl Drop for Scheduler {
use log::{info, warn}; use log::{info, warn};
pub fn schedule_versions( pub fn schedule_versions(scheduler: &mut Scheduler, pool: sqlx::Pool<sqlx::Postgres>) {
scheduler: &mut Scheduler, let version_index_interval =
pool: sqlx::Pool<sqlx::Postgres>, std::time::Duration::from_secs(parse_var("VERSION_INDEX_INTERVAL").unwrap_or(1800));
) {
let version_index_interval = std::time::Duration::from_secs(
parse_var("VERSION_INDEX_INTERVAL").unwrap_or(1800),
);
scheduler.run(version_index_interval, move || { scheduler.run(version_index_interval, move || {
let pool_ref = pool.clone(); let pool_ref = pool.clone();
@@ -82,15 +78,11 @@ struct VersionFormat<'a> {
release_time: DateTime<Utc>, release_time: DateTime<Utc>,
} }
async fn update_versions( async fn update_versions(pool: &sqlx::Pool<sqlx::Postgres>) -> Result<(), VersionIndexingError> {
pool: &sqlx::Pool<sqlx::Postgres>, let input = reqwest::get("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json")
) -> Result<(), VersionIndexingError> { .await?
let input = reqwest::get( .json::<InputFormat>()
"https://piston-meta.mojang.com/mc/game/version_manifest_v2.json", .await?;
)
.await?
.json::<InputFormat>()
.await?;
let mut skipped_versions_count = 0u32; let mut skipped_versions_count = 0u32;
@@ -152,8 +144,7 @@ async fn update_versions(
.chars() .chars()
.all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c)) .all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c))
{ {
if let Some((_, alternate)) = if let Some((_, alternate)) = HALL_OF_SHAME.iter().find(|(version, _)| name == *version)
HALL_OF_SHAME.iter().find(|(version, _)| name == *version)
{ {
name = String::from(*alternate); name = String::from(*alternate);
} else { } else {

View File

@@ -4,11 +4,10 @@ use log::info;
use super::IndexingError; use super::IndexingError;
use crate::database::models::ProjectId; use crate::database::models::ProjectId;
use crate::search::UploadSearchProject; use crate::search::UploadSearchProject;
use serde::Deserialize;
use sqlx::postgres::PgPool; use sqlx::postgres::PgPool;
pub async fn index_local( pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchProject>, IndexingError> {
pool: PgPool,
) -> Result<Vec<UploadSearchProject>, IndexingError> {
info!("Indexing local projects!"); info!("Indexing local projects!");
Ok( 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 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 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 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 FROM mods m
LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id
LEFT OUTER JOIN categories c ON mc.joining_category_id = c.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_versions lv ON lv.version_id = v.id
LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_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 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 project_types pt ON pt.id = m.project_type
INNER JOIN side_types cs ON m.client_side = cs.id INNER JOIN side_types cs ON m.client_side = cs.id
INNER JOIN side_types ss ON m.server_side = ss.id INNER JOIN side_types ss ON m.server_side = ss.id
@@ -70,6 +72,21 @@ pub async fn index_local(
_ => false, _ => 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 { UploadSearchProject {
project_id: project_id.to_string(), project_id: project_id.to_string(),
title: m.title, title: m.title,
@@ -95,6 +112,7 @@ pub async fn index_local(
open_source, open_source,
color: m.color.map(|x| x as u32), color: m.color.map(|x| x as u32),
featured_gallery: m.featured_gallery.unwrap_or_default().first().cloned(), featured_gallery: m.featured_gallery.unwrap_or_default().first().cloned(),
dependencies,
} }
})) }))
}) })

View File

@@ -203,10 +203,10 @@ const DEFAULT_DISPLAYED_ATTRIBUTES: &[&str] = &[
"gallery", "gallery",
"featured_gallery", "featured_gallery",
"color", "color",
"dependencies",
]; ];
const DEFAULT_SEARCHABLE_ATTRIBUTES: &[&str] = const DEFAULT_SEARCHABLE_ATTRIBUTES: &[&str] = &["title", "description", "author", "slug"];
&["title", "description", "author", "slug"];
const DEFAULT_ATTRIBUTES_FOR_FACETING: &[&str] = &[ const DEFAULT_ATTRIBUTES_FOR_FACETING: &[&str] = &[
"categories", "categories",
@@ -224,6 +224,7 @@ const DEFAULT_ATTRIBUTES_FOR_FACETING: &[&str] = &[
"project_id", "project_id",
"open_source", "open_source",
"color", "color",
"dependencies",
]; ];
const DEFAULT_SORTABLE_ATTRIBUTES: &[&str] = const DEFAULT_SORTABLE_ATTRIBUTES: &[&str] =

View File

@@ -99,6 +99,8 @@ pub struct UploadSearchProject {
pub modified_timestamp: i64, pub modified_timestamp: i64,
pub open_source: bool, pub open_source: bool,
pub color: Option<u32>, pub color: Option<u32>,
/// format: {project_id}-{dep_type}
pub dependencies: Vec<String>,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@@ -134,6 +136,8 @@ pub struct ResultSearchProject {
pub gallery: Vec<String>, pub gallery: Vec<String>,
pub featured_gallery: Option<String>, pub featured_gallery: Option<String>,
pub color: Option<u32>, pub color: Option<u32>,
/// format: {project_id}-{dep_type}
pub dependencies: Vec<String>,
} }
pub async fn search_for_project( pub async fn search_for_project(
@@ -177,13 +181,12 @@ pub async fn search_for_project(
None None
}; };
let filters: Cow<_> = let filters: Cow<_> = match (info.filters.as_deref(), info.version.as_deref()) {
match (info.filters.as_deref(), info.version.as_deref()) { (Some(f), Some(v)) => format!("({f}) AND ({v})").into(),
(Some(f), Some(v)) => format!("({f}) AND ({v})").into(), (Some(f), None) => f.into(),
(Some(f), None) => f.into(), (None, Some(v)) => v.into(),
(None, Some(v)) => v.into(), (None, None) => "".into(),
(None, None) => "".into(), };
};
if let Some(facets) = facets { if let Some(facets) = facets {
filter_string.push('('); filter_string.push('(');

View File

@@ -59,8 +59,7 @@ where
{ {
let github_user = get_github_user_from_token(access_token).await?; let github_user = get_github_user_from_token(access_token).await?;
let res = let res = models::User::get_from_github_id(github_user.id, executor).await?;
models::User::get_from_github_id(github_user.id, executor).await?;
match res { match res {
Some(result) => Ok(User { Some(result) => Ok(User {
@@ -190,8 +189,7 @@ pub async fn filter_authorized_projects(
.try_for_each(|e| { .try_for_each(|e| {
if let Some(row) = e.right() { if let Some(row) = e.right() {
check_projects.retain(|x| { check_projects.retain(|x| {
let bool = x.inner.id.0 == row.id let bool = x.inner.id.0 == row.id && x.inner.team_id.0 == row.team_id;
&& x.inner.team_id.0 == row.team_id;
if bool { if bool {
return_projects.push(x.clone().into()); 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 INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2
WHERE m.id = ANY($1) 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, user_id as database::models::ids::UserId,
) )
.fetch_many(&***pool) .fetch_many(&***pool)
.try_for_each(|e| { .try_for_each(|e| {
if let Some(row) = e.right() { if let Some(row) = e.right() {
check_versions.retain(|x| { check_versions.retain(|x| {
let bool = x.inner.project_id.0 == row.id; let bool = x.inner.project_id.0 == row.id;
if bool { if bool {
return_versions.push(x.clone().into()); return_versions.push(x.clone().into());
} }
!bool !bool
}); });
} }
futures::future::ready(Ok(())) futures::future::ready(Ok(()))
}).await?; })
.await?;
} }
} }

View File

@@ -2,9 +2,8 @@ use actix_web::guard::GuardContext;
pub const ADMIN_KEY_HEADER: &str = "Modrinth-Admin"; pub const ADMIN_KEY_HEADER: &str = "Modrinth-Admin";
pub fn admin_key_guard(ctx: &GuardContext) -> bool { pub fn admin_key_guard(ctx: &GuardContext) -> bool {
let admin_key = std::env::var("LABRINTH_ADMIN_KEY").expect( let admin_key = std::env::var("LABRINTH_ADMIN_KEY")
"No admin key provided, this should have been caught by check_env_vars", .expect("No admin key provided, this should have been caught by check_env_vars");
);
ctx.head() ctx.head()
.headers() .headers()

View File

@@ -6,15 +6,10 @@ pub fn get_color_from_img(data: &[u8]) -> Result<Option<u32>, ImageError> {
let image = image::load_from_memory(data)? let image = image::load_from_memory(data)?
.resize(256, 256, FilterType::Nearest) .resize(256, 256, FilterType::Nearest)
.crop_imm(128, 128, 64, 64); .crop_imm(128, 128, 64, 64);
let color = color_thief::get_palette( let color = color_thief::get_palette(image.to_rgb8().as_bytes(), ColorFormat::Rgb, 10, 2)
image.to_rgb8().as_bytes(), .ok()
ColorFormat::Rgb, .and_then(|x| x.get(0).copied())
10, .map(|x| (x.r as u32) << 16 | (x.g as u32) << 8 | (x.b as u32));
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) Ok(color)
} }

View File

@@ -18,9 +18,7 @@ pub async fn read_from_payload(
return Err(ApiError::InvalidInput(String::from(err_msg))); return Err(ApiError::InvalidInput(String::from(err_msg)));
} else { } else {
bytes.extend_from_slice(&item.map_err(|_| { bytes.extend_from_slice(&item.map_err(|_| {
ApiError::InvalidInput( ApiError::InvalidInput("Unable to parse bytes in payload sent!".to_string())
"Unable to parse bytes in payload sent!".to_string(),
)
})?); })?);
} }
} }
@@ -43,9 +41,7 @@ pub async fn read_from_field(
Ok(bytes) Ok(bytes)
} }
pub(crate) fn ok_or_not_found<T, U>( pub(crate) fn ok_or_not_found<T, U>(version_data: Option<T>) -> Result<HttpResponse, ApiError>
version_data: Option<T>,
) -> Result<HttpResponse, ApiError>
where where
U: From<T> + Serialize, U: From<T> + Serialize,
{ {

View File

@@ -4,15 +4,11 @@ use regex::Regex;
use validator::{ValidationErrors, ValidationErrorsKind}; use validator::{ValidationErrors, ValidationErrorsKind};
lazy_static! { lazy_static! {
pub static ref RE_URL_SAFE: Regex = pub static ref RE_URL_SAFE: Regex = Regex::new(r#"^[a-zA-Z0-9!@$()`.+,_"-]*$"#).unwrap();
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! //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( pub fn validation_errors_to_string(errors: ValidationErrors, adder: Option<String>) -> String {
errors: ValidationErrors,
adder: Option<String>,
) -> String {
let mut output = String::new(); let mut output = String::new();
let map = errors.into_errors(); let map = errors.into_errors();
@@ -23,10 +19,7 @@ pub fn validation_errors_to_string(
if let Some(error) = map.get(field) { if let Some(error) = map.get(field) {
return match error { return match error {
ValidationErrorsKind::Struct(errors) => { ValidationErrorsKind::Struct(errors) => {
validation_errors_to_string( validation_errors_to_string(*errors.clone(), Some(format!("of item {field}")))
*errors.clone(),
Some(format!("of item {field}")),
)
} }
ValidationErrorsKind::List(list) => { ValidationErrorsKind::List(list) => {
if let Some((index, errors)) = list.iter().next() { if let Some((index, errors)) = list.iter().next() {

View File

@@ -180,8 +180,7 @@ pub async fn send_discord_webhook(
} }
if !versions.is_empty() { if !versions.is_empty() {
let formatted_game_versions: String = let formatted_game_versions: String = get_gv_range(versions, all_game_versions);
get_gv_range(versions, all_game_versions);
fields.push(DiscordEmbedField { fields.push(DiscordEmbedField {
name: "Versions", name: "Versions",
@@ -229,9 +228,7 @@ pub async fn send_discord_webhook(
thumbnail: DiscordEmbedThumbnail { thumbnail: DiscordEmbedThumbnail {
url: project.icon_url, url: project.icon_url,
}, },
image: if let Some(first) = image: if let Some(first) = project.featured_gallery.unwrap_or_default().first() {
project.featured_gallery.unwrap_or_default().first()
{
Some(first.clone()) Some(first.clone())
} else { } else {
project.gallery.unwrap_or_default().first().cloned() 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} on Modrinth",
display_project_type.remove(0).to_uppercase() display_project_type.remove(0).to_uppercase()
), ),
icon_url: Some( icon_url: Some("https://cdn-raw.modrinth.com/modrinth-new.png".to_string()),
"https://cdn-raw.modrinth.com/modrinth-new.png".to_string(),
),
}), }),
}; };
@@ -253,10 +248,7 @@ pub async fn send_discord_webhook(
client client
.post(&webhook_url) .post(&webhook_url)
.json(&DiscordWebhook { .json(&DiscordWebhook {
avatar_url: Some( avatar_url: Some("https://cdn.modrinth.com/Modrinth_Dark_Logo.png".to_string()),
"https://cdn.modrinth.com/Modrinth_Dark_Logo.png"
.to_string(),
),
username: Some("Modrinth Release".to_string()), username: Some("Modrinth Release".to_string()),
embeds: vec![embed], embeds: vec![embed],
content: message, content: message,
@@ -264,9 +256,7 @@ pub async fn send_discord_webhook(
.send() .send()
.await .await
.map_err(|_| { .map_err(|_| {
ApiError::DiscordError( ApiError::DiscordError("Error while sending projects webhook".to_string())
"Error while sending projects webhook".to_string(),
)
})?; })?;
} }
@@ -310,21 +300,15 @@ fn get_gv_range(
} else { } else {
let interval_base = &intervals[current_interval]; let interval_base = &intervals[current_interval];
if ((index as i32) if ((index as i32) - (interval_base[interval_base.len() - 1][1] as i32) == 1
- (interval_base[interval_base.len() - 1][1] as i32) || (release_index as i32) - (interval_base[interval_base.len() - 1][2] as i32) == 1)
== 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[interval_base[0][1]].type_ == "release"
|| all_game_versions[index].type_ != "release") || all_game_versions[index].type_ != "release")
{ {
if intervals[current_interval].get(1).is_some() { if intervals[current_interval].get(1).is_some() {
intervals[current_interval][1] = intervals[current_interval][1] = vec![i, index, release_index];
vec![i, index, release_index];
} else { } else {
intervals[current_interval] intervals[current_interval].insert(1, vec![i, index, release_index]);
.insert(1, vec![i, index, release_index]);
} }
} else { } else {
current_interval += 1; current_interval += 1;
@@ -336,10 +320,7 @@ fn get_gv_range(
let mut new_intervals = Vec::new(); let mut new_intervals = Vec::new();
for interval in intervals { for interval in intervals {
if interval.len() == 2 if interval.len() == 2 && interval[0][2] != MAX_VALUE && interval[1][2] == MAX_VALUE {
&& interval[0][2] != MAX_VALUE
&& interval[1][2] == MAX_VALUE
{
let mut last_snapshot: Option<usize> = None; let mut last_snapshot: Option<usize> = None;
for j in ((interval[0][1] + 1)..=interval[1][1]).rev() { for j in ((interval[0][1] + 1)..=interval[1][1]).rev() {
@@ -349,16 +330,12 @@ fn get_gv_range(
vec![ vec![
game_versions game_versions
.iter() .iter()
.position(|x| { .position(|x| x.version == all_game_versions[j].version)
x.version == all_game_versions[j].version
})
.unwrap_or(MAX_VALUE), .unwrap_or(MAX_VALUE),
j, j,
all_releases all_releases
.iter() .iter()
.position(|x| { .position(|x| x.version == all_game_versions[j].version)
x.version == all_game_versions[j].version
})
.unwrap_or(MAX_VALUE), .unwrap_or(MAX_VALUE),
], ],
]); ]);
@@ -370,10 +347,7 @@ fn get_gv_range(
game_versions game_versions
.iter() .iter()
.position(|x| { .position(|x| {
x.version x.version == all_game_versions[last_snapshot].version
== all_game_versions
[last_snapshot]
.version
}) })
.unwrap_or(MAX_VALUE), .unwrap_or(MAX_VALUE),
last_snapshot, last_snapshot,
@@ -402,8 +376,7 @@ fn get_gv_range(
if interval.len() == 2 { if interval.len() == 2 {
output.push(format!( output.push(format!(
"{}{}", "{}{}",
&game_versions[interval[0][0]].version, &game_versions[interval[0][0]].version, &game_versions[interval[1][0]].version
&game_versions[interval[1][0]].version
)) ))
} else { } else {
output.push(game_versions[interval[0][0]].version.clone()) output.push(game_versions[interval[0][0]].version.clone())

View File

@@ -1,6 +1,4 @@
use crate::validate::{ use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult};
SupportedGameVersions, ValidationError, ValidationResult,
};
use std::io::Cursor; use std::io::Cursor;
use zip::ZipArchive; use zip::ZipArchive;

View File

@@ -1,6 +1,4 @@
use crate::validate::{ use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult};
SupportedGameVersions, ValidationError, ValidationResult,
};
use chrono::{DateTime, NaiveDateTime, Utc}; use chrono::{DateTime, NaiveDateTime, Utc};
use std::io::Cursor; use std::io::Cursor;
use zip::ZipArchive; use zip::ZipArchive;
@@ -38,9 +36,10 @@ impl super::Validator for FabricValidator {
)); ));
} }
if !archive.file_names().any(|name| { if !archive
name.ends_with("refmap.json") || name.ends_with(".class") .file_names()
}) { .any(|name| name.ends_with("refmap.json") || name.ends_with(".class"))
{
return Ok(ValidationResult::Warning( return Ok(ValidationResult::Warning(
"Fabric mod file is a source file!", "Fabric mod file is a source file!",
)); ));

View File

@@ -1,6 +1,4 @@
use crate::validate::{ use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult};
SupportedGameVersions, ValidationError, ValidationResult,
};
use chrono::{DateTime, NaiveDateTime, Utc}; use chrono::{DateTime, NaiveDateTime, Utc};
use std::io::Cursor; use std::io::Cursor;
use zip::ZipArchive; use zip::ZipArchive;

View File

@@ -1,6 +1,4 @@
use crate::validate::{ use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult};
SupportedGameVersions, ValidationError, ValidationResult,
};
use std::io::Cursor; use std::io::Cursor;
use zip::ZipArchive; use zip::ZipArchive;

View File

@@ -8,9 +8,7 @@ use crate::validate::modpack::ModpackValidator;
use crate::validate::plugin::*; use crate::validate::plugin::*;
use crate::validate::quilt::QuiltValidator; use crate::validate::quilt::QuiltValidator;
use crate::validate::resourcepack::{PackValidator, TexturePackValidator}; use crate::validate::resourcepack::{PackValidator, TexturePackValidator};
use crate::validate::shader::{ use crate::validate::shader::{CanvasShaderValidator, CoreShaderValidator, ShaderValidator};
CanvasShaderValidator, CoreShaderValidator, ShaderValidator,
};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use std::io::Cursor; use std::io::Cursor;
use thiserror::Error; use thiserror::Error;
@@ -119,8 +117,7 @@ pub async fn validate_file(
if let Some(file_type) = file_type { if let Some(file_type) = file_type {
match file_type { match file_type {
FileType::RequiredResourcePack FileType::RequiredResourcePack | FileType::OptionalResourcePack => {
| FileType::OptionalResourcePack => {
project_type = "resourcepack".to_string(); project_type = "resourcepack".to_string();
loaders = vec![Loader("minecraft".to_string())]; loaders = vec![Loader("minecraft".to_string())];
} }
@@ -150,13 +147,12 @@ pub async fn validate_file(
if visited { if visited {
if ALWAYS_ALLOWED_EXT.contains(&&*file_extension) { 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 { } else {
Err(ValidationError::InvalidInput( Err(ValidationError::InvalidInput(
format!( format!("File extension {file_extension} is invalid for input file").into(),
"File extension {file_extension} is invalid for input file"
)
.into(),
)) ))
} }
} else { } else {
@@ -173,24 +169,20 @@ fn game_version_supported(
) -> bool { ) -> bool {
match supported_game_versions { match supported_game_versions {
SupportedGameVersions::All => true, SupportedGameVersions::All => true,
SupportedGameVersions::PastDate(date) => { SupportedGameVersions::PastDate(date) => game_versions.iter().any(|x| {
game_versions.iter().any(|x| { all_game_versions
all_game_versions .iter()
.iter() .find(|y| y.version == x.0)
.find(|y| y.version == x.0) .map(|x| x.created > date)
.map(|x| x.created > date) .unwrap_or(false)
.unwrap_or(false) }),
}) SupportedGameVersions::Range(before, after) => game_versions.iter().any(|x| {
} all_game_versions
SupportedGameVersions::Range(before, after) => { .iter()
game_versions.iter().any(|x| { .find(|y| y.version == x.0)
all_game_versions .map(|x| x.created > before && x.created < after)
.iter() .unwrap_or(false)
.find(|y| y.version == x.0) }),
.map(|x| x.created > before && x.created < after)
.unwrap_or(false)
})
}
SupportedGameVersions::Custom(versions) => { SupportedGameVersions::Custom(versions) => {
versions.iter().any(|x| game_versions.contains(x)) versions.iter().any(|x| game_versions.contains(x))
} }

View File

@@ -1,8 +1,6 @@
use crate::models::pack::{PackFileHash, PackFormat}; use crate::models::pack::{PackFileHash, PackFormat};
use crate::util::validate::validation_errors_to_string; use crate::util::validate::validation_errors_to_string;
use crate::validate::{ use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult};
SupportedGameVersions, ValidationError, ValidationResult,
};
use std::io::{Cursor, Read}; use std::io::{Cursor, Read};
use std::path::Component; use std::path::Component;
use validator::Validate; use validator::Validate;
@@ -32,14 +30,11 @@ impl super::Validator for ModpackValidator {
archive: &mut ZipArchive<Cursor<bytes::Bytes>>, archive: &mut ZipArchive<Cursor<bytes::Bytes>>,
) -> Result<ValidationResult, ValidationError> { ) -> Result<ValidationResult, ValidationError> {
let pack: PackFormat = { let pack: PackFormat = {
let mut file = let mut file = if let Ok(file) = archive.by_name("modrinth.index.json") {
if let Ok(file) = archive.by_name("modrinth.index.json") { file
file } else {
} else { return Ok(ValidationResult::Warning("Pack manifest is missing."));
return Ok(ValidationResult::Warning( };
"Pack manifest is missing.",
));
};
let mut contents = String::new(); let mut contents = String::new();
file.read_to_string(&mut contents)?; file.read_to_string(&mut contents)?;
@@ -48,9 +43,7 @@ impl super::Validator for ModpackValidator {
}; };
pack.validate().map_err(|err| { pack.validate().map_err(|err| {
ValidationError::InvalidInput( ValidationError::InvalidInput(validation_errors_to_string(err, None).into())
validation_errors_to_string(err, None).into(),
)
})?; })?;
if pack.game != "minecraft" { if pack.game != "minecraft" {
@@ -75,11 +68,7 @@ impl super::Validator for ModpackValidator {
let path = std::path::Path::new(&file.path) let path = std::path::Path::new(&file.path)
.components() .components()
.next() .next()
.ok_or_else(|| { .ok_or_else(|| ValidationError::InvalidInput("Invalid pack file path!".into()))?;
ValidationError::InvalidInput(
"Invalid pack file path!".into(),
)
})?;
match path { match path {
Component::CurDir | Component::Normal(_) => {} Component::CurDir | Component::Normal(_) => {}

View File

@@ -1,6 +1,4 @@
use crate::validate::{ use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult};
SupportedGameVersions, ValidationError, ValidationResult,
};
use std::io::Cursor; use std::io::Cursor;
use zip::ZipArchive; use zip::ZipArchive;

View File

@@ -1,6 +1,4 @@
use crate::validate::{ use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult};
SupportedGameVersions, ValidationError, ValidationResult,
};
use chrono::{DateTime, NaiveDateTime, Utc}; use chrono::{DateTime, NaiveDateTime, Utc};
use std::io::Cursor; use std::io::Cursor;
use zip::ZipArchive; use zip::ZipArchive;
@@ -37,9 +35,10 @@ impl super::Validator for QuiltValidator {
)); ));
} }
if !archive.file_names().any(|name| { if !archive
name.ends_with("refmap.json") || name.ends_with(".class") .file_names()
}) { .any(|name| name.ends_with("refmap.json") || name.ends_with(".class"))
{
return Ok(ValidationResult::Warning( return Ok(ValidationResult::Warning(
"Quilt mod file is a source file!", "Quilt mod file is a source file!",
)); ));

View File

@@ -1,6 +1,4 @@
use crate::validate::{ use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult};
SupportedGameVersions, ValidationError, ValidationResult,
};
use chrono::{DateTime, NaiveDateTime, Utc}; use chrono::{DateTime, NaiveDateTime, Utc};
use std::io::Cursor; use std::io::Cursor;
use zip::ZipArchive; use zip::ZipArchive;

View File

@@ -1,6 +1,4 @@
use crate::validate::{ use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult};
SupportedGameVersions, ValidationError, ValidationResult,
};
use std::io::Cursor; use std::io::Cursor;
use zip::ZipArchive; use zip::ZipArchive;