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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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 "
},
"0f6469055265ad8b114136368001aa927b587df9f64f0e19fd37d1f4b4adab60": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Int8"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "\n SELECT id FROM mods\n WHERE status = $1 AND queued < NOW() - INTERVAL '1 day'\n ORDER BY updated ASC\n "
},
"0fb1cca8a2a37107104244953371fe2f8a5e6edd57f4b325c5842c6571eb16b4": {
"describe": {
"columns": [
@ -566,6 +546,178 @@
},
"query": "\n SELECT EXISTS(SELECT 1 FROM mod_follows mf WHERE mf.follower_id = $1 AND mf.mod_id = $2)\n "
},
"113bffbd003f0f32eef61468148a51dd9437be841c5b79fdb52dd6c12ebaba61": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Int8"
},
{
"name": "project_type",
"ordinal": 1,
"type_info": "Int4"
},
{
"name": "title",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "description",
"ordinal": 3,
"type_info": "Varchar"
},
{
"name": "downloads",
"ordinal": 4,
"type_info": "Int4"
},
{
"name": "follows",
"ordinal": 5,
"type_info": "Int4"
},
{
"name": "icon_url",
"ordinal": 6,
"type_info": "Varchar"
},
{
"name": "published",
"ordinal": 7,
"type_info": "Timestamptz"
},
{
"name": "approved",
"ordinal": 8,
"type_info": "Timestamptz"
},
{
"name": "updated",
"ordinal": 9,
"type_info": "Timestamptz"
},
{
"name": "team_id",
"ordinal": 10,
"type_info": "Int8"
},
{
"name": "license",
"ordinal": 11,
"type_info": "Varchar"
},
{
"name": "slug",
"ordinal": 12,
"type_info": "Varchar"
},
{
"name": "status_name",
"ordinal": 13,
"type_info": "Varchar"
},
{
"name": "color",
"ordinal": 14,
"type_info": "Int4"
},
{
"name": "client_side_type",
"ordinal": 15,
"type_info": "Varchar"
},
{
"name": "server_side_type",
"ordinal": 16,
"type_info": "Varchar"
},
{
"name": "project_type_name",
"ordinal": 17,
"type_info": "Varchar"
},
{
"name": "username",
"ordinal": 18,
"type_info": "Varchar"
},
{
"name": "categories",
"ordinal": 19,
"type_info": "VarcharArray"
},
{
"name": "additional_categories",
"ordinal": 20,
"type_info": "VarcharArray"
},
{
"name": "loaders",
"ordinal": 21,
"type_info": "VarcharArray"
},
{
"name": "versions",
"ordinal": 22,
"type_info": "VarcharArray"
},
{
"name": "gallery",
"ordinal": 23,
"type_info": "VarcharArray"
},
{
"name": "featured_gallery",
"ordinal": 24,
"type_info": "VarcharArray"
},
{
"name": "dependencies",
"ordinal": 25,
"type_info": "Jsonb"
}
],
"nullable": [
false,
false,
false,
false,
false,
false,
true,
false,
true,
false,
false,
false,
true,
false,
true,
false,
false,
false,
false,
null,
null,
null,
null,
null,
null,
null
],
"parameters": {
"Left": [
"TextArray",
"TextArray",
"Text"
]
}
},
"query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.published published, m.approved approved, m.updated updated,\n m.team_id team_id, m.license license, m.slug slug, m.status status_name, m.color color,\n cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, u.username username,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,\n ARRAY_AGG(DISTINCT gv.version) filter (where gv.version is not null) versions,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery,\n JSONB_AGG(DISTINCT jsonb_build_object('id', mdep.id, 'dep_type', d.dependency_type)) filter (where mdep.id is not null) dependencies\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id AND v.status != ANY($1)\n LEFT OUTER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id\n LEFT OUTER JOIN game_versions gv ON gvv.game_version_id = gv.id\n LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id\n LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id\n LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id\n LEFT OUTER JOIN dependencies d ON d.dependent_id = v.id\n LEFT OUTER JOIN mods mdep ON mdep.id = d.mod_dependency_id\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE\n INNER JOIN users u ON tm.user_id = u.id\n WHERE m.status = ANY($2)\n GROUP BY m.id, cs.id, ss.id, pt.id, u.id;\n "
},
"1209ffc1ffbea89f7060573275dc7325ac4d7b4885b6c1d1ec92998e6012e455": {
"describe": {
"columns": [],
@ -611,6 +763,18 @@
},
"query": "\n UPDATE mods\n SET webhook_sent = TRUE\n WHERE id = $1\n "
},
"127691940ca7e542e246dd2a1c9cb391041b30ddf0547d73b49c1dd9dc59d2ae": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Int8Array"
]
}
},
"query": "\n UPDATE notifications\n SET read = TRUE\n WHERE id = ANY($1)\n "
},
"1411c9ae3af067679aa21d7f45937cd94d457e4eb17a108566776a9bd1ee77e2": {
"describe": {
"columns": [],
@ -1898,19 +2062,6 @@
},
"query": "\n SELECT r.id, rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created, r.thread_id, r.closed\n FROM reports r\n INNER JOIN report_types rt ON rt.id = r.report_type_id\n WHERE r.id = ANY($1)\n ORDER BY r.created DESC\n "
},
"3ae7c4a29dab8bce0e84a9c47a4a4f50a3be4bcb86e5b13d7dd60975d62e9ea3": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Int8",
"Varchar"
]
}
},
"query": "\n INSERT INTO threads (\n id, thread_type\n )\n VALUES (\n $1, $2\n )\n "
},
"3af747b5543a5a9b10dcce0a1eb9c2a1926dd5a507fe0d8b7f52d8ccc7fcd0af": {
"describe": {
"columns": [],
@ -2673,6 +2824,26 @@
},
"query": "\n UPDATE versions\n SET version_number = $1\n WHERE (id = $2)\n "
},
"5586d60c8f3d58a31e6635ffb3cb30bac389bf21b190dfd1e64a44e837f3879c": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Int8"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "\n SELECT id FROM mods\n WHERE status = $1 AND queued < NOW() - INTERVAL '40 hours'\n ORDER BY updated ASC\n "
},
"57743e20646dab2bcc02fe555d6b8ddb999697b7e95ec732d1a1a9e2bfdb8181": {
"describe": {
"columns": [
@ -4374,6 +4545,32 @@
},
"query": "\n SELECT u.id, u.github_id, u.name, u.email,\n u.avatar_url, u.username, u.bio,\n u.created, u.role, u.badges,\n u.balance, u.payout_wallet, u.payout_wallet_type,\n u.payout_address\n FROM users u\n WHERE u.id = ANY($1)\n "
},
"a962f21969bba402258fca169c45f3d71bc1b71f754cdcc1f5c968e4948653b2": {
"describe": {
"columns": [
{
"name": "notifs_count",
"ordinal": 0,
"type_info": "Int8"
},
{
"name": "followed_projects",
"ordinal": 1,
"type_info": "Int8Array"
}
],
"nullable": [
null,
null
],
"parameters": {
"Left": [
"Int8"
]
}
},
"query": "\n SELECT COUNT(DISTINCT n.id) notifs_count, ARRAY_AGG(mf.mod_id) followed_projects FROM notifications n\n LEFT OUTER JOIN mod_follows mf ON mf.follower_id = $1\n WHERE user_id = $1 AND read = FALSE\n "
},
"aa59f79136ef87dd4121d5f367f5dbdbca80e936c1b986ec99c09c3e95daa756": {
"describe": {
"columns": [],
@ -4752,172 +4949,6 @@
},
"query": "\n DELETE FROM game_versions_versions gvv\n WHERE gvv.joining_version_id = $1\n "
},
"bf4afeda41a54e09a80a4cc505d1fbb72124c442ebaca731a291f022524daf1a": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Int8"
},
{
"name": "project_type",
"ordinal": 1,
"type_info": "Int4"
},
{
"name": "title",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "description",
"ordinal": 3,
"type_info": "Varchar"
},
{
"name": "downloads",
"ordinal": 4,
"type_info": "Int4"
},
{
"name": "follows",
"ordinal": 5,
"type_info": "Int4"
},
{
"name": "icon_url",
"ordinal": 6,
"type_info": "Varchar"
},
{
"name": "published",
"ordinal": 7,
"type_info": "Timestamptz"
},
{
"name": "approved",
"ordinal": 8,
"type_info": "Timestamptz"
},
{
"name": "updated",
"ordinal": 9,
"type_info": "Timestamptz"
},
{
"name": "team_id",
"ordinal": 10,
"type_info": "Int8"
},
{
"name": "license",
"ordinal": 11,
"type_info": "Varchar"
},
{
"name": "slug",
"ordinal": 12,
"type_info": "Varchar"
},
{
"name": "status_name",
"ordinal": 13,
"type_info": "Varchar"
},
{
"name": "color",
"ordinal": 14,
"type_info": "Int4"
},
{
"name": "client_side_type",
"ordinal": 15,
"type_info": "Varchar"
},
{
"name": "server_side_type",
"ordinal": 16,
"type_info": "Varchar"
},
{
"name": "project_type_name",
"ordinal": 17,
"type_info": "Varchar"
},
{
"name": "username",
"ordinal": 18,
"type_info": "Varchar"
},
{
"name": "categories",
"ordinal": 19,
"type_info": "VarcharArray"
},
{
"name": "additional_categories",
"ordinal": 20,
"type_info": "VarcharArray"
},
{
"name": "loaders",
"ordinal": 21,
"type_info": "VarcharArray"
},
{
"name": "versions",
"ordinal": 22,
"type_info": "VarcharArray"
},
{
"name": "gallery",
"ordinal": 23,
"type_info": "VarcharArray"
},
{
"name": "featured_gallery",
"ordinal": 24,
"type_info": "VarcharArray"
}
],
"nullable": [
false,
false,
false,
false,
false,
false,
true,
false,
true,
false,
false,
false,
true,
false,
true,
false,
false,
false,
false,
null,
null,
null,
null,
null,
null
],
"parameters": {
"Left": [
"TextArray",
"TextArray",
"Text"
]
}
},
"query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.published published, m.approved approved, m.updated updated,\n m.team_id team_id, m.license license, m.slug slug, m.status status_name, m.color color,\n cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, u.username username,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,\n ARRAY_AGG(DISTINCT gv.version) filter (where gv.version is not null) versions,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id AND v.status != ANY($1)\n LEFT OUTER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id\n LEFT OUTER JOIN game_versions gv ON gvv.game_version_id = gv.id\n LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id\n LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id\n LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE\n INNER JOIN users u ON tm.user_id = u.id\n WHERE m.status = ANY($2)\n GROUP BY m.id, cs.id, ss.id, pt.id, u.id;\n "
},
"bf7f721664f5e0ed41adc41b5483037256635f28ff6c4e5d3cbcec4387f9c8ef": {
"describe": {
"columns": [
@ -5373,6 +5404,21 @@
},
"query": "\n SELECT v.id version_id, v.mod_id project_id, h.hash hash FROM hashes h\n INNER JOIN files f on h.file_id = f.id\n INNER JOIN versions v on f.version_id = v.id\n WHERE h.algorithm = 'sha1' AND h.hash = ANY($1)\n "
},
"d0a65443aef9d3781000d0a59d8d81d1a8e51613dfee343d079c233375cebd12": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Int8",
"Varchar",
"Int8",
"Int8"
]
}
},
"query": "\n INSERT INTO threads (\n id, thread_type, report_id, project_id\n )\n VALUES (\n $1, $2, $3, $4\n )\n "
},
"d12bc07adb4dc8147d0ddccd72a4f23ed38cd31d7db3d36ebbe2c9b627130f0b": {
"describe": {
"columns": [],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -499,8 +499,7 @@ impl Version {
{
use futures::stream::TryStreamExt;
let version_ids_parsed: Vec<i64> =
version_ids.iter().map(|x| x.0).collect();
let version_ids_parsed: Vec<i64> = version_ids.iter().map(|x| x.0).collect();
sqlx::query!(
"
SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,
@ -648,8 +647,7 @@ impl Version {
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
let project_id_opt =
parse_base62(project_id_or_slug).ok().map(|x| x as i64);
let project_id_opt = parse_base62(project_id_or_slug).ok().map(|x| x as i64);
let id_opt = parse_base62(slug).ok().map(|x| x as i64);
let id = sqlx::query!(
"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,11 +4,10 @@ use log::info;
use super::IndexingError;
use crate::database::models::ProjectId;
use crate::search::UploadSearchProject;
use serde::Deserialize;
use sqlx::postgres::PgPool;
pub async fn index_local(
pool: PgPool,
) -> Result<Vec<UploadSearchProject>, IndexingError> {
pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchProject>, IndexingError> {
info!("Indexing local projects!");
Ok(
@ -23,7 +22,8 @@ pub async fn index_local(
ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,
ARRAY_AGG(DISTINCT gv.version) filter (where gv.version is not null) versions,
ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery,
ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery
ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery,
JSONB_AGG(DISTINCT jsonb_build_object('id', mdep.id, 'dep_type', d.dependency_type)) filter (where mdep.id is not null) dependencies
FROM mods m
LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id
LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id
@ -33,6 +33,8 @@ pub async fn index_local(
LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id
LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id
LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id
LEFT OUTER JOIN dependencies d ON d.dependent_id = v.id
LEFT OUTER JOIN mods mdep ON mdep.id = d.mod_dependency_id
INNER JOIN project_types pt ON pt.id = m.project_type
INNER JOIN side_types cs ON m.client_side = cs.id
INNER JOIN side_types ss ON m.server_side = ss.id
@ -70,6 +72,21 @@ pub async fn index_local(
_ => false,
};
#[derive(Deserialize)]
struct TempDependency {
id: ProjectId,
dep_type: String
}
let dependencies = serde_json::from_value::<Vec<TempDependency>>(
m.dependencies.unwrap_or_default(),
)
.ok()
.unwrap_or_default()
.into_iter()
.map(|x| format!("{}-{}", crate::models::ids::ProjectId::from(x.id), x.dep_type))
.collect();
UploadSearchProject {
project_id: project_id.to_string(),
title: m.title,
@ -95,6 +112,7 @@ pub async fn index_local(
open_source,
color: m.color.map(|x| x as u32),
featured_gallery: m.featured_gallery.unwrap_or_default().first().cloned(),
dependencies,
}
}))
})

View File

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

View File

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

View File

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

View File

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

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)?
.resize(256, 256, FilterType::Nearest)
.crop_imm(128, 128, 64, 64);
let color = color_thief::get_palette(
image.to_rgb8().as_bytes(),
ColorFormat::Rgb,
10,
2,
)
.ok()
.and_then(|x| x.get(0).copied())
.map(|x| (x.r as u32) << 16 | (x.g as u32) << 8 | (x.b as u32));
let color = color_thief::get_palette(image.to_rgb8().as_bytes(), ColorFormat::Rgb, 10, 2)
.ok()
.and_then(|x| x.get(0).copied())
.map(|x| (x.r as u32) << 16 | (x.g as u32) << 8 | (x.b as u32));
Ok(color)
}

View File

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

View File

@ -4,15 +4,11 @@ use regex::Regex;
use validator::{ValidationErrors, ValidationErrorsKind};
lazy_static! {
pub static ref RE_URL_SAFE: Regex =
Regex::new(r#"^[a-zA-Z0-9!@$()`.+,_"-]*$"#).unwrap();
pub static ref RE_URL_SAFE: Regex = Regex::new(r#"^[a-zA-Z0-9!@$()`.+,_"-]*$"#).unwrap();
}
//TODO: In order to ensure readability, only the first error is printed, this may need to be expanded on in the future!
pub fn validation_errors_to_string(
errors: ValidationErrors,
adder: Option<String>,
) -> String {
pub fn validation_errors_to_string(errors: ValidationErrors, adder: Option<String>) -> String {
let mut output = String::new();
let map = errors.into_errors();
@ -23,10 +19,7 @@ pub fn validation_errors_to_string(
if let Some(error) = map.get(field) {
return match error {
ValidationErrorsKind::Struct(errors) => {
validation_errors_to_string(
*errors.clone(),
Some(format!("of item {field}")),
)
validation_errors_to_string(*errors.clone(), Some(format!("of item {field}")))
}
ValidationErrorsKind::List(list) => {
if let Some((index, errors)) = list.iter().next() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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