mirror of
https://github.com/modrinth/code.git
synced 2026-08-30 19:46:33 +00:00
feat(labrinth): project disclosures (#6945)
* feat(labrinth): project disclosures model * feat(labrinth): project disclosures database model * feat(labrinth): project disclosures get endpoint * feat(labrinth): censor user ids if set by moderator * feat(labrinth): wrap disclosures in struct * feat(labrinth): edit project disclosures endpoint * style(labrinth): cargo fmt * style(labrinth): fix typo * feat(labrinth): add fields for ai content disclosure * fix(labrinth): field typo * chore(labrinth): update query cache * feat(labrinth): index disclosures in search * refactor(labrinth): use enum instead of bools * feat(labrinth): trigger incremental index on disclosure change * feat(labrinth): change archived status to disclosure * feat(labrinth): use disclosure for archival status * fix(labrinth): error type for deserialization * fix(labrinth): migration timestamp * fix(labrinth): add disclosures to elasticsearch schema * fix(labrinth): don't remove archival disclosure when changing status via v3 api * feat(labrinth): derive str for ai usages and telemtry consent * feat(labrinth): include ai usages and telemetry consent in disclosure types * refactor(labrinth): move disclosure parsing to document creation * fix(labrinth): prevent removing moderator added archival disclosures * fix(labrinth): dedupe ai usages * fix(labrinth): auth logic on archived disclosure removal * feat(labrinth): add interactions field * style(labrinth): cargo fmt * feat(labrinth): soft delete disclosures * feat(labrinth): return soft-deleted disclosures * fix(labrinth): update disclosure on removal --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -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,
|
||||
@@ -238,28 +241,42 @@ impl LegacyProject {
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
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, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ProjectDisclosureData {
|
||||
#[serde(flatten)]
|
||||
pub disclosure: ProjectDisclosure,
|
||||
pub set_by_moderator: bool,
|
||||
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,
|
||||
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;
|
||||
|
||||
@@ -570,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