upsert and delete featured items (not yet routed or documented)

This commit is contained in:
Brandon Thomas committed 2024-05-05 01:47:37 -04:00
1 parent 44f5e14ded
commit d99eef6b8d
17 files changed
+709 -1

No files matched your search

@@ -0,0 +1,40 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE featured_items (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- ========== ENTITY LINKAGE ==========
-- The type of record this is attached to
-- Currently supported values:
-- * 'model_weight'
-- * 'media_file'
entity_type VARCHAR(32) NOT NULL,
-- The token for the record.
entity_token VARCHAR(32) NOT NULL,
-- ========== SORTING ==========
-- A sorted ordering (optional) for pagination
maybe_sort_order INT UNSIGNED DEFAULT NULL,
-- ========== TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (entity_type, entity_token),
KEY index_maybe_sort_order_asc (maybe_sort_order ASC),
KEY index_maybe_sort_order_desc (maybe_sort_order DESC),
KEY index_created_at (created_at),
KEY index_deleted_at (deleted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,47 @@
use enums::by_table::featured_items::featured_item_entity_type::FeaturedItemEntityType;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::model_weights::ModelWeightToken;
use tokens::tokens::users::UserToken;
// TODO(bt, 2024-01-16): A ref type with <'a> lifetime of inner data instead of ownership?
pub enum FeaturedItemEntity {
MediaFile(MediaFileToken),
ModelWeight(ModelWeightToken),
User(UserToken),
}
// TODO(bt, 2024-01-16): Codegen? Make traits for these? Maybe overkill.
impl FeaturedItemEntity {
pub fn from_entity_type_and_token(entity_type: FeaturedItemEntityType, token: &str) -> Self {
match entity_type {
FeaturedItemEntityType::ModelWeight => Self::ModelWeight(ModelWeightToken::new_from_str(token)),
FeaturedItemEntityType::MediaFile => Self::MediaFile(MediaFileToken::new_from_str(token)),
FeaturedItemEntityType::User => Self::User(UserToken::new_from_str(token)),
}
}
pub fn get_entity_type(&self) -> FeaturedItemEntityType {
match self {
Self::MediaFile(_) => FeaturedItemEntityType::MediaFile,
Self::ModelWeight(_) => FeaturedItemEntityType::ModelWeight,
Self::User(_) => FeaturedItemEntityType::User,
}
}
pub fn get_entity_token_str(&self) -> &str {
match self {
Self::MediaFile(token) => token.as_str(),
Self::ModelWeight(token) => token.as_str(),
Self::User(token) => token.as_str(),
}
}
pub fn get_composite_keys(&self) -> (FeaturedItemEntityType, &str) {
match self {
Self::MediaFile(token) => (FeaturedItemEntityType::MediaFile, token.as_str()),
Self::ModelWeight(token) => (FeaturedItemEntityType::ModelWeight, token.as_str()),
Self::User(token) => (FeaturedItemEntityType::User, token.as_str()),
}
}
}
@@ -0,0 +1 @@
pub mod featured_item_entity;
@@ -1,2 +1,3 @@
pub mod audit_logs;
pub mod batch_generations;
pub mod featured_items;
@@ -24,6 +24,14 @@ pub enum AuditLogEntityAction {
/// Delete action
#[serde(rename = "delete")]
Delete,
/// Create featured item
#[serde(rename = "featured_item_create")]
FeaturedItemCreate,
/// Delete featured item
#[serde(rename = "featured_item_delete")]
FeaturedItemDelete,
}
// TODO(bt, 2023-01-17): This desperately needs MySQL integration tests!
@@ -38,6 +46,8 @@ impl AuditLogEntityAction {
Self::Edit => "edit",
Self::EditFeatures => "edit_features",
Self::Delete => "delete",
Self::FeaturedItemCreate => "featured_item_create",
Self::FeaturedItemDelete => "featured_item_delete",
}
}
@@ -47,6 +57,8 @@ impl AuditLogEntityAction {
"edit" => Ok(Self::Edit),
"edit_features" => Ok(Self::EditFeatures),
"delete" => Ok(Self::Delete),
"featured_item_create" => Ok(Self::FeaturedItemCreate),
"featured_item_delete" => Ok(Self::FeaturedItemDelete),
_ => Err(format!("invalid value: {:?}", value)),
}
}
@@ -66,6 +78,8 @@ mod tests {
assert_serialization(AuditLogEntityAction::Edit, "edit");
assert_serialization(AuditLogEntityAction::EditFeatures, "edit_features");
assert_serialization(AuditLogEntityAction::Delete, "delete");
assert_serialization(AuditLogEntityAction::FeaturedItemCreate, "featured_item_create");
assert_serialization(AuditLogEntityAction::FeaturedItemDelete, "featured_item_delete");
}
}
@@ -78,6 +92,8 @@ mod tests {
assert_eq!(AuditLogEntityAction::Edit.to_str(), "edit");
assert_eq!(AuditLogEntityAction::EditFeatures.to_str(), "edit_features");
assert_eq!(AuditLogEntityAction::Delete.to_str(), "delete");
assert_eq!(AuditLogEntityAction::FeaturedItemCreate.to_str(), "featured_item_create");
assert_eq!(AuditLogEntityAction::FeaturedItemDelete.to_str(), "featured_item_delete");
}
#[test]
@@ -86,6 +102,8 @@ mod tests {
assert_eq!(AuditLogEntityAction::from_str("edit").unwrap(), AuditLogEntityAction::Edit);
assert_eq!(AuditLogEntityAction::from_str("edit_features").unwrap(), AuditLogEntityAction::EditFeatures);
assert_eq!(AuditLogEntityAction::from_str("delete").unwrap(), AuditLogEntityAction::Delete);
assert_eq!(AuditLogEntityAction::from_str("featured_item_create").unwrap(), AuditLogEntityAction::FeaturedItemCreate);
assert_eq!(AuditLogEntityAction::from_str("featured_item_delete").unwrap(), AuditLogEntityAction::FeaturedItemDelete);
assert!(AuditLogEntityAction::from_str("foo").is_err());
}
}
@@ -0,0 +1,125 @@
use std::collections::BTreeSet;
use serde::Deserialize;
use serde::Serialize;
#[cfg(test)]
use strum::EnumCount;
#[cfg(test)]
use strum::EnumIter;
use utoipa::ToSchema;
/// Used in the `user_bookmarks` table in a `VARCHAR(32)` field named `entity_type`.
#[cfg_attr(test, derive(EnumIter, EnumCount))]
#[derive(Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize, Serialize, ToSchema)]
pub enum FeaturedItemEntityType {
/// MediaFile
#[serde(rename = "media_file")]
MediaFile,
/// ModelWeight (the new way to store models)
#[serde(rename = "model_weight")]
ModelWeight,
/// User
#[serde(rename = "user")]
User,
}
// TODO(bt, 2023-01-17): This desperately needs MySQL integration tests!
impl_enum_display_and_debug_using_to_str!(FeaturedItemEntityType);
impl_mysql_enum_coders!(FeaturedItemEntityType);
impl_mysql_from_row!(FeaturedItemEntityType);
/// NB: Legacy API for older code.
impl FeaturedItemEntityType {
pub fn to_str(&self) -> &'static str {
match self {
Self::MediaFile => "media_file",
Self::ModelWeight => "model_weight",
Self::User => "user",
}
}
pub fn from_str(value: &str) -> Result<Self, String> {
match value {
"media_file" => Ok(Self::MediaFile),
"model_weight" => Ok(Self::ModelWeight),
"user" => Ok(Self::User),
_ => Err(format!("invalid value: {:?}", value)),
}
}
pub fn all_variants() -> BTreeSet<Self> {
// NB: BTreeSet is sorted
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
BTreeSet::from([
Self::MediaFile,
Self::ModelWeight,
Self::User,
])
}
}
#[cfg(test)]
mod tests {
use crate::by_table::featured_items::featured_item_entity_type::FeaturedItemEntityType;
use crate::test_helpers::assert_serialization;
mod serde {
use super::*;
#[test]
fn test_serialization() {
assert_serialization(FeaturedItemEntityType::MediaFile, "media_file");
assert_serialization(FeaturedItemEntityType::ModelWeight, "model_weight");
assert_serialization(FeaturedItemEntityType::User, "user");
}
}
mod impl_methods {
use super::*;
#[test]
fn test_to_str() {
assert_eq!(FeaturedItemEntityType::MediaFile.to_str(), "media_file");
assert_eq!(FeaturedItemEntityType::ModelWeight.to_str(), "model_weight");
assert_eq!(FeaturedItemEntityType::User.to_str(), "user");
}
#[test]
fn test_from_str() {
assert_eq!(FeaturedItemEntityType::from_str("media_file").unwrap(), FeaturedItemEntityType::MediaFile);
assert_eq!(FeaturedItemEntityType::from_str("model_weight").unwrap(), FeaturedItemEntityType::ModelWeight);
assert_eq!(FeaturedItemEntityType::from_str("user").unwrap(), FeaturedItemEntityType::User);
assert!(FeaturedItemEntityType::from_str("foo").is_err());
}
}
mod mechanical_checks {
use super::*;
#[test]
fn variant_length() {
use strum::IntoEnumIterator;
assert_eq!(FeaturedItemEntityType::all_variants().len(), FeaturedItemEntityType::iter().len());
}
#[test]
fn round_trip() {
for variant in FeaturedItemEntityType::all_variants() {
assert_eq!(variant, FeaturedItemEntityType::from_str(variant.to_str()).unwrap());
assert_eq!(variant, FeaturedItemEntityType::from_str(&format!("{}", variant)).unwrap());
assert_eq!(variant, FeaturedItemEntityType::from_str(&format!("{:?}", variant)).unwrap());
}
}
#[test]
fn serialized_length_ok_for_database() {
const MAX_LENGTH : usize = 32;
for variant in FeaturedItemEntityType::all_variants() {
let serialized = variant.to_str();
assert!(serialized.len() > 0, "variant {:?} is too short", variant);
assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long", variant);
}
}
}
}
@@ -0,0 +1 @@
pub mod featured_item_entity_type;
+1
View File
@@ -4,6 +4,7 @@ pub mod batch_generations;
pub mod comments;
pub mod email_sender_jobs;
pub mod entity_stats;
pub mod featured_items;
pub mod generic_download_jobs;
pub mod generic_inference_jobs;
pub mod generic_synthetic_ids;
@@ -0,0 +1,32 @@
use sqlx::{Executor, MySql};
use composite_identifiers::by_table::featured_items::featured_item_entity::FeaturedItemEntity;
use errors::AnyhowResult;
pub async fn delete_featured_item<'e, 'c, E>(
entity: &'e FeaturedItemEntity,
mysql_executor: E
)
-> AnyhowResult<()>
where E: 'e + Executor<'c, Database = MySql>
{
let (entity_type, entity_token) = entity.get_composite_keys();
sqlx::query!(
r#"
UPDATE featured_items
SET
deleted_at = CURRENT_TIMESTAMP
WHERE
entity_type = ?
AND entity_token = ?
LIMIT 1
"#,
entity_type,
entity_token
)
.execute(mysql_executor)
.await?;
Ok(())
}
@@ -0,0 +1,2 @@
pub mod upsert_featured_item;
pub mod delete_featured_item;
@@ -0,0 +1,51 @@
use std::marker::PhantomData;
use anyhow::anyhow;
use sqlx::{Executor, MySql};
use composite_identifiers::by_table::featured_items::featured_item_entity::FeaturedItemEntity;
use errors::AnyhowResult;
pub struct UpsertFeaturedItemArgs<'e, 'c, E>
where E: 'e + Executor<'c, Database = MySql>
{
pub entity: &'e FeaturedItemEntity,
pub mysql_executor: E,
// TODO: Not sure if this works to tell the compiler we need the lifetime annotation.
// See: https://doc.rust-lang.org/std/marker/struct.PhantomData.html#unused-lifetime-parameters
pub phantom: PhantomData<&'c E>,
}
pub async fn upsert_featured_item<'e, 'c : 'e, E>(
args: UpsertFeaturedItemArgs<'e, 'c, E>,
)
-> AnyhowResult<()>
where E: 'e + Executor<'c, Database = MySql>
{
let (entity_type, entity_token) = args.entity.get_composite_keys();
let query_result = sqlx::query!(
r#"
INSERT INTO featured_items
SET
entity_type = ?,
entity_token = ?
ON DUPLICATE KEY UPDATE
deleted_at = NULL
"#,
entity_type,
entity_token,
)
.execute(args.mysql_executor)
.await;
let _record_id = match query_result {
Ok(res) => res.last_insert_id(),
Err(err) => return Err(anyhow!("Mysql error: {:?}", err)),
};
Ok(())
}
@@ -5,6 +5,7 @@ pub mod billing;
pub mod comments;
pub mod email_sender_jobs;
pub mod entity_stats;
pub mod featured_items;
pub mod generic_download;
pub mod generic_inference;
pub mod generic_synthetic_ids;
@@ -9,6 +9,8 @@ use tokens::tokens::w2l_results::W2lResultToken;
use tokens::tokens::w2l_templates::W2lTemplateToken;
use tokens::tokens::zs_voices::ZsVoiceToken;
// TODO(bt,2024-05-05): Make this a composite_identifier enum
pub enum UserBookmarkEntityToken {
User(UserToken),
ModelWeight(ModelWeightToken),
@@ -0,0 +1,197 @@
use std::fmt;
use std::sync::Arc;
use actix_web::{HttpRequest, HttpResponse, web};
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use log::{error, warn};
use r2d2_redis::redis::transaction;
use sqlx::Acquire;
use utoipa::ToSchema;
use composite_identifiers::by_table::audit_logs::audit_log_entity::AuditLogEntity;
use composite_identifiers::by_table::featured_items::featured_item_entity::FeaturedItemEntity;
use enums::by_table::audit_logs::audit_log_entity_action::AuditLogEntityAction;
use enums::by_table::featured_items::featured_item_entity_type::FeaturedItemEntityType;
use http_server_common::request::get_request_ip::get_request_ip;
use mysql_queries::queries::audit_logs::insert_audit_log::{insert_audit_log, InsertAuditLogArgs};
use mysql_queries::queries::audit_logs::transactional_insert_audit_log::{transactional_insert_audit_log, TransactionalInsertAuditLogArgs};
use mysql_queries::queries::entity_stats::stats_entity_token::StatsEntityToken;
use mysql_queries::queries::entity_stats::upsert_entity_stats_on_bookmark_event::{BookmarkAction, upsert_entity_stats_on_bookmark_event, UpsertEntityStatsArgs};
use mysql_queries::queries::featured_items::upsert_featured_item::{upsert_featured_item, UpsertFeaturedItemArgs};
use mysql_queries::queries::users::user_bookmarks::get_total_bookmark_count_for_entity::get_total_bookmark_count_for_entity;
use mysql_queries::queries::users::user_bookmarks::get_user_bookmark_transactional_locking::{BookmarkIdentifier, get_user_bookmark_transactional_locking};
use mysql_queries::queries::users::user_bookmarks::user_bookmark_entity_token::UserBookmarkEntityToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::model_weights::ModelWeightToken;
use tokens::tokens::tts_models::TtsModelToken;
use tokens::tokens::tts_results::TtsResultToken;
use tokens::tokens::user_bookmarks::UserBookmarkToken;
use tokens::tokens::users::UserToken;
use tokens::tokens::voice_conversion_models::VoiceConversionModelToken;
use tokens::tokens::w2l_results::W2lResultToken;
use tokens::tokens::w2l_templates::W2lTemplateToken;
use tokens::tokens::zs_voices::ZsVoiceToken;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::server_state::ServerState;
#[derive(Deserialize, ToSchema)]
pub struct CreateFeaturedItemRequest {
entity_token: String,
entity_type: FeaturedItemEntityType,
}
#[derive(Serialize, ToSchema)]
pub struct CreateFeaturedItemSuccessResponse {
pub success: bool,
}
#[derive(Debug, ToSchema)]
pub enum CreateFeaturedItemError {
BadInput(String),
NotAuthorized,
ServerError,
}
impl ResponseError for CreateFeaturedItemError {
fn status_code(&self) -> StatusCode {
match *self {
CreateFeaturedItemError::BadInput(_) => StatusCode::BAD_REQUEST,
CreateFeaturedItemError::NotAuthorized => StatusCode::UNAUTHORIZED,
CreateFeaturedItemError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
CreateFeaturedItemError::BadInput(reason) => reason.to_string(),
CreateFeaturedItemError::NotAuthorized => "unauthorized".to_string(),
CreateFeaturedItemError::ServerError => "server error".to_string(),
};
to_simple_json_error(&error_reason, self.status_code())
}
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
impl fmt::Display for CreateFeaturedItemError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
#[utoipa::path(
post,
tag = "Featured Items",
path = "/v1/featured_item/create",
request_body = CreateFeaturedItemRequest,
responses(
(status = 200, body = CreateFeaturedItemSuccessResponse),
(status = 400, body = CreateFeaturedItemError),
)
)]
pub async fn create_featured_item_handler(
http_request: HttpRequest,
request: web::Json<CreateFeaturedItemRequest>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<HttpResponse, CreateFeaturedItemError>
{
// NB(bt,2023-12-14): Kasisnu found that we're getting entity type mismatches in production. Apart from
// querying the database for entity existence, this is the next best way to prevent incorrect comment
// attachment. This is a bit of a bad process, though, since the token types are supposed to be opaque.
let token = request.entity_token.as_str();
let token_prefix_matches = match request.entity_type {
// NB: Users had an older prefix (U:) that got replaced with the new prefix (user_)
FeaturedItemEntityType::User => token.starts_with(UserToken::token_prefix()) || token.starts_with("U:"),
FeaturedItemEntityType::MediaFile => token.starts_with(MediaFileToken::token_prefix()),
FeaturedItemEntityType::ModelWeight => token.starts_with(ModelWeightToken::token_prefix()),
};
if !token_prefix_matches {
warn!("invalid token prefix: {:?} for {:?}", request.entity_token, request.entity_type);
return Err(CreateFeaturedItemError::BadInput("invalid token prefix".to_string()));
}
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await
.map_err(|err| {
warn!("MySql pool error: {:?}", err);
CreateFeaturedItemError::ServerError
})?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CreateFeaturedItemError::ServerError
})?;
let user_session = match maybe_user_session {
Some(session) => session,
None => {
warn!("not logged in");
return Err(CreateFeaturedItemError::NotAuthorized);
}
};
let is_mod = user_session.can_ban_users;
if !is_mod {
warn!("not moderator");
return Err(CreateFeaturedItemError::NotAuthorized);
}
let entity = FeaturedItemEntity::from_entity_type_and_token(
request.entity_type, &request.entity_token);
let mut transaction = mysql_connection.begin().await
.map_err(|err| {
error!("error creating transaction: {:?}", err);
CreateFeaturedItemError::ServerError
})?;
let upsert_result = upsert_featured_item(UpsertFeaturedItemArgs {
entity: &entity,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
if let Err(err) = upsert_result {
warn!("error upserting user_bookmark: {:?}", err);
return Err(CreateFeaturedItemError::ServerError);
}
let ip_address = get_request_ip(&http_request);
// NB: fail open
let _r = transactional_insert_audit_log(TransactionalInsertAuditLogArgs {
entity: &AuditLogEntity::User(user_session.user_token.clone()),
entity_action: AuditLogEntityAction::FeaturedItemCreate,
maybe_actor_user_token: Some(&user_session.user_token),
maybe_actor_anonymous_visitor_token: None,
actor_ip_address: &ip_address,
is_actor_moderator: true,
transaction: &mut transaction,
}).await;
transaction.commit().await
.map_err(|err| {
error!("error committing transaction: {:?}", err);
CreateFeaturedItemError::ServerError
})?;
let response = CreateFeaturedItemSuccessResponse {
success: true,
};
let body = serde_json::to_string(&response)
.map_err(|_e| CreateFeaturedItemError::ServerError)?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(body))
}
@@ -0,0 +1,186 @@
use std::fmt;
use std::sync::Arc;
use actix_web::{HttpRequest, HttpResponse, web};
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::Path;
use log::{error, info, warn};
use sqlx::Acquire;
use utoipa::ToSchema;
use composite_identifiers::by_table::audit_logs::audit_log_entity::AuditLogEntity;
use composite_identifiers::by_table::featured_items::featured_item_entity::FeaturedItemEntity;
use enums::by_table::audit_logs::audit_log_entity_action::AuditLogEntityAction;
use enums::by_table::featured_items::featured_item_entity_type::FeaturedItemEntityType;
use http_server_common::request::get_request_ip::get_request_ip;
use mysql_queries::queries::audit_logs::insert_audit_log::{insert_audit_log, InsertAuditLogArgs};
use mysql_queries::queries::audit_logs::transactional_insert_audit_log::{transactional_insert_audit_log, TransactionalInsertAuditLogArgs};
use mysql_queries::queries::entity_stats::stats_entity_token::StatsEntityToken;
use mysql_queries::queries::entity_stats::upsert_entity_stats_on_bookmark_event::{BookmarkAction, upsert_entity_stats_on_bookmark_event, UpsertEntityStatsArgs};
use mysql_queries::queries::featured_items::delete_featured_item::delete_featured_item;
use mysql_queries::queries::users::user_bookmarks::delete_user_bookmark::delete_user_bookmark;
use mysql_queries::queries::users::user_bookmarks::get_user_bookmark_transactional_locking::{BookmarkIdentifier, get_user_bookmark_transactional_locking};
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::model_weights::ModelWeightToken;
use tokens::tokens::user_bookmarks::UserBookmarkToken;
use tokens::tokens::users::UserToken;
use crate::http_server::endpoints::featured_items::create_featured_item_handler::CreateFeaturedItemError;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
use crate::server_state::ServerState;
#[derive(Deserialize, ToSchema)]
pub struct DeleteFeaturedItemRequest {
entity_token: String,
entity_type: FeaturedItemEntityType,
}
#[derive(Debug, ToSchema)]
pub enum DeleteFeaturedItemError {
BadInput(String),
NotAuthorized,
NotFound,
ServerError,
}
impl ResponseError for DeleteFeaturedItemError {
fn status_code(&self) -> StatusCode {
match *self {
DeleteFeaturedItemError::BadInput(_) => StatusCode::BAD_REQUEST,
DeleteFeaturedItemError::NotAuthorized => StatusCode::UNAUTHORIZED,
DeleteFeaturedItemError::NotFound => StatusCode::NOT_FOUND,
DeleteFeaturedItemError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
DeleteFeaturedItemError::BadInput(reason) => reason.to_string(),
DeleteFeaturedItemError::NotAuthorized => "unauthorized".to_string(),
DeleteFeaturedItemError::NotFound => "not found".to_string(),
DeleteFeaturedItemError::ServerError => "server error".to_string(),
};
to_simple_json_error(&error_reason, self.status_code())
}
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
impl fmt::Display for DeleteFeaturedItemError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
#[utoipa::path(
delete,
tag = "Featured Items",
path = "/v1/featured_item/delete",
params(
("user_bookmark_token", description = "UserBookmarkToken"),
),
responses(
(status = 200, description = "Delete User Bookmark", body = SimpleGenericJsonSuccess),
(status = 400, description = "Bad input", body = DeleteFeaturedItemError),
(status = 401, description = "Not authorized", body = DeleteFeaturedItemError),
(status = 404, description = "Not found", body = DeleteFeaturedItemError),
(status = 500, description = "Server error", body = DeleteFeaturedItemError),
),
)]
pub async fn delete_user_bookmark_handler(
http_request: HttpRequest,
request: web::Json<DeleteFeaturedItemRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<HttpResponse, DeleteFeaturedItemError> {
// NB(bt,2023-12-14): Kasisnu found that we're getting entity type mismatches in production. Apart from
// querying the database for entity existence, this is the next best way to prevent incorrect comment
// attachment. This is a bit of a bad process, though, since the token types are supposed to be opaque.
let token = request.entity_token.as_str();
let token_prefix_matches = match request.entity_type {
// NB: Users had an older prefix (U:) that got replaced with the new prefix (user_)
FeaturedItemEntityType::User => token.starts_with(UserToken::token_prefix()) || token.starts_with("U:"),
FeaturedItemEntityType::MediaFile => token.starts_with(MediaFileToken::token_prefix()),
FeaturedItemEntityType::ModelWeight => token.starts_with(ModelWeightToken::token_prefix()),
};
if !token_prefix_matches {
warn!("invalid token prefix: {:?} for {:?}", request.entity_token, request.entity_type);
return Err(DeleteFeaturedItemError::BadInput("invalid token prefix".to_string()));
}
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await
.map_err(|err| {
warn!("MySql pool error: {:?}", err);
DeleteFeaturedItemError::ServerError
})?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
DeleteFeaturedItemError::ServerError
})?;
let user_session = match maybe_user_session {
Some(session) => session,
None => {
return Err(DeleteFeaturedItemError::NotAuthorized);
}
};
let is_mod = user_session.can_ban_users;
if !is_mod {
warn!("not moderator");
return Err(DeleteFeaturedItemError::NotAuthorized);
}
let entity = FeaturedItemEntity::from_entity_type_and_token(
request.entity_type, &request.entity_token);
let mut transaction = mysql_connection.begin().await
.map_err(|err| {
error!("error creating transaction: {:?}", err);
DeleteFeaturedItemError::ServerError
})?;
let delete_result = delete_featured_item(
&entity,
&mut *transaction,
).await;
let ip_address = get_request_ip(&http_request);
// NB: fail open
let _r = transactional_insert_audit_log(TransactionalInsertAuditLogArgs {
entity: &AuditLogEntity::User(user_session.user_token.clone()),
entity_action: AuditLogEntityAction::FeaturedItemDelete,
maybe_actor_user_token: Some(&user_session.user_token),
maybe_actor_anonymous_visitor_token: None,
actor_ip_address: &ip_address,
is_actor_moderator: true,
transaction: &mut transaction,
}).await;
transaction.commit().await
.map_err(|err| {
error!("error committing transaction: {:?}", err);
DeleteFeaturedItemError::ServerError
})?;
match delete_result {
Ok(_) => {},
Err(_err) => {
return Err(DeleteFeaturedItemError::ServerError);
}
};
Ok(simple_json_success())
}
@@ -0,0 +1,2 @@
pub mod create_featured_item_handler;
pub mod delete_featured_item_handler;
@@ -6,6 +6,7 @@ pub mod conversion;
pub mod download_job;
pub mod engine;
pub mod events;
pub mod featured_items;
pub mod flags;
pub mod image_gen;
pub mod inference_job;
@@ -30,6 +31,6 @@ pub mod vocoders;
pub mod voice_clone_requests;
pub mod voice_conversion;
pub mod voice_designer;
pub mod w2l;
pub mod weights;
pub mod workflows;
pub mod w2l;