mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 00:55:25 +00:00
rejected projects go to ghost on delete (#7107)
* draft: rejected projects go to ghost on delete * don't free slug * fix sqlx * free slugs but different * add auto reject for processing items given to ghost, as well as send thread messages with a little info on who it came from
This commit is contained in:
Generated
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE mods\n SET organization_id = NULL, slug = COALESCE($1, slug)\n WHERE id = $2\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "26ca3d90c2e2289027ef434e370f4aa746272cd51dd50445057deb213db37049"
|
||||
}
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM mod_follows\n WHERE mod_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5d7d7e33c2952199225d7b93f5a74f3436ba18aa24e6ef1840becbf236447fd6"
|
||||
}
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM collections_mods\n WHERE mod_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6f23a7cd9f7d0ff70d63bb58228042cf53c2e26c050205a52a147c500fad6a97"
|
||||
}
|
||||
Generated
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n m.id AS \"id!\",\n m.status AS \"status!\",\n m.slug,\n t.id AS \"thread_id!\"\n FROM mods m\n INNER JOIN team_members tm\n ON tm.team_id = m.team_id\n AND tm.user_id = $1\n AND tm.is_owner = TRUE\n INNER JOIN threads t ON t.mod_id = m.id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "status!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "slug",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "thread_id!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "7a3ab78f56ae29dd35aadc928e92da7a72fa8604e62c2168e6b98d90772acb16"
|
||||
}
|
||||
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": "82e46320390619773a259aef7431c35cfd16fec52dbf3b4a6ddb3108e50169f2"
|
||||
}
|
||||
Generated
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM team_members\n WHERE team_id = $1\n RETURNING user_id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "user_id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "f2726e5a70cf171654cebb3c909234fc041e6d9f9610b443a26edae44925b358"
|
||||
}
|
||||
Generated
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT EXISTS(\n SELECT 1 FROM mods\n WHERE\n (slug = LOWER($1) OR text_id_lower = LOWER($1))\n AND id != $2\n ) AS \"exists!\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "ff9139f6a7a23e2f25f960f81726085938a42d6506dac1cfdd141ab143d1d28f"
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
use super::ids::{DBProjectId, DBUserId};
|
||||
use super::{DBCollectionId, DBReportId, DBThreadId};
|
||||
use crate::database::models::charge_item::DBCharge;
|
||||
use crate::database::models::thread_item::ThreadMessageBuilder;
|
||||
use crate::database::models::user_subscription_item::DBUserSubscription;
|
||||
use crate::database::models::{DBOrganizationId, DatabaseError};
|
||||
use crate::database::{PgTransaction, models};
|
||||
use crate::models::billing::ChargeStatus;
|
||||
use crate::models::projects::ProjectStatus;
|
||||
use crate::models::threads::MessageBody;
|
||||
use crate::models::users::Badges;
|
||||
use crate::util::error::Context;
|
||||
use ariadne::ids::base62_impl::{parse_base62, to_base62};
|
||||
use chrono::{DateTime, Utc};
|
||||
use dashmap::DashMap;
|
||||
use futures::TryStreamExt;
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
@@ -593,12 +597,120 @@ impl DBUser {
|
||||
.wrap_err("failed to get user by ID")?;
|
||||
|
||||
if let Some(delete_user) = user {
|
||||
let username = delete_user.username.clone();
|
||||
DBUser::clear_caches(&[(id, Some(delete_user.username))], redis)
|
||||
.await
|
||||
.wrap_err("failed to clear caches")?;
|
||||
|
||||
let deleted_user: DBUserId =
|
||||
crate::models::users::DELETED_USER.into();
|
||||
let user_id_str = ariadne::ids::UserId::from(id).to_string();
|
||||
|
||||
let owned_projects = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
m.id AS "id!",
|
||||
m.status AS "status!",
|
||||
m.slug,
|
||||
t.id AS "thread_id!"
|
||||
FROM mods m
|
||||
INNER JOIN team_members tm
|
||||
ON tm.team_id = m.team_id
|
||||
AND tm.user_id = $1
|
||||
AND tm.is_owner = TRUE
|
||||
INNER JOIN threads t ON t.mod_id = m.id
|
||||
"#,
|
||||
id as DBUserId,
|
||||
)
|
||||
.fetch(&mut *transaction)
|
||||
.try_collect::<Vec<_>>()
|
||||
.await
|
||||
.wrap_err("failed to fetch projects owned by deleted user")?;
|
||||
|
||||
for project in &owned_projects {
|
||||
let thread_id = DBThreadId(project.thread_id);
|
||||
|
||||
ThreadMessageBuilder {
|
||||
author_id: Some(deleted_user),
|
||||
body: MessageBody::Text {
|
||||
body: format!(
|
||||
"Project transferred to Ghost when user account `{username}` (`{user_id_str}`) was deleted"
|
||||
),
|
||||
private: true,
|
||||
replying_to: None,
|
||||
associated_images: Vec::new(),
|
||||
},
|
||||
thread_id,
|
||||
hide_identity: false,
|
||||
}
|
||||
.insert(&mut *transaction)
|
||||
.await
|
||||
.wrap_err(
|
||||
"failed to insert project transfer thread message",
|
||||
)?;
|
||||
|
||||
if ProjectStatus::from_string(&project.status)
|
||||
== ProjectStatus::Processing
|
||||
{
|
||||
ThreadMessageBuilder {
|
||||
author_id: Some(deleted_user),
|
||||
body: MessageBody::Text {
|
||||
body: format!(
|
||||
"Automatically rejected when user account `{username}` (`{user_id_str}`) was deleted"
|
||||
),
|
||||
private: true,
|
||||
replying_to: None,
|
||||
associated_images: Vec::new(),
|
||||
},
|
||||
thread_id,
|
||||
hide_identity: false,
|
||||
}
|
||||
.insert(&mut *transaction)
|
||||
.await
|
||||
.wrap_err(
|
||||
"failed to insert automatic rejection thread message",
|
||||
)?;
|
||||
|
||||
ThreadMessageBuilder {
|
||||
author_id: Some(deleted_user),
|
||||
body: MessageBody::StatusChange {
|
||||
new_status: ProjectStatus::Rejected,
|
||||
old_status: ProjectStatus::Processing,
|
||||
},
|
||||
thread_id,
|
||||
hide_identity: false,
|
||||
}
|
||||
.insert(&mut *transaction)
|
||||
.await
|
||||
.wrap_err(
|
||||
"failed to insert automatic rejection status change",
|
||||
)?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE mods
|
||||
SET status = $1
|
||||
WHERE id = $2
|
||||
"#,
|
||||
ProjectStatus::Rejected.as_str(),
|
||||
project.id,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.wrap_err(
|
||||
"failed to reject processing project owned by deleted user",
|
||||
)?;
|
||||
}
|
||||
|
||||
models::DBProject::clear_cache(
|
||||
DBProjectId(project.id),
|
||||
project.slug.clone(),
|
||||
None,
|
||||
redis,
|
||||
)
|
||||
.await
|
||||
.wrap_err("failed to clear project cache")?;
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
@@ -626,7 +738,6 @@ impl DBUser {
|
||||
.await
|
||||
.wrap_err("failed to update versions author_id")?;
|
||||
|
||||
use futures::TryStreamExt;
|
||||
let notifications: Vec<i64> = sqlx::query!(
|
||||
"
|
||||
SELECT n.id FROM notifications n
|
||||
|
||||
@@ -22,8 +22,9 @@ use crate::models::pats::Scopes;
|
||||
use crate::models::projects::{
|
||||
MonetizationStatus, Project, ProjectStatus, SideTypesMigrationReviewStatus,
|
||||
};
|
||||
use crate::models::teams::ProjectPermissions;
|
||||
use crate::models::teams::{DEFAULT_ROLE, ProjectPermissions};
|
||||
use crate::models::threads::MessageBody;
|
||||
use crate::models::users::DELETED_USER;
|
||||
use crate::models::{self, exp};
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
@@ -40,6 +41,7 @@ use chrono::Utc;
|
||||
use eyre::eyre;
|
||||
use futures::TryStreamExt;
|
||||
use itertools::Itertools;
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
use xredis::RedisPool;
|
||||
@@ -2934,6 +2936,169 @@ pub async fn project_delete_internal(
|
||||
.begin()
|
||||
.await
|
||||
.wrap_internal_err("failed to start transaction")?;
|
||||
|
||||
// rejected & withheld projects are transferred to ghost so moderation data is preserved
|
||||
if matches!(
|
||||
project.inner.status,
|
||||
ProjectStatus::Rejected | ProjectStatus::Withheld
|
||||
) {
|
||||
let deleted_user: db_ids::DBUserId = DELETED_USER.into();
|
||||
|
||||
let deleted_slug = if let Some(slug) = &project.inner.slug {
|
||||
let candidate = format!(
|
||||
"{slug}--deleted-{}",
|
||||
ProjectId::from(project.inner.id)
|
||||
);
|
||||
if candidate.len() <= 255 {
|
||||
let taken = sqlx::query!(
|
||||
r#"
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM mods
|
||||
WHERE
|
||||
(slug = LOWER($1) OR text_id_lower = LOWER($1))
|
||||
AND id != $2
|
||||
) AS "exists!"
|
||||
"#,
|
||||
candidate,
|
||||
project.inner.id as db_ids::DBProjectId,
|
||||
)
|
||||
.fetch_one(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err("checking deleted slug availability")?
|
||||
.exists;
|
||||
|
||||
if !taken {
|
||||
Some(candidate.to_lowercase())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET organization_id = NULL, slug = COALESCE($1, slug)
|
||||
WHERE id = $2
|
||||
",
|
||||
deleted_slug,
|
||||
project.inner.id as db_ids::DBProjectId,
|
||||
)
|
||||
.execute(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"failed to detach project from organization and update slug",
|
||||
)?;
|
||||
|
||||
let affected_user_ids = sqlx::query!(
|
||||
"
|
||||
DELETE FROM team_members
|
||||
WHERE team_id = $1
|
||||
RETURNING user_id
|
||||
",
|
||||
project.inner.team_id as db_ids::DBTeamId,
|
||||
)
|
||||
.fetch(&mut transaction)
|
||||
.map_ok(|x| db_ids::DBUserId(x.user_id))
|
||||
.try_collect::<Vec<_>>()
|
||||
.await
|
||||
.wrap_internal_err("failed to remove project team members")?;
|
||||
|
||||
let new_member_id = db_ids::generate_team_member_id(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err("failed to generate team member ID")?;
|
||||
DBTeamMember {
|
||||
id: new_member_id,
|
||||
team_id: project.inner.team_id,
|
||||
user_id: deleted_user,
|
||||
role: DEFAULT_ROLE.to_owned(),
|
||||
is_owner: true,
|
||||
permissions: ProjectPermissions::all(),
|
||||
organization_permissions: None,
|
||||
accepted: true,
|
||||
payouts_split: Decimal::ZERO,
|
||||
ordering: 0,
|
||||
}
|
||||
.insert(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err("failed to transfer project ownership to ghost")?;
|
||||
|
||||
ThreadMessageBuilder {
|
||||
author_id: Some(deleted_user),
|
||||
body: MessageBody::Text {
|
||||
body: format!(
|
||||
"Project transferred to Ghost when user account `{}` (`{}`) deleted this project",
|
||||
user.username,
|
||||
user.id
|
||||
),
|
||||
private: true,
|
||||
replying_to: None,
|
||||
associated_images: Vec::new(),
|
||||
},
|
||||
thread_id: project.thread_id,
|
||||
hide_identity: false,
|
||||
}
|
||||
.insert(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"failed to insert project transfer thread message",
|
||||
)?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM collections_mods
|
||||
WHERE mod_id = $1
|
||||
",
|
||||
project.inner.id as db_ids::DBProjectId,
|
||||
)
|
||||
.execute(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err("failed to delete project from collections_mods")?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM mod_follows
|
||||
WHERE mod_id = $1
|
||||
",
|
||||
project.inner.id as db_ids::DBProjectId,
|
||||
)
|
||||
.execute(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err("failed to delete project followers")?;
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.wrap_internal_err("failed to commit transaction")?;
|
||||
|
||||
let mut cache_user_ids = affected_user_ids;
|
||||
cache_user_ids.push(deleted_user);
|
||||
db_models::DBUser::clear_project_cache(&cache_user_ids, &redis)
|
||||
.await
|
||||
.wrap_internal_err("failed to clear user project cache")?;
|
||||
DBTeamMember::clear_cache(project.inner.team_id, &redis)
|
||||
.await
|
||||
.wrap_internal_err("clearing cached data from Redis")?;
|
||||
db_models::DBProject::clear_cache(
|
||||
project.inner.id,
|
||||
project.inner.slug.clone(),
|
||||
None,
|
||||
&redis,
|
||||
)
|
||||
.await
|
||||
.wrap_internal_err("clearing cached data from Redis")?;
|
||||
search_state
|
||||
.queue
|
||||
.push_project_removal(project.inner.id.into())
|
||||
.await;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
delphi::tech_review_sync::sync_deleted_project_tech_review_exit(
|
||||
project.inner.id,
|
||||
&mut transaction,
|
||||
|
||||
Reference in New Issue
Block a user