Files
Rocketmc/apps/labrinth/src/routes/v2/project_creation.rs
2024-10-18 16:07:35 -07:00

274 lines
9.8 KiB
Rust

use crate::database::models::version_item;
use crate::database::redis::RedisPool;
use crate::file_hosting::FileHost;
use crate::models;
use crate::models::ids::ImageId;
use crate::models::projects::{Loader, Project, ProjectStatus};
use crate::models::v2::projects::{
DonationLink, LegacyProject, LegacySideType,
};
use crate::queue::session::AuthQueue;
use crate::routes::v3::project_creation::default_project_type;
use crate::routes::v3::project_creation::{CreateError, NewGalleryItem};
use crate::routes::{v2_reroute, v3};
use actix_multipart::Multipart;
use actix_web::web::Data;
use actix_web::{post, HttpRequest, HttpResponse};
use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::postgres::PgPool;
use std::collections::HashMap;
use std::sync::Arc;
use validator::Validate;
use super::version_creation::InitialVersionData;
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(project_create);
}
pub fn default_requested_status() -> ProjectStatus {
ProjectStatus::Approved
}
#[derive(Serialize, Deserialize, Validate, Clone)]
struct ProjectCreateData {
#[validate(
length(min = 3, max = 64),
custom(function = "crate::util::validate::validate_name")
)]
#[serde(alias = "mod_name")]
/// The title or name of the project.
pub title: String,
#[validate(length(min = 1, max = 64))]
#[serde(default = "default_project_type")]
/// The project type of this mod
pub project_type: String,
#[validate(
length(min = 3, max = 64),
regex = "crate::util::validate::RE_URL_SAFE"
)]
#[serde(alias = "mod_slug")]
/// The slug of a project, used for vanity URLs
pub slug: String,
#[validate(length(min = 3, max = 255))]
#[serde(alias = "mod_description")]
/// A short description of the project.
pub description: String,
#[validate(length(max = 65536))]
#[serde(alias = "mod_body")]
/// A long description of the project, in markdown.
pub body: String,
/// The support range for the client project
pub client_side: LegacySideType,
/// The support range for the server project
pub server_side: LegacySideType,
#[validate(length(max = 32))]
#[validate]
/// A list of initial versions to upload with the created project
pub initial_versions: Vec<InitialVersionData>,
#[validate(length(max = 3))]
/// A list of the categories that the project is in.
pub categories: Vec<String>,
#[validate(length(max = 256))]
#[serde(default = "Vec::new")]
/// A list of the categories that the project is in.
pub additional_categories: Vec<String>,
#[validate(
custom(function = "crate::util::validate::validate_url"),
length(max = 2048)
)]
/// An optional link to where to submit bugs or issues with the project.
pub issues_url: Option<String>,
#[validate(
custom(function = "crate::util::validate::validate_url"),
length(max = 2048)
)]
/// An optional link to the source code for the project.
pub source_url: Option<String>,
#[validate(
custom(function = "crate::util::validate::validate_url"),
length(max = 2048)
)]
/// An optional link to the project's wiki page or other relevant information.
pub wiki_url: Option<String>,
#[validate(
custom(function = "crate::util::validate::validate_url"),
length(max = 2048)
)]
/// An optional link to the project's license page
pub license_url: Option<String>,
#[validate(
custom(function = "crate::util::validate::validate_url"),
length(max = 2048)
)]
/// An optional link to the project's discord.
pub discord_url: Option<String>,
/// An optional list of all donation links the project has\
#[validate]
pub donation_urls: Option<Vec<DonationLink>>,
/// An optional boolean. If true, the project will be created as a draft.
pub is_draft: Option<bool>,
/// The license id that the project follows
pub license_id: String,
#[validate(length(max = 64))]
#[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,
// Associations to uploaded images in body/description
#[validate(length(max = 10))]
#[serde(default)]
pub uploaded_images: Vec<ImageId>,
/// The id of the organization to create the project in
pub organization_id: Option<models::ids::OrganizationId>,
}
#[post("project")]
pub async fn project_create(
req: HttpRequest,
payload: Multipart,
client: Data<PgPool>,
redis: Data<RedisPool>,
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
session_queue: Data<AuthQueue>,
) -> Result<HttpResponse, CreateError> {
// Convert V2 multipart payload to V3 multipart payload
let payload = v2_reroute::alter_actix_multipart(
payload,
req.headers().clone(),
|legacy_create: ProjectCreateData, _| async move {
// Side types will be applied to each version
let client_side = legacy_create.client_side;
let server_side = legacy_create.server_side;
let project_type = legacy_create.project_type;
let initial_versions = legacy_create
.initial_versions
.into_iter()
.map(|v| {
let mut fields = HashMap::new();
fields.extend(v2_reroute::convert_side_types_v3(
client_side,
server_side,
));
fields.insert(
"game_versions".to_string(),
json!(v.game_versions),
);
// Modpacks now use the "mrpack" loader, and loaders are converted to loader fields.
// Setting of 'project_type' directly is removed, it's loader-based now.
if project_type == "modpack" {
fields.insert(
"mrpack_loaders".to_string(),
json!(v.loaders),
);
}
let loaders = if project_type == "modpack" {
vec![Loader("mrpack".to_string())]
} else {
v.loaders
};
v3::version_creation::InitialVersionData {
project_id: v.project_id,
file_parts: v.file_parts,
version_number: v.version_number,
version_title: v.version_title,
version_body: v.version_body,
dependencies: v.dependencies,
release_channel: v.release_channel,
loaders,
featured: v.featured,
primary_file: v.primary_file,
status: v.status,
file_types: v.file_types,
uploaded_images: v.uploaded_images,
ordering: v.ordering,
fields,
}
})
.collect();
let mut link_urls = HashMap::new();
if let Some(issue_url) = legacy_create.issues_url {
link_urls.insert("issues".to_string(), issue_url);
}
if let Some(source_url) = legacy_create.source_url {
link_urls.insert("source".to_string(), source_url);
}
if let Some(wiki_url) = legacy_create.wiki_url {
link_urls.insert("wiki".to_string(), wiki_url);
}
if let Some(discord_url) = legacy_create.discord_url {
link_urls.insert("discord".to_string(), discord_url);
}
if let Some(donation_urls) = legacy_create.donation_urls {
for donation_url in donation_urls {
link_urls.insert(donation_url.platform, donation_url.url);
}
}
Ok(v3::project_creation::ProjectCreateData {
name: legacy_create.title,
slug: legacy_create.slug,
summary: legacy_create.description, // Description becomes summary
description: legacy_create.body, // Body becomes description
initial_versions,
categories: legacy_create.categories,
additional_categories: legacy_create.additional_categories,
license_url: legacy_create.license_url,
link_urls,
is_draft: legacy_create.is_draft,
license_id: legacy_create.license_id,
gallery_items: legacy_create.gallery_items,
requested_status: legacy_create.requested_status,
uploaded_images: legacy_create.uploaded_images,
organization_id: legacy_create.organization_id,
})
},
)
.await?;
// Call V3 project creation
let response = v3::project_creation::project_create(
req,
payload,
client.clone(),
redis.clone(),
file_host,
session_queue,
)
.await?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::<Project>(response).await {
Ok(project) => {
let version_item = match project.versions.first() {
Some(vid) => {
version_item::Version::get((*vid).into(), &**client, &redis)
.await?
}
None => None,
};
let project = LegacyProject::from(project, version_item);
Ok(HttpResponse::Ok().json(project))
}
Err(response) => Ok(response),
}
}