mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 12:05:53 +00:00
Merge branch 'main' into boris/dev-1126
This commit is contained in:
@@ -3,6 +3,7 @@ use std::time::Duration;
|
||||
use chrono::{DateTime, Utc};
|
||||
use eyre::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_binhum::serde_binhum;
|
||||
use tracing::warn;
|
||||
use validator::Validate;
|
||||
|
||||
@@ -324,7 +325,8 @@ impl ComponentEdit for JavaServerProjectEdit {
|
||||
}
|
||||
|
||||
/// What game content a [`JavaServerProject`] is using.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Clone)]
|
||||
#[serde_binhum(schema)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum ServerContent {
|
||||
/// Server runs modded content with a modpack found on the Modrinth platform.
|
||||
@@ -346,7 +348,8 @@ pub enum ServerContent {
|
||||
}
|
||||
|
||||
/// What game content a [`JavaServerProject`] is using.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Clone)]
|
||||
#[serde_binhum(schema)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum ServerContentQuery {
|
||||
/// Server runs modded content with a modpack found on the Modrinth platform.
|
||||
|
||||
@@ -54,7 +54,7 @@ macro_rules! define_project_components {
|
||||
pub struct ProjectSerial {
|
||||
$(
|
||||
#[validate(nested)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default)]
|
||||
pub $field_name: Option<$ty>,
|
||||
)*
|
||||
}
|
||||
@@ -114,7 +114,6 @@ macro_rules! define_project_components {
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ProjectQuery {
|
||||
$(
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub $field_name: Option<Query<$ty>>,
|
||||
)*
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ pub mod v3;
|
||||
pub use v3::analytics;
|
||||
pub use v3::billing;
|
||||
pub use v3::collections;
|
||||
pub use v3::disclosures;
|
||||
pub use v3::ids;
|
||||
pub use v3::images;
|
||||
pub use v3::moderation_notes;
|
||||
|
||||
@@ -3,7 +3,10 @@ use std::convert::TryFrom;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::super::ids::OrganizationId;
|
||||
use crate::database::models::{DatabaseError, version_item};
|
||||
use crate::database::models::{
|
||||
DBProjectDisclosure, DBProjectId, DatabaseError, version_item,
|
||||
};
|
||||
use crate::models::disclosures::ProjectDisclosureType;
|
||||
use crate::models::ids::{ProjectId, TeamId, ThreadId, VersionId};
|
||||
use crate::models::projects::{
|
||||
Dependency, License, Link, Loader, ModeratorMessage, MonetizationStatus,
|
||||
@@ -28,6 +31,8 @@ pub struct LegacyProject {
|
||||
pub server_side: LegacySideType,
|
||||
/// A list of game versions this project supports
|
||||
pub game_versions: Vec<String>,
|
||||
/// The environments this project supports
|
||||
pub environment: Vec<String>,
|
||||
|
||||
// All other fields are the same as V3
|
||||
// If they change, or their constituent types change, we may need to
|
||||
@@ -56,6 +61,7 @@ pub struct LegacyProject {
|
||||
pub loaders: Vec<String>,
|
||||
pub versions: Vec<VersionId>,
|
||||
pub icon_url: Option<String>,
|
||||
pub raw_icon_url: Option<String>,
|
||||
pub issues_url: Option<String>,
|
||||
pub source_url: Option<String>,
|
||||
pub wiki_url: Option<String>,
|
||||
@@ -125,6 +131,14 @@ impl LegacyProject {
|
||||
.filter_map(|v| v.as_str())
|
||||
.map(|v| v.to_string())
|
||||
.collect();
|
||||
let environment = data
|
||||
.fields
|
||||
.get("environment")
|
||||
.unwrap_or(&Vec::new())
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.map(|v| v.to_string())
|
||||
.collect();
|
||||
|
||||
if let Some(versions_item) = versions_item {
|
||||
// Extract side types from remaining fields
|
||||
@@ -205,6 +219,7 @@ impl LegacyProject {
|
||||
loaders,
|
||||
versions: data.versions,
|
||||
icon_url: data.icon_url,
|
||||
raw_icon_url: data.raw_icon_url,
|
||||
issues_url,
|
||||
source_url,
|
||||
wiki_url,
|
||||
@@ -221,32 +236,47 @@ impl LegacyProject {
|
||||
client_side,
|
||||
server_side,
|
||||
game_versions,
|
||||
environment,
|
||||
}
|
||||
}
|
||||
|
||||
// Because from needs a version_item, this is a helper function to get many from one db query.
|
||||
pub async fn from_many<'a, E>(
|
||||
pub async fn from_many(
|
||||
data: Vec<Project>,
|
||||
exec: E,
|
||||
pool: &crate::database::PgPool,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<Self>, DatabaseError>
|
||||
where
|
||||
E: crate::database::Acquire<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
) -> Result<Vec<Self>, DatabaseError> {
|
||||
let version_ids: Vec<_> = data
|
||||
.iter()
|
||||
.filter_map(|p| p.versions.first().map(|i| (*i).into()))
|
||||
.collect();
|
||||
let project_ids: Vec<DBProjectId> =
|
||||
data.iter().map(|p| p.id.into()).collect();
|
||||
|
||||
let example_versions =
|
||||
version_item::DBVersion::get_many(&version_ids, exec, redis)
|
||||
version_item::DBVersion::get_many(&version_ids, pool, redis)
|
||||
.await?;
|
||||
let archived_disclosure_ids = DBProjectDisclosure::projects_with_type(
|
||||
ProjectDisclosureType::Archived,
|
||||
&project_ids,
|
||||
pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut legacy_projects = Vec::new();
|
||||
for project in data {
|
||||
let version_item = example_versions
|
||||
.iter()
|
||||
.find(|v| v.inner.project_id == project.id.into())
|
||||
.cloned();
|
||||
let project = LegacyProject::from(project, version_item);
|
||||
let has_archived_disclosure =
|
||||
archived_disclosure_ids.contains(&project.id.into());
|
||||
let mut project = LegacyProject::from(project, version_item);
|
||||
if has_archived_disclosure
|
||||
&& project.status == ProjectStatus::Approved
|
||||
{
|
||||
project.status = ProjectStatus::Archived;
|
||||
}
|
||||
legacy_projects.push(project);
|
||||
}
|
||||
Ok(legacy_projects)
|
||||
@@ -302,6 +332,9 @@ pub struct LegacyVersion {
|
||||
/// A list of loaders this project supports (has a newtype struct)
|
||||
pub loaders: Vec<Loader>,
|
||||
|
||||
/// The environment this version supports
|
||||
pub environment: String,
|
||||
|
||||
pub id: VersionId,
|
||||
pub project_id: ProjectId,
|
||||
pub author_id: UserId,
|
||||
@@ -332,6 +365,12 @@ impl From<Version> for LegacyVersion {
|
||||
}
|
||||
}
|
||||
}
|
||||
let environment = data
|
||||
.fields
|
||||
.get("environment")
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
// - if loader is mrpack, this is a modpack
|
||||
// the v2 loaders are whatever the corresponding loader fields are
|
||||
@@ -366,6 +405,7 @@ impl From<Version> for LegacyVersion {
|
||||
dependencies: data.dependencies,
|
||||
game_versions,
|
||||
loaders,
|
||||
environment,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ pub struct LegacyResultSearchProject {
|
||||
pub license: String,
|
||||
pub client_side: String,
|
||||
pub server_side: String,
|
||||
pub environment: Vec<String>,
|
||||
pub gallery: Vec<String>,
|
||||
pub featured_gallery: Option<String>,
|
||||
pub color: Option<u32>,
|
||||
@@ -118,6 +119,14 @@ impl LegacyResultSearchProject {
|
||||
|
||||
let environment =
|
||||
get_one_string_loader_field("environment").unwrap_or("unknown");
|
||||
let environments = result_search_project
|
||||
.loader_fields
|
||||
.get("environment")
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|environment| environment.as_str().map(String::from))
|
||||
.collect();
|
||||
|
||||
let (client_side, server_side) =
|
||||
v2_reroute::convert_v3_environment_to_v2_side_types(
|
||||
@@ -141,6 +150,7 @@ impl LegacyResultSearchProject {
|
||||
all_project_types: result_search_project.all_project_types,
|
||||
client_side,
|
||||
server_side,
|
||||
environment: environments,
|
||||
versions,
|
||||
latest_version: result_search_project.version_id,
|
||||
categories,
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::models::ids::{
|
||||
use ariadne::ids::UserId;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_binhum::serde_binhum;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -14,7 +15,7 @@ pub struct Product {
|
||||
pub unitary: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde_binhum]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
pub enum ProductMetadata {
|
||||
Midas,
|
||||
@@ -55,7 +56,7 @@ pub struct ProductPrice {
|
||||
pub currency_code: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde_binhum]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
pub enum Price {
|
||||
OneTime {
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
use crate::database::models::DBProjectDisclosure;
|
||||
use ariadne::ids::UserId;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeSet;
|
||||
use strum::{EnumDiscriminants, EnumString, IntoStaticStr};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, EnumDiscriminants)]
|
||||
#[serde(rename_all = "snake_case", tag = "type")]
|
||||
#[strum_discriminants(
|
||||
name(ProjectDisclosureType),
|
||||
derive(IntoStaticStr, EnumString),
|
||||
strum(serialize_all = "snake_case")
|
||||
)]
|
||||
pub enum ProjectDisclosure {
|
||||
AiContent {
|
||||
note: Option<String>,
|
||||
uses: BTreeSet<AiUsages>,
|
||||
},
|
||||
Advertisements {
|
||||
note: Option<String>,
|
||||
},
|
||||
EpilepsyTriggers {
|
||||
note: Option<String>,
|
||||
},
|
||||
SystemInteractions {
|
||||
note: Option<String>,
|
||||
interactions: Vec<String>,
|
||||
},
|
||||
Telemetry {
|
||||
consent: TelemetryConsent,
|
||||
data_collected: Vec<String>,
|
||||
},
|
||||
DerivativeWork {
|
||||
sources: Vec<DerivativeSource>,
|
||||
},
|
||||
PaidFeatures {
|
||||
features: Vec<String>,
|
||||
},
|
||||
Archived {
|
||||
note: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ProjectDisclosure {
|
||||
pub fn to_parts(
|
||||
&self,
|
||||
) -> Result<(&'static str, serde_json::Value), serde_json::Error> {
|
||||
let serde_json::Value::Object(mut object) = serde_json::to_value(self)?
|
||||
else {
|
||||
return Err(serde::ser::Error::custom(
|
||||
"project disclosure must serialize to a JSON object",
|
||||
));
|
||||
};
|
||||
object.remove("type");
|
||||
Ok((
|
||||
ProjectDisclosureType::from(self).into(),
|
||||
serde_json::Value::Object(object),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn from_parts(
|
||||
kind: &str,
|
||||
metadata: serde_json::Value,
|
||||
) -> Result<Self, serde_json::Error> {
|
||||
let serde_json::Value::Object(mut object) = metadata else {
|
||||
return Err(serde::de::Error::custom(
|
||||
"project disclosure metadata must be a JSON object",
|
||||
));
|
||||
};
|
||||
object.insert(
|
||||
"type".to_owned(),
|
||||
serde_json::Value::String(kind.to_owned()),
|
||||
);
|
||||
serde_json::from_value(serde_json::Value::Object(object))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
ToSchema,
|
||||
IntoStaticStr,
|
||||
EnumString,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum DisclosureLockStatus {
|
||||
#[default]
|
||||
Unlocked,
|
||||
CannotDisable,
|
||||
FullyLocked,
|
||||
}
|
||||
|
||||
impl DisclosureLockStatus {
|
||||
pub fn allows_edit(self) -> bool {
|
||||
!matches!(self, Self::FullyLocked)
|
||||
}
|
||||
|
||||
pub fn allows_removal(self) -> bool {
|
||||
matches!(self, Self::Unlocked)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ProjectDisclosureData {
|
||||
#[serde(flatten)]
|
||||
pub disclosure: ProjectDisclosure,
|
||||
pub set_by_moderator: bool,
|
||||
pub lock_status: DisclosureLockStatus,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub updated_by: Option<UserId>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl ProjectDisclosureData {
|
||||
pub fn from_db(
|
||||
value: DBProjectDisclosure,
|
||||
viewer_is_moderator: bool,
|
||||
) -> Self {
|
||||
let updated_by = (!value.set_by_moderator || viewer_is_moderator)
|
||||
.then_some(value.updated_by.into());
|
||||
|
||||
Self {
|
||||
disclosure: value.disclosure,
|
||||
set_by_moderator: value.set_by_moderator,
|
||||
lock_status: value.lock_status,
|
||||
updated_at: value.updated_at,
|
||||
updated_by,
|
||||
deleted_at: value.deleted_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
ToSchema,
|
||||
IntoStaticStr,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum AiUsages {
|
||||
Code,
|
||||
Assets,
|
||||
Text,
|
||||
Functionality,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, IntoStaticStr)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum TelemetryConsent {
|
||||
OptIn,
|
||||
OptOut,
|
||||
AlwaysActive,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct DerivativeSource {
|
||||
pub link: Option<String>,
|
||||
pub label: String,
|
||||
pub note: Option<String>,
|
||||
}
|
||||
@@ -3,6 +3,7 @@ pub mod analytics;
|
||||
pub mod analytics_event;
|
||||
pub mod billing;
|
||||
pub mod collections;
|
||||
pub mod disclosures;
|
||||
pub mod ids;
|
||||
pub mod images;
|
||||
pub mod moderation_notes;
|
||||
|
||||
@@ -4,6 +4,7 @@ use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::error::Context as _;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ModerationNote {
|
||||
@@ -37,9 +38,9 @@ pub struct PatchModerationNote {
|
||||
impl PatchModerationNote {
|
||||
pub fn validate_not_empty(&self) -> Result<(), ApiError> {
|
||||
if self.notes.is_none() && self.user_rating.is_none() {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"must specify `notes` or `user_rating`".to_string(),
|
||||
));
|
||||
return Err(ApiError::Request(eyre::eyre!(
|
||||
"must specify `notes` or `user_rating`",
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -53,16 +54,14 @@ pub fn parse_if_match_header(
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let value = value.to_str().map_err(|_| {
|
||||
ApiError::InvalidInput(
|
||||
"`if-match` header must be a valid integer".to_string(),
|
||||
)
|
||||
})?;
|
||||
let value = value.to_str().wrap_request_err(
|
||||
"`if-match` header must be a valid integer".to_string(),
|
||||
)?;
|
||||
|
||||
Some(value.parse::<i32>().map_err(|_| {
|
||||
ApiError::InvalidInput(
|
||||
"`if-match` header must be a valid integer".to_string(),
|
||||
)
|
||||
ApiError::Request(eyre::eyre!(
|
||||
"`if-match` header must be a valid integer",
|
||||
))
|
||||
}))
|
||||
.transpose()
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::routes::ApiError;
|
||||
use ariadne::ids::UserId;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_binhum::serde_binhum;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -151,7 +152,8 @@ impl NotificationType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Clone)]
|
||||
#[serde_binhum]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum NotificationBody {
|
||||
ProjectUpdate {
|
||||
@@ -765,8 +767,8 @@ impl NotificationDeliveryStatus {
|
||||
NotificationDeliveryStatus::Delivered => Ok(()),
|
||||
NotificationDeliveryStatus::SkippedPreferences |
|
||||
NotificationDeliveryStatus::SkippedDefault |
|
||||
NotificationDeliveryStatus::Pending => Err(ApiError::InvalidInput("An error occurred while sending an email to your email address. Please try again later.".to_owned())),
|
||||
NotificationDeliveryStatus::PermanentlyFailed => Err(ApiError::InvalidInput("This email address doesn't exist! Please try another one.".to_owned())),
|
||||
NotificationDeliveryStatus::Pending => Err(ApiError::Request(eyre::eyre!("An error occurred while sending an email to your email address. Please try again later.".to_owned()))),
|
||||
NotificationDeliveryStatus::PermanentlyFailed => Err(ApiError::Request(eyre::eyre!("This email address doesn't exist! Please try another one.".to_owned()))),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,8 @@ pub struct Project {
|
||||
pub versions: Vec<VersionId>,
|
||||
/// The URL of the icon of the project
|
||||
pub icon_url: Option<String>,
|
||||
/// The URL of the unoptimized icon of the project
|
||||
pub raw_icon_url: Option<String>,
|
||||
|
||||
/// A collection of links to the project's various pages.
|
||||
pub link_urls: HashMap<String, Link>,
|
||||
@@ -193,6 +195,7 @@ impl From<ProjectQueryResult> for Project {
|
||||
loaders: m.loaders,
|
||||
versions: data.versions.into_iter().map(|v| v.into()).collect(),
|
||||
icon_url: m.icon_url,
|
||||
raw_icon_url: m.raw_icon_url,
|
||||
link_urls: data
|
||||
.urls
|
||||
.into_iter()
|
||||
@@ -567,11 +570,13 @@ impl ProjectStatus {
|
||||
pub fn can_be_requested(&self) -> bool {
|
||||
match self {
|
||||
ProjectStatus::Approved => true,
|
||||
ProjectStatus::Archived => true,
|
||||
ProjectStatus::Unlisted => true,
|
||||
ProjectStatus::Private => true,
|
||||
ProjectStatus::Draft => true,
|
||||
|
||||
// `archived` is represented by a disclosure, not a status, so it
|
||||
// can no longer be requested or set as a status.
|
||||
ProjectStatus::Archived => false,
|
||||
ProjectStatus::Rejected => false,
|
||||
ProjectStatus::Processing => false,
|
||||
ProjectStatus::Unknown => false,
|
||||
|
||||
Reference in New Issue
Block a user