Files
modrinth/apps/labrinth/src/models/v3/disclosures.rs
T
Sychic 549b333eb2 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>
2026-08-12 17:26:14 -07:00

146 lines
3.7 KiB
Rust

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>,
}