mirror of
https://github.com/modrinth/code.git
synced 2026-08-27 18:14:49 +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::ids::{DBProjectId, DBUserId};
|
||||||
use super::{DBCollectionId, DBReportId, DBThreadId};
|
use super::{DBCollectionId, DBReportId, DBThreadId};
|
||||||
use crate::database::models::charge_item::DBCharge;
|
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::user_subscription_item::DBUserSubscription;
|
||||||
use crate::database::models::{DBOrganizationId, DatabaseError};
|
use crate::database::models::{DBOrganizationId, DatabaseError};
|
||||||
use crate::database::{PgTransaction, models};
|
use crate::database::{PgTransaction, models};
|
||||||
use crate::models::billing::ChargeStatus;
|
use crate::models::billing::ChargeStatus;
|
||||||
|
use crate::models::projects::ProjectStatus;
|
||||||
|
use crate::models::threads::MessageBody;
|
||||||
use crate::models::users::Badges;
|
use crate::models::users::Badges;
|
||||||
use crate::util::error::Context;
|
use crate::util::error::Context;
|
||||||
use ariadne::ids::base62_impl::{parse_base62, to_base62};
|
use ariadne::ids::base62_impl::{parse_base62, to_base62};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
|
use futures::TryStreamExt;
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt::{Debug, Display};
|
use std::fmt::{Debug, Display};
|
||||||
@@ -593,12 +597,120 @@ impl DBUser {
|
|||||||
.wrap_err("failed to get user by ID")?;
|
.wrap_err("failed to get user by ID")?;
|
||||||
|
|
||||||
if let Some(delete_user) = user {
|
if let Some(delete_user) = user {
|
||||||
|
let username = delete_user.username.clone();
|
||||||
DBUser::clear_caches(&[(id, Some(delete_user.username))], redis)
|
DBUser::clear_caches(&[(id, Some(delete_user.username))], redis)
|
||||||
.await
|
.await
|
||||||
.wrap_err("failed to clear caches")?;
|
.wrap_err("failed to clear caches")?;
|
||||||
|
|
||||||
let deleted_user: DBUserId =
|
let deleted_user: DBUserId =
|
||||||
crate::models::users::DELETED_USER.into();
|
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!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
@@ -626,7 +738,6 @@ impl DBUser {
|
|||||||
.await
|
.await
|
||||||
.wrap_err("failed to update versions author_id")?;
|
.wrap_err("failed to update versions author_id")?;
|
||||||
|
|
||||||
use futures::TryStreamExt;
|
|
||||||
let notifications: Vec<i64> = sqlx::query!(
|
let notifications: Vec<i64> = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT n.id FROM notifications n
|
SELECT n.id FROM notifications n
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ use crate::models::pats::Scopes;
|
|||||||
use crate::models::projects::{
|
use crate::models::projects::{
|
||||||
MonetizationStatus, Project, ProjectStatus, SideTypesMigrationReviewStatus,
|
MonetizationStatus, Project, ProjectStatus, SideTypesMigrationReviewStatus,
|
||||||
};
|
};
|
||||||
use crate::models::teams::ProjectPermissions;
|
use crate::models::teams::{DEFAULT_ROLE, ProjectPermissions};
|
||||||
use crate::models::threads::MessageBody;
|
use crate::models::threads::MessageBody;
|
||||||
|
use crate::models::users::DELETED_USER;
|
||||||
use crate::models::{self, exp};
|
use crate::models::{self, exp};
|
||||||
use crate::queue::session::AuthQueue;
|
use crate::queue::session::AuthQueue;
|
||||||
use crate::routes::ApiError;
|
use crate::routes::ApiError;
|
||||||
@@ -40,6 +41,7 @@ use chrono::Utc;
|
|||||||
use eyre::eyre;
|
use eyre::eyre;
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use rust_decimal::Decimal;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
use xredis::RedisPool;
|
use xredis::RedisPool;
|
||||||
@@ -216,7 +218,7 @@ pub async fn projects_get(
|
|||||||
Ok(HttpResponse::Ok().json(projects))
|
Ok(HttpResponse::Ok().json(projects))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a project.
|
/// Get a project.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
context_path = "/project",
|
context_path = "/project",
|
||||||
tag = "projects", responses((status = OK, body = Project))
|
tag = "projects", responses((status = OK, body = Project))
|
||||||
@@ -351,7 +353,7 @@ pub struct EditProject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
/// Update a project.
|
/// Update a project.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
context_path = "/project",
|
context_path = "/project",
|
||||||
tag = "projects", responses((status = NO_CONTENT))
|
tag = "projects", responses((status = NO_CONTENT))
|
||||||
@@ -1313,7 +1315,7 @@ pub async fn edit_project_categories(
|
|||||||
// pub total_hits: usize,
|
// pub total_hits: usize,
|
||||||
// }
|
// }
|
||||||
|
|
||||||
/// Search projects.
|
/// Search projects.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
tag = "search",
|
tag = "search",
|
||||||
get,
|
get,
|
||||||
@@ -1360,7 +1362,7 @@ pub async fn project_search(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// for more complicated search queries
|
// for more complicated search queries
|
||||||
/// Search projects.
|
/// Search projects.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
tag = "search",
|
tag = "search",
|
||||||
request_body = serde_json::Value,
|
request_body = serde_json::Value,
|
||||||
@@ -1380,7 +1382,7 @@ pub async fn project_search_post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
//checks the validity of a project id or slug
|
//checks the validity of a project id or slug
|
||||||
/// Check project availability.
|
/// Check project availability.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
context_path = "/project",
|
context_path = "/project",
|
||||||
tag = "projects", responses((status = OK, body = ProjectCheckResponse))
|
tag = "projects", responses((status = OK, body = ProjectCheckResponse))
|
||||||
@@ -1420,7 +1422,7 @@ pub struct DependencyInfo {
|
|||||||
pub versions: Vec<models::projects::Version>,
|
pub versions: Vec<models::projects::Version>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List project dependencies.
|
/// List project dependencies.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
context_path = "/project",
|
context_path = "/project",
|
||||||
tag = "projects", responses((status = OK, body = DependencyInfo))
|
tag = "projects", responses((status = OK, body = DependencyInfo))
|
||||||
@@ -1944,7 +1946,7 @@ pub struct Extension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
/// Update a project icon.
|
/// Update a project icon.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
context_path = "/project",
|
context_path = "/project",
|
||||||
tag = "projects",
|
tag = "projects",
|
||||||
@@ -2108,7 +2110,7 @@ pub async fn project_icon_edit_internal(
|
|||||||
Ok(HttpResponse::NoContent().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete a project icon.
|
/// Delete a project icon.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
context_path = "/project",
|
context_path = "/project",
|
||||||
tag = "projects", responses((status = NO_CONTENT))
|
tag = "projects", responses((status = NO_CONTENT))
|
||||||
@@ -2249,7 +2251,7 @@ pub struct GalleryCreateQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
/// Add a gallery item.
|
/// Add a gallery item.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
context_path = "/project",
|
context_path = "/project",
|
||||||
tag = "projects",
|
tag = "projects",
|
||||||
@@ -2472,7 +2474,7 @@ pub struct GalleryEditQuery {
|
|||||||
pub ordering: Option<i64>,
|
pub ordering: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update a gallery item.
|
/// Update a gallery item.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
context_path = "/project",
|
context_path = "/project",
|
||||||
tag = "projects",
|
tag = "projects",
|
||||||
@@ -2694,7 +2696,7 @@ pub struct GalleryDeleteQuery {
|
|||||||
pub url: String,
|
pub url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete a gallery item.
|
/// Delete a gallery item.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
context_path = "/project",
|
context_path = "/project",
|
||||||
tag = "projects",
|
tag = "projects",
|
||||||
@@ -2849,7 +2851,7 @@ pub async fn delete_gallery_item_internal(
|
|||||||
Ok(HttpResponse::NoContent().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete a project.
|
/// Delete a project.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
context_path = "/project",
|
context_path = "/project",
|
||||||
tag = "projects", responses((status = NO_CONTENT))
|
tag = "projects", responses((status = NO_CONTENT))
|
||||||
@@ -2934,6 +2936,169 @@ pub async fn project_delete_internal(
|
|||||||
.begin()
|
.begin()
|
||||||
.await
|
.await
|
||||||
.wrap_internal_err("failed to start transaction")?;
|
.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(
|
delphi::tech_review_sync::sync_deleted_project_tech_review_exit(
|
||||||
project.inner.id,
|
project.inner.id,
|
||||||
&mut transaction,
|
&mut transaction,
|
||||||
@@ -3001,7 +3166,7 @@ pub async fn project_delete_internal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Follow a project.
|
/// Follow a project.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
context_path = "/project",
|
context_path = "/project",
|
||||||
tag = "projects", responses((status = NO_CONTENT))
|
tag = "projects", responses((status = NO_CONTENT))
|
||||||
@@ -3108,7 +3273,7 @@ pub async fn project_follow_internal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unfollow a project.
|
/// Unfollow a project.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
context_path = "/project",
|
context_path = "/project",
|
||||||
tag = "projects", responses((status = NO_CONTENT))
|
tag = "projects", responses((status = NO_CONTENT))
|
||||||
@@ -3212,7 +3377,7 @@ pub async fn project_unfollow_internal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a project's organization.
|
/// Get a project's organization.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
context_path = "/project",
|
context_path = "/project",
|
||||||
tag = "projects", responses((status = OK, body = models::organizations::Organization))
|
tag = "projects", responses((status = OK, body = models::organizations::Organization))
|
||||||
|
|||||||
Reference in New Issue
Block a user