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, #[validate(length(max = 3))] /// A list of the categories that the project is in. pub categories: Vec, #[validate(length(max = 256))] #[serde(default = "Vec::new")] /// A list of the categories that the project is in. pub additional_categories: Vec, #[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, #[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, #[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, #[validate( custom(function = "crate::util::validate::validate_url"), length(max = 2048) )] /// An optional link to the project's license page pub license_url: Option, #[validate( custom(function = "crate::util::validate::validate_url"), length(max = 2048) )] /// An optional link to the project's discord. pub discord_url: Option, /// An optional list of all donation links the project has\ #[validate] pub donation_urls: Option>, /// An optional boolean. If true, the project will be created as a draft. pub is_draft: Option, /// 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>, #[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, /// The id of the organization to create the project in pub organization_id: Option, } #[post("project")] pub async fn project_create( req: HttpRequest, payload: Multipart, client: Data, redis: Data, file_host: Data>, session_queue: Data, ) -> Result { // 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::(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), } }