You've already forked AstralRinth
forked from didirus/AstralRinth
Final V2 Changes (#212)
* Redo dependencies, add rejection reasons, make notifications more readable * Fix errors, add dependency route, finish PR * Fix clippy errors
This commit is contained in:
@@ -1,21 +1,23 @@
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::database;
|
||||
use crate::database::cache::project_cache::remove_cache_project;
|
||||
use crate::database::cache::query_project_cache::remove_cache_query_project;
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models;
|
||||
use crate::models::projects::{
|
||||
DonationLink, License, ProjectId, ProjectStatus, SearchRequest, SideType,
|
||||
DonationLink, License, ProjectId, ProjectStatus, RejectionReason, SearchRequest, SideType,
|
||||
};
|
||||
use crate::models::teams::Permissions;
|
||||
use crate::routes::ApiError;
|
||||
use crate::search::indexing::queue::CreationQueue;
|
||||
use crate::search::{search_for_project, SearchConfig, SearchError};
|
||||
use crate::util::auth::get_user_from_headers;
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
|
||||
use futures::StreamExt;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use validator::Validate;
|
||||
|
||||
@@ -91,7 +93,8 @@ 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.clone(), &**pool)
|
||||
.await?;
|
||||
|
||||
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
@@ -129,6 +132,94 @@ pub async fn project_get(
|
||||
}
|
||||
}
|
||||
|
||||
struct DependencyInfo {
|
||||
pub project: Option<models::projects::Project>,
|
||||
pub version: Option<models::projects::Version>,
|
||||
}
|
||||
|
||||
#[get("dependencies")]
|
||||
pub async fn dependency_list(
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let result = database::models::Project::get_from_slug_or_project_id(string, &**pool).await?;
|
||||
|
||||
if let Some(project) = result {
|
||||
let id = project.id;
|
||||
|
||||
use futures::stream::TryStreamExt;
|
||||
|
||||
let dependencies = sqlx::query!(
|
||||
"
|
||||
SELECT d.dependent_id, d.dependency_id, d.mod_dependency_id
|
||||
FROM versions v
|
||||
INNER JOIN dependencies d ON d.dependent_id = v.id
|
||||
WHERE v.mod_id = $1
|
||||
",
|
||||
id as database::models::ProjectId
|
||||
)
|
||||
.fetch_many(&**pool)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|x| {
|
||||
(
|
||||
database::models::VersionId(x.dependent_id),
|
||||
x.dependency_id.map(database::models::VersionId),
|
||||
x.mod_dependency_id.map(database::models::ProjectId),
|
||||
)
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<(
|
||||
database::models::VersionId,
|
||||
Option<database::models::VersionId>,
|
||||
Option<database::models::ProjectId>,
|
||||
)>>()
|
||||
.await?;
|
||||
|
||||
let projects = database::Project::get_many_full(
|
||||
dependencies.iter().map(|x| x.2).flatten().collect(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
let versions = database::Version::get_many_full(
|
||||
dependencies.iter().map(|x| x.1).flatten().collect(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut response: HashMap<models::projects::VersionId, DependencyInfo> = HashMap::new();
|
||||
|
||||
for dependency in dependencies {
|
||||
response.insert(
|
||||
dependency.0.into(),
|
||||
DependencyInfo {
|
||||
project: if let Some(id) = dependency.2 {
|
||||
projects
|
||||
.iter()
|
||||
.find(|x| x.inner.id == id)
|
||||
.map(|x| convert_project(x.clone()))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
version: if let Some(id) = dependency.1 {
|
||||
versions
|
||||
.iter()
|
||||
.find(|x| x.id == id)
|
||||
.map(|x| super::versions::convert_version(x.clone()))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert_project(
|
||||
data: database::models::project_item::QueryProject,
|
||||
) -> models::projects::Project {
|
||||
@@ -146,6 +237,14 @@ pub fn convert_project(
|
||||
published: m.published,
|
||||
updated: m.updated,
|
||||
status: data.status,
|
||||
rejection_data: if let Some(reason) = m.rejection_reason {
|
||||
Some(RejectionReason {
|
||||
reason,
|
||||
body: m.rejection_body,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
},
|
||||
license: License {
|
||||
id: data.license_id,
|
||||
name: data.license_name,
|
||||
@@ -175,10 +274,6 @@ pub fn convert_project(
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref RE_URL_SAFE: Regex = Regex::new(r"^[a-zA-Z0-9_-]*$").unwrap();
|
||||
}
|
||||
|
||||
/// A project returned from the API
|
||||
#[derive(Serialize, Deserialize, Validate)]
|
||||
pub struct EditProject {
|
||||
@@ -188,7 +283,6 @@ pub struct EditProject {
|
||||
pub description: Option<String>,
|
||||
#[validate(length(max = 65536))]
|
||||
pub body: Option<String>,
|
||||
pub status: Option<ProjectStatus>,
|
||||
#[validate(length(max = 3))]
|
||||
pub categories: Option<Vec<String>>,
|
||||
#[serde(
|
||||
@@ -236,8 +330,26 @@ pub struct EditProject {
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
#[validate(length(min = 3, max = 64), regex = "RE_URL_SAFE")]
|
||||
#[validate(
|
||||
length(min = 3, max = 64),
|
||||
regex = "crate::util::validate::RE_URL_SAFE"
|
||||
)]
|
||||
pub slug: Option<Option<String>>,
|
||||
pub status: Option<ProjectStatus>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
#[validate(length(max = 2000))]
|
||||
pub rejection_reason: Option<Option<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
#[validate(length(max = 65536))]
|
||||
pub rejection_body: Option<Option<String>>,
|
||||
}
|
||||
|
||||
#[patch("{id}")]
|
||||
@@ -251,11 +363,14 @@ pub async fn project_edit(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
new_project.validate()?;
|
||||
new_project
|
||||
.validate()
|
||||
.map_err(|err| ApiError::ValidationError(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?;
|
||||
database::models::Project::get_full_from_slug_or_project_id(string.clone(), &**pool)
|
||||
.await?;
|
||||
|
||||
if let Some(project_item) = result {
|
||||
let id = project_item.inner.id;
|
||||
@@ -337,6 +452,12 @@ pub async fn project_edit(
|
||||
));
|
||||
}
|
||||
|
||||
if status == &ProjectStatus::Processing && project_item.versions.is_empty() {
|
||||
return Err(ApiError::InvalidInputError(String::from(
|
||||
"Project submitted for review with no initial versions",
|
||||
)));
|
||||
}
|
||||
|
||||
let status_id = database::models::StatusId::get_id(&status, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
@@ -357,6 +478,30 @@ pub async fn project_edit(
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
if project_item.status == ProjectStatus::Processing {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET rejection_reason = NULL
|
||||
WHERE (id = $1)
|
||||
",
|
||||
id as database::models::ids::ProjectId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET rejection_body = NULL
|
||||
WHERE (id = $1)
|
||||
",
|
||||
id as database::models::ids::ProjectId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if project_item.status.is_searchable() && !status.is_searchable() {
|
||||
delete_from_index(id.into(), config).await?;
|
||||
} else if !project_item.status.is_searchable() && status.is_searchable() {
|
||||
@@ -365,6 +510,15 @@ pub async fn project_edit(
|
||||
.await?;
|
||||
|
||||
indexing_queue.add(index_project);
|
||||
|
||||
if let Ok(webhook_url) = dotenv::var("MODERATION_DISCORD_WEBHOOK") {
|
||||
crate::util::webhook::send_discord_webhook(
|
||||
convert_project(project_item.clone()),
|
||||
webhook_url,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,6 +838,48 @@ pub async fn project_edit(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(rejection_reason) = &new_project.rejection_reason {
|
||||
if !user.role.is_mod() {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have the permissions to edit the rejection reason of this project!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET rejection_reason = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
rejection_reason.as_deref(),
|
||||
id as database::models::ids::ProjectId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(rejection_body) = &new_project.rejection_body {
|
||||
if !user.role.is_mod() {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have the permissions to edit the rejection body of this project!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET rejection_body = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
rejection_body.as_deref(),
|
||||
id as database::models::ids::ProjectId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(body) = &new_project.body {
|
||||
if !perms.contains(Permissions::EDIT_BODY) {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
@@ -705,6 +901,9 @@ pub async fn project_edit(
|
||||
.await?;
|
||||
}
|
||||
|
||||
remove_cache_project(string.clone()).await;
|
||||
remove_cache_query_project(string).await;
|
||||
|
||||
transaction.commit().await?;
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
@@ -736,11 +935,12 @@ pub async fn project_icon_edit(
|
||||
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::InvalidInputError("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
let project_item =
|
||||
database::models::Project::get_from_slug_or_project_id(string.clone(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInputError("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
if !user.role.is_mod() {
|
||||
let team_member = database::models::TeamMember::get_from_user_id(
|
||||
@@ -782,12 +982,14 @@ pub async fn project_icon_edit(
|
||||
)));
|
||||
}
|
||||
|
||||
let hash = sha1::Sha1::from(bytes.clone()).hexdigest();
|
||||
|
||||
let project_id: ProjectId = project_item.id.into();
|
||||
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("data/{}/icon.{}", project_id, ext.ext),
|
||||
&format!("data/{}/{}.{}", project_id, hash, ext.ext),
|
||||
bytes.to_vec(),
|
||||
)
|
||||
.await?;
|
||||
@@ -804,6 +1006,9 @@ pub async fn project_icon_edit(
|
||||
.execute(&**pool)
|
||||
.await?;
|
||||
|
||||
remove_cache_project(string.clone()).await;
|
||||
remove_cache_query_project(string).await;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::InvalidInputError(format!(
|
||||
@@ -823,7 +1028,7 @@ 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)
|
||||
let project = database::models::Project::get_from_slug_or_project_id(string.clone(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInputError("The specified project does not exist!".to_string())
|
||||
@@ -851,7 +1056,14 @@ pub async fn project_delete(
|
||||
}
|
||||
}
|
||||
|
||||
let result = database::models::Project::remove_full(project.id, &**pool).await?;
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let result = database::models::Project::remove_full(project.id, &mut transaction).await?;
|
||||
|
||||
remove_cache_project(string.clone()).await;
|
||||
remove_cache_query_project(string).await;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
delete_from_index(project.id.into(), config).await?;
|
||||
|
||||
@@ -893,6 +1105,8 @@ pub async fn project_follow(
|
||||
.unwrap_or(false);
|
||||
|
||||
if !following {
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
@@ -901,7 +1115,7 @@ pub async fn project_follow(
|
||||
",
|
||||
project_id as database::models::ids::ProjectId,
|
||||
)
|
||||
.execute(&**pool)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
@@ -912,9 +1126,11 @@ pub async fn project_follow(
|
||||
user_id as database::models::ids::UserId,
|
||||
project_id as database::models::ids::ProjectId
|
||||
)
|
||||
.execute(&**pool)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::InvalidInputError(
|
||||
@@ -954,6 +1170,8 @@ pub async fn project_unfollow(
|
||||
.unwrap_or(false);
|
||||
|
||||
if following {
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
@@ -962,7 +1180,7 @@ pub async fn project_unfollow(
|
||||
",
|
||||
project_id as database::models::ids::ProjectId,
|
||||
)
|
||||
.execute(&**pool)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
@@ -973,9 +1191,11 @@ pub async fn project_unfollow(
|
||||
user_id as database::models::ids::UserId,
|
||||
project_id as database::models::ids::ProjectId
|
||||
)
|
||||
.execute(&**pool)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::InvalidInputError(
|
||||
|
||||
Reference in New Issue
Block a user