mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 01:26:23 +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:
Generated
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE mods\n SET approved = NOW()\n WHERE id = $1 AND approved IS NULL\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "115232c68c63fefe5f8f9fdaa056ba1029ebf8a7c52db69a297045e515d244f1"
|
||||
}
|
||||
Generated
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\t\tSELECT 1 FROM project_disclosures\n\t\t\tWHERE project_id = $1 AND type = ANY($2) AND set_by_moderator AND deleted_at IS NULL\n\t\t\tLIMIT 1\n\t\t\t",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "200036925be75644a292282e9638a8c006dcbfae6e06b568872a8fa2cfe90a7e"
|
||||
}
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE mods\n SET moderation_message = NULL, moderation_message_body = NULL, queued = NOW()\n WHERE (id = $1)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2fd123813896a880bc7a4a4cd195766099a04bda40cec97f0707b9c78c7fafdc"
|
||||
}
|
||||
Generated
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT project_id, type, metadata\n FROM project_disclosures\n WHERE project_id = ANY($1) AND deleted_at IS NULL\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "metadata",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "398cd48fc641dc32a126232835cf3be21abbde6bd5dfe4ec064b471f6959f0c0"
|
||||
}
|
||||
Generated
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\t\tINSERT INTO project_disclosures (project_id, type, metadata, updated_by, set_by_moderator)\n\t\t\tVALUES ($1, $2, $3, $4, $5)\n\t\t\tON CONFLICT (project_id, type) DO UPDATE SET\n\t\t\t\tmetadata = $3,\n\t\t\t\tupdated_at = now(),\n\t\t\t\tupdated_by = $4,\n\t\t\t\tset_by_moderator = $5,\n\t\t\t\tdeleted_at = NULL\n\t\t\t",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Int8",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "412be939314ecfe4de9a0725e5170699b86e431e6a500c635d37da8cd16022a1"
|
||||
}
|
||||
Generated
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT project_id, type, metadata\n FROM project_disclosures\n WHERE project_id = ANY($1)\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "metadata",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4ccad9e07ae61ba87b1784b6ee9a136ddbe802cd3a4c649e4982d9f28ae097ed"
|
||||
}
|
||||
Generated
+59
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\t\tSELECT project_id, type AS \"disclosure_type!\", metadata, updated_at, updated_by, set_by_moderator, deleted_at\n\t\t\tFROM project_disclosures\n\t\t\tWHERE project_id = $1 AND ($2 OR deleted_at IS NULL)\n\t\t\tORDER BY updated_at DESC\n\t\t\t",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "disclosure_type!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "metadata",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "updated_by",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "set_by_moderator",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "deleted_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "588bb489a5b8c7231a86d53906c2c632d85f6d595210cfaba364bb537c9e8442"
|
||||
}
|
||||
Generated
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\t\tSELECT 1 FROM project_disclosures\n\t\t\tWHERE project_id = $1 AND type = ANY($2) AND set_by_moderator\n\t\t\tLIMIT 1\n\t\t\t",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "763762b9995a6d7b00b6cf71c7661a8dff02ed316fc0b98754605a6e16e8e846"
|
||||
}
|
||||
Generated
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\t\tSELECT project_id\n\t\t\tFROM project_disclosures\n\t\t\tWHERE type = $1 AND project_id = ANY($2)\n\t\t\t",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "87d4778e52ac3e9fd885f9c77595705f8efc09ac21e0cf9a15c4cce019aaeb05"
|
||||
}
|
||||
Generated
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE mods\n SET approved = NOW()\n WHERE id = $1 AND approved IS NULL\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "92d805d2e13cfc0f2220f15b0a35ff71e654e5e6b386766e6c6047cf3861b26e"
|
||||
}
|
||||
Generated
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\t\tSELECT project_id\n\t\t\tFROM project_disclosures\n\t\t\tWHERE type = $1 AND project_id = ANY($2) AND deleted_at IS NULL\n\t\t\t",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "964f1acc9dfc7b0c6b2ce6adec3c535c77a6e6e98d13c4543f503cf8a86a743c"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT tm.user_id id\n FROM team_members tm\n WHERE tm.team_id = $1 AND tm.accepted\n ",
|
||||
"query": "\n SELECT tm.user_id id\n FROM team_members tm\n WHERE tm.team_id = $1 AND tm.accepted\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -18,5 +18,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "d5ad5a67fe53351b760335b80501f09a2799bf575af90beeac94193fe8c4388b"
|
||||
"hash": "9e5170dddcd089edd4a5c89ec8929f0bdf6f6c2c56d944e7af3b2b13d9d95f3d"
|
||||
}
|
||||
Generated
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\t\tDELETE FROM project_disclosures\n\t\t\tWHERE project_id = $1 AND type = $2\n\t\t\t",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9eea063cb153da407548f957cef7da1a33a059b0a1d00b51e27b4a5399492b13"
|
||||
}
|
||||
Generated
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE mods\n SET status = $1\n WHERE (id = $2)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a74230ad1bb1b13bab850e204436e7746a96f9605afe2ca62d6d8337530cb5ad"
|
||||
}
|
||||
Generated
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\t\tINSERT INTO project_disclosures (project_id, type, metadata, updated_by, set_by_moderator)\n\t\t\tVALUES ($1, $2, $3, $4, $5)\n\t\t\tON CONFLICT (project_id, type) DO UPDATE SET\n\t\t\t\tmetadata = $3,\n\t\t\t\tupdated_at = now(),\n\t\t\t\tupdated_by = $4,\n\t\t\t\tset_by_moderator = $5\n\t\t\t",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Int8",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b6b9b0cd345eddb4c4a9daf701dd0d2e5a708e59b897961f00164a28ab6294a4"
|
||||
}
|
||||
Generated
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE mods\n SET status = $1\n WHERE (id = $2)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c33d32e876b003a82dcf0036ef483924a801d01b426e93050600ae678e2e46bd"
|
||||
}
|
||||
Generated
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE mods\n SET moderation_message = NULL, moderation_message_body = NULL, queued = NOW()\n WHERE (id = $1)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c6693ea80ab1675dd2da72d70add734a92bb25f17a0536968e4b9a4dbe05cf5b"
|
||||
}
|
||||
Generated
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\t\tUPDATE project_disclosures\n\t\t\tSET deleted_at = now(), updated_at = now(), updated_by = $3, set_by_moderator = $4\n\t\t\tWHERE project_id = $1 AND type = $2 AND deleted_at IS NULL\n\t\t\t",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text",
|
||||
"Int8",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ca976dee555cef5affd47c5ca69179c9862e760eb5ebb3951d0546f59e46a2fb"
|
||||
}
|
||||
Generated
+52
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\t\tSELECT project_id, type AS \"disclosure_type!\", metadata, updated_at, updated_by, set_by_moderator\n\t\t\tFROM project_disclosures\n\t\t\tWHERE project_id = $1\n\t\t\tORDER BY updated_at DESC\n\t\t\t",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "disclosure_type!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "metadata",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "updated_by",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "set_by_moderator",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ef602037ceb20dbce5313e4a90c16fb59fbbe0adf002d21fc40ca0ddd9d6ab10"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE project_disclosures (
|
||||
project_id BIGINT NOT NULL REFERENCES mods(id) ON DELETE CASCADE,
|
||||
type TEXT NOT NULL,
|
||||
metadata JSONB NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_by BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
set_by_moderator BOOLEAN NOT NULL,
|
||||
PRIMARY KEY (project_id, type)
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
INSERT INTO project_disclosures (project_id, type, metadata, updated_by, set_by_moderator)
|
||||
SELECT id, 'archived', '{"note": null}'::jsonb, 0, FALSE
|
||||
FROM mods
|
||||
WHERE status = 'archived'
|
||||
ON CONFLICT (project_id, type) DO NOTHING;
|
||||
|
||||
UPDATE mods
|
||||
SET status = 'approved'
|
||||
WHERE status = 'archived';
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE project_disclosures
|
||||
ADD COLUMN deleted_at TIMESTAMPTZ;
|
||||
@@ -30,6 +30,7 @@ pub mod payout_item;
|
||||
pub mod payouts_values_notifications;
|
||||
pub mod product_item;
|
||||
pub mod products_tax_identifier_item;
|
||||
pub mod project_disclosure_item;
|
||||
pub mod project_item;
|
||||
pub mod report_item;
|
||||
pub mod session_item;
|
||||
@@ -53,6 +54,7 @@ pub use image_item::DBImage;
|
||||
pub use oauth_client_item::DBOAuthClient;
|
||||
pub use organization_item::DBOrganization;
|
||||
pub use passkey_item::DBPasskey;
|
||||
pub use project_disclosure_item::DBProjectDisclosure;
|
||||
pub use project_item::DBProject;
|
||||
pub use team_item::DBTeam;
|
||||
pub use team_item::DBTeamMember;
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
database::models::{DBProjectId, DBUserId, DatabaseError},
|
||||
models::v3::disclosures::{ProjectDisclosure, ProjectDisclosureType},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DBProjectDisclosure {
|
||||
pub project_id: DBProjectId,
|
||||
pub disclosure: ProjectDisclosure,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub updated_by: DBUserId,
|
||||
pub set_by_moderator: bool,
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl DBProjectDisclosure {
|
||||
pub async fn upsert(
|
||||
&self,
|
||||
exec: impl crate::database::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let (disclosure_type, metadata) =
|
||||
self.disclosure.to_parts().map_err(|e| {
|
||||
DatabaseError::Internal(eyre::Report::new(e).wrap_err(
|
||||
"failed to serialize project disclosure metadata",
|
||||
))
|
||||
})?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO project_disclosures (project_id, type, metadata, updated_by, set_by_moderator)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (project_id, type) DO UPDATE SET
|
||||
metadata = $3,
|
||||
updated_at = now(),
|
||||
updated_by = $4,
|
||||
set_by_moderator = $5,
|
||||
deleted_at = NULL
|
||||
"#,
|
||||
self.project_id as DBProjectId,
|
||||
disclosure_type,
|
||||
metadata,
|
||||
self.updated_by as DBUserId,
|
||||
self.set_by_moderator,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_many_for_project(
|
||||
project_id: DBProjectId,
|
||||
include_deleted: bool,
|
||||
exec: impl crate::database::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<Vec<DBProjectDisclosure>, DatabaseError> {
|
||||
let rows = sqlx::query!(
|
||||
r#"
|
||||
SELECT project_id, type AS "disclosure_type!", metadata, updated_at, updated_by, set_by_moderator, deleted_at
|
||||
FROM project_disclosures
|
||||
WHERE project_id = $1 AND ($2 OR deleted_at IS NULL)
|
||||
ORDER BY updated_at DESC
|
||||
"#,
|
||||
project_id as DBProjectId,
|
||||
include_deleted,
|
||||
)
|
||||
.fetch_all(exec)
|
||||
.await?;
|
||||
|
||||
rows.into_iter()
|
||||
.map(|row| {
|
||||
Ok(DBProjectDisclosure {
|
||||
project_id: DBProjectId(row.project_id),
|
||||
disclosure: ProjectDisclosure::from_parts(
|
||||
&row.disclosure_type,
|
||||
row.metadata,
|
||||
)
|
||||
.map_err(|e| {
|
||||
DatabaseError::Internal(eyre::Report::new(e).wrap_err(
|
||||
"failed to deserialize project disclosure metadata",
|
||||
))
|
||||
})?,
|
||||
updated_at: row.updated_at,
|
||||
updated_by: DBUserId(row.updated_by),
|
||||
set_by_moderator: row.set_by_moderator,
|
||||
deleted_at: row.deleted_at,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns the subset of `project_ids` that carry a disclosure of the given type.
|
||||
pub async fn projects_with_type(
|
||||
disclosure_type: ProjectDisclosureType,
|
||||
project_ids: &[DBProjectId],
|
||||
exec: impl crate::database::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<HashSet<DBProjectId>, DatabaseError> {
|
||||
let ids = project_ids.iter().map(|id| id.0).collect::<Vec<_>>();
|
||||
let rows = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT project_id
|
||||
FROM project_disclosures
|
||||
WHERE type = $1 AND project_id = ANY($2) AND deleted_at IS NULL
|
||||
"#,
|
||||
<&'static str>::from(disclosure_type),
|
||||
&ids,
|
||||
)
|
||||
.fetch_all(exec)
|
||||
.await?;
|
||||
|
||||
Ok(rows.into_iter().map(DBProjectId).collect())
|
||||
}
|
||||
|
||||
pub async fn any_set_by_moderator(
|
||||
project_id: DBProjectId,
|
||||
types: &[String],
|
||||
exec: impl crate::database::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<bool, DatabaseError> {
|
||||
let existing = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT 1 FROM project_disclosures
|
||||
WHERE project_id = $1 AND type = ANY($2) AND set_by_moderator AND deleted_at IS NULL
|
||||
LIMIT 1
|
||||
"#,
|
||||
project_id as DBProjectId,
|
||||
types,
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
|
||||
Ok(existing.is_some())
|
||||
}
|
||||
|
||||
pub async fn remove(
|
||||
project_id: DBProjectId,
|
||||
disclosure_type: ProjectDisclosureType,
|
||||
updated_by: DBUserId,
|
||||
set_by_moderator: bool,
|
||||
exec: impl crate::database::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<bool, DatabaseError> {
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
UPDATE project_disclosures
|
||||
SET deleted_at = now(), updated_at = now(), updated_by = $3, set_by_moderator = $4
|
||||
WHERE project_id = $1 AND type = $2 AND deleted_at IS NULL
|
||||
"#,
|
||||
project_id as DBProjectId,
|
||||
<&'static str>::from(disclosure_type),
|
||||
updated_by as DBUserId,
|
||||
set_by_moderator,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::database::models as db_models;
|
||||
use crate::database::models::categories::LinkPlatform;
|
||||
use crate::database::models::{project_item, version_item};
|
||||
use crate::database::{PgPool, ReadOnlyPgPool};
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models::disclosures::ProjectDisclosureType;
|
||||
use crate::models::projects::{
|
||||
Link, MonetizationStatus, Project, ProjectStatus, Version,
|
||||
};
|
||||
@@ -206,7 +208,7 @@ pub async fn random_projects_get(
|
||||
match v2_reroute::extract_ok_json::<Vec<Project>>(response).await {
|
||||
Ok(project) => {
|
||||
let legacy_projects =
|
||||
LegacyProject::from_many(project, &**pool, &redis)
|
||||
LegacyProject::from_many(project, &pool, &redis)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"executing `LegacyProject::from_many`",
|
||||
@@ -252,7 +254,7 @@ pub async fn projects_get(
|
||||
match v2_reroute::extract_ok_json::<Vec<Project>>(response).await {
|
||||
Ok(project) => {
|
||||
let legacy_projects =
|
||||
LegacyProject::from_many(project, &**pool, &redis)
|
||||
LegacyProject::from_many(project, &pool, &redis)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"executing `LegacyProject::from_many`",
|
||||
@@ -315,7 +317,19 @@ pub async fn project_get(
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let project = LegacyProject::from(project, version_item);
|
||||
let has_archived_disclosure =
|
||||
db_models::DBProjectDisclosure::projects_with_type(
|
||||
ProjectDisclosureType::Archived,
|
||||
&[project.id.into()],
|
||||
&**pool,
|
||||
)
|
||||
.await
|
||||
.wrap_internal_err("fetching archival disclosure status")?
|
||||
.contains(&project.id.into());
|
||||
let mut project = LegacyProject::from(project, version_item);
|
||||
if has_archived_disclosure && project.status == ProjectStatus::Approved {
|
||||
project.status = ProjectStatus::Archived;
|
||||
}
|
||||
Ok(HttpResponse::Ok().json(project))
|
||||
}
|
||||
|
||||
@@ -402,7 +416,7 @@ pub async fn dependency_list(
|
||||
Ok(dependency_info) => {
|
||||
let converted_projects = LegacyProject::from_many(
|
||||
dependency_info.projects,
|
||||
&**pool,
|
||||
&pool,
|
||||
&redis,
|
||||
)
|
||||
.await
|
||||
@@ -671,6 +685,7 @@ pub async fn project_edit(
|
||||
redis.clone(),
|
||||
session_queue.clone(),
|
||||
search_state.clone(),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.or_else(v2_reroute::flatten_404_error)
|
||||
|
||||
@@ -193,7 +193,7 @@ pub async fn projects_list(
|
||||
match v2_reroute::extract_ok_json::<Vec<Project>>(response).await {
|
||||
Ok(project) => {
|
||||
let legacy_projects =
|
||||
LegacyProject::from_many(project, &**pool, &redis)
|
||||
LegacyProject::from_many(project, &pool, &redis)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"executing `LegacyProject::from_many`",
|
||||
@@ -465,7 +465,7 @@ pub async fn user_follows(
|
||||
match v2_reroute::extract_ok_json::<Vec<Project>>(response).await {
|
||||
Ok(project) => {
|
||||
let legacy_projects =
|
||||
LegacyProject::from_many(project, &**pool, &redis)
|
||||
LegacyProject::from_many(project, &pool, &redis)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"executing `LegacyProject::from_many`",
|
||||
|
||||
@@ -364,7 +364,7 @@ pub async fn get_projects_from_hashes(
|
||||
.collect::<HashMap<_, _>>();
|
||||
let legacy_projects = LegacyProject::from_many(
|
||||
projects_hashes.into_values().collect(),
|
||||
&**pool,
|
||||
&pool,
|
||||
&redis,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
use actix_web::{HttpRequest, get, patch, web};
|
||||
use chrono::Utc;
|
||||
use eyre::eyre;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use xredis::RedisPool;
|
||||
|
||||
use crate::auth::checks::{is_team_member_project, is_visible_project};
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::database::{DBProject, models as db_models};
|
||||
use crate::database::{PgPool, ReadOnlyPgPool};
|
||||
use crate::models::disclosures::{
|
||||
ProjectDisclosure, ProjectDisclosureData, ProjectDisclosureType,
|
||||
};
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::models::teams::ProjectPermissions;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::search::SearchState;
|
||||
use crate::util::error::Context;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(get_project_disclosures)
|
||||
.service(modify_project_disclosures);
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct GetProjectDisclosures {
|
||||
pub disclosures: Vec<ProjectDisclosureData>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
context_path = "/project",
|
||||
tag = "project_disclosures",
|
||||
responses((status = OK, body = GetProjectDisclosures), (status = BAD_REQUEST, description = "Invalid request input"))
|
||||
)]
|
||||
#[get("/{project_id}/disclosures")]
|
||||
pub async fn get_project_disclosures(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
ro_pool: web::Data<ReadOnlyPgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<web::Json<GetProjectDisclosures>, ApiError> {
|
||||
let (string,) = info.into_inner();
|
||||
|
||||
let project = db_models::DBProject::get(&string, &***ro_pool, &redis)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch project")?
|
||||
.wrap_not_found_err("resource not found")?;
|
||||
|
||||
let user_option = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await
|
||||
.map(|(_, user)| user)
|
||||
.ok();
|
||||
|
||||
if !is_visible_project(&project.inner, &user_option, &pool, false)
|
||||
.await
|
||||
.wrap_internal_err("failed to check project visibility")?
|
||||
{
|
||||
return Err(ApiError::NotFound(eyre!("resource not found")));
|
||||
}
|
||||
|
||||
let viewer_is_moderator =
|
||||
user_option.as_ref().is_some_and(|user| user.role.is_mod());
|
||||
|
||||
let include_deleted = viewer_is_moderator
|
||||
|| is_team_member_project(&project.inner, &user_option, &pool)
|
||||
.await
|
||||
.wrap_internal_err("failed to check project team membership")?;
|
||||
|
||||
let disclosures = db_models::DBProjectDisclosure::get_many_for_project(
|
||||
project.inner.id,
|
||||
include_deleted,
|
||||
&***ro_pool,
|
||||
)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch project disclosures")?;
|
||||
|
||||
Ok(web::Json(GetProjectDisclosures {
|
||||
disclosures: disclosures
|
||||
.into_iter()
|
||||
.map(|disclosure| {
|
||||
ProjectDisclosureData::from_db(disclosure, viewer_is_moderator)
|
||||
})
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct ModifyProjectDisclosures {
|
||||
pub set: Vec<ProjectDisclosure>,
|
||||
pub remove: Vec<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
context_path = "/project",
|
||||
tag = "project_disclosures",
|
||||
request_body = ModifyProjectDisclosures,
|
||||
responses((status = NO_CONTENT))
|
||||
)]
|
||||
#[patch("/{project_id}/disclosures")]
|
||||
pub async fn modify_project_disclosures(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
search_state: web::Data<SearchState>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
body: web::Json<ModifyProjectDisclosures>,
|
||||
) -> Result<(), ApiError> {
|
||||
let (string,) = info.into_inner();
|
||||
let body = body.into_inner();
|
||||
|
||||
if body.set.is_empty() && body.remove.is_empty() {
|
||||
return Err(ApiError::Request(eyre!(
|
||||
"must provide at least one disclosure to set or remove"
|
||||
)));
|
||||
}
|
||||
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?
|
||||
.1;
|
||||
|
||||
let project = db_models::DBProject::get(&string, &**pool, &redis)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch project")?
|
||||
.wrap_not_found_err("resource not found")?;
|
||||
|
||||
let (team_member, organization_team_member) =
|
||||
db_models::DBTeamMember::get_for_project_permissions(
|
||||
&project.inner,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch project permissions")?;
|
||||
|
||||
let can_edit_details = ProjectPermissions::get_permissions_by_role(
|
||||
&user.role,
|
||||
&team_member,
|
||||
&organization_team_member,
|
||||
)
|
||||
.is_some_and(|perms| perms.contains(ProjectPermissions::EDIT_DETAILS));
|
||||
|
||||
if !can_edit_details {
|
||||
return Err(ApiError::Auth(eyre!(
|
||||
"you do not have permission to edit this project's disclosures"
|
||||
)));
|
||||
}
|
||||
|
||||
if !user.role.is_mod() {
|
||||
let modified_types = body
|
||||
.set
|
||||
.iter()
|
||||
.map(|disclosure| {
|
||||
<&'static str>::from(ProjectDisclosureType::from(disclosure))
|
||||
.to_owned()
|
||||
})
|
||||
.chain(body.remove.iter().cloned())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if db_models::DBProjectDisclosure::any_set_by_moderator(
|
||||
project.inner.id,
|
||||
&modified_types,
|
||||
&**pool,
|
||||
)
|
||||
.await
|
||||
.wrap_internal_err("failed to check moderator disclosures")?
|
||||
{
|
||||
return Err(ApiError::Auth(eyre!(
|
||||
"you cannot modify a disclosure set by a moderator"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let mut transaction = pool
|
||||
.begin()
|
||||
.await
|
||||
.wrap_internal_err("starting database transaction")?;
|
||||
|
||||
for disclosure in body.set {
|
||||
db_models::DBProjectDisclosure {
|
||||
project_id: project.inner.id,
|
||||
disclosure,
|
||||
updated_at: Utc::now(),
|
||||
updated_by: user.id.into(),
|
||||
set_by_moderator: user.role.is_mod(),
|
||||
deleted_at: None,
|
||||
}
|
||||
.upsert(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err("failed to upsert project disclosure")?;
|
||||
}
|
||||
|
||||
for disclosure_type in &body.remove {
|
||||
let disclosure_type = ProjectDisclosureType::from_str(disclosure_type)
|
||||
.map_err(|_| {
|
||||
ApiError::Request(eyre!(
|
||||
"unknown disclosure type `{disclosure_type}`"
|
||||
))
|
||||
})?;
|
||||
db_models::DBProjectDisclosure::remove(
|
||||
project.inner.id,
|
||||
disclosure_type,
|
||||
user.id.into(),
|
||||
user.role.is_mod(),
|
||||
&mut transaction,
|
||||
)
|
||||
.await
|
||||
.wrap_internal_err("failed to remove project disclosure")?;
|
||||
}
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.wrap_internal_err("failed to commit project disclosure changes")?;
|
||||
|
||||
DBProject::clear_cache(project.inner.id, project.inner.slug, None, &redis)
|
||||
.await
|
||||
.wrap_internal_err("clearing cached data from Redis")?;
|
||||
|
||||
search_state
|
||||
.queue
|
||||
.push_project_change(project.inner.id.into())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -9,6 +9,7 @@ pub mod analytics_get;
|
||||
pub mod blocked_users;
|
||||
pub mod collections;
|
||||
pub mod content;
|
||||
pub mod disclosures;
|
||||
pub mod friends;
|
||||
pub mod images;
|
||||
pub mod limits;
|
||||
@@ -46,7 +47,8 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
web::scope("/v3/project")
|
||||
.wrap(default_cors())
|
||||
.configure(projects::project_config)
|
||||
.configure(project_creation::config),
|
||||
.configure(project_creation::config)
|
||||
.configure(disclosures::config),
|
||||
);
|
||||
cfg.service(
|
||||
web::scope("/v3")
|
||||
@@ -111,6 +113,8 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
projects::project_unfollow,
|
||||
projects::project_get_organization,
|
||||
projects::dependency_list,
|
||||
disclosures::get_project_disclosures,
|
||||
disclosures::modify_project_disclosures,
|
||||
project_creation::project_create,
|
||||
project_creation::project_create_with_id,
|
||||
project_creation::new::create,
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::database::{self, models as db_models};
|
||||
use crate::database::{PgPool, PgTransaction, ReadOnlyPgPool};
|
||||
use crate::env::ENV;
|
||||
use crate::file_hosting::{FileHost, FileHostPublicity};
|
||||
use crate::models::disclosures::{ProjectDisclosure, ProjectDisclosureType};
|
||||
use crate::models::ids::{ProjectId, VersionId};
|
||||
use crate::models::images::ImageContext;
|
||||
use crate::models::notifications::NotificationBody;
|
||||
@@ -376,6 +377,7 @@ pub async fn project_edit(
|
||||
redis,
|
||||
session_queue,
|
||||
search_state,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -388,6 +390,7 @@ pub async fn project_edit_internal(
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
search_state: web::Data<SearchState>,
|
||||
sync_archival_disclosure: bool,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
@@ -488,105 +491,141 @@ pub async fn project_edit_internal(
|
||||
)));
|
||||
}
|
||||
|
||||
if !(user.role.is_mod()
|
||||
|| !project_item.inner.status.is_approved()
|
||||
&& status == &ProjectStatus::Processing
|
||||
|| project_item.inner.status.is_approved()
|
||||
&& status.can_be_requested())
|
||||
{
|
||||
return Err(ApiError::Auth(eyre::eyre!(
|
||||
"You don't have permission to set this status!",
|
||||
)));
|
||||
}
|
||||
let archival_disclosure =
|
||||
db_models::DBProjectDisclosure::get_many_for_project(
|
||||
project_item.inner.id,
|
||||
false,
|
||||
&mut transaction,
|
||||
)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch project disclosures")?
|
||||
.into_iter()
|
||||
.find(|disclosure| {
|
||||
matches!(
|
||||
disclosure.disclosure,
|
||||
ProjectDisclosure::Archived { .. }
|
||||
)
|
||||
});
|
||||
let has_archived_disclosure = archival_disclosure.is_some();
|
||||
|
||||
// If a moderator (non-admin) is completing a review while another moderator holds an
|
||||
// active checklist lock, block them from changing the project status.
|
||||
if user.role.is_mod()
|
||||
&& !user.role.is_admin()
|
||||
&& project_item.inner.status == ProjectStatus::Processing
|
||||
&& status != &ProjectStatus::Processing
|
||||
&& let Some(lock) =
|
||||
DBModerationLock::get_with_user(project_item.inner.id, &pool)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"fetching moderation lock from database",
|
||||
)?
|
||||
&& lock.moderator_id != db_ids::DBUserId::from(user.id)
|
||||
&& !lock.expired
|
||||
{
|
||||
return Err(ApiError::Auth(eyre::eyre!(format!(
|
||||
"This project is currently being moderated by @{}. Please wait for them to finish or for the lock to expire.",
|
||||
lock.moderator_username
|
||||
))));
|
||||
}
|
||||
if status == &ProjectStatus::Archived {
|
||||
if !has_archived_disclosure {
|
||||
db_models::DBProjectDisclosure {
|
||||
project_id: project_item.inner.id,
|
||||
disclosure: ProjectDisclosure::Archived { note: None },
|
||||
updated_at: Utc::now(),
|
||||
updated_by: user.id.into(),
|
||||
set_by_moderator: user.role.is_mod(),
|
||||
deleted_at: None,
|
||||
}
|
||||
.upsert(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err("failed to upsert archival disclosure")?;
|
||||
}
|
||||
} else {
|
||||
if !(user.role.is_mod()
|
||||
|| !project_item.inner.status.is_approved()
|
||||
&& status == &ProjectStatus::Processing
|
||||
|| project_item.inner.status.is_approved()
|
||||
&& status.can_be_requested())
|
||||
{
|
||||
return Err(ApiError::Auth(eyre::eyre!(
|
||||
"You don't have permission to set this status!",
|
||||
)));
|
||||
}
|
||||
|
||||
if status == &ProjectStatus::Processing {
|
||||
if project_item.versions.is_empty() {
|
||||
return Err(ApiError::Request(eyre::eyre!(String::from(
|
||||
"Project submitted for review with no initial versions",
|
||||
// If a moderator (non-admin) is completing a review while another moderator holds an
|
||||
// active checklist lock, block them from changing the project status.
|
||||
if user.role.is_mod()
|
||||
&& !user.role.is_admin()
|
||||
&& project_item.inner.status == ProjectStatus::Processing
|
||||
&& status != &ProjectStatus::Processing
|
||||
&& let Some(lock) = DBModerationLock::get_with_user(
|
||||
project_item.inner.id,
|
||||
&pool,
|
||||
)
|
||||
.await
|
||||
.wrap_internal_err("fetching moderation lock from database")?
|
||||
&& lock.moderator_id != db_ids::DBUserId::from(user.id)
|
||||
&& !lock.expired
|
||||
{
|
||||
return Err(ApiError::Auth(eyre::eyre!(format!(
|
||||
"This project is currently being moderated by @{}. Please wait for them to finish or for the lock to expire.",
|
||||
lock.moderator_username
|
||||
))));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET moderation_message = NULL, moderation_message_body = NULL, queued = NOW()
|
||||
WHERE (id = $1)
|
||||
",
|
||||
id as db_ids::DBProjectId,
|
||||
)
|
||||
.execute(&mut transaction)
|
||||
.await.wrap_internal_err("querying database for `project_edit_internal`")?;
|
||||
}
|
||||
if status == &ProjectStatus::Processing {
|
||||
if project_item.versions.is_empty() {
|
||||
return Err(ApiError::Request(eyre::eyre!(String::from(
|
||||
"Project submitted for review with no initial versions",
|
||||
))));
|
||||
}
|
||||
|
||||
if status.is_approved() && !project_item.inner.status.is_approved() {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET approved = NOW()
|
||||
WHERE id = $1 AND approved IS NULL
|
||||
",
|
||||
id as db_ids::DBProjectId,
|
||||
)
|
||||
.execute(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"querying database for `project_edit_internal`",
|
||||
)?;
|
||||
}
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET moderation_message = NULL, moderation_message_body = NULL, queued = NOW()
|
||||
WHERE (id = $1)
|
||||
",
|
||||
id as db_ids::DBProjectId,
|
||||
)
|
||||
.execute(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"querying database for `project_edit_internal`",
|
||||
)?;
|
||||
}
|
||||
|
||||
if status.is_searchable()
|
||||
&& !project_item.inner.webhook_sent
|
||||
&& !ENV.PUBLIC_DISCORD_WEBHOOK.is_empty()
|
||||
&& project_item.inner.components.minecraft_server.is_none()
|
||||
{
|
||||
crate::util::webhook::send_discord_webhook(
|
||||
project_item.inner.id.into(),
|
||||
&pool,
|
||||
&redis,
|
||||
&ENV.PUBLIC_DISCORD_WEBHOOK,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
if status.is_approved() && !project_item.inner.status.is_approved()
|
||||
{
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET approved = NOW()
|
||||
WHERE id = $1 AND approved IS NULL
|
||||
",
|
||||
id as db_ids::DBProjectId,
|
||||
)
|
||||
.execute(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"querying database for `project_edit_internal`",
|
||||
)?;
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
if status.is_searchable()
|
||||
&& !project_item.inner.webhook_sent
|
||||
&& !ENV.PUBLIC_DISCORD_WEBHOOK.is_empty()
|
||||
&& project_item.inner.components.minecraft_server.is_none()
|
||||
{
|
||||
crate::util::webhook::send_discord_webhook(
|
||||
project_item.inner.id.into(),
|
||||
&pool,
|
||||
&redis,
|
||||
&ENV.PUBLIC_DISCORD_WEBHOOK,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET webhook_sent = TRUE
|
||||
WHERE id = $1
|
||||
",
|
||||
id as db_ids::DBProjectId,
|
||||
)
|
||||
.execute(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"querying database for `project_edit_internal`",
|
||||
)?;
|
||||
}
|
||||
id as db_ids::DBProjectId,
|
||||
)
|
||||
.execute(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"querying database for `project_edit_internal`",
|
||||
)?;
|
||||
}
|
||||
|
||||
if user.role.is_mod() && !ENV.MODERATION_SLACK_WEBHOOK.is_empty() {
|
||||
crate::util::webhook::send_slack_project_webhook(
|
||||
if user.role.is_mod() && !ENV.MODERATION_SLACK_WEBHOOK.is_empty() {
|
||||
crate::util::webhook::send_slack_project_webhook(
|
||||
project_item.inner.id.into(),
|
||||
&pool,
|
||||
&redis,
|
||||
@@ -605,83 +644,102 @@ pub async fn project_edit_internal(
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
if team_member.is_none_or(|x| !x.accepted) {
|
||||
let notified_members = sqlx::query!(
|
||||
"
|
||||
SELECT tm.user_id id
|
||||
FROM team_members tm
|
||||
WHERE tm.team_id = $1 AND tm.accepted
|
||||
",
|
||||
project_item.inner.team_id as db_ids::DBTeamId
|
||||
)
|
||||
.fetch(&mut transaction)
|
||||
.map_ok(|c| db_models::DBUserId(c.id))
|
||||
.try_collect::<Vec<_>>()
|
||||
.await
|
||||
.wrap_internal_err("fetching notified members from database")?;
|
||||
|
||||
NotificationBuilder {
|
||||
body: NotificationBody::StatusChange {
|
||||
project_id: project_item.inner.id.into(),
|
||||
old_status: project_item.inner.status,
|
||||
new_status: *status,
|
||||
},
|
||||
}
|
||||
.insert_many(notified_members.clone(), &mut transaction, &redis)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"inserting database records for `project_edit_internal`",
|
||||
)?;
|
||||
|
||||
NotificationBuilder {
|
||||
body: if status.is_approved() {
|
||||
NotificationBody::ProjectStatusApproved {
|
||||
project_id: project_item.inner.id.into(),
|
||||
}
|
||||
} else {
|
||||
NotificationBody::ProjectStatusNeutral {
|
||||
if team_member.is_none_or(|x| !x.accepted) {
|
||||
let notified_members = sqlx::query!(
|
||||
"
|
||||
SELECT tm.user_id id
|
||||
FROM team_members tm
|
||||
WHERE tm.team_id = $1 AND tm.accepted
|
||||
",
|
||||
project_item.inner.team_id as db_ids::DBTeamId
|
||||
)
|
||||
.fetch(&mut transaction)
|
||||
.map_ok(|c| db_models::DBUserId(c.id))
|
||||
.try_collect::<Vec<_>>()
|
||||
.await
|
||||
.wrap_internal_err("fetching notified members from database")?;
|
||||
|
||||
NotificationBuilder {
|
||||
body: NotificationBody::StatusChange {
|
||||
project_id: project_item.inner.id.into(),
|
||||
old_status: project_item.inner.status,
|
||||
new_status: *status,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
.insert_many(notified_members.clone(), &mut transaction, &redis)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"inserting database records for `project_edit_internal`",
|
||||
)?;
|
||||
|
||||
NotificationBuilder {
|
||||
body: if status.is_approved() {
|
||||
NotificationBody::ProjectStatusApproved {
|
||||
project_id: project_item.inner.id.into(),
|
||||
}
|
||||
} else {
|
||||
NotificationBody::ProjectStatusNeutral {
|
||||
project_id: project_item.inner.id.into(),
|
||||
old_status: project_item.inner.status,
|
||||
new_status: *status,
|
||||
}
|
||||
},
|
||||
}
|
||||
.insert_many(notified_members, &mut transaction, &redis)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"inserting database records for `project_edit_internal`",
|
||||
)?;
|
||||
}
|
||||
.insert_many(notified_members, &mut transaction, &redis)
|
||||
|
||||
ThreadMessageBuilder {
|
||||
author_id: Some(user.id.into()),
|
||||
body: MessageBody::StatusChange {
|
||||
new_status: *status,
|
||||
old_status: project_item.inner.status,
|
||||
},
|
||||
thread_id: project_item.thread_id,
|
||||
hide_identity: user.role.is_mod(),
|
||||
}
|
||||
.insert(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"inserting database records for `project_edit_internal`",
|
||||
)?;
|
||||
}
|
||||
|
||||
ThreadMessageBuilder {
|
||||
author_id: Some(user.id.into()),
|
||||
body: MessageBody::StatusChange {
|
||||
new_status: *status,
|
||||
old_status: project_item.inner.status,
|
||||
},
|
||||
thread_id: project_item.thread_id,
|
||||
hide_identity: user.role.is_mod(),
|
||||
}
|
||||
.insert(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"inserting database records for `project_edit_internal`",
|
||||
)?;
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET status = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
status.as_str(),
|
||||
id as db_ids::DBProjectId,
|
||||
)
|
||||
.execute(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"querying database for `project_edit_internal`",
|
||||
)?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET status = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
status.as_str(),
|
||||
id as db_ids::DBProjectId,
|
||||
)
|
||||
.execute(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err("querying database for `project_edit_internal`")?;
|
||||
if sync_archival_disclosure
|
||||
&& archival_disclosure.is_some_and(|disclosure| {
|
||||
user.role.is_mod() || !disclosure.set_by_moderator
|
||||
})
|
||||
{
|
||||
db_models::DBProjectDisclosure::remove(
|
||||
project_item.inner.id,
|
||||
ProjectDisclosureType::Archived,
|
||||
user.id.into(),
|
||||
user.role.is_mod(),
|
||||
&mut transaction,
|
||||
)
|
||||
.await
|
||||
.wrap_internal_err("failed to remove archival disclosure")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(requested_status) = &new_project.requested_status {
|
||||
|
||||
@@ -688,6 +688,7 @@ impl Elasticsearch {
|
||||
"compatible_dependency_project_ids": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"disclosure_types": {"type": "keyword"},
|
||||
"required_dependency_project_ids": {"type": "keyword"},
|
||||
"optional_dependency_project_ids": {"type": "keyword"},
|
||||
"embedded_dependency_project_ids": {"type": "keyword"},
|
||||
|
||||
@@ -715,6 +715,14 @@ impl SearchField {
|
||||
optional: true,
|
||||
token_separators: &["-"],
|
||||
},
|
||||
SearchField::DisclosureTypes => TypesenseFieldSpec {
|
||||
path: "disclosure_types",
|
||||
ty: "string[]",
|
||||
facet: true,
|
||||
sort: false,
|
||||
optional: true,
|
||||
token_separators: &["_"],
|
||||
},
|
||||
SearchField::RequiredDependencyProjectIds => TypesenseFieldSpec {
|
||||
path: "required_dependency_project_ids",
|
||||
ty: "string[]",
|
||||
|
||||
@@ -18,6 +18,7 @@ use crate::database::models::{
|
||||
DBOrganizationId, DBProjectId, DBUserId, DBVersionId, LoaderFieldEnumId,
|
||||
LoaderFieldEnumValueId, LoaderFieldId,
|
||||
};
|
||||
use crate::models::disclosures::ProjectDisclosure;
|
||||
use crate::models::exp;
|
||||
use crate::models::ids::{ProjectId, VersionId};
|
||||
use crate::models::projects::{DependencyType, from_duplicate_version_fields};
|
||||
@@ -316,6 +317,32 @@ async fn build_search_documents(
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!("Indexing local disclosures!");
|
||||
|
||||
let project_disclosures: DashMap<
|
||||
DBProjectId,
|
||||
Vec<(String, serde_json::Value)>,
|
||||
> = sqlx::query!(
|
||||
"
|
||||
SELECT project_id, type, metadata
|
||||
FROM project_disclosures
|
||||
WHERE project_id = ANY($1) AND deleted_at IS NULL
|
||||
",
|
||||
&*project_ids,
|
||||
)
|
||||
.fetch(pool)
|
||||
.try_fold(
|
||||
DashMap::new(),
|
||||
|acc: DashMap<DBProjectId, Vec<(String, serde_json::Value)>>, m| {
|
||||
acc.entry(DBProjectId(m.project_id))
|
||||
.or_default()
|
||||
.push((m.r#type, m.metadata));
|
||||
async move { Ok(acc) }
|
||||
},
|
||||
)
|
||||
.await
|
||||
.wrap_err("failed to fetch project disclosures")?;
|
||||
|
||||
info!("Indexing local versions!");
|
||||
let mut versions = load_project_versions(pool, project_ids.clone()).await?;
|
||||
|
||||
@@ -497,6 +524,45 @@ async fn build_search_documents(
|
||||
let mut project_categories = categories;
|
||||
project_categories.sort();
|
||||
project_categories.dedup();
|
||||
|
||||
let disclosure_types = project_disclosures
|
||||
.remove(&project.id)
|
||||
.map(|(_, disclosures)| disclosures)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.flat_map(|(kind, metadata)| {
|
||||
let disclosure =
|
||||
match ProjectDisclosure::from_parts(&kind, metadata) {
|
||||
Ok(disclosure) => disclosure,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
project_id = project.id.0,
|
||||
disclosure_type = kind,
|
||||
"indexing project disclosure without its metadata: {e}"
|
||||
);
|
||||
return vec![kind];
|
||||
}
|
||||
};
|
||||
|
||||
let mut disclosure_type_tokens = vec![kind.clone()];
|
||||
|
||||
match &disclosure {
|
||||
ProjectDisclosure::AiContent { uses, .. } => {
|
||||
disclosure_type_tokens.extend(uses.iter().map(|usage| {
|
||||
format!("{kind}_{}", <&'static str>::from(usage))
|
||||
}));
|
||||
}
|
||||
ProjectDisclosure::Telemetry { consent, .. } => {
|
||||
disclosure_type_tokens.push(format!(
|
||||
"{kind}_{}",
|
||||
<&'static str>::from(consent)
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
disclosure_type_tokens
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let dependencies = dependencies
|
||||
.get(&project.id)
|
||||
.map(|x| x.clone())
|
||||
@@ -764,6 +830,7 @@ async fn build_search_documents(
|
||||
embedded_dependency_project_ids,
|
||||
incompatible_dependency_project_ids,
|
||||
dependencies,
|
||||
disclosure_types,
|
||||
project_loader_fields,
|
||||
loader_fields,
|
||||
loaders: project_loaders,
|
||||
|
||||
@@ -218,6 +218,7 @@ pub enum SearchField {
|
||||
MinecraftJavaServerPingData,
|
||||
DependencyProjectIds,
|
||||
CompatibleDependencyProjectIds,
|
||||
DisclosureTypes,
|
||||
RequiredDependencyProjectIds,
|
||||
OptionalDependencyProjectIds,
|
||||
EmbeddedDependencyProjectIds,
|
||||
@@ -302,6 +303,8 @@ pub struct UploadSearchProject {
|
||||
pub incompatible_dependency_project_ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub dependencies: Vec<SearchProjectDependency>,
|
||||
#[serde(default)]
|
||||
pub disclosure_types: Vec<String>,
|
||||
|
||||
// Hidden fields to get the Project model out of the search results.
|
||||
pub loaders: Vec<String>, // Search uses loaders as categories- this is purely for the Project model.
|
||||
@@ -408,6 +411,8 @@ pub struct ResultSearchProject {
|
||||
pub incompatible_dependency_project_ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub dependencies: Vec<SearchProjectDependency>,
|
||||
#[serde(default)]
|
||||
pub disclosure_types: Vec<String>,
|
||||
|
||||
// Hidden fields to get the Project model out of the search results.
|
||||
pub loaders: Vec<String>, // Search uses loaders as categories- this is purely for the Project model.
|
||||
@@ -458,6 +463,7 @@ impl From<UploadSearchProject> for ResultSearchProject {
|
||||
incompatible_dependency_project_ids: source
|
||||
.incompatible_dependency_project_ids,
|
||||
dependencies: source.dependencies,
|
||||
disclosure_types: source.disclosure_types,
|
||||
loaders: source.loaders,
|
||||
project_loader_fields: source.project_loader_fields,
|
||||
components: source.components,
|
||||
|
||||
Reference in New Issue
Block a user