mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-06 02:00:46 +00:00
moderator token info endpoint
This commit is contained in:
1 parent
6c4330294b
commit
80e57774a2
8 files changed
+381
-8
No files matched your search
+8
-4
@@ -15,7 +15,8 @@ use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::helpers::boolean_converters::i8_to_bool;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
// NB: Serialization is for internal endpoints, not for returning to end-users.
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct GenericInferenceJobStatus {
|
||||
pub job_token: InferenceJobToken,
|
||||
|
||||
@@ -39,9 +40,10 @@ pub struct GenericInferenceJobStatus {
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// NB: Serialize is for internal moderator-only endpoints
|
||||
/// Details about the user's original inference request
|
||||
/// (We may want to present it in the "pending" UI.)
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct RequestDetails {
|
||||
pub inference_category: InferenceCategory,
|
||||
pub maybe_model_type: Option<String>, // TODO: Strongly type
|
||||
@@ -52,8 +54,9 @@ pub struct RequestDetails {
|
||||
pub maybe_raw_inference_text: Option<String>,
|
||||
}
|
||||
|
||||
/// NB: Serialize is for internal moderator-only endpoints
|
||||
/// Details about the generated result
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct ResultDetails {
|
||||
pub entity_type: String,
|
||||
pub entity_token: String,
|
||||
@@ -71,8 +74,9 @@ pub struct ResultDetails {
|
||||
}
|
||||
|
||||
/// NB: DO NOT EXPOSE TO FRONTEND.
|
||||
/// NB: Serialize is for internal moderator-only endpoints
|
||||
/// This is used to gate access to job termination
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct UserDetails {
|
||||
pub maybe_creator_user_token: Option<UserToken>,
|
||||
pub maybe_creator_anonymous_visitor_token: Option<AnonymousVisitorTrackingToken>,
|
||||
|
||||
@@ -14,6 +14,8 @@ use tokens::tokens::{model_weights::ModelWeightToken, users::UserToken};
|
||||
// 'weights_type: enums::by_table::model_weights::weights_types::WeightsType' use this to map
|
||||
// Retrieved Model Weight can be constrained to the fields that are needed
|
||||
|
||||
// NB: Serialize is only for internal moderator endpoints
|
||||
#[derive(Serialize)]
|
||||
pub struct RetrievedModelWeight {
|
||||
pub token: ModelWeightToken,
|
||||
pub title: String,
|
||||
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
use anyhow::anyhow;
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::warn;
|
||||
use sqlx::{MySql, MySqlPool};
|
||||
use sqlx::pool::PoolConnection;
|
||||
|
||||
use enums::common::visibility::Visibility;
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::helpers::boolean_converters::i8_to_bool;
|
||||
|
||||
// NB: Serialize for internal moderator endpoints
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct UserProfileResult {
|
||||
pub user_token: UserToken,
|
||||
pub username: String,
|
||||
pub display_name: String,
|
||||
pub email_gravatar_hash: String,
|
||||
pub profile_markdown: String,
|
||||
pub profile_rendered_html: String,
|
||||
pub user_role_slug: String,
|
||||
pub disable_gravatar: bool,
|
||||
|
||||
// NB: Legacy top-level moderator field
|
||||
#[deprecated = "use moderator fields instead"]
|
||||
pub is_banned: bool,
|
||||
|
||||
// Social
|
||||
pub discord_username: Option<String>,
|
||||
pub twitch_username: Option<String>,
|
||||
pub twitter_username: Option<String>,
|
||||
pub patreon_username: Option<String>,
|
||||
pub github_username: Option<String>,
|
||||
pub cashapp_username: Option<String>,
|
||||
pub website_url: Option<String>,
|
||||
|
||||
// Preferences; NB: included for get_profile_handler legacy reasons
|
||||
pub preferred_tts_result_visibility: Visibility,
|
||||
pub preferred_w2l_result_visibility: Visibility,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
// NB: Moderator fields must be cleared by HTTP handlers for non-mods
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub maybe_moderator_fields: Option<UserProfileModeratorFields>,
|
||||
}
|
||||
|
||||
// TODO: Make non-`Serialize` and make the HTTP endpoints do the work
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct UserProfileModeratorFields {
|
||||
pub is_banned: bool,
|
||||
pub maybe_mod_comments: Option<String>,
|
||||
pub maybe_mod_user_token: Option<String>,
|
||||
}
|
||||
|
||||
struct RawUserProfileRecord {
|
||||
user_token: UserToken,
|
||||
username: String,
|
||||
email_gravatar_hash: String,
|
||||
display_name: String,
|
||||
profile_markdown: String,
|
||||
profile_rendered_html: String,
|
||||
user_role_slug: String,
|
||||
disable_gravatar: i8,
|
||||
discord_username: Option<String>,
|
||||
twitch_username: Option<String>,
|
||||
twitter_username: Option<String>,
|
||||
patreon_username: Option<String>,
|
||||
github_username: Option<String>,
|
||||
cashapp_username: Option<String>,
|
||||
website_url: Option<String>,
|
||||
created_at: DateTime<Utc>,
|
||||
|
||||
// Preferences; NB: included for get_profile_handler legacy reasons
|
||||
preferred_tts_result_visibility: Visibility,
|
||||
preferred_w2l_result_visibility: Visibility,
|
||||
|
||||
// Mod fields
|
||||
is_banned: i8,
|
||||
maybe_mod_comments: Option<String>,
|
||||
maybe_mod_user_token: Option<String>,
|
||||
}
|
||||
|
||||
//#[deprecated = "Use the PoolConnection method"]
|
||||
pub async fn get_user_profile_by_token(
|
||||
token: &UserToken,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> AnyhowResult<Option<UserProfileResult>> {
|
||||
let mut connection = mysql_pool.acquire().await?;
|
||||
get_user_profile_by_token_from_connection(token, &mut connection).await
|
||||
}
|
||||
|
||||
pub async fn get_user_profile_by_token_from_connection<'a>(
|
||||
token: &UserToken,
|
||||
connection: &'a mut PoolConnection<MySql>
|
||||
) -> AnyhowResult<Option<UserProfileResult>> {
|
||||
let maybe_profile_record = sqlx::query_as!(
|
||||
RawUserProfileRecord,
|
||||
r#"
|
||||
SELECT
|
||||
users.token as `user_token: tokens::tokens::users::UserToken`,
|
||||
username,
|
||||
display_name,
|
||||
email_gravatar_hash,
|
||||
profile_markdown,
|
||||
profile_rendered_html,
|
||||
user_role_slug,
|
||||
disable_gravatar,
|
||||
preferred_tts_result_visibility as `preferred_tts_result_visibility: enums::common::visibility::Visibility`,
|
||||
preferred_w2l_result_visibility as `preferred_w2l_result_visibility: enums::common::visibility::Visibility`,
|
||||
discord_username,
|
||||
twitch_username,
|
||||
twitter_username,
|
||||
patreon_username,
|
||||
github_username,
|
||||
cashapp_username,
|
||||
website_url,
|
||||
is_banned,
|
||||
maybe_mod_comments,
|
||||
maybe_mod_user_token,
|
||||
created_at
|
||||
FROM users
|
||||
WHERE
|
||||
users.token = ?
|
||||
AND users.user_deleted_at IS NULL
|
||||
AND users.mod_deleted_at IS NULL
|
||||
"#,
|
||||
token,
|
||||
)
|
||||
.fetch_one(&mut **connection)
|
||||
.await;
|
||||
|
||||
let profile_record : RawUserProfileRecord = match maybe_profile_record {
|
||||
Ok(profile_record) => profile_record,
|
||||
Err(err) => {
|
||||
return match err {
|
||||
sqlx::Error::RowNotFound => {
|
||||
Ok(None)
|
||||
},
|
||||
_ => {
|
||||
warn!("User profile query error: {:?}", err);
|
||||
Err(anyhow!("query error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let profile_for_response = UserProfileResult {
|
||||
user_token: profile_record.user_token,
|
||||
username: profile_record.username,
|
||||
display_name: profile_record.display_name,
|
||||
email_gravatar_hash: profile_record.email_gravatar_hash,
|
||||
profile_markdown: profile_record.profile_markdown,
|
||||
profile_rendered_html: profile_record.profile_rendered_html,
|
||||
user_role_slug: profile_record.user_role_slug,
|
||||
is_banned: i8_to_bool(profile_record.is_banned),
|
||||
disable_gravatar: i8_to_bool(profile_record.disable_gravatar),
|
||||
discord_username: profile_record.discord_username,
|
||||
twitch_username: profile_record.twitch_username,
|
||||
twitter_username: profile_record.twitter_username,
|
||||
patreon_username: profile_record.patreon_username,
|
||||
github_username: profile_record.github_username,
|
||||
cashapp_username: profile_record.cashapp_username,
|
||||
website_url: profile_record.website_url,
|
||||
preferred_tts_result_visibility: profile_record.preferred_tts_result_visibility,
|
||||
preferred_w2l_result_visibility: profile_record.preferred_w2l_result_visibility,
|
||||
created_at: profile_record.created_at,
|
||||
maybe_moderator_fields: Some(UserProfileModeratorFields {
|
||||
is_banned: i8_to_bool(profile_record.is_banned),
|
||||
maybe_mod_comments: profile_record.maybe_mod_comments,
|
||||
maybe_mod_user_token: profile_record.maybe_mod_user_token,
|
||||
})
|
||||
};
|
||||
|
||||
Ok(Some(profile_for_response))
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod edit_user_profile_as_account_holder;
|
||||
pub mod edit_user_profile_as_mod;
|
||||
pub mod get_user_profile_by_token;
|
||||
pub mod get_user_profile_by_username;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod moderator_token_info_handler;
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
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::{log, warn};
|
||||
use serde::Serialize;
|
||||
use sqlx::MySqlPool;
|
||||
use utoipa::ToSchema;
|
||||
use errors::AnyhowResult;
|
||||
use mysql_queries::queries::generic_inference::web::get_inference_job_status::get_inference_job_status;
|
||||
use mysql_queries::queries::media_files::get_media_file::get_media_file;
|
||||
use mysql_queries::queries::model_weights::get::get_weight::get_weight_by_token;
|
||||
use mysql_queries::queries::model_weights::model_weight_info_lite::get_model_weight_info_lite::get_model_weight_info_lite;
|
||||
use mysql_queries::queries::prompts::get_prompt::get_prompt;
|
||||
|
||||
use mysql_queries::queries::tts::tts_inference_jobs::get_pending_tts_inference_job_detailed_stats::{get_pending_tts_inference_job_detailed_stats, PendingCountResult};
|
||||
use mysql_queries::queries::users::user_profiles::get_user_profile_by_token::get_user_profile_by_token;
|
||||
use mysql_queries::queries::users::user_profiles::get_user_profile_by_username::get_user_profile_by_username;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::model_weights::ModelWeightToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::http_server::web_utils::serialize_as_json_error::serialize_as_json_error;
|
||||
use crate::server_state::ServerState;
|
||||
|
||||
/// For the URL PathInfo
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct ModeratorTokenInfoPath {
|
||||
token: String,
|
||||
}
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ModeratorTokenInfoResponse {
|
||||
pub success: bool,
|
||||
|
||||
/// Json-encoded payload
|
||||
pub maybe_payload: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub enum ModeratorTokenInfoError {
|
||||
ServerError,
|
||||
Unauthorized,
|
||||
}
|
||||
|
||||
impl ResponseError for ModeratorTokenInfoError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
ModeratorTokenInfoError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ModeratorTokenInfoError::Unauthorized => StatusCode::UNAUTHORIZED,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
serialize_as_json_error(self)
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using DeriveMore since Clion doesn't understand it.
|
||||
impl fmt::Display for ModeratorTokenInfoError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/moderation/token_info/{token}",
|
||||
responses(
|
||||
(status = 200, description = "Found", body = ModeratorTokenInfoResponse),
|
||||
(status = 401, description = "Not authorized", body = ModeratorTokenInfoError),
|
||||
(status = 500, description = "Server error", body = ModeratorTokenInfoError),
|
||||
),
|
||||
params(
|
||||
("path" = ModeratorTokenInfoPath, description = "Path for Request")
|
||||
)
|
||||
)]
|
||||
pub async fn moderator_get_token_info_handler(
|
||||
path: Path<ModeratorTokenInfoPath>,
|
||||
http_request: HttpRequest,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<HttpResponse, ModeratorTokenInfoError> {
|
||||
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
ModeratorTokenInfoError::ServerError
|
||||
})?;
|
||||
|
||||
let user_session = match maybe_user_session {
|
||||
Some(session) => session,
|
||||
None => {
|
||||
warn!("not logged in");
|
||||
return Err(ModeratorTokenInfoError::Unauthorized);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Not a good fit for this permission.
|
||||
if !user_session.can_ban_users {
|
||||
warn!("user is not allowed to view bans: {}", user_session.user_token);
|
||||
return Err(ModeratorTokenInfoError::Unauthorized);
|
||||
}
|
||||
|
||||
let token = path.token.trim();
|
||||
let maybe_result = get_entity_from_token(&server_state.mysql_pool, &path.token)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("get tts pending count error: {:?}", err);
|
||||
ModeratorTokenInfoError::ServerError
|
||||
})?;
|
||||
|
||||
let response = ModeratorTokenInfoResponse {
|
||||
success: true,
|
||||
maybe_payload: maybe_result,
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|e| ModeratorTokenInfoError::ServerError)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body))
|
||||
}
|
||||
|
||||
const LEGACY_USER_TOKEN_PREFIX : &str = "U:";
|
||||
|
||||
async fn get_entity_from_token(mysql_pool: &MySqlPool, token: &str) -> AnyhowResult<Option<String>> {
|
||||
|
||||
if token.starts_with(UserToken::token_prefix()) || token.starts_with(LEGACY_USER_TOKEN_PREFIX) {
|
||||
let typed_token = UserToken::new_from_str(token);
|
||||
let maybe_result = get_user_profile_by_token(&typed_token, mysql_pool)
|
||||
.await?;
|
||||
return maybe_to_string(maybe_result);
|
||||
}
|
||||
|
||||
if token.starts_with(MediaFileToken::token_prefix()) {
|
||||
let typed_token = MediaFileToken::new_from_str(token);
|
||||
let maybe_result = get_media_file(&typed_token, true, mysql_pool)
|
||||
.await?;
|
||||
return maybe_to_string(maybe_result);
|
||||
}
|
||||
|
||||
if token.starts_with(ModelWeightToken::token_prefix()) {
|
||||
let typed_token = ModelWeightToken::new_from_str(token);
|
||||
let maybe_result = get_weight_by_token(&typed_token, true, mysql_pool)
|
||||
.await?;
|
||||
return maybe_to_string(maybe_result);
|
||||
}
|
||||
|
||||
if token.starts_with(InferenceJobToken::token_prefix()) {
|
||||
let typed_token = InferenceJobToken::new_from_str(token);
|
||||
let maybe_result = get_inference_job_status(&typed_token, mysql_pool)
|
||||
.await?;
|
||||
return maybe_to_string(maybe_result);
|
||||
}
|
||||
|
||||
if token.starts_with(PromptToken::token_prefix()) {
|
||||
let typed_token = PromptToken::new_from_str(token);
|
||||
let maybe_result = get_prompt(&typed_token, mysql_pool)
|
||||
.await?;
|
||||
return maybe_to_string(maybe_result);
|
||||
}
|
||||
|
||||
// If nothing else works, try username lookup
|
||||
let maybe_result = get_user_profile_by_username(token, mysql_pool)
|
||||
.await?;
|
||||
|
||||
maybe_to_string(maybe_result)
|
||||
}
|
||||
|
||||
fn maybe_to_string<T: Serialize>(maybe_data: Option<T>) -> AnyhowResult<Option<String>> {
|
||||
let maybe_stringified = maybe_data.map(|data| serde_json::to_string(&data))
|
||||
.transpose()?;
|
||||
Ok(maybe_stringified)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod approval;
|
||||
pub mod categories;
|
||||
pub mod info;
|
||||
pub mod ip_bans;
|
||||
pub mod jobs;
|
||||
pub mod stats;
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::http_server::endpoints::moderation::approval::pending_w2l_templates::
|
||||
use crate::http_server::endpoints::moderation::categories::delete_category::delete_category_handler;
|
||||
use crate::http_server::endpoints::moderation::categories::edit_category::edit_category_handler;
|
||||
use crate::http_server::endpoints::moderation::categories::list_tts_categories_for_moderation::list_tts_categories_for_moderation_handler;
|
||||
use crate::http_server::endpoints::moderation::info::moderator_token_info_handler::moderator_get_token_info_handler;
|
||||
use crate::http_server::endpoints::moderation::ip_bans::add_ip_ban::add_ip_ban_handler;
|
||||
use crate::http_server::endpoints::moderation::ip_bans::delete_ip_ban::delete_ip_ban_handler;
|
||||
use crate::http_server::endpoints::moderation::ip_bans::get_ip_ban::get_ip_ban_handler;
|
||||
@@ -37,10 +38,13 @@ pub fn add_moderator_routes<T, B> (app: App<T>) -> App<T>
|
||||
{
|
||||
app.service(
|
||||
web::scope("/moderation")
|
||||
.service(
|
||||
web::resource("/staff")
|
||||
.route(web::get().to(list_staff_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
.service(web::resource("/staff")
|
||||
.route(web::get().to(list_staff_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(web::resource("/token_info/{token}")
|
||||
.route(web::get().to(moderator_get_token_info_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(
|
||||
web::scope("/ip_bans")
|
||||
|
||||
Reference in new issue
Block a user