Compare commits

...
Author SHA1 Message Date
aecsocket 286d1e6851 Add moderation licenses endpoints 2026-03-18 19:17:05 +00:00
27 changed files with 778 additions and 127 deletions
@@ -1,26 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO versions (\n id, mod_id, author_id, name, version_number,\n changelog, date_published, downloads,\n version_type, featured, status, ordering,\n components\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8,\n $9, $10, $11, $12,\n $13\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Int8",
"Varchar",
"Varchar",
"Varchar",
"Timestamptz",
"Int4",
"Varchar",
"Bool",
"Varchar",
"Int4",
"Jsonb"
]
},
"nullable": []
},
"hash": "07dff8c7711178f10bf85271287704deb72636f4cbd6927108aadfd6e0d63f1c"
}
@@ -29,7 +29,8 @@
"low",
"medium",
"high",
"severe"
"severe",
"malware"
]
}
}
@@ -45,7 +46,7 @@
false,
true,
false,
false
true
]
},
"hash": "10e2a3b31ba94b93ed2d6c9753a5aabf13190a0b336089e6521022069813cf17"
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,\n v.changelog changelog, v.date_published date_published, v.downloads downloads,\n v.version_type version_type, v.featured featured, v.status status, v.requested_status requested_status, v.ordering ordering,\n v.components AS \"components: sqlx::types::Json<exp::VersionSerial>\"\n FROM versions v\n WHERE v.id = ANY($1);\n ",
"query": "\n SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,\n v.changelog changelog, v.date_published date_published, v.downloads downloads,\n\t\t\t\t\tv.version_type version_type, v.featured featured, v.status status, v.requested_status requested_status, v.locked, v.ordering ordering,\n\t\t\t\t\tv.components AS \"components: sqlx::types::Json<exp::VersionSerial>\"\n FROM versions v\n WHERE v.id = ANY($1);\n ",
"describe": {
"columns": [
{
@@ -65,11 +65,16 @@
},
{
"ordinal": 12,
"name": "locked",
"type_info": "Bool"
},
{
"ordinal": 13,
"name": "ordering",
"type_info": "Int4"
},
{
"ordinal": 13,
"ordinal": 14,
"name": "components: sqlx::types::Json<exp::VersionSerial>",
"type_info": "Jsonb"
}
@@ -92,9 +97,10 @@
false,
false,
true,
false,
true,
false
]
},
"hash": "760df5118dcd1f9d28db991caabb3c1ce6fb8ac08829345445b027e5a46c4860"
"hash": "13cf4b4e91dd9214a238ebb88d80a772b1a78d5630515b40a23089905eaa2bfd"
}
@@ -44,7 +44,8 @@
"low",
"medium",
"high",
"severe"
"severe",
"malware"
]
}
}
@@ -79,7 +80,7 @@
true,
false,
false,
false,
true,
null
]
},
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT encode(sha1, 'hex') as \"sha1!\"\n FROM moderation_external_files\n WHERE external_license_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "sha1!",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
null
]
},
"hash": "33eecf7a14eaeb4548332eed187d00a82cba1e15c4cb742875999de639ceeb5d"
}
@@ -0,0 +1,58 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, title, status, link, exceptions, proof, flame_project_id\n FROM moderation_external_licenses\n WHERE id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "title",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "link",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "exceptions",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "proof",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "flame_project_id",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
true,
false,
true,
true,
true,
true
]
},
"hash": "6a13f8ac9ca0d69badc7434b70327093e056fbd512d88afa7e79c7ba1ce3fe7f"
}
@@ -0,0 +1,58 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT mel.id, mel.title, mel.status, mel.link, mel.exceptions, mel.proof, mel.flame_project_id\n FROM moderation_external_files mef\n INNER JOIN moderation_external_licenses mel ON mef.external_license_id = mel.id\n WHERE mef.sha1 = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "title",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "link",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "exceptions",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "proof",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "flame_project_id",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Bytea"
]
},
"nullable": [
false,
true,
false,
true,
true,
true,
true
]
},
"hash": "7791c3b875cf53c596d78bb73e359463f3b3598c05019ca5ab1a01179b5e59c9"
}
@@ -0,0 +1,64 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE moderation_external_licenses\n SET\n title = COALESCE($1, title),\n status = COALESCE($2, status),\n link = COALESCE($3, link),\n exceptions = COALESCE($4, exceptions),\n proof = COALESCE($5, proof),\n flame_project_id = COALESCE($6, flame_project_id)\n WHERE id = $7\n RETURNING id, title, status, link, exceptions, proof, flame_project_id\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "title",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "link",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "exceptions",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "proof",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "flame_project_id",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Text",
"Text",
"Text",
"Int4",
"Int8"
]
},
"nullable": [
false,
true,
false,
true,
true,
true,
true
]
},
"hash": "913dfd25112fe13dea0bcda6021a795400ed2c84eb118267868a6d0e86ac69d3"
}
@@ -25,7 +25,8 @@
"low",
"medium",
"high",
"severe"
"severe",
"malware"
]
}
}
@@ -0,0 +1,60 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, title, status, link, exceptions, proof, flame_project_id\n FROM moderation_external_licenses\n WHERE ($1::text IS NULL OR title ILIKE $1)\n AND ($2::bigint IS NULL OR id > $2)\n ORDER BY id ASC\n LIMIT $3\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "title",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "link",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "exceptions",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "proof",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "flame_project_id",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text",
"Int8",
"Int8"
]
},
"nullable": [
false,
true,
false,
true,
true,
true,
true
]
},
"hash": "9688349081d12d877de1aa851eddcca84e8e7a1fabfc41dafcb0d993e2041dc0"
}
@@ -1,32 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO mods (\n id, team_id, name, summary, description,\n published, downloads, icon_url, raw_icon_url, status, requested_status,\n license_url, license,\n slug, color, monetization_status, organization_id,\n side_types_migration_review_status,\n components\n )\n VALUES (\n $1, $2, $3, $4, $5, $6,\n $7, $8, $9, $10, $11,\n $12, $13,\n LOWER($14), $15, $16, $17,\n $18,\n $19\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Varchar",
"Varchar",
"Varchar",
"Timestamptz",
"Int4",
"Varchar",
"Text",
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Text",
"Int4",
"Varchar",
"Int8",
"Varchar",
"Jsonb"
]
},
"nullable": []
},
"hash": "cf0ce4ce54edc7533332f0bfab27d977a9613f8aa22669c0f8fe7bab2d5d6192"
}
@@ -0,0 +1,27 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\t\tINSERT INTO versions (\n\t\t\t\tid, mod_id, author_id, name, version_number,\n\t\t\t\tchangelog, date_published, downloads,\n\t\t\t\tversion_type, featured, status, locked, ordering,\n\t\t\t\tcomponents\n\t\t\t)\n\t\t\tVALUES (\n\t\t\t\t$1, $2, $3, $4, $5,\n\t\t\t\t$6, $7, $8,\n\t\t\t\t$9, $10, $11, $12, $13,\n\t\t\t\t$14\n\t\t\t)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Int8",
"Varchar",
"Varchar",
"Varchar",
"Timestamptz",
"Int4",
"Varchar",
"Bool",
"Varchar",
"Bool",
"Int4",
"Jsonb"
]
},
"nullable": []
},
"hash": "df39464c131a9639fa2a1b3fedb6a38559f064d0e4c12e25ea3ff46f5c95b02f"
}
@@ -0,0 +1,33 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\t\tINSERT INTO mods (\n\t\t\t\tid, team_id, name, summary, description,\n\t\t\t\tpublished, downloads, icon_url, raw_icon_url, status, requested_status,\n\t\t\t\tlocked, license_url, license,\n\t\t\t\tslug, color, monetization_status, organization_id,\n\t\t\t\tside_types_migration_review_status,\n\t\t\t\tcomponents\n\t\t\t)\n\t\t\tVALUES (\n\t\t\t\t$1, $2, $3, $4, $5, $6,\n\t\t\t\t$7, $8, $9, $10, $11,\n\t\t\t\t$12, $13, $14,\n\t\t\t\tLOWER($15), $16, $17, $18,\n\t\t\t\t$19,\n\t\t\t\t$20\n\t\t\t)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Varchar",
"Varchar",
"Varchar",
"Timestamptz",
"Int4",
"Varchar",
"Text",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Text",
"Int4",
"Varchar",
"Int8",
"Varchar",
"Jsonb"
]
},
"nullable": []
},
"hash": "f117c9a885e1cee996ea2dd03ca7b366af947de65205a0e508e4d0b445a5dbbc"
}
@@ -22,7 +22,8 @@
"low",
"medium",
"high",
"severe"
"severe",
"malware"
]
}
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT m.id id, m.name name, m.summary summary, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.raw_icon_url raw_icon_url, m.description description, m.published published,\n m.approved approved, m.queued, m.status status, m.requested_status requested_status,\n m.license_url license_url,\n m.team_id team_id, m.organization_id organization_id, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n m.webhook_sent, m.color,\n t.id thread_id, m.monetization_status monetization_status,\n m.side_types_migration_review_status side_types_migration_review_status,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n m.components AS \"components: sqlx::types::Json<exp::ProjectSerial>\"\n\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n GROUP BY t.id, m.id\n ",
"query": "\n SELECT m.id id, m.name name, m.summary summary, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.raw_icon_url raw_icon_url, m.description description, m.published published,\n\t\t\t\t\tm.approved approved, m.queued, m.status status, m.requested_status requested_status, m.locked,\n\t\t\t\t\tm.license_url license_url,\n m.team_id team_id, m.organization_id organization_id, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n m.webhook_sent, m.color,\n t.id thread_id, m.monetization_status monetization_status,\n m.side_types_migration_review_status side_types_migration_review_status,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n m.components AS \"components: sqlx::types::Json<exp::ProjectSerial>\"\n\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n GROUP BY t.id, m.id\n ",
"describe": {
"columns": [
{
@@ -70,76 +70,81 @@
},
{
"ordinal": 13,
"name": "locked",
"type_info": "Bool"
},
{
"ordinal": 14,
"name": "license_url",
"type_info": "Varchar"
},
{
"ordinal": 14,
"ordinal": 15,
"name": "team_id",
"type_info": "Int8"
},
{
"ordinal": 15,
"ordinal": 16,
"name": "organization_id",
"type_info": "Int8"
},
{
"ordinal": 16,
"ordinal": 17,
"name": "license",
"type_info": "Varchar"
},
{
"ordinal": 17,
"ordinal": 18,
"name": "slug",
"type_info": "Varchar"
},
{
"ordinal": 18,
"ordinal": 19,
"name": "moderation_message",
"type_info": "Varchar"
},
{
"ordinal": 19,
"ordinal": 20,
"name": "moderation_message_body",
"type_info": "Varchar"
},
{
"ordinal": 20,
"ordinal": 21,
"name": "webhook_sent",
"type_info": "Bool"
},
{
"ordinal": 21,
"ordinal": 22,
"name": "color",
"type_info": "Int4"
},
{
"ordinal": 22,
"ordinal": 23,
"name": "thread_id",
"type_info": "Int8"
},
{
"ordinal": 23,
"ordinal": 24,
"name": "monetization_status",
"type_info": "Varchar"
},
{
"ordinal": 24,
"ordinal": 25,
"name": "side_types_migration_review_status",
"type_info": "Varchar"
},
{
"ordinal": 25,
"ordinal": 26,
"name": "categories",
"type_info": "VarcharArray"
},
{
"ordinal": 26,
"ordinal": 27,
"name": "additional_categories",
"type_info": "VarcharArray"
},
{
"ordinal": 27,
"ordinal": 28,
"name": "components: sqlx::types::Json<exp::ProjectSerial>",
"type_info": "Jsonb"
}
@@ -164,6 +169,7 @@
true,
false,
true,
false,
true,
false,
true,
@@ -181,5 +187,5 @@
false
]
},
"hash": "59b6eea93ce248d2b1eaf14fe8970bba8ccd2df0995a21bdb34ae3214cef6377"
"hash": "f2894d71272dded2e31c51293e9e3c8deeb63618ebe246c76f69b70f5454b3f6"
}
@@ -0,0 +1,5 @@
ALTER TYPE delphi_severity ADD VALUE 'malware';
ALTER TABLE mods ADD COLUMN locked BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE versions ADD COLUMN locked BOOLEAN NOT NULL DEFAULT FALSE;
@@ -0,0 +1 @@
CREATE INDEX idx_moderation_external_files_license_id ON moderation_external_files (external_license_id);
@@ -84,6 +84,8 @@ pub enum DelphiSeverity {
High,
#[serde(alias = "SEVERE")]
Severe,
#[serde(alias = "MALWARE")]
Malware,
}
/// An issue found in a Delphi report. Every issue belongs to a report,
@@ -206,6 +206,7 @@ impl ProjectBuilder {
},
status: self.status,
requested_status: self.requested_status,
locked: false,
downloads: 0,
follows: 0,
icon_url: self.icon_url,
@@ -287,6 +288,7 @@ pub struct DBProject {
pub queued: Option<DateTime<Utc>>,
pub status: ProjectStatus,
pub requested_status: Option<ProjectStatus>,
pub locked: bool,
pub downloads: i32,
pub follows: i32,
pub icon_url: Option<String>,
@@ -311,22 +313,22 @@ impl DBProject {
) -> Result<(), DatabaseError> {
sqlx::query!(
"
INSERT INTO mods (
id, team_id, name, summary, description,
published, downloads, icon_url, raw_icon_url, status, requested_status,
license_url, license,
slug, color, monetization_status, organization_id,
side_types_migration_review_status,
components
)
VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, $9, $10, $11,
$12, $13,
LOWER($14), $15, $16, $17,
$18,
$19
)
INSERT INTO mods (
id, team_id, name, summary, description,
published, downloads, icon_url, raw_icon_url, status, requested_status,
locked, license_url, license,
slug, color, monetization_status, organization_id,
side_types_migration_review_status,
components
)
VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, $9, $10, $11,
$12, $13, $14,
LOWER($15), $16, $17, $18,
$19,
$20
)
",
self.id as DBProjectId,
self.team_id as DBTeamId,
@@ -339,6 +341,7 @@ impl DBProject {
self.raw_icon_url.as_ref(),
self.status.as_str(),
self.requested_status.map(|x| x.as_str()),
self.locked,
self.license_url.as_ref(),
&self.license,
self.slug.as_ref(),
@@ -346,7 +349,8 @@ impl DBProject {
self.monetization_status.as_str(),
self.organization_id.map(|x| x.0 as i64),
self.side_types_migration_review_status.as_str(),
serde_json::to_value(&self.components).expect("serialization shouldn't fail"),
serde_json::to_value(&self.components)
.expect("serialization shouldn't fail"),
)
.execute(&mut *transaction)
.await?;
@@ -807,8 +811,8 @@ impl DBProject {
r#"
SELECT m.id id, m.name name, m.summary summary, m.downloads downloads, m.follows follows,
m.icon_url icon_url, m.raw_icon_url raw_icon_url, m.description description, m.published published,
m.approved approved, m.queued, m.status status, m.requested_status requested_status,
m.license_url license_url,
m.approved approved, m.queued, m.status status, m.requested_status requested_status, m.locked,
m.license_url license_url,
m.team_id team_id, m.organization_id organization_id, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,
m.webhook_sent, m.color,
t.id thread_id, m.monetization_status monetization_status,
@@ -894,13 +898,15 @@ impl DBProject {
published: m.published,
updated: versions.iter().map(|x| x.1).next_back().unwrap_or(m.published),
license_url: m.license_url.clone(),
status: ProjectStatus::from_string(
&m.status,
),
requested_status: m.requested_status.map(|x| ProjectStatus::from_string(
&x,
)),
license: m.license.clone(),
status: ProjectStatus::from_string(
&m.status,
),
requested_status: m
.requested_status
.as_deref()
.map(ProjectStatus::from_string),
locked: m.locked,
license: m.license.clone(),
slug: m.slug.clone(),
description: m.description.clone(),
follows: m.follows,
@@ -210,6 +210,7 @@ impl VersionBuilder {
version_type: self.version_type,
status: self.status,
requested_status: self.requested_status,
locked: false,
ordering: self.ordering,
components: self.components,
};
@@ -305,6 +306,7 @@ pub struct DBVersion {
pub featured: bool,
pub status: VersionStatus,
pub requested_status: Option<VersionStatus>,
pub locked: bool,
pub ordering: Option<i32>,
pub components: exp::VersionSerial,
}
@@ -324,18 +326,18 @@ impl DBVersion {
) -> Result<(), sqlx::error::Error> {
sqlx::query!(
"
INSERT INTO versions (
id, mod_id, author_id, name, version_number,
changelog, date_published, downloads,
version_type, featured, status, ordering,
components
)
VALUES (
$1, $2, $3, $4, $5,
$6, $7, $8,
$9, $10, $11, $12,
$13
)
INSERT INTO versions (
id, mod_id, author_id, name, version_number,
changelog, date_published, downloads,
version_type, featured, status, locked, ordering,
components
)
VALUES (
$1, $2, $3, $4, $5,
$6, $7, $8,
$9, $10, $11, $12, $13,
$14
)
",
self.id as DBVersionId,
self.project_id as DBProjectId,
@@ -348,6 +350,7 @@ impl DBVersion {
&self.version_type,
self.featured,
self.status.as_str(),
self.locked,
self.ordering,
serde_json::to_value(&self.components)
.expect("serialization shouldn't fail"),
@@ -741,8 +744,8 @@ impl DBVersion {
r#"
SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,
v.changelog changelog, v.date_published date_published, v.downloads downloads,
v.version_type version_type, v.featured featured, v.status status, v.requested_status requested_status, v.ordering ordering,
v.components AS "components: sqlx::types::Json<exp::VersionSerial>"
v.version_type version_type, v.featured featured, v.status status, v.requested_status requested_status, v.locked, v.ordering ordering,
v.components AS "components: sqlx::types::Json<exp::VersionSerial>"
FROM versions v
WHERE v.id = ANY($1);
"#,
@@ -779,10 +782,13 @@ impl DBVersion {
downloads: v.downloads,
version_type: v.version_type,
featured: v.featured,
status: VersionStatus::from_string(&v.status),
requested_status: v.requested_status
.map(|x| VersionStatus::from_string(&x)),
ordering: v.ordering,
status: VersionStatus::from_string(&v.status),
requested_status: v
.requested_status
.as_deref()
.map(VersionStatus::from_string),
locked: v.locked,
ordering: v.ordering,
components: components_serial,
},
files: {
@@ -1095,6 +1101,7 @@ mod tests {
featured: false,
status: VersionStatus::Listed,
requested_status: None,
locked: false,
components: exp::VersionSerial::default(),
}
}
+4
View File
@@ -47,6 +47,7 @@ pub struct LegacyProject {
pub queued: Option<DateTime<Utc>>,
pub status: ProjectStatus,
pub requested_status: Option<ProjectStatus>,
pub locked: bool,
pub moderator_message: Option<ModeratorMessage>,
pub license: License,
pub downloads: u32,
@@ -196,6 +197,7 @@ impl LegacyProject {
queued: data.queued,
status: data.status,
requested_status: data.requested_status,
locked: data.locked,
moderator_message: data.moderator_message,
license: data.license,
downloads: data.downloads,
@@ -314,6 +316,7 @@ pub struct LegacyVersion {
pub version_type: VersionType,
pub status: VersionStatus,
pub requested_status: Option<VersionStatus>,
pub locked: bool,
pub files: Vec<VersionFile>,
pub dependencies: Vec<Dependency>,
}
@@ -360,6 +363,7 @@ impl From<Version> for LegacyVersion {
version_type: data.version_type,
status: data.status,
requested_status: data.requested_status,
locked: data.locked,
files: data.files,
dependencies: data.dependencies,
game_versions,
+6
View File
@@ -52,6 +52,8 @@ pub struct Project {
pub status: ProjectStatus,
/// The requested status of this project
pub requested_status: Option<ProjectStatus>,
/// Whether versions on this project are locked against author changes.
pub locked: bool,
/// DEPRECATED: moved to threads system
/// The rejection data of the project
@@ -155,6 +157,7 @@ impl From<ProjectQueryResult> for Project {
queued: m.queued,
status: m.status,
requested_status: m.requested_status,
locked: m.locked,
moderator_message: if let Some(message) = m.moderation_message {
Some(ModeratorMessage {
message,
@@ -678,6 +681,8 @@ pub struct Version {
pub status: VersionStatus,
/// The requested status of the version (used for scheduling)
pub requested_status: Option<VersionStatus>,
/// Whether this version is locked against author changes.
pub locked: bool,
/// A list of files available for download for this version.
pub files: Vec<VersionFile>,
@@ -734,6 +739,7 @@ impl From<VersionQueryResult> for Version {
status: v.status,
requested_status: v.requested_status,
locked: v.locked,
files: data
.files
.into_iter()
@@ -0,0 +1,334 @@
use crate::database::PgPool;
use crate::database::redis::RedisPool;
use crate::models::pats::Scopes;
use crate::queue::session::AuthQueue;
use crate::util::error::Context;
use crate::{auth::check_is_moderator_from_headers, routes::ApiError};
use actix_web::{HttpRequest, get, patch, post, web};
use eyre::eyre;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(search_licenses)
.service(get_license)
.service(get_license_by_sha1)
.service(update_license);
}
#[derive(Debug, Deserialize, ToSchema)]
pub struct SearchLicenses {
/// Optional text query that must be present in the title
#[serde(default)]
pub query: Option<String>,
/// The ID to start pagination from (exclusive)
#[serde(default)]
pub next_id: Option<i64>,
/// Maximum number of results to return
#[serde(default = "default_limit")]
#[schema(default = 20)]
pub limit: u64,
}
fn default_limit() -> u64 {
20
}
#[derive(Debug, Serialize, ToSchema)]
pub struct ExternalLicense {
pub id: i64,
pub title: Option<String>,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub link: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exceptions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub proof: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub flame_project_id: Option<i32>,
pub files: Vec<ModerationExternalFile>,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct ModerationExternalFile {
/// Hex-encoded SHA1 hash
pub sha1: String,
}
#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdateExternalLicense {
/// The title of the license
#[serde(default)]
pub title: Option<String>,
/// The status of the license
#[serde(default)]
pub status: Option<String>,
/// A link to the license
#[serde(default)]
pub link: Option<String>,
/// Exceptions to the license
#[serde(default)]
pub exceptions: Option<String>,
/// Proof of the license
#[serde(default)]
pub proof: Option<String>,
/// The Flame project ID associated with the license
#[serde(default)]
pub flame_project_id: Option<i32>,
}
/// Search moderation external licenses
#[utoipa::path(
responses((status = OK, body = Vec<ExternalLicense>))
)]
#[post("/external-licenses/search")]
async fn search_licenses(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
web::Json(search): web::Json<SearchLicenses>,
) -> Result<web::Json<Vec<ExternalLicense>>, ApiError> {
check_is_moderator_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::PROJECT_READ,
)
.await?;
let SearchLicenses {
query,
next_id,
limit,
} = search;
let query_pattern = query.as_ref().map(|q| format!("%{q}%"));
let results = sqlx::query!(
r#"
SELECT id, title, status, link, exceptions, proof, flame_project_id
FROM moderation_external_licenses
WHERE ($1::text IS NULL OR title ILIKE $1)
AND ($2::bigint IS NULL OR id > $2)
ORDER BY id ASC
LIMIT $3
"#,
query_pattern,
next_id,
limit as i64
)
.fetch_all(&**pool)
.await?;
let licenses = results
.into_iter()
.map(|row| ExternalLicense {
id: row.id,
title: row.title,
status: row.status,
link: row.link,
exceptions: row.exceptions,
proof: row.proof,
flame_project_id: row.flame_project_id,
files: vec![],
})
.collect();
Ok(web::Json(licenses))
}
/// Get a single moderation external license by ID
#[utoipa::path(
params(("id" = i64, Path, description = "License ID")),
responses((status = OK, body = ExternalLicense))
)]
#[get("/external-licenses/{id}")]
async fn get_license(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
path: web::Path<(i64,)>,
) -> Result<web::Json<ExternalLicense>, ApiError> {
check_is_moderator_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::PROJECT_READ,
)
.await?;
let (id,) = path.into_inner();
let result = sqlx::query!(
r#"
SELECT id, title, status, link, exceptions, proof, flame_project_id
FROM moderation_external_licenses
WHERE id = $1
"#,
id
)
.fetch_optional(&**pool)
.await?
.ok_or(ApiError::NotFound)?;
let external_files = sqlx::query!(
r#"
SELECT encode(sha1, 'hex') as "sha1!"
FROM moderation_external_files
WHERE external_license_id = $1
"#,
id
)
.fetch_all(&**pool)
.await?
.into_iter()
.map(|row| ModerationExternalFile { sha1: row.sha1 })
.collect::<Vec<_>>();
Ok(web::Json(ExternalLicense {
id: result.id,
title: result.title,
status: result.status,
link: result.link,
exceptions: result.exceptions,
proof: result.proof,
flame_project_id: result.flame_project_id,
files: external_files,
}))
}
/// Get the external license associated with a file by its SHA1 hash
#[utoipa::path(
params(("sha1" = String, Path, description = "SHA1 hash of the file (hex-encoded)")),
responses((status = OK, body = ExternalLicense))
)]
#[get("/external-licenses/by-sha1/{sha1}")]
async fn get_license_by_sha1(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
path: web::Path<(String,)>,
) -> Result<web::Json<ExternalLicense>, ApiError> {
check_is_moderator_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::PROJECT_READ,
)
.await?;
let (sha1_hex,) = path.into_inner();
// Validate and convert hex string to bytea
let sha1_bytes =
hex::decode(&sha1_hex).wrap_request_err("invalid SHA1 hex string")?;
let sha1_bytes = <[u8; 20]>::try_from(sha1_bytes).map_err(|_| {
ApiError::Request(eyre!(
"SHA1 hash must be exactly 20 bytes".to_string(),
))
})?;
let result = sqlx::query!(
r#"
SELECT mel.id, mel.title, mel.status, mel.link, mel.exceptions, mel.proof, mel.flame_project_id
FROM moderation_external_files mef
INNER JOIN moderation_external_licenses mel ON mef.external_license_id = mel.id
WHERE mef.sha1 = $1
"#,
sha1_bytes.as_slice()
)
.fetch_optional(&**pool)
.await?
.ok_or(ApiError::NotFound)?;
let files = sqlx::query!(
r#"
SELECT encode(sha1, 'hex') as "sha1!"
FROM moderation_external_files
WHERE external_license_id = $1
"#,
result.id
)
.fetch_all(&**pool)
.await?
.into_iter()
.map(|row| ModerationExternalFile { sha1: row.sha1 })
.collect::<Vec<_>>();
Ok(web::Json(ExternalLicense {
id: result.id,
title: result.title,
status: result.status,
link: result.link,
exceptions: result.exceptions,
proof: result.proof,
flame_project_id: result.flame_project_id,
files,
}))
}
/// Update a moderation external license by ID
#[utoipa::path]
#[patch("/external-licenses/{id}")]
async fn update_license(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
path: web::Path<(i64,)>,
web::Json(update): web::Json<UpdateExternalLicense>,
) -> Result<web::Json<ExternalLicense>, ApiError> {
check_is_moderator_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::PROJECT_READ,
)
.await?;
let (id,) = path.into_inner();
let result = sqlx::query!(
r#"
UPDATE moderation_external_licenses
SET
title = COALESCE($1, title),
status = COALESCE($2, status),
link = COALESCE($3, link),
exceptions = COALESCE($4, exceptions),
proof = COALESCE($5, proof),
flame_project_id = COALESCE($6, flame_project_id)
WHERE id = $7
RETURNING id, title, status, link, exceptions, proof, flame_project_id
"#,
update.title,
update.status,
update.link,
update.exceptions,
update.proof,
update.flame_project_id,
id,
)
.fetch_optional(&**pool)
.await?
.ok_or(ApiError::NotFound)?;
Ok(web::Json(ExternalLicense {
id: result.id,
title: result.title,
status: result.status,
link: result.link,
exceptions: result.exceptions,
proof: result.proof,
flame_project_id: result.flame_project_id,
files: vec![],
}))
}
@@ -17,6 +17,7 @@ use ownership::get_projects_ownership;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
mod license;
mod ownership;
mod tech_review;
@@ -28,6 +29,7 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
.service(get_lock_status)
.service(release_lock)
.service(delete_all_locks)
.configure(license::config)
.service(
utoipa_actix_web::scope("/tech-review")
.configure(tech_review::config),
@@ -994,6 +994,7 @@ async fn project_create_inner(
queued: None,
status,
requested_status: project_builder.requested_status,
locked: false,
moderator_message: None,
license: License {
id: project_create_data.license_id.clone(),
@@ -450,6 +450,7 @@ async fn version_create_inner(
version_type: version_data.release_channel,
status: builder.status,
requested_status: builder.requested_status,
locked: false,
ordering: builder.ordering,
files: builder
.files
@@ -46,6 +46,7 @@ pub struct CommonProject {
pub queued: Option<DateTime<Utc>>,
pub status: ProjectStatus,
pub requested_status: Option<ProjectStatus>,
pub locked: bool,
pub moderator_message: Option<ModeratorMessage>,
pub license: License,
pub downloads: u32,
@@ -75,6 +76,7 @@ pub struct CommonVersion {
pub version_type: VersionType,
pub status: VersionStatus,
pub requested_status: Option<VersionStatus>,
pub locked: bool,
pub files: Vec<VersionFile>,
pub dependencies: Vec<Dependency>,
}