Initial work on new status sys + scheduling releases (#489)

* Initial work on new status sys + scheduling releases

* Finish project statuses + begin work on version statuses

* Finish version statuses

* Regenerate prepare

* Run fmt + clippy
This commit is contained in:
Geometrically
2022-12-06 08:14:52 -08:00
committed by GitHub
parent c34e2ab3e1
commit e96d23cc3f
26 changed files with 2456 additions and 1906 deletions

View File

@@ -2,7 +2,7 @@ use crate::database::models::project_item::QueryProject;
use crate::database::models::version_item::{QueryFile, QueryVersion};
use crate::models::projects::{ProjectId, VersionId};
use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers;
use crate::util::auth::{get_user_from_headers, is_authorized_version};
use crate::{database, util::auth::is_authorized};
use actix_web::{get, route, web, HttpRequest, HttpResponse};
use sqlx::PgPool;
@@ -58,12 +58,11 @@ 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_full_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
@@ -81,10 +80,14 @@ pub async fn maven_metadata(
"
SELECT id, version_number, version_type
FROM versions
WHERE mod_id = $1
WHERE mod_id = $1 AND status = ANY($2)
ORDER BY date_published ASC
",
data.inner.id as database::models::ids::ProjectId
data.id as database::models::ids::ProjectId,
&*crate::models::projects::VersionStatus::iterator()
.filter(|x| x.is_listed())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_all(&**pool)
.await?;
@@ -108,7 +111,7 @@ pub async fn maven_metadata(
new_versions.push(value);
}
let project_id: ProjectId = data.inner.id.into();
let project_id: ProjectId = data.id.into();
let respdata = Metadata {
group_id: "maven.modrinth".to_string(),
@@ -122,7 +125,7 @@ pub async fn maven_metadata(
versions: Versions {
versions: new_versions,
},
last_updated: data.inner.updated.format("%Y%m%d%H%M%S").to_string(),
last_updated: data.updated.format("%Y%m%d%H%M%S").to_string(),
},
};
@@ -149,10 +152,13 @@ fn find_file<'a>(
_ => return None,
};
let version_id: VersionId = version.id.into();
let version_id: VersionId = version.inner.id.into();
if file
== format!("{}-{}.{}", &project_id, &version.version_number, fileext)
== format!(
"{}-{}.{}",
&project_id, &version.inner.version_number, fileext
)
|| file == format!("{}-{}.{}", &project_id, &version_id, fileext)
{
version
@@ -191,7 +197,7 @@ pub async fn version_file(
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
if !is_authorized(&project, &user_option, &pool).await? {
if !is_authorized(&project.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
}
@@ -224,8 +230,12 @@ pub async fn version_file(
return Ok(HttpResponse::NotFound().body(""));
};
let version_id: VersionId = version.id.into();
if file == format!("{}-{}.pom", &project_id, &version.version_number)
if !is_authorized_version(&version.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
}
let version_id: VersionId = version.inner.id.into();
if file == format!("{}-{}.pom", &project_id, &version.inner.version_number)
|| file == format!("{}-{}.pom", &project_id, version_id)
{
let respdata = MavenPom {
@@ -276,7 +286,7 @@ pub async fn version_file_sha1(
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
if !is_authorized(&project, &user_option, &pool).await? {
if !is_authorized(&project.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
}
@@ -338,7 +348,7 @@ pub async fn version_file_sha512(
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
if !is_authorized(&project, &user_option, &pool).await? {
if !is_authorized(&project.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
}
@@ -371,6 +381,10 @@ pub async fn version_file_sha512(
return Ok(HttpResponse::NotFound().body(""));
};
if !is_authorized_version(&version.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
}
Ok(find_file(&project_id, &project, &version, &file)
.and_then(|file| file.hashes.get("sha512"))
.and_then(|hash_bytes| std::str::from_utf8(hash_bytes).ok())

View File

@@ -67,6 +67,7 @@ pub fn projects_config(cfg: &mut web::ServiceConfig) {
.service(projects::delete_gallery_item)
.service(projects::project_follow)
.service(projects::project_unfollow)
.service(projects::project_schedule)
.service(teams::team_members_get_project)
.service(
web::scope("{project_id}")
@@ -95,7 +96,8 @@ pub fn versions_config(cfg: &mut web::ServiceConfig) {
.service(versions::version_get)
.service(versions::version_delete)
.service(version_creation::upload_file_to_version)
.service(versions::version_edit),
.service(versions::version_edit)
.service(versions::version_schedule),
);
cfg.service(
web::scope("version_file")

View File

@@ -29,9 +29,7 @@ pub async fn get_projects(
let project_ids = sqlx::query!(
"
SELECT id FROM mods
WHERE status = (
SELECT id FROM statuses WHERE status = $1
)
WHERE status = $1
ORDER BY updated ASC
LIMIT $2;
",

View File

@@ -3,6 +3,7 @@ use crate::file_hosting::{FileHost, FileHostingError};
use crate::models::error::ApiError;
use crate::models::projects::{
DonationLink, License, ProjectId, ProjectStatus, SideType, VersionId,
VersionStatus,
};
use crate::models::users::UserId;
use crate::queue::flameanvil::FlameAnvilQueue;
@@ -131,6 +132,10 @@ fn default_project_type() -> String {
"mod".to_string()
}
fn default_requested_status() -> ProjectStatus {
ProjectStatus::Approved
}
#[derive(Serialize, Deserialize, Validate, Clone)]
struct ProjectCreateData {
#[validate(length(min = 3, max = 64))]
@@ -218,6 +223,9 @@ struct ProjectCreateData {
#[validate]
/// The multipart names of the gallery items to upload
pub gallery_items: Option<Vec<NewGalleryItem>>,
#[serde(default = "default_requested_status")]
/// The status of the mod to be set once it is approved
pub requested_status: ProjectStatus,
}
#[derive(Serialize, Deserialize, Validate, Clone)]
@@ -658,14 +666,12 @@ pub async fn project_create_inner(
}
}
let status_id = models::StatusId::get_id(&status, &mut *transaction)
.await?
.ok_or_else(|| {
CreateError::InvalidInput(format!(
"Status {} does not exist.",
status.clone()
))
})?;
if !project_create_data.requested_status.can_be_requested() {
return Err(CreateError::InvalidInput(String::from(
"Specified requested status is not allowed to be requested",
)));
}
let client_side_id = models::SideTypeId::get_id(
&project_create_data.client_side,
&mut *transaction,
@@ -741,7 +747,8 @@ pub async fn project_create_inner(
categories,
additional_categories,
initial_versions: versions,
status: status_id,
status,
requested_status: Some(project_create_data.requested_status),
client_side: client_side_id,
server_side: server_side_id,
license: license_id.to_string(),
@@ -774,7 +781,8 @@ pub async fn project_create_inner(
published: now,
updated: now,
approved: None,
status: status.clone(),
status,
requested_status: project_builder.requested_status,
moderator_message: None,
license: License {
id: project_create_data.license_id.clone(),
@@ -895,7 +903,9 @@ async fn create_initial_version(
game_versions,
loaders,
featured: version_data.featured,
status: VersionStatus::Listed,
version_type: version_data.release_channel.to_string(),
requested_status: None,
};
Ok(version)

View File

@@ -12,7 +12,7 @@ use crate::util::auth::{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};
use chrono::Utc;
use chrono::{DateTime, Utc};
use futures::StreamExt;
use serde::{Deserialize, Serialize};
use serde_json::json;
@@ -52,7 +52,7 @@ pub async fn projects_get(
let projects: Vec<_> = futures::stream::iter(projects_data)
.filter_map(|data| async {
if is_authorized(&data, &user_option, &pool).await.ok()? {
if is_authorized(&data.inner, &user_option, &pool).await.ok()? {
Some(Project::from(data))
} else {
None
@@ -81,7 +81,7 @@ pub async fn project_get(
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
if let Some(data) = project_data {
if is_authorized(&data, &user_option, &pool).await? {
if is_authorized(&data.inner, &user_option, &pool).await? {
return Ok(HttpResponse::Ok().json(Project::from(data)));
}
}
@@ -159,7 +159,7 @@ pub async fn dependency_list(
) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0;
let result = database::models::Project::get_full_from_slug_or_project_id(
let result = database::models::Project::get_from_slug_or_project_id(
&string, &**pool,
)
.await?;
@@ -171,7 +171,7 @@ pub async fn dependency_list(
return Ok(HttpResponse::NotFound().body(""));
}
let id = project.inner.id;
let id = project.id;
use futures::stream::TryStreamExt;
@@ -333,6 +333,12 @@ pub struct EditProject {
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::double_option"
)]
pub requested_status: Option<Option<ProjectStatus>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::double_option"
)]
#[validate(length(max = 2000))]
pub moderation_message: Option<Option<String>>,
#[serde(
@@ -451,12 +457,11 @@ pub async fn project_edit(
));
}
if (status == &ProjectStatus::Rejected
|| status == &ProjectStatus::Approved)
if (status.is_approved() || !status.can_be_requested())
&& !user.role.is_mod()
{
return Err(ApiError::CustomAuthentication(
"You don't have permission to set this status"
"You don't have permission to set this status!"
.to_string(),
));
}
@@ -502,20 +507,7 @@ pub async fn project_edit(
}
}
let status_id = database::models::StatusId::get_id(
status,
&mut *transaction,
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(
"No database entry for status provided.".to_string(),
)
})?;
if status == &ProjectStatus::Approved
|| status == &ProjectStatus::Unlisted
{
if status.is_approved() {
sqlx::query!(
"
UPDATE mods
@@ -534,19 +526,49 @@ pub async fn project_edit(
SET status = $1
WHERE (id = $2)
",
status_id as database::models::ids::StatusId,
status.as_str(),
id as database::models::ids::ProjectId,
)
.execute(&mut *transaction)
.await?;
if project_item.status.is_searchable()
if project_item.inner.status.is_searchable()
&& !status.is_searchable()
{
delete_from_index(id.into(), config).await?;
}
}
if let Some(requested_status) = &new_project.requested_status {
if !perms.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the requested status of this project!"
.to_string(),
));
}
if !requested_status
.map(|x| x.can_be_requested())
.unwrap_or(true)
{
return Err(ApiError::InvalidInput(String::from(
"Specified status cannot be requested!",
)));
}
sqlx::query!(
"
UPDATE mods
SET requested_status = $1
WHERE (id = $2)
",
requested_status.map(|x| x.as_str()),
id as database::models::ids::ProjectId,
)
.execute(&mut *transaction)
.await?;
}
if perms.contains(Permissions::EDIT_DETAILS) {
if new_project.categories.is_some() {
sqlx::query!(
@@ -863,7 +885,7 @@ pub async fn project_edit(
license = models::projects::DEFAULT_LICENSE_ID.to_string();
}
spdx::Expression::parse(&*license).map_err(|err| {
spdx::Expression::parse(&license).map_err(|err| {
ApiError::InvalidInput(format!(
"Invalid SPDX license identifier: {}",
err
@@ -931,7 +953,7 @@ pub async fn project_edit(
if let Some(moderation_message) = &new_project.moderation_message {
if !user.role.is_mod()
&& project_item.status != ProjectStatus::Approved
&& project_item.inner.status != ProjectStatus::Approved
{
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the moderation message of this project!"
@@ -956,7 +978,7 @@ pub async fn project_edit(
&new_project.moderation_message_body
{
if !user.role.is_mod()
&& project_item.status != ProjectStatus::Approved
&& 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!"
@@ -1096,6 +1118,77 @@ pub async fn project_edit(
}
}
#[derive(Deserialize)]
pub struct SchedulingData {
pub time: DateTime<Utc>,
pub requested_status: ProjectStatus,
}
#[post("{id}/schedule")]
pub async fn project_schedule(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
scheduling_data: web::Json<SchedulingData>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
if scheduling_data.time < Utc::now() {
return Err(ApiError::InvalidInput(
"You cannot schedule a project to be released in the past!"
.to_string(),
));
}
if !scheduling_data.requested_status.can_be_requested() {
return Err(ApiError::InvalidInput(
"Specified requested status cannot be requested!".to_string(),
));
}
let string = info.into_inner().0;
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(
project_item.team_id,
user.id.into(),
&**pool,
)
.await?;
if user.role.is_mod()
|| team_member
.map(|x| x.permissions.contains(Permissions::EDIT_DETAILS))
.unwrap_or(false)
{
return Err(ApiError::CustomAuthentication(
"You do not have permission to edit this project's scheduling data!".to_string(),
));
}
sqlx::query!(
"
UPDATE mods
SET status = $1, approved = $2
WHERE (id = $3)
",
ProjectStatus::Scheduled.as_str(),
scheduling_data.time,
project_item.id as database::models::ids::ProjectId,
)
.execute(&**pool)
.await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
}
}
#[derive(Serialize, Deserialize)]
pub struct Extension {
pub ext: String,

View File

@@ -11,12 +11,12 @@ pub async fn get_stats(
"
SELECT COUNT(id)
FROM mods
WHERE
status = ( SELECT id FROM statuses WHERE status = $1 ) OR
status = ( SELECT id FROM statuses WHERE status = $2 )
WHERE status = ANY($1)
",
crate::models::projects::ProjectStatus::Approved.as_str(),
crate::models::projects::ProjectStatus::Archived.as_str()
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_searchable())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_one(&**pool);
@@ -24,13 +24,17 @@ pub async fn get_stats(
"
SELECT COUNT(v.id)
FROM versions v
INNER JOIN mods m on v.mod_id = m.id
WHERE
status = ( SELECT id FROM statuses WHERE status = $1 ) OR
status = ( SELECT id FROM statuses WHERE status = $2 )
INNER JOIN mods m on v.mod_id = m.id AND m.status = ANY($1)
WHERE v.status = ANY($2)
",
crate::models::projects::ProjectStatus::Approved.as_str(),
crate::models::projects::ProjectStatus::Archived.as_str()
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_searchable())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
&*crate::models::projects::VersionStatus::iterator()
.filter(|x| x.is_listed())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_one(&**pool);
@@ -39,27 +43,29 @@ pub async fn get_stats(
SELECT COUNT(DISTINCT u.id)
FROM users u
INNER JOIN team_members tm on u.id = tm.user_id AND tm.accepted = TRUE
INNER JOIN mods m on tm.team_id = m.team_id AND (
m.status = ( SELECT s.id FROM statuses s WHERE s.status = $1 ) OR
m.status = ( SELECT s.id FROM statuses s WHERE s.status = $2 )
)
INNER JOIN mods m on tm.team_id = m.team_id AND m.status = ANY($1)
",
crate::models::projects::ProjectStatus::Approved.as_str(),
crate::models::projects::ProjectStatus::Archived.as_str()
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_searchable())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_one(&**pool);
let files = sqlx::query!(
"
SELECT COUNT(f.id) FROM files f
INNER JOIN versions v on f.version_id = v.id
INNER JOIN mods m on v.mod_id = m.id
WHERE
status = ( SELECT id FROM statuses WHERE status = $1 ) OR
status = ( SELECT id FROM statuses WHERE status = $2 )
INNER JOIN versions v on f.version_id = v.id AND v.status = ANY($2)
INNER JOIN mods m on v.mod_id = m.id AND m.status = ANY($1)
",
crate::models::projects::ProjectStatus::Approved.as_str(),
crate::models::projects::ProjectStatus::Archived.as_str()
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_searchable())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
&*crate::models::projects::VersionStatus::iterator()
.filter(|x| x.is_listed())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_one(&**pool);

View File

@@ -332,13 +332,13 @@ pub async fn license_text(
) -> Result<HttpResponse, ApiError> {
let license_id = params.into_inner().0;
if license_id == crate::models::projects::DEFAULT_LICENSE_ID.to_string() {
if license_id == *crate::models::projects::DEFAULT_LICENSE_ID {
return Ok(HttpResponse::Ok().json(LicenseText {
body: "All rights reserved unless explicitly stated.".to_string(),
}));
}
if let Some(license) = spdx::license_id(&*license_id) {
if let Some(license) = spdx::license_id(&license_id) {
return Ok(HttpResponse::Ok().json(LicenseText {
body: license.text().to_string(),
}));

View File

@@ -6,7 +6,10 @@ use sqlx::PgPool;
use crate::database;
use crate::models::projects::{Version, VersionType};
use crate::util::auth::{get_user_from_headers, is_authorized};
use crate::util::auth::{
get_user_from_headers, is_authorized, is_authorized_version,
};
use futures::StreamExt;
use super::ApiError;
@@ -20,11 +23,10 @@ pub async fn forge_updates(
let (id,) = info.into_inner();
let project = database::models::Project::get_full_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();
@@ -33,16 +35,32 @@ pub async fn forge_updates(
}
let version_ids = database::models::Version::get_project_versions(
project.inner.id,
project.id,
None,
Some(vec!["forge".to_string()]),
&**pool,
)
.await?;
let mut versions =
let versions =
database::models::Version::get_many_full(version_ids, &**pool).await?;
versions.sort_by(|a, b| b.date_published.cmp(&a.date_published));
let mut versions = futures::stream::iter(versions)
.filter_map(|data| async {
if is_authorized_version(&data.inner, &user_option, &pool)
.await
.ok()?
{
Some(data)
} else {
None
}
})
.collect::<Vec<_>>()
.await;
versions
.sort_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published));
#[derive(Serialize)]
struct ForgeUpdates {

View File

@@ -1,7 +1,7 @@
use crate::database::models::User;
use crate::file_hosting::FileHost;
use crate::models::notifications::Notification;
use crate::models::projects::{Project, ProjectStatus};
use crate::models::projects::Project;
use crate::models::users::{
Badges, RecipientType, RecipientWallet, Role, UserId,
};
@@ -91,35 +91,23 @@ pub async fn projects_list(
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await.ok();
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 {
let user_id: UserId = id.into();
let project_data = if let Some(current_user) = user {
if current_user.role.is_mod() || current_user.id == user_id {
User::get_projects_private(id, &**pool).await?
} else {
User::get_projects(
id,
ProjectStatus::Approved.as_str(),
&**pool,
)
.await?
}
} else {
User::get_projects(id, ProjectStatus::Approved.as_str(), &**pool)
.await?
};
let can_view_private = user
.map(|y| y.role.is_mod() || y.id == user_id)
.unwrap_or(false);
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_approved())
.map(Project::from)
.collect();

View File

@@ -20,9 +20,7 @@ pub async fn get_mods(
let project_ids = sqlx::query!(
"
SELECT id FROM mods
WHERE status = (
SELECT id FROM statuses WHERE status = $1
)
WHERE status = $1
ORDER BY updated ASC
LIMIT $2;
",

View File

@@ -108,7 +108,7 @@ pub async fn mods_get(
// can't use `map` and `collect` here since `is_authorized` must be async
for proj in projects_data {
if is_authorized(&proj, &user_option, &pool).await? {
if is_authorized(&proj.inner, &user_option, &pool).await? {
projects.push(crate::models::projects::Project::from(proj))
}
}

View File

@@ -1,6 +1,6 @@
use crate::database::models::User;
use crate::models::ids::UserId;
use crate::models::projects::{ProjectId, ProjectStatus};
use crate::models::projects::ProjectId;
use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers;
use actix_web::web;
@@ -24,26 +24,19 @@ pub async fn mods_list(
if let Some(id) = id_option {
let user_id: UserId = id.into();
let project_data = if let Some(current_user) = user {
if current_user.role.is_mod() || current_user.id == user_id {
User::get_projects_private(id, &**pool).await?
} else {
User::get_projects(
id,
ProjectStatus::Approved.as_str(),
&**pool,
)
.await?
}
} else {
User::get_projects(id, ProjectStatus::Approved.as_str(), &**pool)
.await?
};
let can_view_private = user
.map(|y| y.role.is_mod() || y.id == user_id)
.unwrap_or(false);
let response = project_data
.into_iter()
.map(|v| v.into())
.collect::<Vec<crate::models::ids::ProjectId>>();
let project_data = User::get_projects(id, &**pool).await?;
let response: Vec<_> =
crate::database::Project::get_many(project_data, &**pool)
.await?
.into_iter()
.filter(|x| can_view_private || x.status.is_approved())
.map(|x| x.id.into())
.collect::<Vec<ProjectId>>();
Ok(HttpResponse::Ok().json(response))
} else {

View File

@@ -92,14 +92,16 @@ pub async fn version_list(
.filter(|version| {
filters
.featured
.map(|featured| featured == version.featured)
.map(|featured| featured == version.inner.featured)
.unwrap_or(true)
})
.map(Version::from)
.map(convert_to_legacy)
.collect::<Vec<_>>();
versions.sort_by(|a, b| b.date_published.cmp(&a.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()

View File

@@ -7,7 +7,7 @@ use crate::file_hosting::FileHost;
use crate::models::pack::PackFileHash;
use crate::models::projects::{
Dependency, DependencyType, GameVersion, Loader, ProjectId, Version,
VersionFile, VersionId, VersionType,
VersionFile, VersionId, VersionStatus, VersionType,
};
use crate::models::teams::Permissions;
use crate::queue::flameanvil::{FlameAnvilQueue, UploadFile};
@@ -59,6 +59,7 @@ pub struct InitialVersionData {
pub loaders: Vec<Loader>,
pub featured: bool,
pub primary_file: Option<String>,
pub status: VersionStatus,
}
#[derive(Serialize, Deserialize, Clone)]
@@ -160,6 +161,12 @@ async fn version_create_inner(
))
})?;
if !version_create_data.status.can_be_requested() {
return Err(CreateError::InvalidInput(
"Status specified cannot be requested".to_string(),
));
}
let project_id: models::ProjectId =
version_create_data.project_id.unwrap().into();
@@ -274,6 +281,8 @@ async fn version_create_inner(
loaders,
version_type: version_create_data.release_channel.to_string(),
featured: version_create_data.featured,
status: version_create_data.status,
requested_status: None,
});
continue;
@@ -415,6 +424,8 @@ async fn version_create_inner(
date_published: Utc::now(),
downloads: 0,
version_type: version_data.release_channel,
status: builder.status,
requested_status: builder.requested_status,
files: builder
.files
.iter()
@@ -449,8 +460,6 @@ async fn version_create_inner(
Ok(HttpResponse::Ok().json(response))
}
// TODO: file deletion, listing, etc
// under /api/v1/version/{version_id}
#[post("{version_id}/file")]
pub async fn upload_file_to_version(
@@ -551,7 +560,7 @@ async fn upload_file_to_version_inner(
));
}
let project_id = ProjectId(version.project_id.0 as u64);
let project_id = ProjectId(version.inner.project_id.0 as u64);
let project_type = sqlx::query!(
"
@@ -559,7 +568,7 @@ async fn upload_file_to_version_inner(
INNER JOIN mods ON mods.project_type = pt.id
WHERE mods.id = $1
",
version.project_id as models::ProjectId,
version.inner.project_id as models::ProjectId,
)
.fetch_one(&mut *transaction)
.await?
@@ -628,9 +637,9 @@ async fn upload_file_to_version_inner(
all_game_versions.clone(),
true,
false,
version.name.clone(),
version.changelog.clone(),
version.version_type.clone(),
version.inner.name.clone(),
version.inner.changelog.clone(),
version.inner.version_type.clone(),
flame_anvil_queue,
None,
None,

View File

@@ -35,14 +35,20 @@ pub async fn get_version_from_hash(
SELECT f.version_id version_id
FROM hashes h
INNER JOIN files f ON h.file_id = f.id
INNER JOIN versions v on f.version_id = v.id
INNER JOIN versions v on f.version_id = v.id AND v.status != ANY($1)
INNER JOIN mods m on v.mod_id = m.id
INNER JOIN statuses s on m.status = s.id
WHERE h.algorithm = $2 AND h.hash = $1 AND s.status != $3
WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ANY($4)
",
&*crate::models::projects::VersionStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
hash.as_bytes(),
algorithm.algorithm,
models::projects::ProjectStatus::Rejected.to_string()
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_optional(&**pool)
.await?;
@@ -83,14 +89,14 @@ pub async fn download_version(
"
SELECT f.url url, f.id id, f.version_id version_id, v.mod_id project_id FROM hashes h
INNER JOIN files f ON h.file_id = f.id
INNER JOIN versions v ON v.id = f.version_id
INNER JOIN versions v ON v.id = f.version_id AND v.status != ANY($1)
INNER JOIN mods m on v.mod_id = m.id
INNER JOIN statuses s on m.status = s.id
WHERE h.algorithm = $2 AND h.hash = $1 AND s.status != $3
WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ANY($4)
",
&*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
hash.as_bytes(),
algorithm.algorithm,
models::projects::ProjectStatus::Rejected.to_string()
&*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
)
.fetch_optional(&mut *transaction)
.await?;
@@ -234,14 +240,20 @@ pub async fn get_update_from_hash(
"
SELECT v.mod_id project_id FROM hashes h
INNER JOIN files f ON h.file_id = f.id
INNER JOIN versions v ON v.id = f.version_id
INNER JOIN versions v ON v.id = f.version_id AND v.status != ANY($1)
INNER JOIN mods m on v.mod_id = m.id
INNER JOIN statuses s on m.status = s.id
WHERE h.algorithm = $2 AND h.hash = $1 AND s.status != $3
WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ANY($4)
",
&*crate::models::projects::VersionStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
hash.as_bytes(),
algorithm.algorithm,
models::projects::ProjectStatus::Rejected.to_string()
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_optional(&**pool)
.await?;
@@ -306,14 +318,14 @@ pub async fn get_versions_from_hashes(
"
SELECT h.hash hash, h.algorithm algorithm, f.version_id version_id FROM hashes h
INNER JOIN files f ON h.file_id = f.id
INNER JOIN versions v ON v.id = f.version_id
INNER JOIN versions v ON v.id = f.version_id AND v.status != ANY($1)
INNER JOIN mods m on v.mod_id = m.id
INNER JOIN statuses s on m.status = s.id
WHERE h.algorithm = $2 AND h.hash = ANY($1::bytea[]) AND s.status != $3
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>>(),
hashes_parsed.as_slice(),
file_data.algorithm,
models::projects::ProjectStatus::Rejected.to_string()
&*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
)
.fetch_all(&**pool)
.await?;
@@ -333,7 +345,7 @@ pub async fn get_versions_from_hashes(
versions_data
.clone()
.into_iter()
.find(|x| x.id.0 == row.version_id)
.find(|x| x.inner.id.0 == row.version_id)
.map(|v| {
if let Ok(parsed_hash) = String::from_utf8(row.hash) {
Ok((
@@ -369,14 +381,14 @@ pub async fn download_files(
"
SELECT f.url url, h.hash hash, h.algorithm algorithm, f.version_id version_id, v.mod_id project_id FROM hashes h
INNER JOIN files f ON h.file_id = f.id
INNER JOIN versions v ON v.id = f.version_id
INNER JOIN versions v ON v.id = f.version_id AND v.status != ANY($1)
INNER JOIN mods m on v.mod_id = m.id
INNER JOIN statuses s on m.status = s.id
WHERE h.algorithm = $2 AND h.hash = ANY($1::bytea[]) AND s.status != $3
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>>(),
hashes_parsed.as_slice(),
file_data.algorithm,
models::projects::ProjectStatus::Rejected.to_string()
&*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
)
.fetch_all(&mut *transaction)
.await?;
@@ -423,14 +435,14 @@ pub async fn update_files(
"
SELECT f.url url, h.hash hash, h.algorithm algorithm, f.version_id version_id, v.mod_id project_id FROM hashes h
INNER JOIN files f ON h.file_id = f.id
INNER JOIN versions v ON v.id = f.version_id
INNER JOIN versions v ON v.id = f.version_id AND v.status != ANY($1)
INNER JOIN mods m on v.mod_id = m.id
INNER JOIN statuses s on m.status = s.id
WHERE h.algorithm = $2 AND h.hash = ANY($1::bytea[]) AND s.status != $3
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>>(),
hashes_parsed.as_slice(),
update_data.algorithm,
models::projects::ProjectStatus::Rejected.to_string()
&*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
)
.fetch_all(&mut *transaction)
.await?;
@@ -482,7 +494,7 @@ pub async fn update_files(
let mut response = HashMap::new();
for version in versions {
let hash = version_ids.get(&version.id);
let hash = version_ids.get(&version.inner.id);
if let Some(hash) = hash {
if let Ok(parsed_hash) = String::from_utf8(hash.clone()) {
@@ -491,7 +503,8 @@ pub async fn update_files(
models::projects::Version::from(version),
);
} else {
let version_id: models::projects::VersionId = version.id.into();
let version_id: models::projects::VersionId =
version.inner.id.into();
return Err(ApiError::Database(DatabaseError::Other(format!(
"Could not parse hash for version {}",

View File

@@ -1,11 +1,15 @@
use super::ApiError;
use crate::database;
use crate::models;
use crate::models::projects::{Dependency, Version};
use crate::models::projects::{Dependency, Version, VersionStatus};
use crate::models::teams::Permissions;
use crate::util::auth::{get_user_from_headers, is_authorized};
use crate::util::auth::{
get_user_from_headers, is_authorized, is_authorized_version,
};
use crate::util::validate::validation_errors_to_string;
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use chrono::{DateTime, Utc};
use futures::StreamExt;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use validator::Validate;
@@ -26,7 +30,7 @@ pub async fn version_list(
) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0;
let result = database::models::Project::get_full_from_slug_or_project_id(
let result = database::models::Project::get_from_slug_or_project_id(
&string, &**pool,
)
.await?;
@@ -38,7 +42,7 @@ pub async fn version_list(
return Ok(HttpResponse::NotFound().body(""));
}
let id = project.inner.id;
let id = project.id;
let version_ids = database::models::Version::get_project_versions(
id,
@@ -58,19 +62,27 @@ pub async fn version_list(
database::models::Version::get_many_full(version_ids, &**pool)
.await?;
let mut response = versions
.iter()
.cloned()
.filter(|version| {
filters
.featured
.map(|featured| featured == version.featured)
.unwrap_or(true)
let mut response = futures::stream::iter(versions.clone())
.filter_map(|data| async {
if is_authorized_version(&data.inner, &user_option, &pool)
.await
.ok()?
&& filters
.featured
.map(|featured| featured == data.inner.featured)
.unwrap_or(true)
{
Some(Version::from(data))
} else {
None
}
})
.map(Version::from)
.collect::<Vec<_>>();
.collect::<Vec<_>>()
.await;
versions.sort_by(|a, b| b.date_published.cmp(&a.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()
@@ -131,6 +143,7 @@ pub struct VersionIds {
#[get("versions")]
pub async fn versions_get(
req: HttpRequest,
web::Query(ids): web::Query<VersionIds>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
@@ -142,15 +155,28 @@ pub async fn versions_get(
let versions_data =
database::models::Version::get_many_full(version_ids, &**pool).await?;
let versions = versions_data
.into_iter()
.map(Version::from)
.collect::<Vec<_>>();
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
let versions: Vec<_> = futures::stream::iter(versions_data)
.filter_map(|data| async {
if is_authorized_version(&data.inner, &user_option, &pool)
.await
.ok()?
{
Some(Version::from(data))
} else {
None
}
})
.collect()
.await;
Ok(HttpResponse::Ok().json(versions))
}
#[get("{version_id}")]
pub async fn version_get(
req: HttpRequest,
info: web::Path<(models::ids::VersionId,)>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
@@ -158,11 +184,17 @@ pub async fn version_get(
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 {
Ok(HttpResponse::Ok().json(models::projects::Version::from(data)))
} else {
Ok(HttpResponse::NotFound().body(""))
if is_authorized_version(&data.inner, &user_option, &pool).await? {
return Ok(
HttpResponse::Ok().json(models::projects::Version::from(data))
);
}
}
Ok(HttpResponse::NotFound().body(""))
}
#[derive(Serialize, Deserialize, Validate)]
@@ -187,6 +219,7 @@ pub struct EditVersion {
pub featured: Option<bool>,
pub primary_file: Option<(String, String)>,
pub downloads: Option<u32>,
pub status: Option<VersionStatus>,
}
#[patch("{id}")]
@@ -209,14 +242,14 @@ pub async fn version_edit(
if let Some(version_item) = result {
let project_item = database::models::Project::get_full(
version_item.project_id,
version_item.inner.project_id,
&**pool,
)
.await?;
let team_member =
database::models::TeamMember::get_from_user_id_version(
version_item.id,
version_item.inner.id,
user.id.into(),
&**pool,
)
@@ -310,7 +343,7 @@ pub async fn version_edit(
for dependency in builders {
dependency
.insert(version_item.id, &mut transaction)
.insert(version_item.inner.id, &mut transaction)
.await?;
}
}
@@ -481,7 +514,7 @@ pub async fn version_edit(
.execute(&mut *transaction)
.await?;
let diff = *downloads - (version_item.downloads as u32);
let diff = *downloads - (version_item.inner.downloads as u32);
sqlx::query!(
"
@@ -490,7 +523,28 @@ pub async fn version_edit(
WHERE (id = $2)
",
diff as i32,
version_item.project_id as database::models::ids::ProjectId,
version_item.inner.project_id
as database::models::ids::ProjectId,
)
.execute(&mut *transaction)
.await?;
}
if let Some(status) = &new_version.status {
if !status.can_be_requested() {
return Err(ApiError::InvalidInput(
"The requested status cannot be set!".to_string(),
));
}
sqlx::query!(
"
UPDATE versions
SET status = $1
WHERE (id = $2)
",
status.as_str(),
id as database::models::ids::VersionId,
)
.execute(&mut *transaction)
.await?;
@@ -508,6 +562,76 @@ pub async fn version_edit(
}
}
#[derive(Deserialize)]
pub struct SchedulingData {
pub time: DateTime<Utc>,
pub requested_status: VersionStatus,
}
#[post("{id}/schedule")]
pub async fn version_schedule(
req: HttpRequest,
info: web::Path<(models::ids::VersionId,)>,
pool: web::Data<PgPool>,
scheduling_data: web::Json<SchedulingData>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
if scheduling_data.time < Utc::now() {
return Err(ApiError::InvalidInput(
"You cannot schedule a version to be released in the past!"
.to_string(),
));
}
if !scheduling_data.requested_status.can_be_requested() {
return Err(ApiError::InvalidInput(
"Specified requested status cannot be requested!".to_string(),
));
}
let string = info.into_inner().0;
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?;
if user.role.is_mod()
|| team_member
.map(|x| x.permissions.contains(Permissions::EDIT_DETAILS))
.unwrap_or(false)
{
return Err(ApiError::CustomAuthentication(
"You do not have permission to edit this version's scheduling data!".to_string(),
));
}
sqlx::query!(
"
UPDATE versions
SET status = $1, date_published = $2
WHERE (id = $3)
",
VersionStatus::Scheduled.as_str(),
scheduling_data.time,
version_item.inner.id as database::models::ids::VersionId,
)
.execute(&**pool)
.await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
}
}
#[delete("{version_id}")]
pub async fn version_delete(
req: HttpRequest,