use std::collections::{HashMap, HashSet}; use super::ids::{Base62Id, OrganizationId}; use super::teams::TeamId; use super::users::UserId; use crate::database::models::project_item::{LinkUrl, QueryProject}; use crate::database::models::version_item::QueryVersion; use crate::models::threads::ThreadId; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use validator::Validate; /// The ID of a specific project, encoded as base62 for usage in the API #[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] #[serde(from = "Base62Id")] #[serde(into = "Base62Id")] pub struct ProjectId(pub u64); /// The ID of a specific version of a project #[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Debug)] #[serde(from = "Base62Id")] #[serde(into = "Base62Id")] pub struct VersionId(pub u64); /// A project returned from the API #[derive(Serialize, Deserialize, Clone)] pub struct Project { /// The ID of the project, encoded as a base62 string. pub id: ProjectId, /// The slug of a project, used for vanity URLs pub slug: Option, /// The aggregated project typs of the versions of this project pub project_types: Vec, /// The aggregated games of the versions of this project pub games: Vec, /// The team of people that has ownership of this project. pub team_id: TeamId, /// The optional organization of people that have ownership of this project. pub organization: Option, /// The title or name of the project. pub name: String, /// A short description of the project. pub summary: String, /// A long form description of the project. pub description: String, /// The date at which the project was first published. pub published: DateTime, /// The date at which the project was first published. pub updated: DateTime, /// The date at which the project was first approved. //pub approved: Option>, pub approved: Option>, /// The date at which the project entered the moderation queue pub queued: Option>, /// The status of the project pub status: ProjectStatus, /// The requested status of this projct pub requested_status: Option, /// DEPRECATED: moved to threads system /// The rejection data of the project pub moderator_message: Option, /// The license of this project pub license: License, /// The total number of downloads the project has had. pub downloads: u32, /// The total number of followers this project has accumulated pub followers: u32, /// A list of the categories that the project is in. pub categories: Vec, /// A list of the categories that the project is in. pub additional_categories: Vec, /// A list of loaders this project supports pub loaders: Vec, /// A list of ids for versions of the project. pub versions: Vec, /// The URL of the icon of the project pub icon_url: Option, /// A collection of links to the project's various pages. pub link_urls: HashMap, /// A string of URLs to visual content featuring the project pub gallery: Vec, /// The color of the project (picked from icon) pub color: Option, /// The thread of the moderation messages of the project pub thread_id: ThreadId, /// The monetization status of this project pub monetization_status: MonetizationStatus, /// Aggregated loader-fields across its myriad of versions #[serde(flatten)] pub fields: HashMap>, } fn remove_duplicates(values: Vec) -> Vec { let mut seen = HashSet::new(); values .into_iter() .filter(|value| { // Convert the JSON value to a string for comparison let as_string = value.to_string(); // Check if the string is already in the set seen.insert(as_string) }) .collect() } impl From for Project { fn from(data: QueryProject) -> Self { let mut fields: HashMap> = HashMap::new(); for vf in data.aggregate_version_fields { // We use a string directly, so we can remove duplicates let serialized = if let Some(inner_array) = vf.value.serialize_internal().as_array() { inner_array.clone() } else { vec![vf.value.serialize_internal()] }; // Create array if doesnt exist, otherwise push, or if json is an array, extend if let Some(arr) = fields.get_mut(&vf.field_name) { arr.extend(serialized); } else { fields.insert(vf.field_name, serialized); } } // Remove duplicates by converting to string and back for (_, v) in fields.iter_mut() { *v = remove_duplicates(v.clone()); } let m = data.inner; Self { id: m.id.into(), slug: m.slug, project_types: data.project_types, games: data.games, team_id: m.team_id.into(), organization: m.organization_id.map(|i| i.into()), name: m.name, summary: m.summary, description: m.description, published: m.published, updated: m.updated, approved: m.approved, queued: m.queued, status: m.status, requested_status: m.requested_status, moderator_message: if let Some(message) = m.moderation_message { Some(ModeratorMessage { message, body: m.moderation_message_body, }) } else { None }, license: License { id: m.license.clone(), name: match spdx::Expression::parse(&m.license) { Ok(spdx_expr) => { let mut vec: Vec<&str> = Vec::new(); for node in spdx_expr.iter() { if let spdx::expression::ExprNode::Req(req) = node { if let Some(id) = req.req.license.id() { vec.push(id.full_name); } } } // spdx crate returns AND/OR operations in postfix order // and it would be a lot more effort to make it actually in order // so let's just ignore that and make them comma-separated vec.join(", ") } Err(_) => "".to_string(), }, url: m.license_url, }, downloads: m.downloads as u32, followers: m.follows as u32, categories: data.categories, additional_categories: data.additional_categories, loaders: m.loaders, versions: data.versions.into_iter().map(|v| v.into()).collect(), icon_url: m.icon_url, link_urls: data .urls .into_iter() .map(|d| (d.platform_name.clone(), Link::from(d))) .collect(), gallery: data .gallery_items .into_iter() .map(|x| GalleryItem { url: x.image_url, featured: x.featured, name: x.name, description: x.description, created: x.created, ordering: x.ordering, }) .collect(), color: m.color, thread_id: data.thread_id.into(), monetization_status: m.monetization_status, fields, } } } #[derive(Serialize, Deserialize, Clone, Debug)] pub struct GalleryItem { pub url: String, pub featured: bool, pub name: Option, pub description: Option, pub created: DateTime, pub ordering: i64, } #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ModeratorMessage { pub message: String, pub body: Option, } pub const DEFAULT_LICENSE_ID: &str = "LicenseRef-All-Rights-Reserved"; #[derive(Serialize, Deserialize, Clone)] pub struct License { pub id: String, pub name: String, pub url: Option, } #[derive(Serialize, Deserialize, Validate, Clone, Eq, PartialEq)] pub struct Link { pub platform: String, pub donation: bool, #[validate( custom(function = "crate::util::validate::validate_url"), length(max = 2048) )] pub url: String, } impl From for Link { fn from(data: LinkUrl) -> Self { Self { platform: data.platform_name, donation: data.donation, url: data.url, } } } /// A status decides the visibility of a project in search, URLs, and the whole site itself. /// Approved - Project is displayed on search, and accessible by URL /// Rejected - Project is not displayed on search, and not accessible by URL (Temporary state, project can reapply) /// Draft - Project is not displayed on search, and not accessible by URL /// Unlisted - Project is not displayed on search, but accessible by URL /// Withheld - Same as unlisted, but set by a moderator. Cannot be switched to another type without moderator approval /// Processing - Project is not displayed on search, and not accessible by URL (Temporary state, project under review) /// Scheduled - Project is scheduled to be released in the future /// Private - Project is approved, but is not viewable to the public #[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)] #[serde(rename_all = "lowercase")] pub enum ProjectStatus { Approved, Archived, Rejected, Draft, Unlisted, Processing, Withheld, Scheduled, Private, Unknown, } impl std::fmt::Display for ProjectStatus { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { write!(fmt, "{}", self.as_str()) } } impl ProjectStatus { pub fn from_string(string: &str) -> ProjectStatus { match string { "processing" => ProjectStatus::Processing, "rejected" => ProjectStatus::Rejected, "approved" => ProjectStatus::Approved, "draft" => ProjectStatus::Draft, "unlisted" => ProjectStatus::Unlisted, "archived" => ProjectStatus::Archived, "withheld" => ProjectStatus::Withheld, "private" => ProjectStatus::Private, _ => ProjectStatus::Unknown, } } pub fn as_str(&self) -> &'static str { match self { ProjectStatus::Approved => "approved", ProjectStatus::Rejected => "rejected", ProjectStatus::Draft => "draft", ProjectStatus::Unlisted => "unlisted", ProjectStatus::Processing => "processing", ProjectStatus::Unknown => "unknown", ProjectStatus::Archived => "archived", ProjectStatus::Withheld => "withheld", ProjectStatus::Scheduled => "scheduled", ProjectStatus::Private => "private", } } pub fn as_friendly_str(&self) -> &'static str { match self { ProjectStatus::Approved => "Listed", ProjectStatus::Rejected => "Rejected", ProjectStatus::Draft => "Draft", ProjectStatus::Unlisted => "Unlisted", ProjectStatus::Processing => "Under review", ProjectStatus::Unknown => "Unknown", ProjectStatus::Archived => "Archived", ProjectStatus::Withheld => "Withheld", ProjectStatus::Scheduled => "Scheduled", ProjectStatus::Private => "Private", } } pub fn iterator() -> impl Iterator { [ ProjectStatus::Approved, ProjectStatus::Archived, ProjectStatus::Rejected, ProjectStatus::Draft, ProjectStatus::Unlisted, ProjectStatus::Processing, ProjectStatus::Withheld, ProjectStatus::Scheduled, ProjectStatus::Private, ProjectStatus::Unknown, ] .iter() .copied() } // Project pages + info cannot be viewed pub fn is_hidden(&self) -> bool { match self { ProjectStatus::Rejected => true, ProjectStatus::Draft => true, ProjectStatus::Processing => true, ProjectStatus::Unknown => true, ProjectStatus::Scheduled => true, ProjectStatus::Private => true, ProjectStatus::Approved => false, ProjectStatus::Unlisted => false, ProjectStatus::Archived => false, ProjectStatus::Withheld => false, } } // Project can be displayed in search pub fn is_searchable(&self) -> bool { matches!(self, ProjectStatus::Approved | ProjectStatus::Archived) } // Project is "Approved" by moderators pub fn is_approved(&self) -> bool { matches!( self, ProjectStatus::Approved | ProjectStatus::Archived | ProjectStatus::Unlisted | ProjectStatus::Private ) } // Project status can be requested after moderator approval pub fn can_be_requested(&self) -> bool { match self { ProjectStatus::Approved => true, ProjectStatus::Archived => true, ProjectStatus::Unlisted => true, ProjectStatus::Private => true, ProjectStatus::Draft => true, ProjectStatus::Rejected => false, ProjectStatus::Processing => false, ProjectStatus::Unknown => false, ProjectStatus::Withheld => false, ProjectStatus::Scheduled => false, } } } #[derive(Serialize, Deserialize, Copy, Clone, Debug, Eq, PartialEq)] #[serde(rename_all = "kebab-case")] pub enum MonetizationStatus { ForceDemonetized, Demonetized, Monetized, } impl std::fmt::Display for MonetizationStatus { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { fmt.write_str(self.as_str()) } } impl MonetizationStatus { pub fn from_string(string: &str) -> MonetizationStatus { match string { "force-demonetized" => MonetizationStatus::ForceDemonetized, "demonetized" => MonetizationStatus::Demonetized, "monetized" => MonetizationStatus::Monetized, _ => MonetizationStatus::Monetized, } } // These are constant, so this can remove unnecessary allocations (`to_string`) pub fn as_str(&self) -> &'static str { match self { MonetizationStatus::ForceDemonetized => "force-demonetized", MonetizationStatus::Demonetized => "demonetized", MonetizationStatus::Monetized => "monetized", } } } /// A specific version of a project #[derive(Serialize, Deserialize, Clone)] pub struct Version { /// The ID of the version, encoded as a base62 string. pub id: VersionId, /// The ID of the project this version is for. pub project_id: ProjectId, /// The ID of the author who published this version pub author_id: UserId, /// Whether the version is featured or not pub featured: bool, /// The name of this version pub name: String, /// The version number. Ideally will follow semantic versioning pub version_number: String, /// Project types for which this version is compatible with, extracted from Loader pub project_types: Vec, /// Games for which this version is compatible with, extracted from Loader/Project types pub games: Vec, /// The changelog for this version of the project. pub changelog: String, /// The date that this version was published. pub date_published: DateTime, /// The number of downloads this specific version has had. pub downloads: u32, /// The type of the release - `Alpha`, `Beta`, or `Release`. pub version_type: VersionType, /// The status of tne version pub status: VersionStatus, /// The requested status of the version (used for scheduling) pub requested_status: Option, /// A list of files available for download for this version. pub files: Vec, /// A list of projects that this version depends on. pub dependencies: Vec, /// The loaders that this version works on pub loaders: Vec, /// Ordering override, lower is returned first pub ordering: Option, // All other fields are loader-specific VersionFields // These are flattened during serialization #[serde(deserialize_with = "skip_nulls")] #[serde(flatten)] pub fields: HashMap, } pub fn skip_nulls<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de>, { let mut map = HashMap::deserialize(deserializer)?; map.retain(|_, v: &mut serde_json::Value| !v.is_null()); Ok(map) } impl From for Version { fn from(data: QueryVersion) -> Version { let v = data.inner; Version { id: v.id.into(), project_id: v.project_id.into(), author_id: v.author_id.into(), featured: v.featured, name: v.name, version_number: v.version_number, project_types: data.project_types, games: data.games, changelog: v.changelog, date_published: v.date_published, downloads: v.downloads as u32, version_type: match v.version_type.as_str() { "release" => VersionType::Release, "beta" => VersionType::Beta, "alpha" => VersionType::Alpha, _ => VersionType::Release, }, ordering: v.ordering, status: v.status, requested_status: v.requested_status, files: data .files .into_iter() .map(|f| VersionFile { url: f.url, filename: f.filename, hashes: f.hashes, primary: f.primary, size: f.size, file_type: f.file_type, }) .collect(), dependencies: data .dependencies .into_iter() .map(|d| Dependency { 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_string(d.dependency_type.as_str()), }) .collect(), loaders: data.loaders.into_iter().map(Loader).collect(), // Only add the internal component of the field for display // "ie": "game_versions",["1.2.3"] instead of "game_versions",ArrayEnum(...) fields: data .version_fields .into_iter() .map(|vf| (vf.field_name, vf.value.serialize_internal())) .collect(), } } } /// A status decides the visibility of a project in search, URLs, and the whole site itself. /// Listed - Version is displayed on project, and accessible by URL /// Archived - Identical to listed but has a message displayed stating version is unsupported /// Draft - Version is not displayed on project, and not accessible by URL /// Unlisted - Version is not displayed on project, and accessible by URL /// Scheduled - Version is scheduled to be released in the future #[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)] #[serde(rename_all = "lowercase")] pub enum VersionStatus { Listed, Archived, Draft, Unlisted, Scheduled, Unknown, } impl std::fmt::Display for VersionStatus { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { write!(fmt, "{}", self.as_str()) } } impl VersionStatus { pub fn from_string(string: &str) -> VersionStatus { match string { "listed" => VersionStatus::Listed, "draft" => VersionStatus::Draft, "unlisted" => VersionStatus::Unlisted, "scheduled" => VersionStatus::Scheduled, _ => VersionStatus::Unknown, } } pub fn as_str(&self) -> &'static str { match self { VersionStatus::Listed => "listed", VersionStatus::Archived => "archived", VersionStatus::Draft => "draft", VersionStatus::Unlisted => "unlisted", VersionStatus::Unknown => "unknown", VersionStatus::Scheduled => "scheduled", } } pub fn iterator() -> impl Iterator { [ VersionStatus::Listed, VersionStatus::Archived, VersionStatus::Draft, VersionStatus::Unlisted, VersionStatus::Scheduled, VersionStatus::Unknown, ] .iter() .copied() } // Version pages + info cannot be viewed pub fn is_hidden(&self) -> bool { match self { VersionStatus::Listed => false, VersionStatus::Archived => false, VersionStatus::Unlisted => false, VersionStatus::Draft => true, VersionStatus::Scheduled => true, VersionStatus::Unknown => true, } } // Whether version is listed on project / returned in aggregate routes pub fn is_listed(&self) -> bool { matches!(self, VersionStatus::Listed | VersionStatus::Archived) } // Whether a version status can be requested pub fn can_be_requested(&self) -> bool { match self { VersionStatus::Listed => true, VersionStatus::Archived => true, VersionStatus::Draft => true, VersionStatus::Unlisted => true, VersionStatus::Scheduled => false, VersionStatus::Unknown => false, } } } /// A single project file, with a url for the file and the file's hash #[derive(Serialize, Deserialize, Clone)] pub struct VersionFile { /// A map of hashes of the file. The key is the hashing algorithm /// and the value is the string version of the hash. pub hashes: std::collections::HashMap, /// A direct link to the file for downloading it. pub url: String, /// The filename of the file. pub filename: String, /// Whether the file is the primary file of a version pub primary: bool, /// The size in bytes of the file pub size: u32, /// The type of the file pub file_type: Option, } /// A dendency which describes what versions are required, break support, or are optional to the /// version's functionality #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct Dependency { /// The specific version id that the dependency uses pub version_id: Option, /// The project ID that the dependency is synced with and auto-updated pub project_id: Option, /// The filename of the dependency. Used exclusively for external mods on modpacks pub file_name: Option, /// The type of the dependency pub dependency_type: DependencyType, } #[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)] #[serde(rename_all = "lowercase")] pub enum VersionType { Release, Beta, Alpha, } impl std::fmt::Display for VersionType { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { fmt.write_str(self.as_str()) } } impl VersionType { // These are constant, so this can remove unneccessary allocations (`to_string`) pub fn as_str(&self) -> &'static str { match self { VersionType::Release => "release", VersionType::Beta => "beta", VersionType::Alpha => "alpha", } } } #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum DependencyType { Required, Optional, Incompatible, Embedded, } impl std::fmt::Display for DependencyType { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { fmt.write_str(self.as_str()) } } impl DependencyType { // These are constant, so this can remove unneccessary allocations (`to_string`) pub fn as_str(&self) -> &'static str { match self { DependencyType::Required => "required", DependencyType::Optional => "optional", DependencyType::Incompatible => "incompatible", DependencyType::Embedded => "embedded", } } pub fn from_string(string: &str) -> DependencyType { match string { "required" => DependencyType::Required, "optional" => DependencyType::Optional, "incompatible" => DependencyType::Incompatible, "embedded" => DependencyType::Embedded, _ => DependencyType::Required, } } } #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub enum FileType { RequiredResourcePack, OptionalResourcePack, Unknown, } impl std::fmt::Display for FileType { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { fmt.write_str(self.as_str()) } } impl FileType { // These are constant, so this can remove unnecessary allocations (`to_string`) pub fn as_str(&self) -> &'static str { match self { FileType::RequiredResourcePack => "required-resource-pack", FileType::OptionalResourcePack => "optional-resource-pack", FileType::Unknown => "unknown", } } pub fn from_string(string: &str) -> FileType { match string { "required-resource-pack" => FileType::RequiredResourcePack, "optional-resource-pack" => FileType::OptionalResourcePack, "unknown" => FileType::Unknown, _ => FileType::Unknown, } } } /// A project loader #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(transparent)] pub struct Loader(pub String); // These fields must always succeed parsing; deserialize errors aren't // processed correctly (don't return JSON errors) #[derive(Serialize, Deserialize, Debug)] pub struct SearchRequest { pub query: Option, pub offset: Option, pub index: Option, pub limit: Option, pub new_filters: Option, // TODO: Deprecated values below. WILL BE REMOVED V3! pub facets: Option, pub filters: Option, pub version: Option, }