moderator token info endpoint

This commit is contained in:
Brandon Thomas committed 2024-03-28 19:42:16 -04:00
1 parent 6c4330294b
commit 80e57774a2
8 files changed
+381 -8

No files matched your search

@@ -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,
@@ -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;
@@ -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")